diff --git a/.claude/rules/commits.md b/.agents/rules/commits.md similarity index 100% rename from .claude/rules/commits.md rename to .agents/rules/commits.md diff --git a/.claude/rules/dependencies.md b/.agents/rules/dependencies.md similarity index 100% rename from .claude/rules/dependencies.md rename to .agents/rules/dependencies.md diff --git a/.claude/rules/docs.md b/.agents/rules/docs.md similarity index 100% rename from .claude/rules/docs.md rename to .agents/rules/docs.md diff --git a/.claude/rules/guardrails.md b/.agents/rules/guardrails.md similarity index 100% rename from .claude/rules/guardrails.md rename to .agents/rules/guardrails.md diff --git a/.claude/rules/security.md b/.agents/rules/security.md similarity index 100% rename from .claude/rules/security.md rename to .agents/rules/security.md diff --git a/.claude/rules/testing.md b/.agents/rules/testing.md similarity index 100% rename from .claude/rules/testing.md rename to .agents/rules/testing.md diff --git a/.claude/rules/token-optimization.md b/.agents/rules/token-optimization.md similarity index 100% rename from .claude/rules/token-optimization.md rename to .agents/rules/token-optimization.md diff --git a/.agents/skills/_shared/review-protocol.md b/.agents/skills/_shared/review-protocol.md new file mode 100644 index 00000000..8de960e1 --- /dev/null +++ b/.agents/skills/_shared/review-protocol.md @@ -0,0 +1,57 @@ +# Review Protocol + +`/self-review`, `/security-review`가 공유하는 절차. 각 스킬은 분석 렌즈(무엇을 볼지)만 +자기 파일에 두고, 대상 수집부터 보고까지의 절차는 이 문서를 따른다. + +## 1. 변경 사항 수집 + +- 인자가 전달되면 (`/ `) 해당 브랜치를 기준점으로 사용한다. +- 인자가 없으면: `git fetch`로 remote를 최신화하고(실패 시 경고 후 local ref로 진행) + `git rev-parse --abbrev-ref @{upstream}`로 tracking branch를 찾는다. 없으면 + `git remote show origin | grep 'HEAD branch'`의 결과를 `origin/`로 쓴다. + 둘 다 실패하면 사용자에게 base branch를 요청한다. +- `git log ..HEAD --oneline`으로 unpushed commit을 확인한다. 없으면 + "리뷰 대상 없음"을 보고하고 즉시 종료한다. +- `git diff ..HEAD`로 전체 변경 범위를 파악한다. +- uncommitted 변경이 있으면 리뷰 범위에 포함할지 사용자에게 확인한다. 포함하면 + `git diff`와 `git diff --staged`도 수집한다. 커밋할 때는 리뷰 수정분만 staging하여 + 사용자의 작업이 섞이지 않게 한다. +- 변경된 파일은 전체를 읽는다 — diff만 보면 주변 컨텍스트를 놓친다. 바이너리, + lockfile, 자동 생성물은 제외한다. + +## 2. 평가 기준 + +발견 항목을 세 갈래로 분류한다. 각 갈래의 판단 경계는 스킬별 정의를 따른다. + +- **즉시 반영** — 코드를 직접 수정한다. +- **사용자 판단 필요** — 수정하지 않고 보고만 한다. +- **무시** — 개별 내용은 보고하지 않고, 건수와 대표 사유만 보고에 포함한다. + +## 3. 수정 적용 + +- 수정 후 프로젝트의 빌드와 테스트를 실행해 다른 것이 깨지지 않았는지 확인한다. +- 테스트가 실패하면 원인을 먼저 분류한다. + - 수정이 원인: 수정을 되돌리고 **사용자 판단 필요**로 재분류한다. + - 기존 flaky 또는 환경 문제: 수정을 유지하고 실패 원인을 보고한다. +- 확인이 끝나면 수정 사항을 commit한다. + +## 4. 중단 기준 + +다음에 해당하면 push를 진행하지 않고 사용자에게 보고한다. 스킬별 추가 기준도 함께 적용한다. + +- 수정 적용 후 빌드 또는 테스트가 실패하고 단순 수정으로 해결되지 않는 경우 +- 동일한 지적이 수렴하지 않고 계속 발생하는 경우 (순환 판단) + +## 5. 보고 형식 + +### 즉시 반영한 항목 +(각 항목: 파일, 변경 내용, 발견 근거. 없으면 `없음`) + +### 사용자 판단이 필요한 항목 +(각 항목: 파일, 지적 내용, 판단을 미룬 이유. 없으면 `없음`) + +### 무시한 항목 +(건수와 대표 사유. 없으면 `없음`) + +### 리뷰 요약 +(1-2문장 평가) diff --git a/.claude/skills/plan/SKILL.md b/.agents/skills/plan/SKILL.md similarity index 100% rename from .claude/skills/plan/SKILL.md rename to .agents/skills/plan/SKILL.md diff --git a/.agents/skills/security-review/SKILL.md b/.agents/skills/security-review/SKILL.md new file mode 100644 index 00000000..bd0adb72 --- /dev/null +++ b/.agents/skills/security-review/SKILL.md @@ -0,0 +1,63 @@ +--- +name: security-review +description: 구현 완료 후 보안 관점 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다. clean pass까지 반복이 필요하면 ralph를 통해 실행한다 +user-invocable: true +--- + +# Security Review + +구현 완료 후 변경 사항을 보안 관점에서 심층 분석하고, 타당한 보안 개선을 코드에 반영한다. +반복 실행이 필요하면 `/ralph /security-review`로 ralph에 위임한다. + +대상 수집, 수정 적용, 중단, 보고 절차는 `.agents/skills/_shared/review-protocol.md`를 +읽고 그대로 따른다. 이 문서는 무엇을 볼지만 정한다. + +추가로 읽을 것: `.agents/rules/security.md` — 프로젝트가 이미 정한 보안 규칙. + +## 분석 렌즈 (extended thinking) + +`security.md`가 정한 규칙의 준수 여부를 변경된 코드에서 확인하고, 그 위에 다음을 본다. + +### 입력 검증 +- 시스템 경계(사용자 입력, 외부 API 응답, 파일, URL 파라미터, 헤더)의 검증 여부. +- SQL/Command Injection, XSS, Path Traversal 등 OWASP Top 10 노출 경로. +- 신뢰할 수 없는 입력의 역직렬화. + +### 인증 및 권한 +- 인증 로직의 우회 가능성. +- 권한 검사가 누락된 엔드포인트나 기능. +- 세션/토큰의 만료, 무효화, 저장 방식. +- 권한 상승 경로. + +### 비밀 정보와 데이터 보호 +- 키/비밀번호/토큰의 하드코딩. +- 민감 정보가 로그, 에러 메시지, 응답 본문, 커밋 히스토리에 노출되는지. +- PII·금융 정보의 암호화/마스킹, 전송 채널(TLS). + +### 의존성 +- 새 의존성의 알려진 CVE, 유지보수 상태, 보안 이력. +- 불필요하게 넓은 권한을 요구하는 의존성. + +### 최소 권한 +- 파일, 네트워크, API 접근이 필요한 최소 범위인지. +- root/admin 권한을 요구하는 구현. +- CORS, CSP 등 브라우저 보안 정책 설정. + +### 에러 처리와 정보 노출 +- 에러 응답의 내부 세부사항(스택 트레이스, DB 스키마, 내부 경로) 노출. +- 에러 처리가 보안 검사를 우회하는 경로를 만드는지. + +## 분류 경계 + +- **즉시 반영**: 보안 취약점, 민감 정보 노출, 인증/권한 우회, 입력 검증 누락. +- **사용자 판단 필요**: 보안-편의성 트레이드오프, 위협 모델에 따라 달라지는 항목, + 대규모 리팩토링이 필요한 보안 개선. +- **무시**: 현재 위협 모델에서 위험도가 낮은 항목, 범위 밖 개선 제안. + +보고 시에는 공통 형식의 각 항목에 **취약점 유형**을 함께 적는다. + +## 추가 중단 기준 + +- 보안 취약점(인증 우회, 권한 상승, 데이터 유출)이 즉시 반영으로 해결되지 않은 경우 +- 민감 정보가 소스 코드나 커밋 히스토리에 포함된 경우 +- 알려진 CVE가 있는 의존성이 추가된 경우 diff --git a/.agents/skills/self-review/SKILL.md b/.agents/skills/self-review/SKILL.md new file mode 100644 index 00000000..08c8a0a9 --- /dev/null +++ b/.agents/skills/self-review/SKILL.md @@ -0,0 +1,51 @@ +--- +name: self-review +description: 구현 완료 후 thinking mode 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다. clean pass까지 반복이 필요하면 ralph를 통해 실행한다 +user-invocable: true +--- + +# Self Review + +구현 완료 후 thinking mode로 변경 사항을 심층 분석하고 타당한 개선을 코드에 반영한다. +반복 실행이 필요하면 `/ralph /self-review`로 ralph에 위임한다. + +대상 수집, 수정 적용, 중단, 보고 절차는 `.agents/skills/_shared/review-protocol.md`를 +읽고 그대로 따른다. 이 문서는 무엇을 볼지만 정한다. + +추가로 읽을 것: 현재 작업의 scope 문서가 있으면 함께 읽어 범위를 확인한다. + +## 분석 렌즈 (extended thinking) + +### 정합성 +- 호출하는 함수, 의존하는 타입, 참조하는 상수가 실제로 존재하고 올바른지. +- 새 인터페이스/타입과 기존 구현체 간 계약이 맞는지. +- import 경로, export 누락, 순환 참조. + +### 로직 +- 분기의 완전성 (switch/if-else). +- 에러 경로에서의 리소스 정리와 상태 롤백. +- 경계 조건 (null, empty, 0, max). +- 비동기 코드의 await 누락, 에러 전파 누락. + +### 설계 정합성 +- `docs/architecture.md`의 계층 책임과 일치하는지. +- `.agents/rules/`의 규칙을 위반하지 않는지. +- scope 문서가 있으면 그 범위 내인지. +- 모듈 간 의존 방향이 설계 의도와 맞는지. +- 문서 간 충돌은 Architecture > Rules > Scope 우선순위로 해소한다. + +### 테스트 충분성 +- 변경된 로직의 주요 경로와 에러 경로에 대응하는 테스트가 있는지. +- 테스트가 구현 세부사항이 아니라 계약/동작을 검증하는지. + +## 분류 경계 + +- **즉시 반영**: 버그, 누락된 에러 처리, 계약 불일치, 명확한 설계 위반. +- **사용자 판단 필요**: 설계 트레이드오프, 의도적일 수 있는 선택, scope 경계 항목. +- **무시**: 이전 리뷰에서 이미 다뤄진 항목, 스타일 선호, 범위 밖 개선 제안. + +## 추가 중단 기준 + +- 심각도 높은 버그(보안, 데이터 손실, 인증/권한 우회)가 즉시 반영으로 해결되지 않은 경우 +- 동일 변경에서 버그가 2개 이상 발견되고 즉시 반영으로 해결되지 않은 경우 +- 설계 원칙 위반이 발견된 경우 diff --git a/.claude/rules b/.claude/rules new file mode 120000 index 00000000..2d5c9a97 --- /dev/null +++ b/.claude/rules @@ -0,0 +1 @@ +../.agents/rules \ No newline at end of file diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 00000000..2b7a412b --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.claude/skills/security-review/SKILL.md b/.claude/skills/security-review/SKILL.md deleted file mode 100644 index 4e7fc6ef..00000000 --- a/.claude/skills/security-review/SKILL.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -name: security-review -description: 구현 완료 후 보안 관점 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다. clean pass까지 반복이 필요하면 ralph를 통해 실행한다 -user-invocable: true ---- - -# Security Review - -구현 완료 후 변경 사항을 보안 관점에서 심층 분석하고, 타당한 보안 개선을 코드에 반영한다. -반복 실행이 필요하면 `/ralph /security-review`로 ralph에 위임한다. - -## 1. 변경 사항 수집 - -- 인자가 전달되면 (`/security-review `) 해당 브랜치를 기준점으로 사용한다. -- 인자가 없으면 아래 절차로 기준점을 결정한다. -- `git fetch`로 remote 상태를 최신화한다. 실패하면 경고를 출력하고 기존 local ref로 진행한다. -- remote tracking branch를 기준점으로 결정한다. - - `git rev-parse --abbrev-ref @{upstream}`로 tracking branch를 확인한다. - - tracking branch가 없으면 `git remote show origin | grep 'HEAD branch'`로 remote 기본 브랜치를 조회하고, `origin/` 형태로 사용한다. - - 위 방법이 모두 실패하면 사용자에게 base branch를 직접 지정하도록 요청한다. -- `git log ..HEAD --oneline`으로 unpushed commit 목록을 확인한다. -- unpushed commit이 없으면 "리뷰 대상 없음"을 보고하고 즉시 종료한다. -- `git diff ..HEAD`로 push되지 않은 커밋의 전체 변경 범위를 파악한다. -- uncommitted 변경이 있으면 사용자에게 알리고, 리뷰 범위에 포함할지 확인한다. 포함하면 `git diff` + `git diff --staged`도 함께 수집한다. 포함하지 않으면 커밋된 변경만 리뷰한다. 리뷰 중 수정 사항을 커밋할 때는 리뷰 수정분만 staging하여 uncommitted 작업이 섞이지 않게 한다. -- 변경된 파일의 전체 내용을 읽는다. diff만으로는 주변 컨텍스트를 놓칠 수 있다. 단, 바이너리 파일, lockfile, 자동 생성물은 제외한다. -- `.claude/rules/security.md`를 읽어 프로젝트의 보안 규칙을 확인한다. - -## 2. 보안 심층 분석 (thinking mode) - -extended thinking을 활용하여 다음을 분석한다: - -### 입력 검증 (Input Validation) -- 시스템 경계(사용자 입력, 외부 API 응답, 파일 읽기, URL 파라미터, 헤더)에서 들어오는 데이터가 검증되는지 확인한다. -- SQL Injection, XSS, Command Injection, Path Traversal 등 OWASP Top 10 취약점에 노출되는 입력 처리가 없는지 확인한다. -- 역직렬화(deserialization) 시 신뢰할 수 없는 입력을 처리하는 경우가 없는지 확인한다. - -### 인증 및 권한 (Authentication & Authorization) -- 인증 로직의 우회 가능성을 확인한다. -- 권한 검사가 누락된 엔드포인트나 기능이 없는지 확인한다. -- 세션/토큰 처리가 안전한지 확인한다 (만료, 무효화, 저장 방식). -- 권한 상승(privilege escalation) 경로가 없는지 확인한다. - -### 비밀 정보 관리 (Secrets Management) -- API 키, 비밀번호, 토큰 등이 소스 코드에 하드코딩되어 있지 않은지 확인한다. -- 민감 정보가 로그, 에러 메시지, 응답 본문에 노출되지 않는지 확인한다. -- `.env`, credentials 파일이 `.gitignore`에 포함되어 있는지 확인한다. -- 민감 데이터가 커밋 히스토리에 포함되지 않았는지 확인한다. - -### 데이터 보호 (Data Protection) -- 민감 데이터(PII, 금융 정보)가 적절히 암호화/마스킹되는지 확인한다. -- 로그에 민감 정보가 출력되지 않는지 확인한다. -- 데이터 전송 시 안전한 채널(TLS 등)을 사용하는지 확인한다. - -### 의존성 보안 (Dependency Security) -- 새로 추가된 의존성에 알려진 취약점(CVE)이 없는지 확인한다. -- 의존성의 유지보수 상태와 보안 이력을 확인한다. -- 불필요하게 넓은 권한을 요구하는 의존성이 없는지 확인한다. - -### 최소 권한 원칙 (Least Privilege) -- 파일, 네트워크, API 접근 권한이 필요한 최소 범위로 제한되어 있는지 확인한다. -- 불필요한 권한 상승(root, admin)을 요구하는 구현이 없는지 확인한다. -- CORS, CSP 등 브라우저 보안 정책이 적절히 설정되어 있는지 확인한다. - -### 에러 처리 및 정보 노출 -- 에러 응답에서 내부 구현 세부사항(스택 트레이스, DB 스키마, 내부 경로)이 노출되지 않는지 확인한다. -- 에러 처리가 보안 검사를 우회하는 경로를 만들지 않는지 확인한다. - -## 3. 평가 기준 - -각 발견 항목을 다음 기준으로 분류한다: - -- **즉시 반영**: 보안 취약점, 민감 정보 노출, 인증/권한 우회, 입력 검증 누락 등 명확한 보안 문제. 코드에 직접 수정을 적용한다. -- **사용자 판단 필요**: 보안-편의성 트레이드오프, 위협 모델에 따라 달라지는 항목, 대규모 리팩토링이 필요한 보안 개선. 수정하지 않고 보고만 한다. -- **무시**: 현재 위협 모델에서 위험도가 낮은 항목, 범위 밖 개선 제안. 개별 내용은 보고하지 않되, 건수와 대표 사유는 보고 형식에 포함한다. - -## 4. 수정 적용 - -- **즉시 반영** 항목은 코드를 직접 수정한다. -- 수정 후 프로젝트의 빌드 검증과 테스트를 실행하여 수정이 다른 것을 깨뜨리지 않았는지 확인한다. -- 테스트가 실패하면 원인을 먼저 분류한다: - - 수정이 원인인 경우: 수정을 되돌리고 **사용자 판단 필요**로 재분류한다. - - 기존 flaky 테스트나 환경 문제인 경우: 수정을 유지하고 실패 원인을 보고한다. -- 수정이 완료되면 수정 사항을 commit한다. - -## 5. 중단 기준 - -다음 중 하나라도 해당하면 push 진행을 중단하고 사용자에게 보고한다: - -- 보안 취약점이 발견되고 즉시 반영으로 해결되지 않은 경우 (인증 우회, 권한 상승, 데이터 유출) -- 민감 정보가 소스 코드나 커밋 히스토리에 포함된 경우 -- 알려진 CVE가 있는 의존성이 추가된 경우 -- 수정 적용 후 빌드 또는 테스트가 실패하고, 단순 수정으로 해결되지 않는 경우 -- 동일한 지적이 수렴하지 않고 계속 발생하는 경우 (순환 판단) - -## 6. 보고 형식 - -리뷰 결과를 사용자에게 다음 형식으로 보고한다: - -### 즉시 반영한 항목 -(각 항목: 파일, 취약점 유형, 변경 내용, 발견 근거. 없으면 `없음`) - -### 사용자 판단이 필요한 항목 -(각 항목: 파일, 취약점 유형, 지적 내용, 판단을 미룬 이유. 없으면 `없음`) - -### 무시한 항목 -(건수와 대표 사유. 없으면 `없음`) - -### 보안 리뷰 요약 -(전체 변경의 보안 상태에 대한 1-2문장 평가) diff --git a/.claude/skills/self-review/SKILL.md b/.claude/skills/self-review/SKILL.md deleted file mode 100644 index ff31102d..00000000 --- a/.claude/skills/self-review/SKILL.md +++ /dev/null @@ -1,95 +0,0 @@ ---- -name: self-review -description: 구현 완료 후 thinking mode 심층 리뷰 — 즉시 반영 항목을 수정하고 결과를 보고한다. clean pass까지 반복이 필요하면 ralph를 통해 실행한다 -user-invocable: true ---- - -# Self Review - -구현 완료 후 thinking mode로 변경 사항을 심층 분석하고 타당한 개선을 코드에 반영한다. -반복 실행이 필요하면 `/ralph /self-review`로 ralph에 위임한다. - -## 1. 변경 사항 수집 - -- 인자가 전달되면 (`/self-review `) 해당 브랜치를 기준점으로 사용한다. -- 인자가 없으면 아래 절차로 기준점을 결정한다. -- `git fetch`로 remote 상태를 최신화한다. 실패하면 경고를 출력하고 기존 local ref로 진행한다. -- remote tracking branch를 기준점으로 결정한다. - - `git rev-parse --abbrev-ref @{upstream}`로 tracking branch를 확인한다. - - tracking branch가 없으면 `git remote show origin | grep 'HEAD branch'`로 remote 기본 브랜치를 조회하고, `origin/` 형태로 사용한다. - - 위 방법이 모두 실패하면 사용자에게 base branch를 직접 지정하도록 요청한다. -- `git log ..HEAD --oneline`으로 unpushed commit 목록을 확인한다. -- unpushed commit이 없으면 "리뷰 대상 없음"을 보고하고 즉시 종료한다. -- `git diff ..HEAD`로 push되지 않은 커밋의 전체 변경 범위를 파악한다. -- uncommitted 변경이 있으면 사용자에게 알리고, 리뷰 범위에 포함할지 확인한다. 포함하면 `git diff` + `git diff --staged`도 함께 수집한다. 포함하지 않으면 커밋된 변경만 리뷰한다. 리뷰 중 수정 사항을 커밋할 때는 리뷰 수정분만 staging하여 uncommitted 작업이 섞이지 않게 한다. -- 변경된 파일의 전체 내용을 읽는다. diff만으로는 주변 컨텍스트를 놓칠 수 있다. 단, 바이너리 파일, lockfile, 자동 생성물은 제외한다. -- 현재 작업의 scope 문서가 있으면 함께 읽어 scope을 확인한다. - -## 2. 심층 분석 (thinking mode) - -extended thinking을 활용하여 다음을 분석한다: - -### 정합성 검증 -- 변경된 코드가 호출하는 함수, 의존하는 타입, 참조하는 상수가 실제로 존재하고 올바른지 확인한다. -- 새로 추가된 인터페이스/타입과 기존 구현체 간의 계약이 맞는지 검증한다. -- import 경로, export 누락, 순환 참조를 확인한다. - -### 로직 검증 -- 분기가 빠짐없이 처리되는지 확인한다 (switch/if-else 분기의 완전성). -- 에러 경로에서 리소스 정리, 상태 롤백이 올바른지 확인한다. -- 경계 조건 (null, empty, 0, max) 처리를 확인한다. -- 비동기 코드에서 await 누락, 에러 전파 누락을 확인한다. - -### 설계 정합성 -- 프로젝트의 architecture 문서가 있으면 계층 책임과 일치하는지 확인한다. -- `.claude/rules/`의 현재 구현 규칙을 위반하지 않는지 확인한다. -- 현재 작업의 scope 문서가 있으면 scope 범위 내인지 확인한다. -- 문서 간 충돌이 있으면 Architecture > Rules > Scope 우선순위를 따른다. -- 모듈 간 의존 방향이 설계 의도와 맞는지 확인한다. - -### 테스트 충분성 -- 변경된 로직의 주요 경로와 에러 경로에 대응하는 테스트가 있는지 확인한다. -- 테스트가 구현 세부사항이 아니라 계약/동작을 검증하는지 확인한다. - -## 3. 평가 기준 - -각 발견 항목을 다음 기준으로 분류한다: - -- **즉시 반영**: 버그, 누락된 에러 처리, 계약 불일치, 명확한 설계 위반. 코드에 직접 수정을 적용한다. -- **사용자 판단 필요**: 설계 트레이드오프, 의도적일 수 있는 선택, scope 경계의 항목. 수정하지 않고 보고만 한다. -- **무시**: 이전 리뷰 단계에서 이미 다뤄진 항목, 스타일 선호, 범위 밖 개선 제안. 개별 내용은 보고하지 않되, 건수와 대표 사유는 보고 형식에 포함한다. - -## 4. 수정 적용 - -- **즉시 반영** 항목은 코드를 직접 수정한다. -- 수정 후 프로젝트의 빌드 검증과 테스트를 실행하여 수정이 다른 것을 깨뜨리지 않았는지 확인한다. -- 테스트가 실패하면 원인을 먼저 분류한다: - - 수정이 원인인 경우: 수정을 되돌리고 **사용자 판단 필요**로 재분류한다. - - 기존 flaky 테스트나 환경 문제인 경우: 수정을 유지하고 실패 원인을 보고한다. -- 수정이 완료되면 수정 사항을 commit한다. - -## 5. 중단 기준 - -다음 중 하나라도 해당하면 push 진행을 중단하고 사용자에게 보고한다: - -- 심각도 높은 버그가 발견되고 즉시 반영으로 해결되지 않은 경우 (보안, 데이터 손실, 인증/권한 우회) -- 동일 변경에서 버그가 2개 이상 발견되고 즉시 반영으로 해결되지 않은 경우 -- 설계 원칙 위반이 발견된 경우 -- 수정 적용 후 빌드 또는 테스트가 실패하고, 단순 수정으로 해결되지 않는 경우 -- 동일한 지적이 수렴하지 않고 계속 발생하는 경우 (순환 판단) - -## 6. 보고 형식 - -리뷰 결과를 사용자에게 다음 형식으로 보고한다: - -### 즉시 반영한 항목 -(각 항목: 파일, 변경 내용, 발견 근거. 없으면 `없음`) - -### 사용자 판단이 필요한 항목 -(각 항목: 파일, 지적 내용, 판단을 미룬 이유. 없으면 `없음`) - -### 무시한 항목 -(건수와 대표 사유. 없으면 `없음`) - -### 리뷰 요약 -(전체 변경 품질에 대한 1-2문장 평가) diff --git a/.gitignore b/.gitignore index d3ecda86..e1c14ede 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ # Vitest run from the repo root drops a cache here /node_modules/ /viewer-ui/tsconfig.tsbuildinfo +# Agent tool scratch state +/.atl/ diff --git a/AGENTS.md b/AGENTS.md index 45f37e7f..3968515e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,17 +1,29 @@ # nightcrow Agent-adjacent Rust TUI: 상단은 git diff/commit log 뷰어, 하단은 split-view 멀티 터미널 패널. -자세한 설계는 `docs/architecture.md`, 사용법은 `README.md`를 참고한다. +설계는 `docs/architecture.md`, 사용법은 `README.md`. -## 구조 +## 에이전트 설정 -- `.claude/rules/` — 개발 규칙 (코드 품질, 테스트, 보안, 커밋, 문서, 의존성, 토큰 절약). 모든 작업에 항상 적용된다. -- `.claude/skills/` — `/plan`, `/self-review`, `/security-review` +원본은 `.agents/`에 두고 도구별 디렉터리는 symlink만 둔다 (`.claude/rules`, +`.claude/skills` → `../.agents/...`). 새 도구를 붙일 때도 복사하지 말고 링크한다. +Windows에서 링크를 체크아웃하려면 개발자 모드 + `git config core.symlinks true`가 +필요하고, 없으면 링크가 경로 문자열이 담긴 일반 파일로 풀린다. + +- `.agents/rules/` — 항상 적용되는 개발 규칙. 무엇을 지킬지는 각 파일이 정한다. +- `.agents/skills/` — `/plan`, `/self-review`, `/security-review`. 각 스킬의 절차는 + 해당 `SKILL.md`가 정하므로 이 문서에 옮겨 적지 않는다. +- `.agents/skills/_shared/` — 스킬이 공유하는 절차 문서. ## 개발 흐름 -1. **Plan** — 변경이 단순하지 않으면(3개 이상 파일 수정 또는 설계 판단 필요) `/plan`으로 요구사항/대안/구현 계획을 정리하고 사용자와 정렬한 뒤 구현한다. 단순한 버그 수정·설정 변경은 바로 구현한다. -2. **Implement** — `docs/architecture.md`의 설계 제약을 따른다. 구현이 문서와 어긋나면 문서를 먼저 갱신하거나 구현을 조정한다. -3. **Verify** — `cargo build`, `cargo test`, `cargo clippy --all-targets --all-features -- -D warnings`가 통과해야 한다. `.githooks/pre-commit`(`git config core.hooksPath .githooks`로 활성화)이 커밋 전 동일 게이트를 실행한다. -4. **Review** — 구현 완료 후 `/self-review`로 자체 점검하고, 인증/보안/공개 API 등 민감한 변경이면 `/security-review`도 실행한다. 즉시 반영 항목은 코드에 반영하고, 사용자 판단이 필요한 항목은 보고한다. -5. **Commit** — `commits.md`의 단위/메시지 규칙을 따른다. push는 사용자가 직접 결정한다. \ No newline at end of file +1. **Plan** — 변경이 단순하지 않으면 `/plan`으로 사용자와 정렬한 뒤 구현한다. + 단순한 버그 수정·설정 변경은 바로 구현한다. +2. **Implement** — `docs/architecture.md`의 설계 제약을 따른다. 구현이 문서와 어긋나면 + 문서를 먼저 갱신하거나 구현을 조정한다. +3. **Verify** — `cargo build`, `cargo test`, + `cargo clippy --all-targets --all-features -- -D warnings`가 통과해야 한다. + `.githooks/pre-commit`(`git config core.hooksPath .githooks`)이 커밋 전 동일 게이트를 실행한다. +4. **Review** — `/self-review`로 자체 점검하고, 인증/보안/공개 API 등 민감한 변경이면 + `/security-review`도 실행한다. +5. **Commit** — `.agents/rules/commits.md`를 따른다. push는 사용자가 결정한다. diff --git a/Cargo.lock b/Cargo.lock index 299646bd..4debb89e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1265,6 +1265,17 @@ dependencies = [ "two-face", ] +[[package]] +name = "nightcrow-recovery" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "serde", + "serde_json", + "tempfile", +] + [[package]] name = "nix" version = "0.28.0" diff --git a/Cargo.toml b/Cargo.toml index cc0f3199..20a4b403 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,9 @@ +# The root package is also the workspace root: `plugins/*` are separate builds +# that speak the plugin protocol over a pipe, never link against nightcrow, and +# are excluded from the published crate. +[workspace] +members = [".", "plugins/nightcrow-recovery"] + [package] name = "nightcrow" version = "0.1.0" @@ -21,6 +27,7 @@ exclude = [ "viewer-ui/package-lock.json", "viewer-ui/tsconfig.json", "viewer-ui/vite.config.ts", + "plugins", ".claude", ".github", ".githooks", diff --git a/README.md b/README.md index f677fa67..c0a42849 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ visible from the terminal pane. | ` w` | Close active terminal pane — terminal focus only, since without it no pane is highlighted as the close target | | ` s` then `3`…`9`,`0` | Swap the active terminal pane with pane 1…8 (focus follows the pane; same pane numbering as the jump keys, so in terminal fullscreen the swap digits are `1`…`8`) — terminal focus only, like `w`, and needs at least two panes | | ` z` | Resize this project's terminal panes to fit this screen. The session gives the sizing to whichever client attached most recently — a PTY has one size, and a program drawing on an alternate screen cannot be re-flowed afterwards — so while another client (a second terminal, or the browser) holds it, this one renders that grid: padded if it is smaller than the pane, cropped if larger. Advertised in the hint bar only while that is the case | +| ` c` | Give up on the recovery a plugin has pending for a pane — the held slot is released, so nothing can be relaunched into it, and every attached client is told. Targets the focused pane's recovery, or the pane whose process has already ended while its slot was being held (that pane has no tab to focus). Advertised in the hint bar only while something is actually pending | | ` l` | Toggle between status view and commit log view | | ` b` | Toggle the read-only file-tree view (returns to status view) | | ` f` | Fullscreen the focused pane. For the terminal it cycles `off → grid (all panes) → zoom (active pane only) → off`; with a single pane it toggles straight off/on. File list and diff viewer toggle off/on | @@ -310,6 +311,68 @@ A bare `nightcrow` reopens those tabs and lands on the one that was in front, wi The two halves have two owners. The session writes which repositories are open and which tab is in front; an attached client writes what it had selected and scrolled, and never the tab list — detaching must not roll the session back to one client's view of it. To start empty, close every tab before stopping the session. +## Plugins + +nightcrow itself knows nothing about the CLIs you run in its panes — an agent +and a person get the same PTY. Behaviour that *does* need to know a particular +tool lives in a plugin: a separate executable that nightcrow launches and talks +to over a pipe. Plugins are off unless you turn one on, and one can only ever +see a pane you handed it by name. + +The bundled plugin is `nightcrow-recovery`. When an opted-in pane's CLI hits its +usage limit, it waits for the reset time the provider reported and then re-opens +that exact session. It only waits — it does not bypass, raise, or work around +any provider limit, and it sends nothing while a limit is in effect. Claude +Code, Codex CLI, and OpenCode are supported; OpenCode is only ever observed, +never interrupted, because it retries on its own. + +```bash +cargo build --release -p nightcrow-recovery +nightcrow plugin install target/release/nightcrow-recovery --name recovery +nightcrow plugin list # what is installed, and how config refers to it +nightcrow plugin remove recovery +``` + +`install` prints the exact `[[plugin]]` block to paste, using whatever `--name` +you chose — that name is what a pane opts in with, so keep the two in step. + +Installing only puts the binary in `~/.nightcrow/plugins`. It stays inert until +you edit `~/.nightcrow/config.toml` yourself — enabling something that can type +into a terminal should be a change you read before it takes effect: + +```toml +[[plugin]] +name = "recovery" +command = "nightcrow-recovery" +enabled = true +# Flags the plugin may append to re-open a session. Empty by default, which +# refuses every relaunch. nightcrow cannot know what a CLI's flags mean, so it +# will not add one you did not list — that is what keeps a plugin from changing +# how a CLI asks for your approval. +allowed_resume_flags = ["--resume", "resume", "--session"] + +[[startup_command]] +name = "Claude" +command = "claude" +plugin = "recovery" # without this line, no plugin ever sees this pane +``` + +For Claude Code, let the plugin install its hook and statusline entries so it +can read the exact session id and reset time instead of guessing from what is +printed on screen. It merges into your existing `~/.claude/settings.json` and +backs it up first: + +```bash +nightcrow-recovery install-hooks +nightcrow-recovery uninstall-hooks # removes only what it added +``` + +A pane that is waiting shows its state and deadline on its tab. Cancel it with +`` then the recovery key (see [Leader commands](#leader-commands-press-prefix-then-the-key)), +or from the web viewer; typing into the pane yourself also cancels it. + +Design and trust boundary: [`docs/architecture.md`](docs/architecture.md) → "Plugin Host". + ## Web viewer A browser surface that renders the same git data as a native web page — @@ -545,9 +608,25 @@ live_watch = true # watch expanded dirs and refresh the tree live; set f [[startup_command]] name = "Claude" # optional tab label; falls back to the command text command = "claude" # required; must not be empty +plugin = "recovery" # optional; names the [[plugin]] allowed to act on this + # pane. Omitted — the default — means no plugin ever + # sees it. [[startup_command]] command = "cargo test --watch" + +# External plugin processes — see "Plugins" above. Up to 8 entries, names unique. +# Nothing runs unless an entry exists AND enabled = true AND a pane opted in. +[[plugin]] +name = "recovery" # the name panes opt in with +command = "nightcrow-recovery" # found on PATH or in ~/.nightcrow/plugins +args = [] # passed to the plugin verbatim +enabled = false # off by default +allowed_resume_flags = [] # flags the plugin may append to re-open a + # session; empty refuses every relaunch + +[plugin.env] # plugin process only, never terminal panes +NIGHTCROW_RECOVERY_LOG = "info" ``` ## License diff --git a/config.example.toml b/config.example.toml index 28b0f9cd..be248403 100644 --- a/config.example.toml +++ b/config.example.toml @@ -93,6 +93,51 @@ live_watch = true # watch expanded dirs and refresh the tree live; set f # # [[startup_command]] # command = "cargo test --watch" +# +# A pane may hand itself to a plugin (see [[plugin]] below) by naming one. Only +# panes that name a plugin are ever visible to it — everything else, including +# your plain shells, stays untouched: +# +# [[startup_command]] +# name = "Claude" +# command = "claude" +# plugin = "recovery" # optional; omitted means no plugin sees this pane + +# External plugin processes. nightcrow launches each enabled entry and speaks +# its protocol; it knows nothing about what the plugin is for, so the same +# section fits any provider or tool. +# +# Nothing here runs unless you both add an entry AND set enabled = true, and a +# plugin can only act on panes whose [[startup_command]] named it. +# +# The bundled "recovery" plugin watches an opted-in pane for its provider's +# usage-limit message, waits for the stated reset time to pass, and then resumes +# the pane. It only waits — it does not bypass, raise, or work around any +# provider limit, and it sends nothing while a limit is in effect. +# +# Up to 8 entries; names must be unique, since that name is what a pane opts in +# with. `env` applies to the plugin process only, never to terminal panes. +# +# To let a plugin re-open an exited pane's session you must also list the flags +# it may append. nightcrow does not know what any CLI's flags mean, so it will +# not invent that permission: a flag you have not listed here cannot reach the +# pane's command line. That is what stops a plugin from quietly changing how a +# CLI asks for your approval — do not list a flag that weakens it. +# Leave it empty (the default) and the plugin can still wait and type into a +# live pane, but never relaunch one. +# +# [[plugin]] +# name = "recovery" # required; the name panes opt in with +# command = "nightcrow-recovery" # required; found on PATH or in the plugin dir +# args = [] # passed to the plugin verbatim +# enabled = false # off by default; set true to actually run it +# allowed_resume_flags = ["--resume", "resume", "--session"] +# # empty by default, which refuses relaunches. +# # These three are what the bundled recovery +# # plugin needs for Claude/Codex/OpenCode. +# +# [plugin.env] +# NIGHTCROW_RECOVERY_LOG = "info" # The browser surface: renders git data as a real web page and serves the # session's terminals. Always on — it is part of the session, not an add-on — diff --git a/docs/architecture.md b/docs/architecture.md index ca4eef65..fe8fcee6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,6 +8,9 @@ nightcrow는 **세션 데몬 하나 + 프론트엔드 N개** 구조의 agent-adj 화면은 상단 패널에서 git diff를 실시간 추적하고, 하단 패널에서 임의의 프로세스(주로 LLM CLI나 빌드/테스트 러너)를 동시에 실행한다. 전환 과정과 남은 단계는 `docs/session-daemon-plan.md`를 참고한다. nightcrow 자체는 AI에 대한 ontology를 갖지 않는다 — agent든 사람이든 동일한 PTY와 파일 mtime을 본다. +provider를 아는 동작(예: rate limit이 풀릴 때까지 기다렸다 세션을 재개하는 것)이 필요하면 코어가 아니라 +**plugin**이 갖는다. 코어는 pane을 외부 프로세스에 보여주고 그 프로세스가 요청한 것을 검증할 뿐, +어떤 CLI가 무엇을 출력하는지는 끝까지 모른다 — `### Plugin Host` 참고. **대상 사용자**: 터미널 중심으로 작업하면서, 옆 패널의 LLM CLI(Claude Code, Codex, aider 등)나 빌드/테스트 러너가 만든 코드 변경을 실시간으로 따라잡고 싶은 개발자. @@ -54,7 +57,7 @@ below for the layout and resize rules. ## Module Structure -모든 소스 파일은 300줄 이하(LOC 규칙, `.claude/rules/guardrails.md` 참고). 테스트는 +모든 소스 파일은 300줄 이하(LOC 규칙, `.agents/rules/guardrails.md` 참고). 테스트는 `#[cfg(test)] mod tests;`로 별도 파일에 분리한다. ``` @@ -155,7 +158,18 @@ src/ │ └── tests/ # ui integration tests (chrome, hint, hit-test, notice) ├── backend/ │ ├── mod.rs # TerminalBackend trait + BackendEvent -│ └── pty.rs # PtyBackend (portable-pty, the only backend) +│ ├── identity.rs # PaneToken / PaneGeneration: a pane slot's name outside this process +│ ├── slot.rs # per-slot bookkeeping (launch, idle clock) + resume arg validation +│ ├── pty.rs # PtyBackend (portable-pty, the only backend) +│ └── pty_spawn.rs # open_pane / relaunch_pane: the spawn path and env injection +├── plugin/ # provider-agnostic plugin host — see "Plugin Host" +│ ├── mod.rs # module root; states the trust posture +│ ├── protocol.rs # NDJSON wire contract (events out, commands in) +│ ├── host.rs # one long-lived child per plugin +│ ├── host_pump.rs # stdin/stdout/stderr pump threads + capped line reader +│ ├── guard.rs # the trust boundary: PluginCommand -> Approved | Refused +│ ├── guard_budget.rs # per-slot rate ceilings, keyed by PaneToken +│ └── registry.rs # ~/.nightcrow/plugins: install / list / remove ├── git/ │ ├── mod.rs │ ├── diff.rs # module root: pub use re-exports, MAX_FILE_VIEW_BYTES @@ -502,7 +516,7 @@ background even while scrolled out of the window. 라우팅은 leader(prefix) 모델을 따른다. 1순위 사용자는 패널에서 LLM CLI를 굴리는 cockpit 사용자이므로, `Ctrl+W`/`Ctrl+L` 같은 프롬프트 편집 Ctrl 키가 nightcrow에 가로채이지 않고 PTY로 통과해야 한다. 앱 전역 명령은 leader 뒤에 한 키를 눌러야만 실행된다. -- **Leader (prefix)**: 기본값 `Ctrl+F`, `[input] leader`로 변경 가능(`config.rs::parse_leader`가 `ctrl+`만 허용하고 예약키·인코딩 불가 chord는 거부). leader를 누르면 `App.prefix_armed` 플래그가 켜지고, 다음 키 한 개가 앱 명령(`input::prefix_action`)으로 해석된다. **타임아웃은 없다** — armed 상태는 follow-up 키나 `Esc`/`Ctrl+C`로만 해제된다. 해제 경로는 셋뿐이다: 매핑된 키 → Action 실행 후 해제, 미매핑 키 → 소비 후 해제, `Esc`/`Ctrl+C` → 취소. ` `는 terminal focus에서 leader를 `encode_key`로 리터럴 PTY 전송한다. prefix 매핑: `t`=NewPane, `w`=ClosePane(terminal focus 한정 — unfocus 시 active pane이 다른 pane과 동일하게 그려져 닫힐 대상이 보이지 않으므로, 키는 소비하되 no-op이고 힌트 바에도 노출하지 않는다), `s`=pane swap 대기 모드 arm(같은 terminal-focus 스코프 + pane 2개 이상 필요 — 상세는 "Split-View Terminal Panel"의 swap 항목), `l`=ToggleLogView, `b`=ToggleTreeView(트리 뷰 ↔ status 뷰), `f`=ToggleFullscreen, `o`=OpenProject(저장소를 새 프로젝트 탭으로 — 제자리 교체 명령은 없다), `x`=CloseProject, `p`=CycleTheme, `r`=Redraw, `q`=Quit. 숫자는 지금 body가 보여주는 것을 지시한다: `1`=FocusList, `2`=FocusDiff, `3`–`9`,`0`=pane 0–7로 focus 이동(`0`은 digit이 9까지뿐이라 8번째 pane을 가리킨다). bare F키는 별개 축이며 프로젝트 탭을 고르므로 이 digit들과 충돌하지 않고, 서로 자리를 비워줄 필요도 없다. pane 포커스 이동은 tab 전환이 아니라 어떤 pane이 active인지만 바꾼다 — split-view grid는 이동 전후로 계속 여러 pane을 동시에 그린다. +- **Leader (prefix)**: 기본값 `Ctrl+F`, `[input] leader`로 변경 가능(`config.rs::parse_leader`가 `ctrl+`만 허용하고 예약키·인코딩 불가 chord는 거부). leader를 누르면 `App.prefix_armed` 플래그가 켜지고, 다음 키 한 개가 앱 명령(`input::prefix_action`)으로 해석된다. **타임아웃은 없다** — armed 상태는 follow-up 키나 `Esc`/`Ctrl+C`로만 해제된다. 해제 경로는 셋뿐이다: 매핑된 키 → Action 실행 후 해제, 미매핑 키 → 소비 후 해제, `Esc`/`Ctrl+C` → 취소. ` `는 terminal focus에서 leader를 `encode_key`로 리터럴 PTY 전송한다. prefix 매핑: `t`=NewPane, `w`=ClosePane(terminal focus 한정 — unfocus 시 active pane이 다른 pane과 동일하게 그려져 닫힐 대상이 보이지 않으므로, 키는 소비하되 no-op이고 힌트 바에도 노출하지 않는다), `s`=pane swap 대기 모드 arm(같은 terminal-focus 스코프 + pane 2개 이상 필요 — 상세는 "Split-View Terminal Panel"의 swap 항목), `c`=CancelRecovery(plugin이 대기 중인 pane recovery를 포기 — 대기 중인 것이 있을 때만 힌트에 노출된다), `l`=ToggleLogView, `b`=ToggleTreeView(트리 뷰 ↔ status 뷰), `f`=ToggleFullscreen, `o`=OpenProject(저장소를 새 프로젝트 탭으로 — 제자리 교체 명령은 없다), `x`=CloseProject, `p`=CycleTheme, `r`=Redraw, `q`=Quit. 숫자는 지금 body가 보여주는 것을 지시한다: `1`=FocusList, `2`=FocusDiff, `3`–`9`,`0`=pane 0–7로 focus 이동(`0`은 digit이 9까지뿐이라 8번째 pane을 가리킨다). bare F키는 별개 축이며 프로젝트 탭을 고르므로 이 digit들과 충돌하지 않고, 서로 자리를 비워줄 필요도 없다. pane 포커스 이동은 tab 전환이 아니라 어떤 pane이 active인지만 바꾼다 — split-view grid는 이동 전후로 계속 여러 pane을 동시에 그린다. - **No-prefix 예약키**: `F1`–`F10`(프로젝트 탭 1–10 전환 — layout에 따라 바뀌지 않는 유일한 점프 축), `Shift+←/→`(focus cycle — terminal focus 상태에서는 active pane을 앞/뒤로 이동), `Shift+↑/↓`·`Shift+PgUp/PgDn`(터미널 스크롤, active pane 기준 — 전달 방식은 "Scroll Routing" 참조)는 leader 없이 항상 앱이 먼저 처리한다. modifier 또는 F-key라서 프롬프트 텍스트와 혼동되지 않는다. - **Upper panel focused**: leader 명령과 no-prefix 예약키를 제외한 나머지는 로컬 네비게이션(`j`/`k`, `/`, `v`, `n`/`N`, `Enter`, `Esc`, 화살표, `PgUp`/`PgDn`)으로 처리된다. `j`/`k`는 upper-pane handler 내부에서 vim navigation으로 변환되며, `map_key`는 plain character로 통과시켜 terminal focus에서 PTY로 그대로 전달되게 한다. - **Lower panel focused (terminal)**: leader/예약키가 아닌 모든 키는 active backend의 stdin으로 직접 통과한다(`encode_key`가 화살표/F-key/제어문자를 VT100 시퀀스로 인코딩). 단독 `Ctrl+T/W/L/O/P/Q` 등은 앱 명령이 아니므로 control byte로 PTY에 전달된다(리더 `Ctrl+F`만 prefix를 arm하고 통과하지 않는다). bare F키는 앱이 가로채므로 pane 안 프로그램(htop, mc 등)의 F키 메뉴는 동작하지 않는다 — 수정자를 붙인 `Ctrl+F1`, `Shift+F5` 등은 통과한다. @@ -649,7 +663,7 @@ worker, commit-log fetch, PTY당 reader/wait 쌍). 60개 자체는 문제가 아 ### Notice Row -힌트 바 바로 위 한 행. 평상시에는 `ui::mod::render_repo_header`가 repo 경로(`~/...` 형식으로 home-relative 표기), 현재 브랜치, upstream tracking 상태(`↑N ↓M`)를 노출한다. 브랜치/추적 정보는 snapshot worker가 채워주고, detached HEAD/unborn branch처럼 값이 없으면 해당 칩만 생략한다. +힌트 바 바로 위 한 행. 평상시에는 `ui::mod::render_repo_header`가 repo 경로(`~/...` 형식으로 home-relative 표기), 현재 브랜치, upstream tracking 상태(`↑N ↓M`)를 노출한다. 브랜치/추적 정보는 snapshot worker가 채워주고, detached HEAD/unborn branch처럼 값이 없으면 해당 칩만 생략한다. 마지막 칩은 plugin이 보고한 pane recovery(state·deadline·attempt·detail)이며, 대기 중인 것이 있을 때만 나타난다 — 자세한 내용은 "Recovery Surface" 참고. **알림(`App::notice`)이 올라오면 이 행을 덮는다.** 전용 행을 따로 만들지 않은 이유는 알림이 뜨고 사라질 때마다 body가 한 행씩 줄었다 늘어나면서 **열려 있는 모든 PTY가 리사이즈**되기 때문이다(전체화면 프로그램이 매번 다시 그려진다). 이 행의 내용은 매 프레임 `App`에서 다시 계산되는 ambient 정보라 잠시 덮어도 잃는 것이 없다 — 반대로 아래 hint bar는 사용자가 편집 중인 repo 입력 텍스트를 담고 있어 덮으면 안 된다. @@ -709,6 +723,95 @@ hint bar는 오버레이(repo 입력·prefix armed·swap target)가 열리면 snapshot worker는 매 폴 사이클마다 현재 HEAD oid를 함께 보고한다. UI 스레드는 `poll_snapshot`에서 oid 변동을 감지하면 `refresh_commit_log_after_head_change`로 commit log와 drill-down 상태를 동일 oid 기준으로 재정렬해, 터미널에서 새 커밋·amend·force-push·브랜치 전환이 일어났을 때도 로그 뷰가 즉시 따라잡는다. +### Plugin Host (`src/plugin/`, `plugins/`) + +어떤 CLI가 사용량 한도에 걸렸는지 알아보고 한도가 풀린 뒤 세션을 재개하는 일은 provider를 +아는 동작이다. `## Overview`가 못박은 대로 코어는 그런 ontology를 갖지 않으므로, 그 지식은 +**별도 프로세스로 분리한다**. 코어에는 provider를 모르는 host만 두고, Claude Code / Codex / +OpenCode를 아는 코드는 `plugins/nightcrow-recovery`에 산다. 코어 `src/plugin/` 어디에도 그 +세 이름은 나오지 않으며, 그것이 이 경계가 지켜지고 있다는 검사 가능한 조건이다. + +**이 기능은 provider의 한도를 우회하지 않는다.** 하는 일은 사람이 손으로 하던 것 — +한도가 풀릴 시각까지 기다렸다가 같은 세션을 다시 열는 것 — 을 대신하는 것뿐이다. +한도를 늘리거나 회피하거나 감지를 피하는 경로는 없고, 있어서도 안 된다. + +- **왜 자식 프로세스 + NDJSON인가**: Rust에는 안정 ABI가 없어 `libloading` 기반 dylib plugin은 + 버전이 어긋나는 순간 UB다. cargo feature 게이트는 재컴파일을 요구하므로 "설치·제거 가능"이 + 아니다. 남는 것은 프로세스 경계이고, 그 편이 신뢰 모델도 정직하다 — plugin은 우리 주소 공간에 + 없다. 프레이밍은 stdin/stdout의 개행 구분 JSON이고 버전(`v`)이 맞지 않는 줄은 거부한다. +- **opt-in이 곧 도달 범위**: plugin은 `[[startup_command]]`이 `plugin = "이름"`으로 지목한 pane만 + 본다. 클라이언트가 직접 연 pane에는 연결이 생기지 않는다 — 임의의 셸 pane이 자동으로 감지되고 + 조작되는 일은 구조적으로 불가능하다. `[[plugin]]`은 `enabled = false`가 기본이다. +- **신뢰 경계는 `guard.rs` 하나다**: `protocol::decode_command`는 모양과 크기만 본다. 권한은 + `Guard::judge`만 판단하고, plugin이 우회할 경로가 없다. 규칙: pane이 존재하고 opt-in했는가, + `generation`이 현재와 같은가(이것이 교체된 프로세스에 대한 결정이 후임에게 닿는 것을 막는다), + 살아 있고 조용할 때만 입력을 넣는가, 죽었을 때만 relaunch하는가, 제어문자가 섞이지 않았는가, + slot당 횟수 상한 안인가. 거부는 로그로 남고 재시도되지 않는다. +- **`PaneToken`이 정체성인 이유**: `PaneId`는 backend별 카운터라 backend가 다시 만들어지면 1로 + 돌아간다. 프로세스 밖에서 pane을 가리키기에 부적합하고, cwd도 답이 못 된다 — 한 저장소에 + 여러 pane을 두는 것이 지원되는 레이아웃이다. 그래서 난수 토큰을 spawn 시각에 자식 환경 + (`NIGHTCROW_PANE_TOKEN`)으로 넣는다. provider가 띄우는 hook/statusline 자식들이 이를 상속하므로, + plugin은 어떤 pane에서 온 사건인지 추측 없이 안다. +- **횟수 상한은 slot(토큰) 기준으로 센다**: relaunch는 반드시 새 `PaneId`를 만든다. 상한을 id로 + 세면 relaunch마다 예산이 새로 생겨서, 즉시 끝나는 명령과 매 종료마다 relaunch하는 plugin이 + 만나면 상한에 영원히 닿지 않는다. 토큰은 relaunch를 건너 살아남는 유일한 값이라 상한이 + 붙어야 하는 곳이다. +- **relaunch는 같은 id를 되살리지 않는다**: id는 단조 증가하고 모든 클라이언트가 `Exited`를 + 그 id의 종결로 취급한다. 그래서 교체는 새 id로 태어나되 토큰을 물려받고 generation이 오른다. + 레이아웃은 새 pane을 원래 인덱스에 넣고 기존 `Reordered`를 브로드캐스트해 보존한다 — 와이어 + 포맷에 relaunch 전용 메시지를 추가하지 않는다. +- **프로세스 해제와 slot 폐기를 분리한다**: 한도 대기는 몇 시간일 수 있다. 죽은 자식의 fd와 + 스레드를 그 시간 내내 붙잡고 토큰만 보존하는 것은 낭비이므로, `release_process`는 PTY를 놓고 + slot만 남긴다. 아무도 relaunch하지 않으면 `PENDING_RELAUNCH_TTL`에 slot을 폐기한다. +- **권한 인자는 사용자가 선언한다**: relaunch가 덧붙일 수 있는 플래그는 `[[plugin]]`의 + `allowed_resume_flags`뿐이고 기본은 빈 목록이다. 코어가 특정 CLI의 위험 플래그 이름을 + 하드코딩하는 대안은 곧 코어가 provider를 아는 것이라 택하지 않았다. 인자는 셸 메타문자를 + 거부한 뒤 개별로 quote되며, 원래 명령 문자열은 수정되지 않는다(다음 relaunch가 인자를 + 누적하지 않도록 보존되는 것도 원래 명령이다). +- **관측 부담을 지지 않는 쪽으로**: 출력 텍스트는 chunk 단위로 escape를 벗겨 넘기므로 두 read에 + 걸친 escape는 완전히 제거되지 않는다. 이것이 허용되는 이유는 출력 텍스트가 언제나 fallback + 신호일 뿐이라는 것이다 — Claude는 hook과 statusline, Codex는 rollout JSONL, OpenCode는 로컬 + 서버의 세션 상태가 1차 신호다. +- **OpenCode에는 개입하지 않는다**: 자체 재시도가 상한 없이 계속되므로 "재시도 소진"을 기다리는 + 설계가 성립하지 않는다. 프로세스가 끝났거나 상태가 `idle`로 바뀐 뒤에만 손을 댄다. +- **와이어 계약이 두 벌 있다**: plugin은 독립 빌드라 `plugins/nightcrow-recovery`가 프로토콜 + 타입을 따로 갖는다. `PROTOCOL_VERSION`을 진짜 주장으로 만들려면 그래야 하고, 양쪽 모두 JSON + 모양을 리터럴로 고정한 테스트가 있어 드리프트는 테스트 실패로 나타난다. + +#### Recovery Surface (사람이 보고 취소하는 쪽) + +plugin의 `status` 보고는 `ServerMessage::Recovery { pane, state, detail?, deadline_epoch?, +attempt }`로 모든 클라이언트에 브로드캐스트되고, 사람은 `ClientMessage::CancelRecovery { pane }`로 +되돌려 준다. 설계 결정은 다음과 같다. + +- **hub는 보고를 보관하지 않는다**: 도착한 그대로 브로드캐스트하고 잊는다. hub가 소유하는 것은 + hold(exited pane의 slot)뿐이고, 사람이 빼앗을 수 있는 것도 그것뿐이다. 따라서 표시 상태는 + 클라이언트가 최신 보고를 들고 있는 것으로 성립한다. +- **`state`는 해석하지 않는다**: plugin이 고른 짧은 문자열이며 코어는 뜻을 모른다. 유일한 예외가 + hub 자신이 보내는 `"cancelled"`(`hub_recovery::RECOVERY_CANCELLED`)이고, 클라이언트는 이것을 + "이 pane에 더는 대기 중인 것이 없다"로 읽어 엔트리를 **지운다**. +- **hold가 끝나는 모든 경로가 `cancelled`를 보낸다**: 취소, TTL 만료, relaunch 성공, 명시적 + close. 하나라도 빠지면 클라이언트에 지나간 deadline이 영구히 남는다. +- **취소는 hold를 근거로 판정한다**: `claim_pending`이 비면 아무 일도 하지 않는다(에러가 아니다 — + 클라이언트는 만료보다 한 박자 늦을 수 있다). hold가 있으면 `pane_closed` → `Plugins::forget` → + `retire_slot` 순서다. `forget`이 slot의 토큰으로 예산을 지우므로 `retire_slot`보다 앞이어야 한다. +- **TUI는 행을 추가하지 않는다**: 표시는 (1) pane 탭 라벨의 짧은 마커(`⏳17:45` / `⚠3`, + `ui/terminal_tab/recovery.rs`)와 (2) notice row 마지막 칩(state·deadline·attempt·detail, + `ui/notice.rs`)뿐이다. 전용 행이나 오버레이를 만들지 않은 이유는 "Layout"·"Notice Row"와 + 같다 — 행이 생겼다 사라지면 열려 있는 모든 PTY가 리사이즈된다. 좁은 pane에서는 제목이 + 먼저 잘리고 마커가 남는다(`RECOVERY_TITLE_MAX_CHARS`). +- **취소 키는 leader 뒤에 있다**: ` c`. bare 키는 pane 안 프로그램의 것이라는 "Keyboard + Routing" 규칙 그대로이며, 대기 중인 것이 있을 때만 힌트에 노출된다. +- **탭이 없는 pane도 가리킬 수 있어야 한다**: 프로세스가 끝나고 slot만 남은 pane은 클라이언트의 + pane 목록에 없다. 그래서 표시·취소 대상은 "focus된 pane의 보고, 없으면 목록에 없는 pane의 + 보고(가장 낮은 id)"로 정의된다(`TerminalState::recovery_focus`, 웹은 + `lib/recovery.ts::orphanRecovery`). 웹에서는 그런 보고가 pane 셀 대신 패널 툴바에 뜬다. +- **deadline은 절대 추측하지 않는다**: `deadline_epoch`가 없으면 시각을 아무것도 그리지 않는다. + 틀린 벽시계 시각은 사실처럼 읽힌다. TUI는 날짜 크레이트 없이 `libc::localtime_r`로 `HH:MM`만 + 만들고(`ui/wall_clock.rs`), unix가 아닌 플랫폼에서는 UTC로 떨어진다. +- **터미널 렌더링과 결합하지 않는다**: 화면 내용이 아니라 pane 메타데이터이므로 emulator/xterm + 경로에 닿지 않는다. TUI는 `TerminalState.recovery` 맵, 웹은 컨트롤 프레임에서 파생된 상태다. + ### 공용 웹 계층 (`src/web/common/`) 인증·HTTP 프레이밍·SSE·연결 회계는 뷰어가 무엇을 서빙하는지와 무관한 프리미티브라 diff --git a/plugins/nightcrow-recovery/Cargo.toml b/plugins/nightcrow-recovery/Cargo.toml new file mode 100644 index 00000000..4fa73e16 --- /dev/null +++ b/plugins/nightcrow-recovery/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nightcrow-recovery" +version = "0.1.0" +edition = "2024" +description = "nightcrow plugin: notices a coding CLI hit its usage limit and resumes it when the limit resets" +license = "Apache-2.0" +publish = false + +[dependencies] +# Deliberately the same versions the host uses, and nothing more: this process +# reads lines, parses JSON and waits on a clock, so an async runtime or an HTTP +# client would be weight with no purpose. +anyhow = "1" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +tempfile = "3" diff --git a/plugins/nightcrow-recovery/src/helper.rs b/plugins/nightcrow-recovery/src/helper.rs new file mode 100644 index 00000000..fd66b7cd --- /dev/null +++ b/plugins/nightcrow-recovery/src/helper.rs @@ -0,0 +1,143 @@ +//! The two modes a provider CLI invokes, not a human. +//! +//! Both run inside a child of the provider's own process, on that process's +//! critical path: Claude Code runs the statusline command on a refresh interval +//! and the hook command as a turn ends. So both do the least possible work — +//! read one JSON object, forward a handful of whitelisted fields, exit — and +//! neither ever reports a failure to its caller. A recovery plugin that is not +//! running must look exactly like one that was never installed. +//! +//! Whitelisting is the privacy boundary. A `StopFailure` payload names a +//! transcript file and carries a provider's own error prose; a statusline payload +//! carries whatever else the provider decided to include. Only the fields the +//! state machine actually reads cross the socket, so nothing else can be +//! accidentally logged, buffered, or written down later. + +use crate::ipc::{IpcMessage, send, socket_path}; +use crate::protocol::PANE_TOKEN_ENV; +use crate::provider::SignalKind; +use serde_json::{Map, Value}; +use std::io::Read; +use std::process::ExitCode; + +/// Most stdin a helper will read. +/// +/// A hook payload is a handful of short strings plus an error message; 64 KiB is +/// far past any of that and keeps a provider that streams into us from making +/// this process grow. +const MAX_HELPER_STDIN_BYTES: u64 = 64 * 1024; + +/// Fields of a `StopFailure` payload the state machine reads. Everything else — +/// `error_message`, `transcript_path`, `prompt_id`, `cwd` — stays in the +/// provider's process. +const STOP_FAILURE_FIELDS: [&str; 3] = ["session_id", "error_type", "hook_event_name"]; + +/// The only statusline field this plugin wants. +const RATE_LIMITS_FIELD: &str = "rate_limits"; + +/// Shown when the statusline payload carries no usage numbers — which is normal: +/// `rate_limits` is absent for accounts without a subscription window and before +/// the session's first response. +const STATUSLINE_FALLBACK: &str = "nightcrow: watching"; + +/// Forward a `StopFailure` payload. Always succeeds from the caller's point of +/// view; `StopFailure` ignores our exit code anyway, and a hook that fails +/// loudly would be worse than one that does nothing. +pub fn hook() -> ExitCode { + if let Some((token, payload)) = read_stdin_object().and_then(|body| { + let token = pane_token()?; + Some((token, pick(&body, &STOP_FAILURE_FIELDS))) + }) { + let _ = send( + &socket_path().unwrap_or_default(), + &IpcMessage { + token, + kind: SignalKind::StopFailure, + payload: Value::Object(payload), + }, + ); + } + ExitCode::SUCCESS +} + +/// Forward the statusline's `rate_limits` and print a line, so installing this +/// plugin does not cost the user their statusline. +pub fn statusline() -> ExitCode { + let body = read_stdin_object(); + let rate_limits = body + .as_ref() + .and_then(|b| b.get(RATE_LIMITS_FIELD)) + .and_then(Value::as_object) + .cloned(); + if let (Some(token), Some(limits)) = (pane_token(), rate_limits.clone()) { + let _ = send( + &socket_path().unwrap_or_default(), + &IpcMessage { + token, + kind: SignalKind::RateLimits, + payload: Value::Object(limits), + }, + ); + } + println!("{}", render_statusline(rate_limits.as_ref())); + ExitCode::SUCCESS +} + +/// A short line built only from fields whose meaning is documented: the usage +/// percentage of each window the provider reported. +fn render_statusline(rate_limits: Option<&Map>) -> String { + let Some(limits) = rate_limits else { + return STATUSLINE_FALLBACK.to_string(); + }; + let mut parts = Vec::new(); + for (label, key) in [("5h", "five_hour"), ("7d", "seven_day")] { + if let Some(used) = limits + .get(key) + .and_then(|w| w.get("used_percentage")) + .and_then(Value::as_f64) + { + parts.push(format!("{label} {}%", used.round() as i64)); + } + } + if parts.is_empty() { + return STATUSLINE_FALLBACK.to_string(); + } + parts.join(" | ") +} + +/// The pane this helper belongs to, from the environment its provider inherited. +/// Absent means this provider was not started by nightcrow, so there is nothing +/// to correlate and nothing to send. +fn pane_token() -> Option { + std::env::var(PANE_TOKEN_ENV) + .ok() + .filter(|t| !t.trim().is_empty()) +} + +fn read_stdin_object() -> Option> { + let mut body = String::new(); + std::io::stdin() + .take(MAX_HELPER_STDIN_BYTES) + .read_to_string(&mut body) + .ok()?; + match serde_json::from_str::(&body) { + Ok(Value::Object(map)) => Some(map), + _ => None, + } +} + +/// Copy only the named string fields. A field that is present but not a string +/// is dropped rather than coerced. +fn pick(body: &Map, fields: &[&str]) -> Map { + let mut out = Map::new(); + for field in fields { + if let Some(value) = body.get(*field).filter(|v| v.is_string()) { + out.insert((*field).to_string(), value.clone()); + } + } + out +} + +#[cfg(test)] +#[path = "helper_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/helper_tests.rs b/plugins/nightcrow-recovery/src/helper_tests.rs new file mode 100644 index 00000000..ab942798 --- /dev/null +++ b/plugins/nightcrow-recovery/src/helper_tests.rs @@ -0,0 +1,94 @@ +//! The whitelisting and the statusline text. Reading stdin and connecting to the +//! socket are covered by `ipc_tests`; what matters here is that nothing outside +//! the whitelist can be forwarded, whatever a provider puts in its payload. + +use super::*; + +fn stop_failure_payload() -> Map { + match serde_json::json!({ + "session_id": "11111111-2222-3333-4444-555555555555", + "prompt_id": "p_1", + "transcript_path": "/home/x/.claude/projects/repo/session.jsonl", + "cwd": "/w/repo", + "hook_event_name": "StopFailure", + "error_type": "rate_limit", + "error_message": "You have exceeded your usage for account billing@example.com", + "agent_id": "a_1" + }) { + Value::Object(map) => map, + _ => unreachable!("the fixture is an object"), + } +} + +#[test] +fn only_the_three_whitelisted_hook_fields_are_forwarded() { + let picked = pick(&stop_failure_payload(), &STOP_FAILURE_FIELDS); + let mut keys: Vec<&String> = picked.keys().collect(); + keys.sort(); + assert_eq!( + keys, + vec!["error_type", "hook_event_name", "session_id"], + "nothing else may leave the provider's process" + ); +} + +#[test] +fn a_transcript_path_and_an_error_message_are_never_forwarded() { + let picked = pick(&stop_failure_payload(), &STOP_FAILURE_FIELDS); + assert!(picked.get("transcript_path").is_none()); + assert!(picked.get("error_message").is_none()); + assert!(picked.get("cwd").is_none()); + assert!(picked.get("prompt_id").is_none()); + let serialised = Value::Object(picked).to_string(); + assert!(!serialised.contains("billing@example.com"), "{serialised}"); +} + +#[test] +fn a_whitelisted_field_of_the_wrong_type_is_dropped_rather_than_coerced() { + let payload = match serde_json::json!({ + "session_id": 42, + "error_type": null, + "hook_event_name": {"name": "StopFailure"} + }) { + Value::Object(map) => map, + _ => unreachable!("the fixture is an object"), + }; + assert!(pick(&payload, &STOP_FAILURE_FIELDS).is_empty()); +} + +#[test] +fn a_payload_missing_every_whitelisted_field_forwards_nothing() { + assert!(pick(&Map::new(), &STOP_FAILURE_FIELDS).is_empty()); +} + +#[test] +fn a_statusline_reports_the_usage_of_every_window_the_provider_gave() { + let limits = match serde_json::json!({ + "five_hour": {"used_percentage": 23.5, "resets_at": 1_767_225_600i64}, + "seven_day": {"used_percentage": 41.2, "resets_at": 1_767_657_600i64} + }) { + Value::Object(map) => map, + _ => unreachable!("the fixture is an object"), + }; + assert_eq!(render_statusline(Some(&limits)), "5h 24% | 7d 41%"); +} + +#[test] +fn a_statusline_with_one_window_reports_only_that_window() { + let limits = match serde_json::json!({"seven_day": {"used_percentage": 8.0}}) { + Value::Object(map) => map, + _ => unreachable!("the fixture is an object"), + }; + assert_eq!(render_statusline(Some(&limits)), "7d 8%"); +} + +#[test] +fn a_statusline_still_prints_a_line_when_the_provider_reported_no_windows() { + assert_eq!(render_statusline(None), STATUSLINE_FALLBACK); + assert_eq!(render_statusline(Some(&Map::new())), STATUSLINE_FALLBACK); + let unusable = match serde_json::json!({"five_hour": {"used_percentage": "lots"}}) { + Value::Object(map) => map, + _ => unreachable!("the fixture is an object"), + }; + assert_eq!(render_statusline(Some(&unusable)), STATUSLINE_FALLBACK); +} diff --git a/plugins/nightcrow-recovery/src/hooks.rs b/plugins/nightcrow-recovery/src/hooks.rs new file mode 100644 index 00000000..42d3766f --- /dev/null +++ b/plugins/nightcrow-recovery/src/hooks.rs @@ -0,0 +1,209 @@ +//! Install and uninstall this plugin's entries in Claude Code's +//! `~/.claude/settings.json`. +//! +//! That file belongs to the user, not to us: it may hold keys and hook events we +//! know nothing about, so every edit here is a merge that preserves what we did +//! not put there, and a refusal when the file cannot be understood. The JSON +//! surgery itself lives in [`merge`]; this module is the filesystem around it. + +use anyhow::{Context, Result}; +use serde_json::{Map, Value, json}; +use std::fs; +use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; + +#[path = "hooks_merge.rs"] +mod merge; + +use merge::{MARKER, STATUSLINE_KEY}; + +const CLAUDE_DIR: &str = ".claude"; +const SETTINGS_FILE: &str = "settings.json"; +const BACKUP_FILE: &str = "settings.json.bak"; +/// Sidecar name: prefixed with the crate name because it lives in a directory +/// the provider owns, and must never look like something the provider wrote. +const SIDECAR_FILE: &str = "nightcrow-recovery.displaced.json"; + +/// `settings.json` is user configuration in the user's home directory: readable +/// and writable by its owner only. +const SETTINGS_MODE: u32 = 0o600; + +/// Where the Claude Code settings we edit live. A struct so tests can point at a +/// temp dir instead of the real home directory. +#[derive(Debug, Clone)] +pub struct SettingsPaths { + pub settings: PathBuf, + pub backup: PathBuf, + /// Sidecar holding what we displaced, so uninstall can put it back. + pub sidecar: PathBuf, +} + +impl SettingsPaths { + /// `~/.claude/settings.json` and its siblings. + pub fn from_home(home: &Path) -> Self { + let dir = home.join(CLAUDE_DIR); + Self { + settings: dir.join(SETTINGS_FILE), + backup: dir.join(BACKUP_FILE), + sidecar: dir.join(SIDECAR_FILE), + } + } + + /// Resolves `$HOME`, erroring when it is unset. + pub fn discover() -> Result { + let home = std::env::var("HOME") + .map_err(|_| anyhow::anyhow!("HOME is unset, so ~/{CLAUDE_DIR} cannot be located"))?; + anyhow::ensure!( + !home.is_empty(), + "HOME is empty, so ~/{CLAUDE_DIR} cannot be located" + ); + Ok(Self::from_home(Path::new(&home))) + } +} + +/// Install our `StopFailure` hook and statusline entries, merging into whatever +/// is already there. Returns one line per change, for printing. +pub fn install(paths: &SettingsPaths, exe: &str) -> Result> { + let command = merge::hook_command(exe); + anyhow::ensure!( + merge::is_ours(&command), + "hook command `{command}` does not contain `{MARKER}`, so uninstall could not \ + recognise it later; install through a binary whose path contains `{MARKER}`" + ); + + let mut settings = read_settings(&paths.settings)?; + let (mut changes, displaced) = merge::merge_into(&mut settings, exe) + .with_context(|| format!("cannot merge our entries into {}", paths.settings.display()))?; + if changes.is_empty() { + return Ok(vec![format!( + "{} already has our hook and statusline; nothing changed", + paths.settings.display() + )]); + } + + back_up(&paths.settings, &paths.backup)?; + if let Some(previous) = displaced { + let mut sidecar = Map::new(); + sidecar.insert(STATUSLINE_KEY.to_string(), previous); + write_json(&paths.sidecar, &Value::Object(sidecar))?; + changes.push(format!( + "recorded the displaced {STATUSLINE_KEY} in {}", + paths.sidecar.display() + )); + } + write_json(&paths.settings, &settings)?; + Ok(changes) +} + +/// Remove only what [`install`] added. Returns one line per change. +pub fn uninstall(paths: &SettingsPaths) -> Result> { + if !paths.settings.exists() { + return Ok(vec![format!( + "{} does not exist; nothing to remove", + paths.settings.display() + )]); + } + let mut settings = read_settings(&paths.settings)?; + let restore = read_sidecar(&paths.sidecar); + let changes = merge::strip_from(&mut settings, restore).with_context(|| { + format!( + "cannot remove our entries from {}", + paths.settings.display() + ) + })?; + if changes.is_empty() { + return Ok(vec![format!( + "{} has none of our entries; nothing to remove", + paths.settings.display() + )]); + } + + write_json(&paths.settings, &settings)?; + if paths.sidecar.exists() { + fs::remove_file(&paths.sidecar) + .with_context(|| format!("cannot delete {}", paths.sidecar.display()))?; + } + Ok(changes) +} + +/// Absent or empty means "no settings yet"; anything that is not a JSON object +/// is a file we do not understand, and guessing at it is worse than stopping. +fn read_settings(path: &Path) -> Result { + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(json!({})), + Err(e) => return Err(anyhow::anyhow!("cannot read {}: {e}", path.display())), + }; + if text.trim().is_empty() { + return Ok(json!({})); + } + let value: Value = serde_json::from_str(&text).map_err(|e| { + anyhow::anyhow!( + "{} is not valid JSON ({e}); refusing to edit a file we cannot parse", + path.display() + ) + })?; + anyhow::ensure!( + value.is_object(), + "{} holds a JSON {} at its top level, not an object; refusing to edit it", + path.display(), + merge::kind_of(&value) + ); + Ok(value) +} + +/// The recorded `statusLine`, or `None` when there is no usable sidecar. A +/// damaged sidecar is not fatal: the worst outcome is that we drop a key the user +/// can set again, whereas failing would leave our entries installed for good. +fn read_sidecar(path: &Path) -> Option { + let text = fs::read_to_string(path).ok()?; + let value: Value = serde_json::from_str(&text).ok()?; + value.get(STATUSLINE_KEY).cloned() +} + +/// Copy the current settings verbatim before the first write, so a bad merge is +/// recoverable. Nothing to copy when the file does not exist yet, and in that +/// case an older backup from a previous install is left as it is. +fn back_up(settings: &Path, backup: &Path) -> Result<()> { + if !settings.exists() { + return Ok(()); + } + fs::copy(settings, backup).map_err(|e| { + anyhow::anyhow!( + "cannot copy {} to {}: {e}", + settings.display(), + backup.display() + ) + })?; + Ok(()) +} + +/// Write via a temp file in the same directory and rename over the target, so a +/// crash mid-write cannot leave the provider with a half-written settings file. +fn write_json(path: &Path, value: &Value) -> Result<()> { + if let Some(dir) = path.parent() { + fs::create_dir_all(dir) + .map_err(|e| anyhow::anyhow!("cannot create {}: {e}", dir.display()))?; + } + let mut text = serde_json::to_string_pretty(value) + .map_err(|e| anyhow::anyhow!("cannot encode JSON for {}: {e}", path.display()))?; + text.push('\n'); + + let temp = path.with_extension("json.tmp"); + fs::write(&temp, &text).map_err(|e| anyhow::anyhow!("cannot write {}: {e}", temp.display()))?; + // Mode is set before the rename, so the target is never briefly world-readable. + fs::set_permissions(&temp, fs::Permissions::from_mode(SETTINGS_MODE)) + .map_err(|e| anyhow::anyhow!("cannot set mode on {}: {e}", temp.display()))?; + fs::rename(&temp, path).map_err(|e| { + anyhow::anyhow!( + "cannot rename {} to {}: {e}", + temp.display(), + path.display() + ) + })?; + Ok(()) +} + +#[cfg(test)] +#[path = "hooks_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/hooks_merge.rs b/plugins/nightcrow-recovery/src/hooks_merge.rs new file mode 100644 index 00000000..d3c20b4e --- /dev/null +++ b/plugins/nightcrow-recovery/src/hooks_merge.rs @@ -0,0 +1,234 @@ +//! The JSON surgery behind `install`/`uninstall`, as pure functions over +//! [`Value`] so the tricky cases are testable without touching a filesystem. +//! +//! Our entries are identified by [`MARKER`] appearing as a substring of a +//! `command` string. `command` is the only field Claude Code's hook and +//! statusline schema lets us write free text into; a custom key of our own (say +//! `"nightcrowOwned": true`) may be rejected or warned about as unknown by the +//! provider, so it is not a safe place to keep our bookkeeping. + +use anyhow::Result; +use serde_json::{Map, Value, json}; + +/// Substring that marks a settings entry as ours. It is the crate name, which is +/// also the installed binary's name, so `" hook"` carries it for free. +pub(crate) const MARKER: &str = "nightcrow-recovery"; + +pub(crate) const HOOKS_KEY: &str = "hooks"; +pub(crate) const HOOK_EVENT: &str = "StopFailure"; +pub(crate) const STATUSLINE_KEY: &str = "statusLine"; +const MATCHER_KEY: &str = "matcher"; +const COMMAND_KEY: &str = "command"; + +/// The one `error_type` we ask to be woken for. Least privilege: a rate limit is +/// the only failure this plugin acts on, so payloads for unrelated failures +/// (`authentication_failed`, `billing_error`, ...) never reach this process at +/// all. The consequence is deliberate: transient `overloaded`/`server_error` +/// conditions are recognised from the pane's terminal output instead of here. +pub(crate) const HOOK_MATCHER: &str = "rate_limit"; + +/// Seconds Claude Code should wait for our hook. The hook only parses one JSON +/// payload and hands it to the running host, so this is already generous; the cap +/// matters because a wedged helper must not sit in the provider's path, and +/// `StopFailure` ignores our output and exit code anyway. +const HOOK_TIMEOUT_SECS: u64 = 5; + +/// Columns of gutter around the rendered statusline, so it does not butt against +/// the terminal edge. Matches the value in Claude Code's own documented example. +const STATUSLINE_PADDING: u64 = 2; + +pub(crate) fn hook_command(exe: &str) -> String { + format!("{exe} hook") +} + +pub(crate) fn statusline_command(exe: &str) -> String { + format!("{exe} statusline") +} + +pub(crate) fn is_ours(command: &str) -> bool { + command.contains(MARKER) +} + +fn command_of(entry: &Value) -> Option<&str> { + entry.get(COMMAND_KEY).and_then(Value::as_str) +} + +fn hook_entry(command: &str) -> Value { + json!({ "type": "command", "command": command, "timeout": HOOK_TIMEOUT_SECS }) +} + +fn statusline_entry(command: &str) -> Value { + json!({ "type": "command", "command": command, "padding": STATUSLINE_PADDING }) +} + +fn matcher_group() -> Value { + let mut group = Map::new(); + group.insert( + MATCHER_KEY.to_string(), + Value::String(HOOK_MATCHER.to_string()), + ); + group.insert(HOOKS_KEY.to_string(), Value::Array(Vec::new())); + Value::Object(group) +} + +pub(crate) fn kind_of(value: &Value) -> &'static str { + match value { + Value::Null => "null", + Value::Bool(_) => "boolean", + Value::Number(_) => "number", + Value::String(_) => "string", + Value::Array(_) => "array", + Value::Object(_) => "object", + } +} + +fn object_mut<'a>(value: &'a mut Value, what: &str) -> Result<&'a mut Map> { + let found = kind_of(value); + value + .as_object_mut() + .ok_or_else(|| anyhow::anyhow!("{what} is a JSON {found}, not an object")) +} + +fn array_mut<'a>(value: &'a mut Value, what: &str) -> Result<&'a mut Vec> { + let found = kind_of(value); + value + .as_array_mut() + .ok_or_else(|| anyhow::anyhow!("{what} is a JSON {found}, not an array")) +} + +/// Add our hook entry and statusline to `settings`, leaving every other key and +/// every array entry we did not add exactly as it was. Returns one line per +/// change plus the `statusLine` value we displaced, which the caller must record +/// before writing so uninstall can put it back. +pub(crate) fn merge_into(settings: &mut Value, exe: &str) -> Result<(Vec, Option)> { + let mut changes = Vec::new(); + let root = object_mut(settings, "the settings root")?; + + let command = hook_command(exe); + { + let hooks = root.entry(HOOKS_KEY).or_insert_with(|| json!({})); + let hooks = object_mut(hooks, "`hooks`")?; + let groups = hooks.entry(HOOK_EVENT).or_insert_with(|| json!([])); + let groups = array_mut(groups, "`hooks.StopFailure`")?; + let index = match groups + .iter() + .position(|g| g.get(MATCHER_KEY).and_then(Value::as_str) == Some(HOOK_MATCHER)) + { + Some(index) => index, + None => { + groups.push(matcher_group()); + groups.len() - 1 + } + }; + let group = object_mut(&mut groups[index], "the `rate_limit` matcher group")?; + let entries = group.entry(HOOKS_KEY).or_insert_with(|| json!([])); + let entries = array_mut(entries, "the `rate_limit` group's `hooks`")?; + // An exact command match already present is what makes a second install + // a no-op instead of a duplicate entry. + let present = entries + .iter() + .any(|e| command_of(e) == Some(command.as_str())); + if !present { + entries.push(hook_entry(&command)); + changes.push(format!( + "added {HOOK_EVENT} hook for matcher `{HOOK_MATCHER}`: {command}" + )); + } + } + + let mut displaced = None; + let ours = root + .get(STATUSLINE_KEY) + .and_then(command_of) + .is_some_and(is_ours); + if !ours { + let previous = root.get(STATUSLINE_KEY).cloned().unwrap_or(Value::Null); + let command = statusline_command(exe); + root.insert(STATUSLINE_KEY.to_string(), statusline_entry(&command)); + changes.push(if previous.is_null() { + format!("set {STATUSLINE_KEY}: {command}") + } else { + format!("replaced {STATUSLINE_KEY} with: {command} (previous value recorded)") + }); + displaced = Some(previous); + } + + Ok((changes, displaced)) +} + +/// Remove exactly what [`merge_into`] added, putting `restore` back as +/// `statusLine` when it holds a value we recorded. Containers we empty are +/// collapsed so the file returns to its original shape. +pub(crate) fn strip_from(settings: &mut Value, restore: Option) -> Result> { + let mut changes = Vec::new(); + let root = object_mut(settings, "the settings root")?; + + let removed = strip_hooks(root); + if removed > 0 { + changes.push(format!("removed {removed} {HOOK_EVENT} hook command(s)")); + } + + let ours = root + .get(STATUSLINE_KEY) + .and_then(command_of) + .is_some_and(is_ours); + if ours { + match restore { + Some(previous) if !previous.is_null() => { + root.insert(STATUSLINE_KEY.to_string(), previous); + changes.push(format!("restored the previous {STATUSLINE_KEY}")); + } + _ => { + root.remove(STATUSLINE_KEY); + changes.push(format!("removed our {STATUSLINE_KEY}")); + } + } + } + + Ok(changes) +} + +/// Drop our hook commands and return how many went. A `hooks` subtree of an +/// unexpected shape is left alone rather than rewritten: it cannot be ours. +fn strip_hooks(root: &mut Map) -> usize { + let mut removed = 0usize; + let mut hooks_empty = false; + if let Some(hooks) = root.get_mut(HOOKS_KEY).and_then(Value::as_object_mut) { + let mut groups_empty = false; + if let Some(groups) = hooks.get_mut(HOOK_EVENT).and_then(Value::as_array_mut) { + let mut drop_indexes = Vec::new(); + for (index, group) in groups.iter_mut().enumerate() { + let Some(entries) = group.get_mut(HOOKS_KEY).and_then(Value::as_array_mut) else { + continue; + }; + let before = entries.len(); + entries.retain(|e| !command_of(e).is_some_and(is_ours)); + if entries.len() == before { + continue; + } + removed += before - entries.len(); + if entries.is_empty() { + drop_indexes.push(index); + } + } + for index in drop_indexes.into_iter().rev() { + groups.remove(index); + } + groups_empty = groups.is_empty(); + } + // Only collapse containers we emptied ourselves, so a user's own empty + // `StopFailure` list survives an uninstall that found nothing of ours. + if groups_empty && removed > 0 { + hooks.remove(HOOK_EVENT); + } + hooks_empty = hooks.is_empty(); + } + if hooks_empty && removed > 0 { + root.remove(HOOKS_KEY); + } + removed +} + +#[cfg(test)] +#[path = "hooks_merge_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/hooks_merge_tests.rs b/plugins/nightcrow-recovery/src/hooks_merge_tests.rs new file mode 100644 index 00000000..ce2871cd --- /dev/null +++ b/plugins/nightcrow-recovery/src/hooks_merge_tests.rs @@ -0,0 +1,202 @@ +use super::*; + +/// A path containing [`MARKER`], as the installed binary's path does. +const EXE: &str = "/opt/nightcrow/libexec/nightcrow-recovery"; + +fn hook_cmd() -> String { + format!("{EXE} hook") +} + +fn our_group(settings: &Value) -> &Value { + &settings[HOOKS_KEY][HOOK_EVENT][0] +} + +#[test] +fn merging_into_an_empty_object_adds_one_matcher_group_and_a_statusline() { + let mut settings = json!({}); + + let (changes, displaced) = merge_into(&mut settings, EXE).unwrap(); + + assert_eq!(changes.len(), 2, "{changes:?}"); + assert_eq!(displaced, Some(Value::Null)); + assert_eq!(our_group(&settings)["matcher"], json!(HOOK_MATCHER)); + assert_eq!( + settings[STATUSLINE_KEY]["command"], + json!(format!("{EXE} statusline")) + ); +} + +#[test] +fn merging_keeps_unknown_nested_keys_and_unrelated_hook_events() { + let mut settings = json!({ + "unknownTopLevel": [1, 2, 3], + "hooks": { + "unknownNested": "kept", + "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "audit.sh" }] }] + } + }); + + merge_into(&mut settings, EXE).unwrap(); + + assert_eq!(settings["unknownTopLevel"], json!([1, 2, 3])); + assert_eq!(settings["hooks"]["unknownNested"], json!("kept")); + assert_eq!( + settings["hooks"]["PreToolUse"][0]["hooks"][0]["command"], + json!("audit.sh") + ); +} + +#[test] +fn merging_reuses_a_matcher_group_that_already_exists() { + let mut settings = json!({ + "hooks": { "StopFailure": [ + { "matcher": "billing_error", "hooks": [{ "type": "command", "command": "page-me" }] }, + { "matcher": "rate_limit", "hooks": [{ "type": "command", "command": "theirs" }] } + ]} + }); + + merge_into(&mut settings, EXE).unwrap(); + + let groups = settings["hooks"]["StopFailure"].as_array().unwrap(); + assert_eq!(groups.len(), 2, "no new group should be appended"); + assert_eq!(groups[1]["hooks"][0]["command"], json!("theirs")); + assert_eq!(groups[1]["hooks"][1]["command"], json!(hook_cmd())); +} + +#[test] +fn merging_twice_reports_no_change_the_second_time() { + let mut settings = json!({}); + merge_into(&mut settings, EXE).unwrap(); + let after_first = settings.clone(); + + let (changes, displaced) = merge_into(&mut settings, EXE).unwrap(); + + assert!(changes.is_empty(), "{changes:?}"); + assert_eq!(displaced, None); + assert_eq!(settings, after_first); +} + +#[test] +fn merging_over_a_statusline_of_our_own_leaves_it_and_records_nothing() { + let mut settings = json!({ + "statusLine": { "type": "command", "command": format!("{EXE} statusline --wide") } + }); + let before = settings[STATUSLINE_KEY].clone(); + + let (_, displaced) = merge_into(&mut settings, EXE).unwrap(); + + assert_eq!(displaced, None); + assert_eq!(settings[STATUSLINE_KEY], before); +} + +#[test] +fn merging_when_hooks_is_not_an_object_is_refused_and_names_the_field() { + let mut settings = json!({ "hooks": "surprise" }); + + let error = merge_into(&mut settings, EXE).unwrap_err().to_string(); + + assert!(error.contains("`hooks`"), "{error}"); + assert!(error.contains("string"), "{error}"); +} + +#[test] +fn merging_when_the_stop_failure_event_is_not_an_array_is_refused() { + let mut settings = json!({ "hooks": { "StopFailure": { "matcher": "" } } }); + + let error = merge_into(&mut settings, EXE).unwrap_err().to_string(); + + assert!(error.contains("`hooks.StopFailure`"), "{error}"); +} + +#[test] +fn stripping_what_was_merged_returns_the_settings_to_their_original_shape() { + let original = json!({ + "model": "opus", + "hooks": { "PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "audit.sh" }] }] } + }); + let mut settings = original.clone(); + let (_, displaced) = merge_into(&mut settings, EXE).unwrap(); + + let changes = strip_from(&mut settings, displaced).unwrap(); + + assert_eq!(changes.len(), 2, "{changes:?}"); + assert_eq!(settings, original); +} + +#[test] +fn stripping_drops_the_whole_hooks_subtree_when_it_only_held_our_entry() { + let mut settings = json!({ "model": "opus" }); + let (_, displaced) = merge_into(&mut settings, EXE).unwrap(); + + strip_from(&mut settings, displaced).unwrap(); + + assert_eq!(settings, json!({ "model": "opus" })); +} + +#[test] +fn stripping_restores_the_recorded_statusline() { + let theirs = json!({ "type": "command", "command": "mine.sh", "refreshInterval": 1 }); + let mut settings = json!({ "statusLine": theirs.clone() }); + let (_, displaced) = merge_into(&mut settings, EXE).unwrap(); + + strip_from(&mut settings, displaced).unwrap(); + + assert_eq!(settings[STATUSLINE_KEY], theirs); +} + +#[test] +fn stripping_without_a_recorded_value_removes_our_statusline() { + let mut settings = json!({}); + merge_into(&mut settings, EXE).unwrap(); + + strip_from(&mut settings, None).unwrap(); + + assert_eq!(settings.get(STATUSLINE_KEY), None); +} + +#[test] +fn stripping_leaves_a_foreign_hook_command_and_its_group_in_place() { + let original = json!({ + "hooks": { "StopFailure": [ + { "matcher": "rate_limit", "hooks": [{ "type": "command", "command": "theirs" }] } + ]}, + "statusLine": { "type": "command", "command": "theirs.sh" } + }); + let mut settings = original.clone(); + + let changes = strip_from(&mut settings, Some(json!("bogus"))).unwrap(); + + assert!(changes.is_empty(), "{changes:?}"); + assert_eq!(settings, original); +} + +#[test] +fn stripping_keeps_a_users_own_empty_stop_failure_list() { + let original = json!({ "hooks": { "StopFailure": [] } }); + let mut settings = original.clone(); + + strip_from(&mut settings, None).unwrap(); + + assert_eq!(settings, original); +} + +#[test] +fn stripping_a_hooks_subtree_of_an_unexpected_shape_changes_nothing() { + let original = json!({ "hooks": "surprise" }); + let mut settings = original.clone(); + + let changes = strip_from(&mut settings, None).unwrap(); + + assert!(changes.is_empty(), "{changes:?}"); + assert_eq!(settings, original); +} + +#[test] +fn stripping_a_non_object_root_is_refused() { + let mut settings = json!([1, 2]); + + let error = strip_from(&mut settings, None).unwrap_err().to_string(); + + assert!(error.contains("settings root"), "{error}"); + assert!(error.contains("array"), "{error}"); +} diff --git a/plugins/nightcrow-recovery/src/hooks_tests.rs b/plugins/nightcrow-recovery/src/hooks_tests.rs new file mode 100644 index 00000000..966adf2e --- /dev/null +++ b/plugins/nightcrow-recovery/src/hooks_tests.rs @@ -0,0 +1,266 @@ +use super::*; +use tempfile::TempDir; + +/// A path containing [`MARKER`], as the installed binary's path does. +const EXE: &str = "/opt/nightcrow/libexec/nightcrow-recovery"; + +fn home() -> (TempDir, SettingsPaths) { + let dir = TempDir::new().unwrap(); + let paths = SettingsPaths::from_home(dir.path()); + (dir, paths) +} + +fn write_settings(paths: &SettingsPaths, text: &str) { + fs::create_dir_all(paths.settings.parent().unwrap()).unwrap(); + fs::write(&paths.settings, text).unwrap(); +} + +fn read_value(path: &Path) -> Value { + serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap() +} + +fn hook_cmd() -> String { + format!("{EXE} hook") +} + +fn statusline_cmd() -> String { + format!("{EXE} statusline") +} + +#[test] +fn installing_with_no_settings_file_creates_one_holding_only_our_entries() { + let (_dir, paths) = home(); + + let changes = install(&paths, EXE).unwrap(); + + assert!(!changes.is_empty()); + assert_eq!( + read_value(&paths.settings), + json!({ + "hooks": { + "StopFailure": [{ + "matcher": "rate_limit", + "hooks": [{ "type": "command", "command": hook_cmd(), "timeout": 5 }], + }], + }, + "statusLine": { "type": "command", "command": statusline_cmd(), "padding": 2 }, + }) + ); +} + +#[test] +fn installing_into_a_settings_file_preserves_unknown_keys() { + let (_dir, paths) = home(); + write_settings( + &paths, + r#"{ + "model": "opus", + "hooks": { + "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "audit.sh" }] }], + "someFutureKey": { "kept": true } + } + }"#, + ); + + install(&paths, EXE).unwrap(); + + let after = read_value(&paths.settings); + assert_eq!(after["model"], json!("opus")); + assert_eq!( + after["hooks"]["PreToolUse"][0]["hooks"][0]["command"], + json!("audit.sh") + ); + assert_eq!(after["hooks"]["someFutureKey"], json!({ "kept": true })); + assert_eq!( + after["hooks"]["StopFailure"][0]["hooks"][0]["command"], + json!(hook_cmd()) + ); +} + +#[test] +fn installing_beside_someone_elses_stop_failure_hook_keeps_both() { + let (_dir, paths) = home(); + write_settings( + &paths, + r#"{"hooks":{"StopFailure":[{"matcher":"rate_limit","hooks":[ + {"type":"command","command":"notify-send limit"}]}]}}"#, + ); + + install(&paths, EXE).unwrap(); + + let entries = read_value(&paths.settings)["hooks"]["StopFailure"][0]["hooks"].clone(); + let commands: Vec<&str> = entries + .as_array() + .unwrap() + .iter() + .map(|e| e["command"].as_str().unwrap()) + .collect(); + assert_eq!(commands, vec!["notify-send limit", hook_cmd().as_str()]); +} + +#[test] +fn installing_twice_changes_nothing_the_second_time_and_reports_so() { + let (_dir, paths) = home(); + install(&paths, EXE).unwrap(); + let first = read_value(&paths.settings); + + let changes = install(&paths, EXE).unwrap(); + + assert_eq!(changes.len(), 1); + assert!(changes[0].contains("nothing changed"), "{}", changes[0]); + assert_eq!(read_value(&paths.settings), first); +} + +#[test] +fn uninstalling_after_installing_restores_the_original_settings() { + let (_dir, paths) = home(); + let original = r#"{ + "model": "opus", + "statusLine": { "type": "command", "command": "~/.claude/mine.sh", "padding": 0 }, + "hooks": { "PreToolUse": [{ "matcher": "", "hooks": [{ "type": "command", "command": "audit.sh" }] }] } + }"#; + write_settings(&paths, original); + install(&paths, EXE).unwrap(); + + let changes = uninstall(&paths).unwrap(); + + assert!(!changes.is_empty()); + assert_eq!( + read_value(&paths.settings), + serde_json::from_str::(original).unwrap() + ); + assert!(!paths.sidecar.exists()); +} + +#[test] +fn uninstalling_leaves_a_statusline_the_user_replaced_alone() { + let (_dir, paths) = home(); + install(&paths, EXE).unwrap(); + let mut settings = read_value(&paths.settings); + settings["statusLine"] = json!({ "type": "command", "command": "theirs.sh" }); + write_settings(&paths, &settings.to_string()); + + uninstall(&paths).unwrap(); + + let after = read_value(&paths.settings); + assert_eq!( + after["statusLine"], + json!({ "type": "command", "command": "theirs.sh" }) + ); + assert_eq!(after.get("hooks"), None, "our hook should still be gone"); +} + +#[test] +fn uninstalling_with_no_settings_file_is_not_an_error() { + let (_dir, paths) = home(); + + let changes = uninstall(&paths).unwrap(); + + assert_eq!(changes.len(), 1); + assert!(changes[0].contains("nothing to remove"), "{}", changes[0]); +} + +#[test] +fn uninstalling_without_a_sidecar_removes_our_statusline_and_invents_none() { + let (_dir, paths) = home(); + install(&paths, EXE).unwrap(); + fs::remove_file(&paths.sidecar).unwrap(); + + uninstall(&paths).unwrap(); + + assert_eq!(read_value(&paths.settings), json!({})); +} + +#[test] +fn uninstalling_when_nothing_of_ours_is_present_reports_and_writes_nothing() { + let (_dir, paths) = home(); + let original = r#"{"statusLine":{"type":"command","command":"theirs.sh"}}"#; + write_settings(&paths, original); + + let changes = uninstall(&paths).unwrap(); + + assert_eq!(changes.len(), 1); + assert!(changes[0].contains("nothing to remove"), "{}", changes[0]); + assert_eq!(fs::read_to_string(&paths.settings).unwrap(), original); +} + +#[test] +fn installing_over_a_json_array_is_refused_and_the_file_is_untouched() { + assert_refused(r#"[{"statusLine":1}]"#, "array"); +} + +#[test] +fn installing_over_a_json_scalar_is_refused_and_the_file_is_untouched() { + assert_refused("42", "number"); +} + +#[test] +fn installing_over_malformed_json_is_refused_and_the_file_is_untouched() { + assert_refused("{ this is not json", "not valid JSON"); +} + +fn assert_refused(original: &str, expected_in_error: &str) { + let (_dir, paths) = home(); + write_settings(&paths, original); + + let error = install(&paths, EXE).unwrap_err().to_string(); + + assert!(error.contains(expected_in_error), "{error}"); + assert!( + error.contains(&paths.settings.display().to_string()), + "error must name the file: {error}" + ); + assert_eq!(fs::read_to_string(&paths.settings).unwrap(), original); + assert!(!paths.backup.exists()); + assert!(!paths.sidecar.exists()); +} + +#[test] +fn installing_backs_up_the_previous_file_and_writes_mode_0600() { + let (_dir, paths) = home(); + let original = r#"{"model":"opus"}"#; + write_settings(&paths, original); + + install(&paths, EXE).unwrap(); + + assert_eq!(fs::read_to_string(&paths.backup).unwrap(), original); + let mode = fs::metadata(&paths.settings).unwrap().permissions().mode(); + assert_eq!(mode & 0o777, 0o600, "mode was {mode:o}"); +} + +#[test] +fn installing_without_a_settings_file_writes_no_backup() { + let (_dir, paths) = home(); + + install(&paths, EXE).unwrap(); + + assert!(!paths.backup.exists()); +} + +#[test] +fn installing_through_a_binary_without_the_marker_is_refused() { + let (_dir, paths) = home(); + + let error = install(&paths, "/usr/bin/renamed").unwrap_err().to_string(); + + assert!(error.contains(MARKER), "{error}"); + assert!(!paths.settings.exists()); +} + +#[test] +fn settings_paths_from_home_composes_the_documented_paths() { + let paths = SettingsPaths::from_home(Path::new("/home/dev")); + + assert_eq!( + paths.settings, + PathBuf::from("/home/dev/.claude/settings.json") + ); + assert_eq!( + paths.backup, + PathBuf::from("/home/dev/.claude/settings.json.bak") + ); + assert_eq!( + paths.sidecar, + PathBuf::from("/home/dev/.claude/nightcrow-recovery.displaced.json") + ); +} diff --git a/plugins/nightcrow-recovery/src/ipc.rs b/plugins/nightcrow-recovery/src/ipc.rs new file mode 100644 index 00000000..61f93a77 --- /dev/null +++ b/plugins/nightcrow-recovery/src/ipc.rs @@ -0,0 +1,272 @@ +//! The private socket a provider's helper processes report through. +//! +//! Claude Code invokes a hook command and a statusline command as children of +//! the `claude` process, which means they inherit its environment — including the +//! [`PANE_TOKEN_ENV`] value nightcrow injected when it spawned the pane. Those +//! children live for milliseconds and must not block their parent, so they do the +//! smallest possible thing: connect, write one line, exit. This module is that +//! line's format and both ends of the socket. +//! +//! Trust posture: anything that can reach the socket can claim to be any pane, so +//! the socket is created 0600 inside a 0700 directory and every field of every +//! message is validated before it reaches the state machine. The token is a +//! correlation key, never an authorisation: the worst a forged message can do is +//! make this plugin ask the host for something, and the host judges that again. + +use crate::protocol::PaneToken; +use crate::provider::{OutOfBand, SignalKind}; +use anyhow::{Context, Result, bail, ensure}; +use serde_json::Value; +use std::ffi::OsStr; +use std::fs; +use std::io::{BufRead, BufReader, Read, Write}; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +/// IPC message version, independent of the host protocol: this socket is between +/// two copies of this same binary, so a mismatch means a half-finished upgrade. +const IPC_VERSION: u32 = 1; + +/// Directory and file name under the chosen runtime root. +const RUNTIME_DIR: &str = "nightcrow"; +const SOCKET_FILE: &str = "recovery.sock"; +/// Fallback root when `$XDG_RUNTIME_DIR` is unset (it usually is on macOS). +const HOME_RUNTIME_DIR: &str = ".nightcrow/run"; + +/// Only the owner may traverse the directory or speak to the socket. A pane +/// token is a correlation key and not a secret, but there is no reason to let +/// another local user inject one. +const DIR_MODE: u32 = 0o700; +const SOCKET_MODE: u32 = 0o600; + +/// Longest IPC line accepted. Both senders forward a whitelist of small scalar +/// fields, so anything larger is a bug or an attempt to make this process +/// allocate. +pub const MAX_IPC_LINE_BYTES: usize = 8 * 1024; + +/// Longest token accepted. The host mints 32 hex characters; the cap is double +/// that so a future widening does not need a change here. +const MAX_TOKEN_LEN: usize = 64; + +/// How long either end will block on the socket. +/// +/// The sender runs inside a provider's hook child, so it must give up quickly +/// rather than hold up someone's CLI; the receiver uses the same bound so one +/// stalled client cannot park the accept loop. +const IPC_TIMEOUT: Duration = Duration::from_millis(500); + +/// One report from a provider helper process. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IpcMessage { + pub token: PaneToken, + pub kind: SignalKind, + pub payload: Value, +} + +impl IpcMessage { + pub fn into_signal(self) -> (PaneToken, OutOfBand) { + ( + self.token, + OutOfBand { + kind: self.kind, + payload: self.payload, + }, + ) + } +} + +/// Where the socket lives: `$XDG_RUNTIME_DIR/nightcrow/` when the system offers +/// one, else `~/.nightcrow/run/`. +pub fn socket_path() -> Result { + socket_path_from( + std::env::var_os("XDG_RUNTIME_DIR").as_deref(), + std::env::var_os("HOME").as_deref(), + ) +} + +/// The path rule itself, separated from the environment so it can be tested +/// without mutating this process's variables. +fn socket_path_from(runtime_dir: Option<&OsStr>, home: Option<&OsStr>) -> Result { + if let Some(dir) = runtime_dir.filter(|d| !d.is_empty()) { + return Ok(PathBuf::from(dir).join(RUNTIME_DIR).join(SOCKET_FILE)); + } + let home = home.filter(|h| !h.is_empty()).context( + "neither XDG_RUNTIME_DIR nor HOME is set, so there is nowhere to put the socket", + )?; + Ok(PathBuf::from(home).join(HOME_RUNTIME_DIR).join(SOCKET_FILE)) +} + +pub fn encode(msg: &IpcMessage) -> Result { + let line = serde_json::json!({ + "v": IPC_VERSION, + "token": msg.token, + "kind": msg.kind.as_wire(), + "payload": msg.payload, + }) + .to_string(); + ensure!( + line.len() <= MAX_IPC_LINE_BYTES, + "ipc message is {} bytes, over the {MAX_IPC_LINE_BYTES}-byte limit", + line.len() + ); + Ok(line) +} + +/// Parse and fully validate one line from the socket. +/// +/// Every failure names what was wrong: this is the boundary where untrusted +/// input becomes state, so a silently-coerced field would be the bug. +pub fn parse_line(line: &str) -> Result { + ensure!( + line.len() <= MAX_IPC_LINE_BYTES, + "ipc line is {} bytes, over the {MAX_IPC_LINE_BYTES}-byte limit", + line.len() + ); + let value: Value = + serde_json::from_str(line).map_err(|e| anyhow::anyhow!("ipc line is not JSON: {e}"))?; + let object = value.as_object().context("ipc line is not a JSON object")?; + let v = object + .get("v") + .and_then(Value::as_u64) + .context("ipc line has no numeric \"v\"")?; + ensure!( + v == u64::from(IPC_VERSION), + "ipc line claims version {v}, this build speaks {IPC_VERSION}" + ); + let token = object + .get("token") + .and_then(Value::as_str) + .context("ipc line has no string \"token\"")?; + ensure!(!token.is_empty(), "ipc line carries an empty token"); + ensure!( + token.len() <= MAX_TOKEN_LEN, + "ipc token is {} characters, over the {MAX_TOKEN_LEN} limit", + token.len() + ); + ensure!( + token.chars().all(|c| c.is_ascii_alphanumeric()), + "ipc token holds characters a pane token cannot contain" + ); + let kind_name = object + .get("kind") + .and_then(Value::as_str) + .context("ipc line has no string \"kind\"")?; + let kind = SignalKind::from_wire(kind_name) + .with_context(|| format!("unknown ipc kind {kind_name:?}"))?; + let payload = object + .get("payload") + .context("ipc line has no \"payload\"")?; + ensure!( + payload.is_object(), + "ipc payload is not a JSON object, so there is nothing to read from it" + ); + Ok(IpcMessage { + token: token.to_string(), + kind, + payload: payload.clone(), + }) +} + +/// Send one message and return. Used by the short-lived `hook` and `statusline` +/// modes, where a failure must be silent: the provider's helper process has no +/// business reporting our problems to its user. +pub fn send(path: &Path, msg: &IpcMessage) -> Result<()> { + let line = encode(msg)?; + let mut stream = UnixStream::connect(path) + .with_context(|| format!("no recovery plugin listening at {}", path.display()))?; + stream.set_write_timeout(Some(IPC_TIMEOUT))?; + stream.write_all(line.as_bytes())?; + stream.write_all(b"\n")?; + stream.flush()?; + Ok(()) +} + +/// The listening end. Unlinks its socket when dropped, so a normal exit leaves +/// nothing behind for the next run to trip over. +#[derive(Debug)] +pub struct Ipc { + path: PathBuf, + listener: UnixListener, +} + +impl Ipc { + pub fn bind(path: PathBuf) -> Result { + let dir = path + .parent() + .context("socket path has no parent directory")?; + fs::create_dir_all(dir) + .with_context(|| format!("cannot create runtime directory {}", dir.display()))?; + fs::set_permissions(dir, fs::Permissions::from_mode(DIR_MODE)) + .with_context(|| format!("cannot restrict {} to its owner", dir.display()))?; + // A socket file left by a crashed run refuses bind with EADDRINUSE, and + // there is no live listener behind it to protect. + if path.exists() && UnixStream::connect(&path).is_err() { + let _ = fs::remove_file(&path); + } + let listener = UnixListener::bind(&path) + .with_context(|| format!("cannot listen on {}", path.display()))?; + fs::set_permissions(&path, fs::Permissions::from_mode(SOCKET_MODE)) + .with_context(|| format!("cannot restrict {} to its owner", path.display()))?; + Ok(Self { path, listener }) + } + + pub fn path(&self) -> &Path { + &self.path + } + + /// Start accepting in a background thread. One line per connection: a + /// helper process has exactly one thing to say. + /// + /// `sink` returns `false` when the receiver is gone, which ends the thread. + pub fn serve(&self, sink: S) -> Result<()> + where + S: Fn(IpcMessage) -> bool + Send + 'static, + { + let listener = self + .listener + .try_clone() + .context("cannot clone the ipc listener for its accept thread")?; + std::thread::Builder::new() + .name("recovery-ipc".to_string()) + .spawn(move || { + for stream in listener.incoming() { + let Ok(stream) = stream else { continue }; + let _ = stream.set_read_timeout(Some(IPC_TIMEOUT)); + match read_one(stream) { + Ok(msg) => { + if !sink(msg) { + return; // the main loop is gone + } + } + // A malformed or truncated message is dropped: there is + // no channel back to a caller that has already exited. + Err(_) => continue, + } + } + }) + .context("cannot spawn the ipc accept thread")?; + Ok(()) + } +} + +impl Drop for Ipc { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +fn read_one(stream: UnixStream) -> Result { + let mut reader = BufReader::new(stream).take(MAX_IPC_LINE_BYTES as u64 + 1); + let mut line = String::new(); + let read = reader.read_line(&mut line)?; + if read == 0 { + bail!("ipc connection closed without sending a line"); + } + parse_line(line.trim_end_matches('\n')) +} + +#[cfg(test)] +#[path = "ipc_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/ipc_tests.rs b/plugins/nightcrow-recovery/src/ipc_tests.rs new file mode 100644 index 00000000..22945fb5 --- /dev/null +++ b/plugins/nightcrow-recovery/src/ipc_tests.rs @@ -0,0 +1,219 @@ +use super::*; +use std::sync::mpsc::channel; + +const TOKEN: &str = "0123456789abcdef0123456789abcdef"; +const OTHER_TOKEN: &str = "ffffffffffffffffffffffffffffffff"; + +fn message(token: &str, kind: SignalKind) -> IpcMessage { + IpcMessage { + token: token.to_string(), + kind, + payload: serde_json::json!({"session_id": "abc", "error_type": "rate_limit"}), + } +} + +fn line(token: &str) -> String { + format!(r#"{{"v":1,"token":"{token}","kind":"stop_failure","payload":{{"a":1}}}}"#) +} + +#[test] +fn a_message_round_trips_through_its_wire_line() { + let sent = message(TOKEN, SignalKind::StopFailure); + let parsed = parse_line(&encode(&sent).expect("encodable")).expect("parsable"); + assert_eq!(parsed, sent); +} + +#[test] +fn a_statusline_message_round_trips_too() { + let sent = IpcMessage { + token: TOKEN.to_string(), + kind: SignalKind::RateLimits, + payload: serde_json::json!({"five_hour": {"resets_at": 1_767_225_600}}), + }; + let parsed = parse_line(&encode(&sent).expect("encodable")).expect("parsable"); + assert_eq!(parsed, sent); +} + +#[test] +fn a_line_that_is_not_a_json_object_is_refused() { + for bad in ["", " ", "not json", "[1,2]", "\"a\"", "null", "7"] { + assert!(parse_line(bad).is_err(), "{bad:?} is not a message"); + } +} + +#[test] +fn a_line_from_another_ipc_version_is_refused() { + let bad = line(TOKEN).replace("\"v\":1", "\"v\":2"); + let err = parse_line(&bad) + .expect_err("a version mismatch") + .to_string(); + assert!(err.contains('2'), "{err}"); +} + +#[test] +fn a_line_with_no_version_is_refused() { + let bad = line(TOKEN).replace("\"v\":1,", ""); + assert!(parse_line(&bad).is_err()); +} + +#[test] +fn a_token_that_no_host_would_mint_is_refused() { + for token in ["", "abc def", "abc;rm", "../../etc/passwd", "tokén"] { + let err = parse_line(&line(token)); + assert!(err.is_err(), "{token:?} is not a pane token"); + } + let long = "a".repeat(MAX_IPC_LINE_BYTES.min(200)); + assert!(parse_line(&line(&long)).is_err(), "an over-long token"); +} + +#[test] +fn an_unknown_kind_is_refused_rather_than_ignored() { + let bad = line(TOKEN).replace("stop_failure", "transcript"); + let err = parse_line(&bad).expect_err("an unknown kind").to_string(); + assert!(err.contains("transcript"), "{err}"); +} + +#[test] +fn a_payload_that_is_not_an_object_is_refused() { + for payload in ["null", "\"text\"", "[1]", "3"] { + let bad = line(TOKEN).replace(r#"{"a":1}"#, payload); + assert!(parse_line(&bad).is_err(), "{payload} is not a payload"); + } +} + +#[test] +fn a_line_over_the_length_limit_is_refused_before_it_is_parsed() { + let bad = format!("{}{}", line(TOKEN), " ".repeat(MAX_IPC_LINE_BYTES)); + let err = parse_line(&bad).expect_err("over the limit").to_string(); + assert!(err.contains("limit"), "{err}"); +} + +#[test] +fn encoding_refuses_a_payload_too_large_to_send() { + let huge = IpcMessage { + token: TOKEN.to_string(), + kind: SignalKind::StopFailure, + payload: serde_json::json!({"error_message": "x".repeat(MAX_IPC_LINE_BYTES)}), + }; + assert!(encode(&huge).is_err()); +} + +#[test] +fn a_message_becomes_a_signal_keyed_by_its_pane_token() { + let (token, signal) = message(TOKEN, SignalKind::RateLimits).into_signal(); + assert_eq!(token, TOKEN); + assert_eq!(signal.kind, SignalKind::RateLimits); +} + +#[test] +fn the_socket_path_prefers_the_systems_runtime_directory() { + let path = socket_path_from( + Some(OsStr::new("/run/user/1000")), + Some(OsStr::new("/home/x")), + ) + .expect("a path"); + assert_eq!( + path, + PathBuf::from("/run/user/1000/nightcrow/recovery.sock") + ); +} + +#[test] +fn the_socket_path_falls_back_to_the_home_directory() { + for runtime in [None, Some(OsStr::new(""))] { + let path = socket_path_from(runtime, Some(OsStr::new("/home/x"))).expect("a path"); + assert_eq!(path, PathBuf::from("/home/x/.nightcrow/run/recovery.sock")); + } +} + +#[test] +fn a_process_with_neither_variable_set_has_nowhere_to_put_the_socket() { + let err = socket_path_from(None, None) + .expect_err("nowhere to put it") + .to_string(); + assert!( + err.contains("XDG_RUNTIME_DIR") && err.contains("HOME"), + "{err}" + ); + assert!(socket_path_from(Some(OsStr::new("")), Some(OsStr::new(""))).is_err()); +} + +#[test] +fn a_helpers_line_reaches_the_plugin_and_names_its_pane() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("run").join("recovery.sock"); + let ipc = Ipc::bind(path.clone()).expect("a listener"); + let (tx, rx) = channel(); + ipc.serve(move |msg| tx.send(msg).is_ok()).expect("serving"); + + send(&path, &message(TOKEN, SignalKind::StopFailure)).expect("sent"); + send(&path, &message(OTHER_TOKEN, SignalKind::RateLimits)).expect("sent"); + + let first = rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("first"); + let second = rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("second"); + let tokens = [first.token.clone(), second.token.clone()]; + assert!(tokens.contains(&TOKEN.to_string())); + assert!(tokens.contains(&OTHER_TOKEN.to_string())); +} + +#[test] +fn a_malformed_line_is_dropped_without_ending_the_listener() { + use std::io::Write as _; + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("recovery.sock"); + let ipc = Ipc::bind(path.clone()).expect("a listener"); + let (tx, rx) = channel(); + ipc.serve(move |msg| tx.send(msg).is_ok()).expect("serving"); + + let mut stream = std::os::unix::net::UnixStream::connect(&path).expect("connected"); + stream.write_all(b"garbage\n").expect("wrote"); + drop(stream); + + send(&path, &message(TOKEN, SignalKind::StopFailure)).expect("sent"); + let received = rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("the good line"); + assert_eq!(received.token, TOKEN); +} + +#[test] +fn the_socket_is_readable_only_by_its_owner_and_removed_on_exit() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("recovery.sock"); + { + let ipc = Ipc::bind(path.clone()).expect("a listener"); + assert_eq!(ipc.path(), path.as_path()); + let mode = fs::metadata(&path).expect("metadata").permissions().mode() & 0o777; + assert_eq!(mode, SOCKET_MODE); + let dir_mode = fs::metadata(dir.path()) + .expect("metadata") + .permissions() + .mode() + & 0o777; + assert_eq!(dir_mode, DIR_MODE); + } + assert!(!path.exists(), "a normal exit leaves no socket behind"); +} + +#[test] +fn a_socket_left_by_a_crashed_run_is_replaced() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("recovery.sock"); + fs::write(&path, b"stale").expect("a leftover file"); + let ipc = Ipc::bind(path.clone()).expect("bind over the leftover"); + assert!(ipc.path().exists()); +} + +#[test] +fn sending_to_a_plugin_that_is_not_running_fails_without_blocking() { + let dir = tempfile::tempdir().expect("a temp dir"); + let path = dir.path().join("absent.sock"); + let err = send(&path, &message(TOKEN, SignalKind::StopFailure)) + .expect_err("nothing is listening") + .to_string(); + assert!(err.contains("absent.sock"), "{err}"); +} diff --git a/plugins/nightcrow-recovery/src/main.rs b/plugins/nightcrow-recovery/src/main.rs new file mode 100644 index 00000000..1c86d525 --- /dev/null +++ b/plugins/nightcrow-recovery/src/main.rs @@ -0,0 +1,98 @@ +//! A nightcrow plugin that notices a coding CLI stopped because it hit its +//! plan's usage limit, waits for the limit to reset, and resumes the exact +//! session it was in. +//! +//! With no subcommand this is the plugin itself: NDJSON on stdin and stdout, +//! spoken to nightcrow. The subcommands are the parts a provider invokes or a +//! human runs once — see each one's help text. +//! +//! What this program will not do, by construction: name the program a pane runs +//! (the host owns that), alter a CLI's permission flags (only resume arguments +//! are ever passed), or write anything down beyond the recovery metadata it +//! needs while it is running. + +mod helper; +mod hooks; +mod ipc; +mod protocol; +mod provider; +mod runloop; +mod state; +mod wait; + +use clap::{Parser, Subcommand}; +use std::process::ExitCode; + +#[derive(Debug, Parser)] +#[command(name = "nightcrow-recovery", version, about, long_about = None)] +struct Cli { + #[command(subcommand)] + mode: Option, +} + +#[derive(Debug, Subcommand)] +enum Mode { + /// Add the Claude Code StopFailure hook and statusline entries to + /// ~/.claude/settings.json, merging into whatever is already there. + InstallHooks, + /// Remove only the entries install-hooks added. + UninstallHooks, + /// Internal: the command Claude Code runs for StopFailure. Reads the hook + /// payload on stdin and forwards a few fields to the running plugin. + Hook, + /// Internal: the command Claude Code runs for its statusline. Forwards the + /// usage windows to the running plugin and prints a short line. + Statusline, +} + +fn main() -> ExitCode { + match Cli::parse().mode { + None => report(runloop::run()), + Some(Mode::InstallHooks) => report(install()), + Some(Mode::UninstallHooks) => report(uninstall()), + Some(Mode::Hook) => helper::hook(), + Some(Mode::Statusline) => helper::statusline(), + } +} + +fn install() -> anyhow::Result<()> { + let paths = hooks::SettingsPaths::discover()?; + print_changes(hooks::install(&paths, ¤t_exe()?)?); + Ok(()) +} + +fn uninstall() -> anyhow::Result<()> { + let paths = hooks::SettingsPaths::discover()?; + print_changes(hooks::uninstall(&paths)?); + Ok(()) +} + +fn print_changes(changes: Vec) { + for change in changes { + println!("{change}"); + } +} + +/// The absolute path to write into the provider's settings. +/// +/// Resolved rather than taken from `argv[0]`: the settings file is read by a +/// different process with a different working directory, so a relative name +/// there would silently stop working. +fn current_exe() -> anyhow::Result { + let exe = std::env::current_exe()?; + exe.to_str() + .map(str::to_string) + .ok_or_else(|| anyhow::anyhow!("this executable's path is not valid UTF-8: {exe:?}")) +} + +/// A human-facing mode's exit status. The message goes to stderr so a mode that +/// also prints data keeps its stdout clean. +fn report(result: anyhow::Result<()>) -> ExitCode { + match result { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + eprintln!("nightcrow-recovery: {e:#}"); + ExitCode::FAILURE + } + } +} diff --git a/plugins/nightcrow-recovery/src/protocol.rs b/plugins/nightcrow-recovery/src/protocol.rs new file mode 100644 index 00000000..ffa27e00 --- /dev/null +++ b/plugins/nightcrow-recovery/src/protocol.rs @@ -0,0 +1,200 @@ +//! The plugin's side of nightcrow's NDJSON plugin contract. +//! +//! Deliberately a standalone copy of the host's `src/plugin/protocol.rs` rather +//! than a shared crate: a plugin is built and shipped separately from the host, +//! so it is written against a *version* of the contract. [`PROTOCOL_VERSION`] +//! is what makes a mismatch loud instead of half-understood, and a copy is what +//! makes the version claim honest. + +use serde::{Deserialize, Serialize}; + +/// Contract version this plugin speaks. The host refuses anything else. +pub const PROTOCOL_VERSION: u32 = 1; + +/// Longest line the host will read from us; also the cap we apply to what we +/// read, so a corrupt stream cannot make this process allocate without bound. +pub const MAX_LINE_BYTES: usize = 64 * 1024; + +/// Longest `data` the host accepts in one [`PluginCommand::SendInput`]. +pub const MAX_INPUT_BYTES: usize = 8 * 1024; + +/// Opaque pane-slot name. Random hex minted by the host; we only ever compare +/// and forward it. +pub type PaneToken = String; + +/// Which spawn of a pane slot an event or command refers to. +pub type PaneGeneration = u32; + +/// Env var carrying the pane token into the pane's child processes, and hence +/// into a provider CLI's hook and statusline helpers. That inheritance is how an +/// out-of-band signal is attributed to a pane; cwd cannot do it, because +/// nightcrow allows several panes on one repository. +pub const PANE_TOKEN_ENV: &str = "NIGHTCROW_PANE_TOKEN"; + +/// Something the host observed. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "event", rename_all = "snake_case")] +pub enum PluginEvent { + PaneOpened { + v: u32, + token: PaneToken, + generation: PaneGeneration, + title: Option, + command: Option, + cwd: String, + }, + PaneOutput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + text: String, + }, + PaneIdle { + v: u32, + token: PaneToken, + generation: PaneGeneration, + idle_ms: u64, + }, + PaneExited { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + PaneClosed { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + UserInput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + Shutdown { + v: u32, + }, +} + +impl PluginEvent { + /// Which slot this addresses, or `None` for [`Self::Shutdown`]. + pub fn token(&self) -> Option<&PaneToken> { + match self { + Self::PaneOpened { token, .. } + | Self::PaneOutput { token, .. } + | Self::PaneIdle { token, .. } + | Self::PaneExited { token, .. } + | Self::PaneClosed { token, .. } + | Self::UserInput { token, .. } => Some(token), + Self::Shutdown { .. } => None, + } + } + + pub fn generation(&self) -> Option { + match self { + Self::PaneOpened { generation, .. } + | Self::PaneOutput { generation, .. } + | Self::PaneIdle { generation, .. } + | Self::PaneExited { generation, .. } + | Self::PaneClosed { generation, .. } + | Self::UserInput { generation, .. } => Some(*generation), + Self::Shutdown { .. } => None, + } + } +} + +/// Something we ask the host to do. The host judges every one of these and +/// refusal is ordinary traffic, never a reason to retry in a loop. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "cmd", rename_all = "snake_case")] +pub enum PluginCommand { + SendInput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + data: String, + }, + Relaunch { + v: u32, + token: PaneToken, + generation: PaneGeneration, + resume_args: Vec, + }, + Status { + v: u32, + token: PaneToken, + generation: PaneGeneration, + state: String, + detail: Option, + deadline_epoch: Option, + attempt: u32, + }, + Log { + v: u32, + level: LogLevel, + message: String, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, +} + +/// Parse one line the host wrote to our stdin. +/// +/// Refuses an over-long line and a version we do not speak; both are conditions +/// this process cannot recover from by guessing. +pub fn decode_event(line: &str) -> anyhow::Result { + anyhow::ensure!( + line.len() <= MAX_LINE_BYTES, + "host line is {} bytes, over the {MAX_LINE_BYTES}-byte limit", + line.len() + ); + let event: PluginEvent = serde_json::from_str(line) + .map_err(|e| anyhow::anyhow!("cannot parse host event: {e}; unknown or malformed event"))?; + let v = match &event { + PluginEvent::PaneOpened { v, .. } + | PluginEvent::PaneOutput { v, .. } + | PluginEvent::PaneIdle { v, .. } + | PluginEvent::PaneExited { v, .. } + | PluginEvent::PaneClosed { v, .. } + | PluginEvent::UserInput { v, .. } + | PluginEvent::Shutdown { v } => *v, + }; + anyhow::ensure!( + v == PROTOCOL_VERSION, + "host speaks protocol version {v}, this plugin speaks {PROTOCOL_VERSION}" + ); + Ok(event) +} + +/// Serialise one command as exactly one NDJSON line, without its terminator. +/// +/// The framing is the newline, so an embedded one would split the frame; +/// `serde_json` escapes newlines inside strings, and this checks rather than +/// trusts that. +pub fn encode_command(cmd: &PluginCommand) -> anyhow::Result { + let line = serde_json::to_string(cmd) + .map_err(|e| anyhow::anyhow!("cannot encode plugin command: {e}"))?; + anyhow::ensure!( + !line.contains('\n'), + "encoded plugin command contains a newline, which would split the frame" + ); + Ok(line) +} + +pub fn log(level: LogLevel, message: impl Into) -> PluginCommand { + PluginCommand::Log { + v: PROTOCOL_VERSION, + level, + message: message.into(), + } +} + +#[cfg(test)] +#[path = "protocol_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/protocol_tests.rs b/plugins/nightcrow-recovery/src/protocol_tests.rs new file mode 100644 index 00000000..ff912edf --- /dev/null +++ b/plugins/nightcrow-recovery/src/protocol_tests.rs @@ -0,0 +1,146 @@ +use super::*; + +const TOKEN: &str = "0123456789abcdef0123456789abcdef"; + +fn opened_line() -> String { + format!( + r#"{{"event":"pane_opened","v":1,"token":"{TOKEN}","generation":2,"title":null,"command":"claude","cwd":"/w/repo"}}"# + ) +} + +#[test] +fn a_host_event_is_parsed_from_its_wire_shape() { + let event = decode_event(&opened_line()).expect("a well-formed event"); + assert_eq!( + event, + PluginEvent::PaneOpened { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation: 2, + title: None, + command: Some("claude".to_string()), + cwd: "/w/repo".to_string(), + } + ); + assert_eq!(event.token(), Some(&TOKEN.to_string())); + assert_eq!(event.generation(), Some(2)); +} + +#[test] +fn an_event_from_another_protocol_version_is_refused_and_both_versions_named() { + let line = opened_line().replace("\"v\":1", "\"v\":2"); + let err = decode_event(&line) + .expect_err("a version this build cannot speak") + .to_string(); + assert!(err.contains('2') && err.contains('1'), "{err}"); +} + +#[test] +fn an_unknown_event_is_refused_rather_than_guessed() { + let line = r#"{"event":"pane_teleported","v":1,"token":"abc","generation":1}"#; + assert!(decode_event(line).is_err()); +} + +#[test] +fn an_event_missing_a_required_field_is_refused() { + let line = r#"{"event":"pane_idle","v":1,"token":"abc","generation":1}"#; + assert!(decode_event(line).is_err(), "idle_ms is required"); +} + +#[test] +fn a_line_over_the_length_limit_is_refused_before_it_is_parsed() { + let line = "x".repeat(MAX_LINE_BYTES + 1); + let err = decode_event(&line).expect_err("over the limit").to_string(); + assert!(err.contains("limit"), "{err}"); +} + +#[test] +fn a_shutdown_names_no_pane() { + let event = decode_event(r#"{"event":"shutdown","v":1}"#).expect("a shutdown"); + assert_eq!(event.token(), None); + assert_eq!(event.generation(), None); +} + +#[test] +fn a_command_is_encoded_as_one_line_tagged_with_its_name() { + let line = encode_command(&PluginCommand::Relaunch { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation: 2, + resume_args: vec!["--resume".to_string(), "abc".to_string()], + }) + .expect("encodable"); + assert!(!line.contains('\n')); + assert!(line.contains("\"cmd\":\"relaunch\""), "{line}"); + assert!(line.contains("\"v\":1"), "{line}"); +} + +#[test] +fn text_holding_a_newline_is_escaped_rather_than_splitting_the_line() { + let line = encode_command(&PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation: 1, + data: "first\nsecond".to_string(), + }) + .expect("encodable"); + assert!(!line.contains('\n')); + assert!(line.contains("first\\nsecond"), "{line}"); +} + +#[test] +fn a_log_levels_json_shape_is_lowercase() { + let line = encode_command(&log(LogLevel::Warn, "careful")).expect("encodable"); + assert!(line.contains("\"level\":\"warn\""), "{line}"); +} + +#[test] +fn the_input_limit_matches_what_the_host_accepts() { + // Both sides cap typed input; a plugin that believed a larger number would + // spend attempts on requests the host refuses. + assert_eq!(MAX_INPUT_BYTES, 8 * 1024); + assert_eq!(MAX_LINE_BYTES, 64 * 1024); + assert_eq!(PANE_TOKEN_ENV, "NIGHTCROW_PANE_TOKEN"); +} + +/// Pull `pub const NAME: ty = ;` out of the host's source. +/// +/// Reading the host's file is the only way to compare: a plugin is a separate +/// build that deliberately does not link nightcrow, so the two copies of the +/// contract cannot share a constant. Re-asserting our own literals — which the +/// test above does — proves nothing about the host, and that is exactly how two +/// copies drift apart in silence. +fn host_const(source: &str, name: &str) -> String { + let needle = format!("pub const {name}"); + let line = source + .lines() + .find(|l| l.trim_start().starts_with(&needle)) + .unwrap_or_else(|| panic!("host no longer declares {name}")); + line.split_once('=') + .expect("a const declaration has a value") + .1 + .trim() + .trim_end_matches(';') + .trim() + .to_string() +} + +#[test] +fn the_hosts_copy_of_the_contract_still_says_the_same_thing() { + // Located from this crate's own manifest, and this crate is never published, + // so the path is always valid where the test can run at all. + let host = concat!(env!("CARGO_MANIFEST_DIR"), "/../../src/plugin/protocol.rs"); + let source = std::fs::read_to_string(host) + .unwrap_or_else(|e| panic!("cannot read the host's protocol at {host}: {e}")); + + assert_eq!(host_const(&source, "PROTOCOL_VERSION"), "1"); + assert_eq!(host_const(&source, "MAX_LINE_BYTES"), "64 * 1024"); + assert_eq!(host_const(&source, "MAX_INPUT_BYTES"), "8 * 1024"); + assert_eq!( + host_const(&source, "PROTOCOL_VERSION") + .parse::() + .expect("a version is a number"), + PROTOCOL_VERSION, + "the host and this plugin claim different protocol versions" + ); +} diff --git a/plugins/nightcrow-recovery/src/provider/claude.rs b/plugins/nightcrow-recovery/src/provider/claude.rs new file mode 100644 index 00000000..de437dba --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/claude.rs @@ -0,0 +1,255 @@ +//! Claude Code adapter. +//! +//! Three sources, in descending order of trust: the `StopFailure` hook (says +//! exactly why a turn ended), the statusline's `rate_limits` object (says when a +//! window resets, but never that we are blocked), and pane text (a fallback for +//! users who have neither, i.e. no hook installed and no Pro/Max subscription). + +use super::{ + LimitEvent, LimitKind, OutOfBand, PaneContext, Provider, ResumePlan, SignalKind, + reset_epoch_from_json, +}; +use crate::protocol::PaneGeneration; +use serde_json::Value; + +/// Hook name we require when the payload names itself, so a mislabelled or +/// replayed payload cannot be read as a stop failure. +const STOP_FAILURE_EVENT: &str = "StopFailure"; + +/// Windows the statusline may report. Each is independently optional: the object +/// only exists for Pro/Max accounts, and only after the session's first response. +const RATE_LIMIT_WINDOWS: &[&str] = &["five_hour", "seven_day"]; + +/// A session id is a UUID (36 chars); the cap is deliberately loose but finite so +/// an over-long value is rejected rather than handed to a command line. +const MAX_SESSION_ID_BYTES: usize = 64; + +/// How much recent output is kept so a needle split across two `pane_output` +/// events is still found. One screenful of a wide terminal is a few KiB; 4 KiB +/// spans that without holding transcript-sized history in memory. +const OUTPUT_TAIL_BYTES: usize = 4 * 1024; + +/// Phrasings that unambiguously mean the account is blocked by usage. Compared +/// against lowercased text. Kept narrow on purpose: a false positive parks a +/// working pane, and the hook already covers the common case. +const LIMIT_NEEDLES: &[&str] = &[ + "usage limit reached", + // Covers both the ASCII and the typographic apostrophe in "you've". + "hit your usage limit", +]; + +/// Phrasings that look similar but only warn. Checked before [`LIMIT_NEEDLES`]; +/// suppressing is the safe direction, because a missed limit still reaches the +/// machine through the hook or the next output chunk. +const NOT_A_LIMIT_NEEDLES: &[&str] = &[ + "approaching your usage limit", + "approaching the usage limit", +]; + +/// Typed into a live pane. Claude Code keeps running after an API error, so the +/// recovery is a nudge, not a relaunch. A plain continuation word only — never a +/// flag, never a permission grant. +const NUDGE_INPUT: &str = "continue\r"; + +/// Flag that resumes a named session; the id follows as a positional argument. +const RESUME_FLAG: &str = "--resume"; + +const NEEDS_HUMAN_HOLD: &str = + "claude reported an auth or billing failure; waiting cannot clear it"; +const NO_SESSION_HOLD: &str = + "no Claude session id; resuming the wrong session is worse than stopping"; +const OUTPUT_FALLBACK_DETAIL: &str = "claude output says the usage limit is reached"; + +/// Adapter state for one pane. Nothing here is written to disk, and neither +/// `error_message` nor transcript text is ever retained. +#[derive(Debug, Default)] +pub struct Claude { + /// Generation the rest of this state belongs to; a change re-arms the latch. + generation: Option, + /// Earliest plausible reset time the statusline has reported. + resets_at: Option, + /// Last validated session id seen on a hook payload. + session_id: Option, + /// Lowercased tail of recent output, at most [`OUTPUT_TAIL_BYTES`]. + tail: String, + /// Whether the output fallback has already fired for this generation. + fired: bool, +} + +impl Claude { + fn sync_generation(&mut self, ctx: &PaneContext) { + if self.generation == Some(ctx.generation) { + return; + } + self.generation = Some(ctx.generation); + self.fired = false; + self.tail.clear(); + // A respawn is a different session, so the old id must not be reused. + // The reset time is an account-wide fact and survives the respawn. + self.session_id = None; + } + + fn remember_reset(&mut self, at: i64) { + // The earliest window to reopen is the one that decides when work can + // continue, so the minimum is the useful deadline. + self.resets_at = Some(match self.resets_at { + Some(known) => known.min(at), + None => at, + }); + } + + fn push_tail(&mut self, text: &str) { + self.tail.push_str(&text.to_lowercase()); + if self.tail.len() <= OUTPUT_TAIL_BYTES { + return; + } + let want = self.tail.len() - OUTPUT_TAIL_BYTES; + let cut = (want..=self.tail.len()) + .find(|i| self.tail.is_char_boundary(*i)) + .unwrap_or(self.tail.len()); + self.tail.drain(..cut); + } + + fn on_rate_limits(&mut self, payload: &Value, now_epoch: i64) { + for window in RATE_LIMIT_WINDOWS { + if let Some(at) = reset_epoch_from_json(payload, &[window, "resets_at"], now_epoch) { + self.remember_reset(at); + } + } + // `used_percentage` is deliberately ignored, including at 100: a full + // window corroborates a limit but does not declare one, and only + // StopFailure or the output fallback may declare. + } + + fn on_stop_failure(&mut self, payload: &Value) -> Option { + let named = payload.get("hook_event_name").and_then(Value::as_str); + if named.is_some_and(|name| name != STOP_FAILURE_EVENT) { + return None; + } + let error_type = payload.get("error_type").and_then(Value::as_str)?; + let (kind, detail) = classify(error_type)?; + if let Some(id) = payload + .get("session_id") + .and_then(Value::as_str) + .and_then(validated_session_id) + { + self.session_id = Some(id); + } + Some(LimitEvent { + session_id: self.session_id.clone(), + resets_at: self.resets_at, + kind, + detail: detail.to_string(), + }) + } +} + +impl Provider for Claude { + fn name(&self) -> &'static str { + "claude" + } + + fn on_signal( + &mut self, + ctx: &PaneContext, + signal: &OutOfBand, + now_epoch: i64, + ) -> Option { + self.sync_generation(ctx); + match signal.kind { + SignalKind::RateLimits => { + self.on_rate_limits(&signal.payload, now_epoch); + None + } + SignalKind::StopFailure => self.on_stop_failure(&signal.payload), + } + } + + fn on_output(&mut self, ctx: &PaneContext, text: &str, _now_epoch: i64) -> Option { + self.sync_generation(ctx); + self.push_tail(text); + if self.fired { + return None; + } + if NOT_A_LIMIT_NEEDLES.iter().any(|n| self.tail.contains(n)) { + return None; + } + if !LIMIT_NEEDLES.iter().any(|n| self.tail.contains(n)) { + return None; + } + // A TUI redraws the same line many times, so latch and drop the matched + // text instead of reporting it again on the next repaint. + self.fired = true; + self.tail.clear(); + // No wall-clock time is parsed out of the text: it is printed without an + // offset, so a deadline read from it would be ambiguous. + Some(LimitEvent::usage( + self.session_id.clone(), + self.resets_at, + OUTPUT_FALLBACK_DETAIL, + )) + } + + fn on_exit(&mut self, _ctx: &PaneContext) { + self.fired = false; + self.tail.clear(); + } + + fn resume(&self, _ctx: &PaneContext, limit: &LimitEvent, alive: bool) -> Option { + if limit.kind == LimitKind::NeedsHuman { + return Some(ResumePlan::Hold(NEEDS_HUMAN_HOLD)); + } + if alive { + return Some(ResumePlan::Input(NUDGE_INPUT.to_string())); + } + let Some(id) = limit.session_id.as_deref().and_then(validated_session_id) else { + return Some(ResumePlan::Hold(NO_SESSION_HOLD)); + }; + Some(ResumePlan::Relaunch(vec![RESUME_FLAG.to_string(), id])) + } +} + +/// Map a hook `error_type` to a kind and a fixed detail string. +/// +/// `None` means "not a limit, nothing to recover". The detail is a literal, not +/// the payload's `error_message`, which can carry account and quota text. +fn classify(error_type: &str) -> Option<(LimitKind, &'static str)> { + match error_type { + "rate_limit" => Some((LimitKind::UsageLimit, "claude api error: rate_limit")), + "overloaded" => Some((LimitKind::Transient, "claude api error: overloaded")), + "server_error" => Some((LimitKind::Transient, "claude api error: server_error")), + "authentication_failed" => Some(( + LimitKind::NeedsHuman, + "claude api error: authentication_failed", + )), + "oauth_org_not_allowed" => Some(( + LimitKind::NeedsHuman, + "claude api error: oauth_org_not_allowed", + )), + "billing_error" => Some((LimitKind::NeedsHuman, "claude api error: billing_error")), + _ => None, + } +} + +/// Accept a session id only if it is safe to hand back as a command-line +/// argument: non-empty, bounded, and made of ASCII alphanumerics, `-`, or `_`. +fn validated_session_id(raw: &str) -> Option { + if raw.is_empty() || raw.len() > MAX_SESSION_ID_BYTES { + return None; + } + if !raw + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return None; + } + Some(raw.to_string()) +} + +#[cfg(test)] +#[path = "claude_tests.rs"] +mod tests; + +#[cfg(test)] +#[path = "claude_output_tests.rs"] +mod output_tests; diff --git a/plugins/nightcrow-recovery/src/provider/claude_output_tests.rs b/plugins/nightcrow-recovery/src/provider/claude_output_tests.rs new file mode 100644 index 00000000..710dbcae --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/claude_output_tests.rs @@ -0,0 +1,108 @@ +//! The terminal-output fallback and the resume plan, sharing the fixtures in +//! `claude_tests`. Split from that file only to stay inside the 300-line limit. + +use super::tests::{NOW, SESSION, ctx}; +use super::*; + +#[test] +fn a_usage_limit_line_in_output_reports_a_limit_exactly_once() { + let mut claude = Claude::default(); + let text = "Claude usage limit reached. Your limit will reset later.\n"; + let event = claude + .on_output(&ctx(1), text, NOW) + .expect("a blocked account"); + assert_eq!(event.kind, LimitKind::UsageLimit); + assert_eq!( + event.resets_at, None, + "no offset is printed, so no deadline" + ); + assert_eq!(event.session_id, None); + assert_eq!( + claude.on_output(&ctx(1), text, NOW), + None, + "redraw must not refire" + ); +} + +#[test] +fn output_matching_ignores_case() { + let mut claude = Claude::default(); + let shouted = "YOU'VE HIT YOUR USAGE LIMIT"; + assert!(claude.on_output(&ctx(1), shouted, NOW).is_some()); +} + +#[test] +fn a_warning_that_the_limit_is_approaching_does_not_report_a_limit() { + let mut claude = Claude::default(); + let text = "Heads up: you are approaching your usage limit for this window.\n"; + assert_eq!(claude.on_output(&ctx(1), text, NOW), None); +} + +#[test] +fn a_needle_split_across_two_output_chunks_is_still_found() { + let mut claude = Claude::default(); + assert_eq!(claude.on_output(&ctx(1), "Claude usage li", NOW), None); + let event = claude.on_output(&ctx(1), "mit reached\n", NOW); + assert!(event.is_some(), "the tail must span the chunk boundary"); +} + +#[test] +fn output_older_than_the_tail_budget_is_dropped() { + let mut claude = Claude::default(); + assert_eq!(claude.on_output(&ctx(1), "usage li", NOW), None); + let filler = "-".repeat(OUTPUT_TAIL_BYTES); + assert_eq!(claude.on_output(&ctx(1), &filler, NOW), None); + assert_eq!(claude.on_output(&ctx(1), "mit reached", NOW), None); +} + +#[test] +fn an_exit_or_a_generation_change_rearms_the_output_latch() { + let mut claude = Claude::default(); + let text = "Claude usage limit reached\n"; + assert!(claude.on_output(&ctx(1), text, NOW).is_some()); + assert_eq!(claude.on_output(&ctx(1), text, NOW), None); + claude.on_exit(&ctx(1)); + assert!( + claude.on_output(&ctx(1), text, NOW).is_some(), + "exit re-arms" + ); + assert!( + claude.on_output(&ctx(2), text, NOW).is_some(), + "respawn re-arms" + ); +} + +#[test] +fn a_live_pane_is_resumed_by_typing_one_continuation_line() { + let claude = Claude::default(); + let limit = LimitEvent::usage(Some(SESSION.to_string()), None, "d"); + let plan = claude.resume(&ctx(1), &limit, true); + assert_eq!(plan, Some(ResumePlan::Input(NUDGE_INPUT.to_string()))); + assert!(NUDGE_INPUT.ends_with('\r')); +} + +#[test] +fn an_exited_pane_relaunches_only_when_the_session_id_is_known() { + let claude = Claude::default(); + let with_id = LimitEvent::usage(Some(SESSION.to_string()), None, "d"); + let expected = vec![RESUME_FLAG.to_string(), SESSION.to_string()]; + let plan = claude.resume(&ctx(1), &with_id, false); + assert_eq!(plan, Some(ResumePlan::Relaunch(expected))); + let without_id = LimitEvent::usage(None, None, "d"); + let plan = claude.resume(&ctx(1), &without_id, false); + assert_eq!(plan, Some(ResumePlan::Hold(NO_SESSION_HOLD))); +} + +#[test] +fn a_needs_human_limit_holds_even_while_the_pane_is_alive() { + let claude = Claude::default(); + let limit = LimitEvent { + session_id: Some(SESSION.to_string()), + resets_at: None, + kind: LimitKind::NeedsHuman, + detail: "d".to_string(), + }; + let hold = Some(ResumePlan::Hold(NEEDS_HUMAN_HOLD)); + assert_eq!(claude.resume(&ctx(1), &limit, true), hold); + assert_eq!(claude.resume(&ctx(1), &limit, false), hold); +} diff --git a/plugins/nightcrow-recovery/src/provider/claude_tests.rs b/plugins/nightcrow-recovery/src/provider/claude_tests.rs new file mode 100644 index 00000000..329b5631 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/claude_tests.rs @@ -0,0 +1,222 @@ +use super::*; +use serde_json::json; + +/// Feb 2025, comfortably inside the plausible band the shared helpers enforce. +pub(super) const NOW: i64 = 1_738_400_000; +const FIVE_HOUR_RESET: i64 = 1_738_425_600; +const SEVEN_DAY_RESET: i64 = 1_738_857_600; +pub(super) const SESSION: &str = "0199f0aa-1111-4222-8333-abcdef123456"; + +pub(super) fn ctx(generation: PaneGeneration) -> PaneContext { + PaneContext { + token: "pane0".to_string(), + generation, + cwd: "/repo".to_string(), + command: Some("claude".to_string()), + } +} + +fn rate_limits(payload: Value) -> OutOfBand { + OutOfBand { + kind: SignalKind::RateLimits, + payload, + } +} + +fn stop_failure(payload: Value) -> OutOfBand { + OutOfBand { + kind: SignalKind::StopFailure, + payload, + } +} + +/// A realistic hook payload, `error_message` included so tests can prove it never +/// reaches a `detail`. +fn stop_failure_of(error_type: &str) -> OutOfBand { + stop_failure(json!({ + "hook_event_name": "StopFailure", + "session_id": SESSION, + "transcript_path": "/home/u/.claude/t.jsonl", + "cwd": "/repo", + "error_type": error_type, + "error_message": "Quota exceeded for account acct_secret_42", + })) +} + +/// Feed one `rate_limits` object and report the deadline it left behind, which a +/// later StopFailure would carry. +fn deadline_after(payload: Value) -> Option { + let mut claude = Claude::default(); + assert_eq!(claude.on_signal(&ctx(1), &rate_limits(payload), NOW), None); + claude + .on_signal(&ctx(1), &stop_failure_of("rate_limit"), NOW) + .expect("rate_limit is a limit") + .resets_at +} + +fn window(resets_at: Value) -> Value { + json!({"five_hour": {"used_percentage": 99.0, "resets_at": resets_at}}) +} + +#[test] +fn the_adapter_reports_its_stable_name() { + assert_eq!(Claude::default().name(), "claude"); +} + +#[test] +fn both_rate_limit_windows_present_picks_the_earliest_reset() { + let payload = json!({ + "five_hour": {"used_percentage": 23.5, "resets_at": FIVE_HOUR_RESET}, + "seven_day": {"used_percentage": 41.2, "resets_at": SEVEN_DAY_RESET}, + }); + assert_eq!(deadline_after(payload), Some(FIVE_HOUR_RESET)); +} + +#[test] +fn a_single_rate_limit_window_is_used_as_the_deadline() { + let payload = json!({"seven_day": {"used_percentage": 41.2, "resets_at": SEVEN_DAY_RESET}}); + assert_eq!(deadline_after(payload), Some(SEVEN_DAY_RESET)); +} + +/// The object is absent for non-Pro/Max accounts and empty before the session's +/// first response; a window may also arrive without its `resets_at`. +#[test] +fn an_absent_or_incomplete_rate_limits_object_yields_no_deadline() { + for payload in [json!({}), json!({"five_hour": {"used_percentage": 12.0}})] { + assert_eq!(deadline_after(payload.clone()), None, "{payload:?}"); + } +} + +/// Null, wrong type, non-positive, and beyond the believable horizon must all +/// degrade to "no deadline" rather than to a wait of the wrong length. +#[test] +fn an_unusable_resets_at_yields_no_deadline() { + let far = NOW + crate::provider::MAX_RESET_HORIZON_SECS + 1; + let bad = [ + Value::Null, + json!("1738425600"), + json!(-1), + json!(0), + json!(far), + json!(1.5), + ]; + for value in bad { + assert_eq!(deadline_after(window(value.clone())), None, "{value:?}"); + } +} + +#[test] +fn a_rate_limits_signal_alone_is_never_a_limit_even_at_a_full_window() { + let payload = json!({ + "five_hour": {"used_percentage": 100.0, "resets_at": FIVE_HOUR_RESET}, + "seven_day": {"used_percentage": 100.0, "resets_at": SEVEN_DAY_RESET}, + }); + let mut claude = Claude::default(); + assert_eq!(claude.on_signal(&ctx(1), &rate_limits(payload), NOW), None); +} + +#[test] +fn a_stop_failure_with_error_type_rate_limit_reports_a_usage_limit() { + let mut claude = Claude::default(); + let event = claude + .on_signal(&ctx(1), &stop_failure_of("rate_limit"), NOW) + .expect("rate_limit is a usage limit"); + assert_eq!(event.kind, LimitKind::UsageLimit); + assert_eq!(event.session_id.as_deref(), Some(SESSION)); + assert_eq!(event.resets_at, None); +} + +/// `None` means "not a limit": no wait and no retry can fix it, so the machine +/// must be told nothing at all rather than told to back off. +#[test] +fn every_documented_error_type_maps_to_its_own_kind() { + let cases = [ + ("rate_limit", Some(LimitKind::UsageLimit)), + ("overloaded", Some(LimitKind::Transient)), + ("server_error", Some(LimitKind::Transient)), + ("authentication_failed", Some(LimitKind::NeedsHuman)), + ("oauth_org_not_allowed", Some(LimitKind::NeedsHuman)), + ("billing_error", Some(LimitKind::NeedsHuman)), + ("invalid_request", None), + ("model_not_found", None), + ("max_output_tokens", None), + ("unknown", None), + ("an_error_type_from_a_future_release", None), + ]; + for (error_type, want) in cases { + let mut claude = Claude::default(); + let got = claude + .on_signal(&ctx(1), &stop_failure_of(error_type), NOW) + .map(|event| event.kind); + assert_eq!(got, want, "{error_type}"); + } +} + +#[test] +fn a_stop_failure_without_an_error_type_reports_nothing() { + let mut claude = Claude::default(); + let signal = stop_failure(json!({"hook_event_name": "StopFailure", "session_id": SESSION})); + assert_eq!(claude.on_signal(&ctx(1), &signal, NOW), None); +} + +#[test] +fn a_payload_naming_a_different_hook_event_is_ignored() { + let mut claude = Claude::default(); + let signal = stop_failure(json!({ + "hook_event_name": "Stop", + "session_id": SESSION, + "error_type": "rate_limit", + })); + assert_eq!(claude.on_signal(&ctx(1), &signal, NOW), None); +} + +#[test] +fn a_stop_failure_detail_never_repeats_the_error_message() { + let mut claude = Claude::default(); + let event = claude + .on_signal(&ctx(1), &stop_failure_of("rate_limit"), NOW) + .expect("rate_limit is a limit"); + assert!(!event.detail.contains("acct_secret_42"), "{}", event.detail); + assert!(event.detail.contains("rate_limit"), "{}", event.detail); +} + +#[test] +fn a_stop_failure_prefers_the_remembered_statusline_reset() { + let mut claude = Claude::default(); + let payload = json!({"five_hour": {"resets_at": FIVE_HOUR_RESET}}); + assert_eq!(claude.on_signal(&ctx(1), &rate_limits(payload), NOW), None); + let event = claude + .on_signal(&ctx(1), &stop_failure_of("rate_limit"), NOW) + .expect("rate_limit is a limit"); + assert_eq!(event.resets_at, Some(FIVE_HOUR_RESET)); +} + +/// Still a limit, just not resumable: a rejected id must leave the event without +/// one so the machine holds rather than resuming some other session. +#[test] +fn a_session_id_that_is_absent_or_unsafe_as_an_argument_is_rejected() { + let over_long = json!("x".repeat(MAX_SESSION_ID_BYTES + 1)); + let bad = [ + None, + Some(Value::Null), + Some(json!("")), + Some(json!("abc; rm -rf /")), + Some(json!("abc def")), + Some(json!("abc$(id)")), + Some(json!(42)), + Some(over_long), + ]; + for id in bad { + let mut payload = json!({"hook_event_name": "StopFailure", "error_type": "rate_limit"}); + if let Some(id) = id.clone() { + payload["session_id"] = id; + } + let mut claude = Claude::default(); + let event = claude + .on_signal(&ctx(1), &stop_failure(payload), NOW) + .expect("a rate limit is still reported"); + assert_eq!(event.session_id, None, "{id:?}"); + let plan = claude.resume(&ctx(1), &event, false); + assert_eq!(plan, Some(ResumePlan::Hold(NO_SESSION_HOLD)), "{id:?}"); + } +} diff --git a/plugins/nightcrow-recovery/src/provider/codex.rs b/plugins/nightcrow-recovery/src/provider/codex.rs new file mode 100644 index 00000000..c577df0c --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex.rs @@ -0,0 +1,157 @@ +//! Codex adapter. +//! +//! Codex has no hook, no statusline and no `status` subcommand, and it *exits* +//! when the usage limit is hit — with exit code 1, indistinguishable from any +//! other failure — so neither the exit code nor a still-running process can be +//! used as a signal. What codex does have is a per-session rollout file, and that +//! is the primary source here: [`Provider::poll`] tails the pane's rollout and +//! acts on the `turn_complete` record whose `error.codex_error_info` is +//! `usage_limit_exceeded`, taking the deadline from the most recent `token_count` +//! record. `EventMsg::Error` is not persisted to the rollout, so it is not looked +//! for. Terminal text is a documented fallback only, and a reset time is never +//! parsed out of it. +//! +//! Recovery is always a relaunch (`codex resume `), never typed +//! input. `codex resume --last` is deliberately never used: nightcrow allows +//! several codex panes on one repository, so "the last session" could belong to +//! another pane. Without an unambiguous session id this adapter holds. +//! +//! Layout: `codex_pane.rs` holds the per-pane watching state, +//! `codex_sessions.rs` finds the pane's rollout file and `codex_rollout.rs` holds +//! the pure record grammar. This file holds only the `Provider` contract. + +use super::{LimitEvent, PaneContext, Provider, ResumePlan}; +use crate::protocol::PaneToken; +use pane::PaneState; +use rollout::valid_session_id; +use std::collections::HashMap; +use std::path::PathBuf; + +#[path = "codex_pane.rs"] +mod pane; +#[path = "codex_rollout.rs"] +mod rollout; +#[path = "codex_sessions.rs"] +mod sessions; + +/// Env var codex reads for its state directory. +const CODEX_HOME_ENV: &str = "CODEX_HOME"; +/// Env var giving the home directory the default state directory hangs off. +const HOME_ENV: &str = "HOME"; +/// `CODEX_HOME` defaults to this directory under `$HOME`. +const DEFAULT_HOME_DIR: &str = ".codex"; +/// Rollout files live under `/sessions///
/`. +const SESSIONS_DIR: &str = "sessions"; + +/// First arg of the relaunch. The host supplies the program, so this can never +/// name a different binary. +const RESUME_SUBCOMMAND: &str = "resume"; + +const HOLD_ALIVE: &str = "codex is still running; nothing to resume"; +const HOLD_NO_ID: &str = + "no unambiguous codex session id; --last could resume another pane's session"; + +#[derive(Debug)] +pub struct Codex { + home: PathBuf, + panes: HashMap, +} + +impl Default for Codex { + fn default() -> Self { + let home = std::env::var_os(CODEX_HOME_ENV) + .filter(|v| !v.is_empty()) + .map(PathBuf::from) + .or_else(|| { + std::env::var_os(HOME_ENV) + .filter(|v| !v.is_empty()) + .map(|h| PathBuf::from(h).join(DEFAULT_HOME_DIR)) + }) + .unwrap_or_else(|| PathBuf::from(DEFAULT_HOME_DIR)); + Self::with_home(home) + } +} + +impl Codex { + /// Testing seam: point the adapter at a specific `CODEX_HOME`. + pub fn with_home(home: PathBuf) -> Self { + Self { + home, + panes: HashMap::new(), + } + } + + /// This pane's state, reset when the pane has been respawned: a new + /// generation is a new codex process writing a new rollout, so nothing about + /// the old one may carry over. + fn state_for(&mut self, ctx: &PaneContext, now_epoch: i64) -> &mut PaneState { + let state = self + .panes + .entry(ctx.token.clone()) + .or_insert_with(|| PaneState::new(ctx.generation, now_epoch)); + if state.generation() != ctx.generation { + *state = PaneState::new(ctx.generation, now_epoch); + } + state + } + + fn session_id_for(&self, ctx: &PaneContext) -> Option { + self.panes + .get(&ctx.token) + .filter(|state| state.generation() == ctx.generation) + .and_then(PaneState::session_id) + } +} + +impl Provider for Codex { + fn name(&self) -> &'static str { + "codex" + } + + fn poll(&mut self, ctx: &PaneContext, now_epoch: i64) -> Option { + let sessions = self.home.join(SESSIONS_DIR); + self.state_for(ctx, now_epoch).tail(&sessions, now_epoch) + } + + fn on_output(&mut self, ctx: &PaneContext, text: &str, now_epoch: i64) -> Option { + self.state_for(ctx, now_epoch).on_output(text) + } + + fn on_exit(&mut self, ctx: &PaneContext) { + let Some(state) = self.panes.get_mut(&ctx.token) else { + return; + }; + if state.generation() != ctx.generation { + return; + } + state.rearm_output(); + } + + fn resume(&self, ctx: &PaneContext, limit: &LimitEvent, alive: bool) -> Option { + if alive { + // Codex exits on a usage limit, so a live process is either working + // or waiting on the user; there is nothing to resume and typed input + // would land in whatever it is doing. + return Some(ResumePlan::Hold(HOLD_ALIVE)); + } + let id = limit + .session_id + .clone() + .or_else(|| self.session_id_for(ctx)) + .filter(|id| valid_session_id(id)); + match id { + Some(id) => Some(ResumePlan::Relaunch(vec![ + RESUME_SUBCOMMAND.to_string(), + id, + ])), + None => Some(ResumePlan::Hold(HOLD_NO_ID)), + } + } +} + +#[cfg(test)] +#[path = "codex_output_tests.rs"] +mod output_tests; +#[cfg(test)] +#[path = "codex_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/provider/codex_output_tests.rs b/plugins/nightcrow-recovery/src/provider/codex_output_tests.rs new file mode 100644 index 00000000..19c92816 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_output_tests.rs @@ -0,0 +1,163 @@ +//! Terminal-output fallback and resume tests. Neither touches the filesystem, so +//! they need no `CODEX_HOME` tree; the rollout-tailing cases live in +//! `codex_tests.rs`. + +use super::pane::{OUTPUT_DETAIL, USAGE_LIMIT_NEEDLES}; +use super::rollout::USAGE_LIMIT_ERROR_INFO; +use super::*; +use crate::protocol::PaneGeneration; +use crate::provider::LimitKind; + +const UUID: &str = "0199cbb1-2b70-7f11-9f0f-0f8e9d1c2b3a"; +/// Any plausible "now"; the output path never reads a time out of text, so the +/// value cannot change an outcome here. +const NOW: i64 = 1_800_000_000; +/// A home that does not exist: these tests must not depend on one. +const NO_HOME: &str = "/nonexistent/codex-home"; + +fn ctx(generation: PaneGeneration) -> PaneContext { + PaneContext { + token: "pane-0".to_string(), + generation, + cwd: "/repo".to_string(), + command: Some("codex".to_string()), + } +} + +fn adapter() -> Codex { + Codex::with_home(PathBuf::from(NO_HOME)) +} + +#[test] +fn the_adapter_is_named_codex() { + assert_eq!(adapter().name(), "codex"); +} + +#[test] +fn every_terminal_output_needle_fires_once() { + for needle in USAGE_LIMIT_NEEDLES { + let mut codex = adapter(); + // Upper-cased, because matching must not depend on codex's casing. + let shouted = needle.to_uppercase(); + let event = codex + .on_output(&ctx(1), &shouted, NOW) + .expect("the needle fires"); + assert_eq!(event.kind, LimitKind::UsageLimit); + assert_eq!(event.detail, OUTPUT_DETAIL); + assert_eq!(event.resets_at, None, "no time is ever read out of text"); + assert_eq!(event.session_id, None); + assert_eq!(codex.on_output(&ctx(1), &shouted, NOW), None); + } +} + +#[test] +fn a_repeated_output_chunk_does_not_fire_again() { + let mut codex = adapter(); + let chunk = "Codex: You've hit your usage limit. Try again at 3:45 PM."; + assert!(codex.on_output(&ctx(1), chunk, NOW).is_some()); + assert_eq!(codex.on_output(&ctx(1), chunk, NOW), None); +} + +#[test] +fn a_needle_split_across_two_chunks_is_found() { + let mut codex = adapter(); + let (head, tail) = "You've hit your usage limit".split_at(12); + assert_eq!(codex.on_output(&ctx(1), head, NOW), None); + assert!(codex.on_output(&ctx(1), tail, NOW).is_some()); +} + +#[test] +fn ordinary_output_never_fires() { + let mut codex = adapter(); + let chunk = "reading your usage of the limit parser\n"; + assert_eq!(codex.on_output(&ctx(1), chunk, NOW), None); +} + +#[test] +fn a_new_generation_re_arms_the_output_latch() { + let mut codex = adapter(); + let chunk = "You've hit your usage limit"; + assert!(codex.on_output(&ctx(1), chunk, NOW).is_some()); + assert!(codex.on_output(&ctx(2), chunk, NOW).is_some()); +} + +#[test] +fn on_exit_re_arms_the_output_latch() { + let mut codex = adapter(); + let chunk = "Quota exceeded. Check your plan and billing details."; + assert!(codex.on_output(&ctx(1), chunk, NOW).is_some()); + codex.on_exit(&ctx(1)); + assert!(codex.on_output(&ctx(1), chunk, NOW).is_some()); +} + +#[test] +fn on_exit_for_an_unknown_pane_or_an_old_generation_does_nothing() { + let mut codex = adapter(); + codex.on_exit(&ctx(1)); + assert!( + codex + .on_output(&ctx(1), "You've hit your usage limit", NOW) + .is_some() + ); + // Exit of a generation this pane has moved on from must not re-arm the latch. + codex.on_exit(&ctx(0)); + assert_eq!( + codex.on_output(&ctx(1), "You've hit your usage limit", NOW), + None + ); +} + +#[test] +fn resume_while_the_pane_is_alive_holds() { + let limit = LimitEvent::usage(Some(UUID.to_string()), None, USAGE_LIMIT_ERROR_INFO); + assert_eq!( + adapter().resume(&ctx(1), &limit, true), + Some(ResumePlan::Hold(HOLD_ALIVE)) + ); +} + +#[test] +fn resume_after_exit_with_a_session_id_relaunches_with_resume_and_the_id() { + let limit = LimitEvent::usage(Some(UUID.to_string()), None, USAGE_LIMIT_ERROR_INFO); + assert_eq!( + adapter().resume(&ctx(1), &limit, false), + Some(ResumePlan::Relaunch(vec![ + "resume".to_string(), + UUID.to_string() + ])) + ); +} + +#[test] +fn resume_after_exit_without_a_session_id_holds() { + let limit = LimitEvent::usage(None, None, USAGE_LIMIT_ERROR_INFO); + assert_eq!( + adapter().resume(&ctx(1), &limit, false), + Some(ResumePlan::Hold(HOLD_NO_ID)) + ); +} + +#[test] +fn resume_refuses_a_session_id_that_is_not_argv_safe() { + let limit = LimitEvent::usage(Some("a b; rm -rf /".to_string()), None, "x"); + assert_eq!( + adapter().resume(&ctx(1), &limit, false), + Some(ResumePlan::Hold(HOLD_NO_ID)) + ); +} + +#[test] +fn a_session_id_the_output_path_learned_survives_into_the_resume_plan() { + let mut codex = adapter(); + assert!( + codex + .on_output(&ctx(1), "You've hit your usage limit", NOW) + .is_some() + ); + // Nothing bound a rollout, so no id was ever learned: hold rather than guess. + let limit = LimitEvent::usage(None, None, OUTPUT_DETAIL); + assert_eq!( + codex.resume(&ctx(1), &limit, false), + Some(ResumePlan::Hold(HOLD_NO_ID)) + ); +} diff --git a/plugins/nightcrow-recovery/src/provider/codex_pane.rs b/plugins/nightcrow-recovery/src/provider/codex_pane.rs new file mode 100644 index 00000000..a370d3c8 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_pane.rs @@ -0,0 +1,237 @@ +//! Per-pane, per-generation state for the codex adapter: which rollout file this +//! pane's session is writing, how far into it we have read, and the tail of +//! terminal output kept for the fallback needle match. +//! +//! Split out of `codex.rs` to keep both files inside the project's 300-line +//! limit; `codex.rs` keeps the `Provider` contract and this file keeps the +//! watching. + +use super::rollout::{ + MAX_RECORD_BYTES, Record, USAGE_LIMIT_ERROR_INFO, classify_line, session_id_from_filename, +}; +use super::sessions::candidate_rollouts; +use crate::protocol::PaneGeneration; +use crate::provider::LimitEvent; +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::path::{Path, PathBuf}; + +/// Most appended bytes read from a rollout in one poll. A poll must stay short; +/// whatever is left over is read on the next tick. +const MAX_POLL_READ_BYTES: usize = 256 * 1024; + +/// Bytes of recent terminal output kept for needle matching. Enough that a +/// message split across chunks is still matched, small enough to be free. +const OUTPUT_TAIL_BYTES: usize = 4 * 1024; + +/// Every verified phrasing codex uses when it refuses for a limit or billing +/// reason, lowercased for case-insensitive matching. The reset suffix codex +/// appends (" Try again at 3:45 PM.") is deliberately neither matched nor +/// parsed: it is rendered in local time with no offset, so it is ambiguous. +pub(super) const USAGE_LIMIT_NEEDLES: [&str; 4] = [ + "you've hit your usage limit", + "your workspace is out of credits.", + "you hit your spend cap set in your workspace.", + "quota exceeded. check your plan and billing details.", +]; + +/// Detail reported when only terminal text said so. Deliberately says where the +/// belief came from, because this path is the weaker one. +pub(super) const OUTPUT_DETAIL: &str = "usage limit seen in codex output"; + +#[derive(Debug)] +pub(super) struct PaneState { + generation: PaneGeneration, + /// Epoch second watching began. Only a rollout modified at or after this can + /// belong to this generation. + watch_start: i64, + bound: Option, + /// Set when more than one rollout could have been this pane's. Sticky: once + /// the pane cannot be told apart from a sibling pane, binding later is no + /// safer than binding now. + ambiguous: bool, + offset: u64, + /// Bytes of an incomplete trailing line carried to the next poll. + pending: Vec, + session_id: Option, + resets_at: Option, + /// Which window codex reported as reached. Parsed because the record is seen + /// only once, but kept out of `detail`, which carries `codex_error_info` + /// alone so no other provider-side string can widen what this plugin says. + reached_type: Option, + output_tail: String, + output_latched: bool, +} + +impl PaneState { + pub(super) fn new(generation: PaneGeneration, now_epoch: i64) -> Self { + Self { + generation, + watch_start: now_epoch, + bound: None, + ambiguous: false, + offset: 0, + pending: Vec::new(), + session_id: None, + resets_at: None, + reached_type: None, + output_tail: String::new(), + output_latched: false, + } + } + + pub(super) fn generation(&self) -> PaneGeneration { + self.generation + } + + pub(super) fn session_id(&self) -> Option { + self.session_id.clone() + } + + /// Bind to this pane's rollout if that is still possible, then apply whatever + /// was appended since the last call. + pub(super) fn tail(&mut self, sessions_dir: &Path, now_epoch: i64) -> Option { + if self.bound.is_none() { + self.bind(sessions_dir); + } + let path = self.bound.clone()?; + let chunk = self.read_appended(&path)?; + self.consume(&chunk, now_epoch) + } + + /// Bind only when exactly one rollout was modified at or after the watch + /// start: with two, either could be a sibling pane's session, and resuming + /// the wrong one would hijack someone else's work. + fn bind(&mut self, sessions_dir: &Path) { + if self.ambiguous { + return; + } + let mut candidates = candidate_rollouts(sessions_dir, self.watch_start); + match candidates.len() { + 0 => {} + 1 => { + let path = candidates.remove(0); + self.session_id = path + .file_name() + .and_then(|name| name.to_str()) + .and_then(session_id_from_filename); + self.bound = Some(path); + } + _ => self.ambiguous = true, + } + } + + /// Bytes appended since the last read, or `None` when there are none or the + /// file cannot be read. A file shorter than the offset was truncated or + /// replaced, so reading restarts from zero. + fn read_appended(&mut self, path: &Path) -> Option> { + let mut file = File::open(path).ok()?; + let len = file.metadata().ok()?.len(); + if len < self.offset { + self.offset = 0; + self.pending.clear(); + } + let available = len.saturating_sub(self.offset); + if available == 0 { + return None; + } + let want = available.min(MAX_POLL_READ_BYTES as u64); + file.seek(SeekFrom::Start(self.offset)).ok()?; + let mut buf = Vec::new(); + file.take(want).read_to_end(&mut buf).ok()?; + self.offset = self.offset.saturating_add(buf.len() as u64); + Some(buf) + } + + /// Apply every complete line in `chunk`, returning the first limit it + /// reports. Consumed records lie behind the offset, so none fires twice. + fn consume(&mut self, chunk: &[u8], now_epoch: i64) -> Option { + self.pending.extend_from_slice(chunk); + let mut event = None; + while let Some(nl) = self.pending.iter().position(|b| *b == b'\n') { + let record = std::str::from_utf8(&self.pending[..nl]) + .ok() + .and_then(|line| classify_line(line, now_epoch)); + self.pending.drain(..=nl); + if let Some(found) = record.and_then(|r| self.apply(r)) { + event = event.or(Some(found)); + } + } + if self.pending.len() > MAX_RECORD_BYTES { + // A line longer than any real record: drop it rather than buffer a + // corrupt stream without bound. + self.pending.clear(); + } + event + } + + fn apply(&mut self, record: Record) -> Option { + match record { + Record::SessionMeta { id } => { + if id.is_some() { + self.session_id = id; + } + None + } + Record::TokenCount { + resets_at, + reached_type, + } => { + if resets_at.is_some() { + self.resets_at = resets_at; + } + if reached_type.is_some() { + self.reached_type = reached_type; + } + None + } + Record::UsageLimit => Some(LimitEvent::usage( + self.session_id.clone(), + self.resets_at, + USAGE_LIMIT_ERROR_INFO, + )), + } + } + + /// The fallback: match the needles against recent output, at most once per + /// generation. A reset time is never taken from text, so the deadline stays + /// whatever the rollout said. + pub(super) fn on_output(&mut self, text: &str) -> Option { + if self.output_latched { + return None; + } + self.push_output(text); + if !USAGE_LIMIT_NEEDLES + .iter() + .any(|needle| self.output_tail.contains(needle)) + { + return None; + } + self.output_latched = true; + Some(LimitEvent::usage( + self.session_id.clone(), + self.resets_at, + OUTPUT_DETAIL, + )) + } + + /// Let the output fallback fire again: the run it already reported on is over. + pub(super) fn rearm_output(&mut self) { + self.output_latched = false; + self.output_tail.clear(); + } + + /// Keep the tail of recent output, lowercased and bounded, so a needle split + /// across two chunks is still found. + fn push_output(&mut self, text: &str) { + self.output_tail.push_str(&text.to_lowercase()); + if self.output_tail.len() <= OUTPUT_TAIL_BYTES { + return; + } + let cut = self.output_tail.len() - OUTPUT_TAIL_BYTES; + let at = (cut..self.output_tail.len()) + .find(|i| self.output_tail.is_char_boundary(*i)) + .unwrap_or(self.output_tail.len()); + self.output_tail.drain(..at); + } +} diff --git a/plugins/nightcrow-recovery/src/provider/codex_rollout.rs b/plugins/nightcrow-recovery/src/provider/codex_rollout.rs new file mode 100644 index 00000000..b6940a42 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_rollout.rs @@ -0,0 +1,155 @@ +//! Pure parsing of codex rollout (`*.jsonl`) records and rollout file names. +//! +//! Split out of `codex.rs` so the record grammar can be exercised without a +//! filesystem, and so every file stays inside the project's 300-line limit. +//! +//! Every rollout line has the shape +//! `{"timestamp":..,"ordinal":N,"type":"","payload":{..}}`. Only three tags +//! matter to recovery; everything else — including tags added by a future codex +//! release — is ignored silently, because an adapter that fails on unknown +//! records would break on every upgrade. + +use crate::provider::reset_epoch_from_json; +use serde_json::Value; + +/// Longest line handed to the JSON parser. Real rollout records are far smaller; +/// a longer one is a corrupt or concatenated stream, and parsing it would only +/// spend memory on garbage. +pub const MAX_RECORD_BYTES: usize = 64 * 1024; + +/// Longest accepted session id. A uuid is 36 bytes; the slack covers a future id +/// format, and the cap exists because the id becomes a command-line argument. +const MAX_SESSION_ID_BYTES: usize = 128; + +/// Longest remembered `rate_limit_reached_type`. It is a provider enum name, so +/// anything longer is not one and is not worth keeping. +const MAX_REACHED_TYPE_BYTES: usize = 64; + +/// The one `codex_error_info` value that means "usage limit". Any other value is +/// a different failure mode that waiting cannot fix, so it is not ours. +pub const USAGE_LIMIT_ERROR_INFO: &str = "usage_limit_exceeded"; + +/// Payload keys that may carry the session id, most specific first. +const SESSION_ID_KEYS: [&str; 3] = ["id", "session_id", "conversation_id"]; + +/// Where the deadline lives inside a `token_count` payload. +const RESETS_AT_PATH: [&str; 3] = ["rate_limits", "primary", "resets_at"]; + +const ROLLOUT_PREFIX: &str = "rollout-"; +const ROLLOUT_EXT: &str = ".jsonl"; + +/// A codex thread id is a uuid: five dash-separated hex groups of these lengths. +/// The rollout file name ends with it, and the timestamp before it also contains +/// dashes, so the uuid is recognised by shape rather than by position. +const UUID_GROUP_LENS: [usize; 5] = [8, 4, 4, 4, 12]; + +/// A rollout record this adapter acts on. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Record { + /// First line of a session. `id` is `None` when no payload key carried a + /// usable id, in which case the caller falls back to the file name. + SessionMeta { id: Option }, + /// A usage snapshot. `resets_at` is already validated as a plausible + /// absolute unix second, or `None`. + TokenCount { + resets_at: Option, + reached_type: Option, + }, + /// A turn that ended because the usage limit was hit. + UsageLimit, +} + +/// Classify one rollout line. +/// +/// Returns `None` for an over-long line, malformed JSON, a missing or non-string +/// `type`, a tag this adapter does not act on, and a `turn_complete` that is not +/// a usage limit. `now_epoch` is needed only to sanity-check a deadline. +pub fn classify_line(line: &str, now_epoch: i64) -> Option { + if line.len() > MAX_RECORD_BYTES { + return None; + } + let value: Value = serde_json::from_str(line.trim()).ok()?; + let tag = value.get("type")?.as_str()?; + match tag { + "session_meta" => Some(Record::SessionMeta { + id: value.get("payload").and_then(session_id_from_payload), + }), + "token_count" => { + let payload = value.get("payload")?; + Some(Record::TokenCount { + resets_at: reset_epoch_from_json(payload, &RESETS_AT_PATH, now_epoch), + reached_type: reached_type_from_payload(payload), + }) + } + "turn_complete" => { + let info = value + .get("payload")? + .get("error")? + .get("codex_error_info")? + .as_str()?; + (info == USAGE_LIMIT_ERROR_INFO).then_some(Record::UsageLimit) + } + _ => None, + } +} + +/// The session id a `session_meta` payload carries, if any key holds a valid one. +fn session_id_from_payload(payload: &Value) -> Option { + SESSION_ID_KEYS + .iter() + .filter_map(|key| payload.get(key).and_then(Value::as_str)) + .find(|id| valid_session_id(id)) + .map(str::to_string) +} + +/// Which rate-limit window codex says was reached, when it is a short plain +/// string. A long or non-ASCII value is not an enum name and is dropped. +fn reached_type_from_payload(payload: &Value) -> Option { + let raw = payload + .get("rate_limits")? + .get("rate_limit_reached_type")? + .as_str()?; + let ok = !raw.is_empty() + && raw.len() <= MAX_REACHED_TYPE_BYTES + && raw.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_'); + ok.then(|| raw.to_string()) +} + +/// Whether an id is safe to hand back as a command-line argument. +/// +/// A session id reaches the host as `codex resume `, so it must not be +/// empty, must be bounded, and must contain nothing but characters a shell and +/// an argv both treat as ordinary. +pub fn valid_session_id(id: &str) -> bool { + !id.is_empty() + && id.len() <= MAX_SESSION_ID_BYTES + && id + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_') +} + +/// The thread uuid inside `rollout--.jsonl`. +/// +/// The timestamp is itself dash-separated, so the uuid is taken as the trailing +/// five groups and only accepted when each has the expected hex shape. Returns +/// `None` for anything that is not a rollout file name. +pub fn session_id_from_filename(name: &str) -> Option { + let core = name + .strip_prefix(ROLLOUT_PREFIX)? + .strip_suffix(ROLLOUT_EXT)?; + let groups: Vec<&str> = core.split('-').collect(); + if groups.len() < UUID_GROUP_LENS.len() { + return None; + } + let uuid = &groups[groups.len() - UUID_GROUP_LENS.len()..]; + for (group, len) in uuid.iter().zip(UUID_GROUP_LENS) { + if group.len() != len || !group.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + } + Some(uuid.join("-")) +} + +#[cfg(test)] +#[path = "codex_rollout_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/provider/codex_rollout_tests.rs b/plugins/nightcrow-recovery/src/provider/codex_rollout_tests.rs new file mode 100644 index 00000000..d5c6d6b0 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_rollout_tests.rs @@ -0,0 +1,217 @@ +use super::*; +use serde_json::json; + +/// A plausible "now" (2027-01-15) for deadline checks: well past the minimum +/// plausible epoch, so only the value under test decides the outcome. +const NOW: i64 = 1_800_000_000; +/// Inside the accepted horizon. +const SOON: i64 = NOW + 3_600; +/// Outside it (30 days ahead, against an 8-day horizon). +const TOO_FAR: i64 = NOW + 30 * 24 * 60 * 60; + +const UUID: &str = "0199cbb1-2b70-7f11-9f0f-0f8e9d1c2b3a"; + +fn record(tag: &str, payload: Value) -> String { + json!({"timestamp":"2027-01-15T00:00:00Z","ordinal":7,"type":tag,"payload":payload}).to_string() +} + +fn deadline_for(payload: Value) -> Option { + match classify_line(&record("token_count", payload), NOW) { + Some(Record::TokenCount { resets_at, .. }) => resets_at, + other => panic!("expected a token_count record, got {other:?}"), + } +} + +fn rate_limits(primary: Value) -> Value { + json!({"rate_limits": {"primary": primary, "rate_limit_reached_type": "primary"}}) +} + +#[test] +fn session_id_from_filename_on_a_valid_rollout_name_returns_the_uuid() { + let name = format!("rollout-2026-07-30T19-23-45-{UUID}.jsonl"); + assert_eq!(session_id_from_filename(&name).as_deref(), Some(UUID)); +} + +#[test] +fn session_id_from_filename_without_a_uuid_returns_none() { + assert_eq!( + session_id_from_filename("rollout-2026-07-30T19-23-45.jsonl"), + None + ); +} + +#[test] +fn session_id_from_filename_on_an_empty_string_returns_none() { + assert_eq!(session_id_from_filename(""), None); +} + +#[test] +fn session_id_from_filename_with_a_wrong_prefix_returns_none() { + let name = format!("session-2026-07-30T19-23-45-{UUID}.jsonl"); + assert_eq!(session_id_from_filename(&name), None); +} + +#[test] +fn session_id_from_filename_with_a_wrong_extension_returns_none() { + let name = format!("rollout-2026-07-30T19-23-45-{UUID}.json"); + assert_eq!(session_id_from_filename(&name), None); +} + +#[test] +fn session_id_from_filename_with_a_non_hex_uuid_group_returns_none() { + let name = "rollout-2026-07-30T19-23-45-zzzzzzzz-2b70-7f11-9f0f-0f8e9d1c2b3a.jsonl"; + assert_eq!(session_id_from_filename(name), None); +} + +#[test] +fn a_session_meta_prefers_the_payload_id() { + let line = record("session_meta", json!({"id": UUID, "session_id": "other"})); + assert_eq!( + classify_line(&line, NOW), + Some(Record::SessionMeta { + id: Some(UUID.to_string()) + }) + ); +} + +#[test] +fn a_session_meta_falls_through_to_conversation_id() { + let line = record("session_meta", json!({"conversation_id": UUID})); + assert_eq!( + classify_line(&line, NOW), + Some(Record::SessionMeta { + id: Some(UUID.to_string()) + }) + ); +} + +#[test] +fn a_session_meta_whose_id_is_not_argv_safe_reports_no_id() { + let line = record("session_meta", json!({"id": "id with space; rm -rf /"})); + assert_eq!( + classify_line(&line, NOW), + Some(Record::SessionMeta { id: None }) + ); +} + +#[test] +fn a_session_meta_without_any_payload_reports_no_id() { + let line = json!({"type": "session_meta"}).to_string(); + assert_eq!( + classify_line(&line, NOW), + Some(Record::SessionMeta { id: None }) + ); +} + +#[test] +fn a_token_count_with_a_valid_resets_at_is_accepted() { + assert_eq!( + deadline_for(rate_limits(json!({"resets_at": SOON}))), + Some(SOON) + ); +} + +#[test] +fn a_token_count_remembers_a_short_rate_limit_reached_type() { + let payload = rate_limits(json!({"resets_at": SOON})); + assert_eq!( + classify_line(&record("token_count", payload), NOW), + Some(Record::TokenCount { + resets_at: Some(SOON), + reached_type: Some("primary".to_string()), + }) + ); +} + +#[test] +fn a_resets_at_that_is_missing_is_rejected() { + assert_eq!(deadline_for(rate_limits(json!({"used_percent": 90}))), None); +} + +#[test] +fn a_resets_at_that_is_null_is_rejected() { + assert_eq!(deadline_for(rate_limits(json!({"resets_at": null}))), None); +} + +#[test] +fn a_resets_at_that_is_a_string_is_rejected() { + let payload = rate_limits(json!({"resets_at": SOON.to_string()})); + assert_eq!(deadline_for(payload), None); +} + +#[test] +fn a_resets_at_that_is_negative_is_rejected() { + assert_eq!(deadline_for(rate_limits(json!({"resets_at": -1}))), None); +} + +#[test] +fn a_resets_at_far_in_the_future_is_rejected() { + assert_eq!( + deadline_for(rate_limits(json!({"resets_at": TOO_FAR}))), + None + ); +} + +#[test] +fn a_token_count_without_rate_limits_is_rejected() { + assert_eq!(deadline_for(json!({"total_tokens": 12})), None); +} + +#[test] +fn a_token_count_without_primary_is_rejected() { + let payload = json!({"rate_limits": {"secondary": {"resets_at": SOON}}}); + assert_eq!(deadline_for(payload), None); +} + +#[test] +fn a_turn_complete_with_usage_limit_exceeded_is_a_limit() { + let payload = json!({"error": {"codex_error_info": USAGE_LIMIT_ERROR_INFO}}); + assert_eq!( + classify_line(&record("turn_complete", payload), NOW), + Some(Record::UsageLimit) + ); +} + +#[test] +fn a_turn_complete_with_another_error_is_not_classified() { + let payload = json!({"error": {"codex_error_info": "stream_disconnected"}}); + assert_eq!(classify_line(&record("turn_complete", payload), NOW), None); +} + +#[test] +fn a_turn_complete_without_an_error_is_not_classified() { + let payload = json!({"usage": {"input_tokens": 10}}); + assert_eq!(classify_line(&record("turn_complete", payload), NOW), None); +} + +#[test] +fn a_malformed_line_is_not_classified() { + assert_eq!(classify_line("{\"type\":\"token_count\"", NOW), None); + assert_eq!(classify_line("", NOW), None); +} + +#[test] +fn a_record_without_a_string_type_is_not_classified() { + assert_eq!(classify_line(&json!({"type": 3}).to_string(), NOW), None); +} + +#[test] +fn an_unknown_record_type_is_not_classified() { + assert_eq!(classify_line(&record("event_msg", json!({})), NOW), None); +} + +#[test] +fn an_over_long_line_is_not_classified() { + let pad = "a".repeat(MAX_RECORD_BYTES); + let line = record("token_count", json!({"pad": pad})); + assert!(line.len() > MAX_RECORD_BYTES); + assert_eq!(classify_line(&line, NOW), None); +} + +#[test] +fn an_empty_or_over_long_session_id_is_not_argv_safe() { + assert!(!valid_session_id("")); + assert!(!valid_session_id(&"a".repeat(MAX_SESSION_ID_BYTES + 1))); + assert!(valid_session_id(UUID)); + assert!(valid_session_id("plain_id-1")); +} diff --git a/plugins/nightcrow-recovery/src/provider/codex_sessions.rs b/plugins/nightcrow-recovery/src/provider/codex_sessions.rs new file mode 100644 index 00000000..db3dfbe1 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_sessions.rs @@ -0,0 +1,85 @@ +//! Finding the rollout file a codex pane is writing, inside `CODEX_HOME`. +//! +//! Every filesystem error yields no candidate rather than a panic: a missing +//! `CODEX_HOME` is the normal state before codex has ever run in this account. + +use super::rollout::session_id_from_filename; +use std::fs::Metadata; +use std::path::{Path, PathBuf}; +use std::time::UNIX_EPOCH; + +/// Length of the year directory name, and of the month and day directory names. +const YEAR_DIR_LEN: usize = 4; +const MONTH_DAY_DIR_LEN: usize = 2; + +/// How many day directories are searched for the pane's session. +/// +/// The directories are named in *local* time and this crate has no date library, +/// so instead of computing today's name the `sessions/` tree is listed and the +/// lexicographically greatest day directories are taken — zero-padded +/// `YYYY/MM/DD` sorts chronologically. Two of them, because a session started +/// before local midnight keeps writing into yesterday's directory. +const CANDIDATE_DAY_DIRS: usize = 2; + +/// Rollout files in the newest day directories that were modified at or after +/// `since`. +pub(super) fn candidate_rollouts(sessions_dir: &Path, since: i64) -> Vec { + let mut out = Vec::new(); + for day in newest_day_dirs(sessions_dir) { + let Ok(entries) = std::fs::read_dir(&day) else { + continue; + }; + for entry in entries.flatten() { + let named = entry + .file_name() + .to_str() + .and_then(session_id_from_filename) + .is_some(); + let fresh = entry + .metadata() + .ok() + .and_then(|meta| mtime_secs(&meta)) + .is_some_and(|t| t >= since); + if named && fresh { + out.push(entry.path()); + } + } + } + out +} + +fn newest_day_dirs(sessions_dir: &Path) -> Vec { + let mut days = Vec::new(); + for year in numeric_children(sessions_dir, YEAR_DIR_LEN) { + for month in numeric_children(&year, MONTH_DAY_DIR_LEN) { + days.extend(numeric_children(&month, MONTH_DAY_DIR_LEN)); + } + } + days.sort(); + let start = days.len().saturating_sub(CANDIDATE_DAY_DIRS); + days.split_off(start) +} + +/// Subdirectories whose name is exactly `len` ASCII digits, sorted by name. +fn numeric_children(dir: &Path, len: usize) -> Vec { + let Ok(entries) = std::fs::read_dir(dir) else { + return Vec::new(); + }; + let mut out: Vec = entries + .flatten() + .filter(|e| e.file_type().is_ok_and(|t| t.is_dir())) + .filter(|e| { + e.file_name() + .to_str() + .is_some_and(|n| n.len() == len && n.bytes().all(|b| b.is_ascii_digit())) + }) + .map(|e| e.path()) + .collect(); + out.sort(); + out +} + +fn mtime_secs(meta: &Metadata) -> Option { + let since_epoch = meta.modified().ok()?.duration_since(UNIX_EPOCH).ok()?; + i64::try_from(since_epoch.as_secs()).ok() +} diff --git a/plugins/nightcrow-recovery/src/provider/codex_tests.rs b/plugins/nightcrow-recovery/src/provider/codex_tests.rs new file mode 100644 index 00000000..4d3020ec --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/codex_tests.rs @@ -0,0 +1,294 @@ +//! Rollout-tailing tests, against real `CODEX_HOME` trees under a `TempDir`. +//! +//! The exhaustive record-grammar cases (every bad `resets_at` shape, every file +//! name shape) live in `codex_rollout_tests.rs`, next to the parser that decides +//! them; terminal-output and resume cases live in `codex_output_tests.rs`. + +use super::rollout::USAGE_LIMIT_ERROR_INFO; +use super::*; +use crate::protocol::PaneGeneration; +use crate::provider::LimitKind; +use serde_json::{Value, json}; +use std::io::Write as _; +use std::path::Path; +use std::time::{SystemTime, UNIX_EPOCH}; +use tempfile::TempDir; + +const UUID_A: &str = "0199cbb1-2b70-7f11-9f0f-0f8e9d1c2b3a"; +const UUID_B: &str = "0199cbb1-2b70-7f11-9f0f-aaaabbbbcccc"; +/// One local day directory. The adapter takes the newest two that exist, so only +/// the shape has to match codex's. +const DAY_DIR: &str = "sessions/2026/07/30"; +/// Watching starts a minute in the past, so a rollout a test just wrote counts as +/// modified at or after the watch start. +const WATCH_SLACK_SECS: i64 = 60; +/// A deadline inside the accepted horizon. +const RESET_AHEAD_SECS: i64 = 3_600; + +fn now() -> i64 { + let secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + i64::try_from(secs).unwrap_or(0) +} + +fn watch_now() -> i64 { + now() - WATCH_SLACK_SECS +} + +fn ctx(generation: PaneGeneration) -> PaneContext { + PaneContext { + token: "pane-0".to_string(), + generation, + cwd: "/repo".to_string(), + command: Some("codex".to_string()), + } +} + +fn record(tag: &str, payload: Value) -> String { + json!({"timestamp":"2026-07-30T19:23:45Z","ordinal":1,"type":tag,"payload":payload}).to_string() +} + +fn meta(id: Option<&str>) -> String { + let payload = match id { + Some(id) => json!({"id": id}), + None => json!({}), + }; + record("session_meta", payload) +} + +fn token_count(resets_at: Value) -> String { + record("token_count", rate_limits(json!({"resets_at": resets_at}))) +} + +fn rate_limits(primary: Value) -> Value { + json!({"rate_limits": {"primary": primary, "rate_limit_reached_type": "primary"}}) +} + +fn limit_turn() -> String { + record( + "turn_complete", + json!({"error": {"codex_error_info": USAGE_LIMIT_ERROR_INFO}}), + ) +} + +fn joined(lines: &[String]) -> String { + lines.iter().map(|line| format!("{line}\n")).collect() +} + +fn write_rollout(home: &Path, uuid: &str, lines: &[String]) -> PathBuf { + let dir = home.join(DAY_DIR); + std::fs::create_dir_all(&dir).expect("create day directory"); + let path = dir.join(format!("rollout-2026-07-30T19-23-45-{uuid}.jsonl")); + std::fs::write(&path, joined(lines)).expect("write rollout"); + path +} + +fn append(path: &Path, lines: &[String]) { + let mut file = std::fs::OpenOptions::new() + .append(true) + .open(path) + .expect("open rollout for append"); + file.write_all(joined(lines).as_bytes()) + .expect("append rollout lines"); +} + +/// A home holding one rollout with `lines`, plus the result of one poll. The +/// `TempDir` is returned because dropping it would delete the tree. +fn poll_once(lines: &[String]) -> (TempDir, Option) { + let home = TempDir::new().expect("temp home"); + write_rollout(home.path(), UUID_A, lines); + let mut codex = Codex::with_home(home.path().to_path_buf()); + let event = codex.poll(&ctx(1), watch_now()); + (home, event) +} + +/// The deadline a usage limit carries when `payload` was the `token_count` before +/// it. +fn deadline_for(payload: Value) -> Option { + let lines = [ + meta(Some(UUID_A)), + record("token_count", payload), + limit_turn(), + ]; + let (_home, event) = poll_once(&lines); + event.expect("a usage limit event").resets_at +} + +#[test] +fn a_token_count_with_a_valid_resets_at_supplies_the_event_deadline() { + let reset = now() + RESET_AHEAD_SECS; + assert_eq!( + deadline_for(rate_limits(json!({"resets_at": reset}))), + Some(reset) + ); +} + +#[test] +fn a_resets_at_that_is_null_leaves_the_deadline_unknown() { + assert_eq!(deadline_for(rate_limits(json!({"resets_at": null}))), None); +} + +#[test] +fn a_malformed_line_is_skipped_and_later_records_still_parse() { + let reset = now() + RESET_AHEAD_SECS; + let lines = [ + meta(Some(UUID_A)), + "{\"type\":\"token_count\",".to_string(), + token_count(json!(reset)), + limit_turn(), + ]; + let (_home, event) = poll_once(&lines); + let event = event.expect("a usage limit event"); + assert_eq!(event.resets_at, Some(reset)); + assert_eq!(event.kind, LimitKind::UsageLimit); + assert_eq!(event.detail, USAGE_LIMIT_ERROR_INFO); + assert_eq!(event.session_id.as_deref(), Some(UUID_A)); +} + +#[test] +fn a_turn_complete_with_another_error_emits_nothing() { + let other = record( + "turn_complete", + json!({"error": {"codex_error_info": "context_length_exceeded"}}), + ); + let (_home, event) = poll_once(&[meta(Some(UUID_A)), other]); + assert_eq!(event, None); +} + +#[test] +fn a_turn_complete_without_an_error_emits_nothing() { + let clean = record("turn_complete", json!({"usage": {"input_tokens": 1}})); + let (_home, event) = poll_once(&[meta(Some(UUID_A)), clean]); + assert_eq!(event, None); +} + +#[test] +fn a_token_count_arriving_after_the_turn_complete_does_not_fire_a_second_event() { + let reset = now() + RESET_AHEAD_SECS; + let home = TempDir::new().expect("temp home"); + let path = write_rollout( + home.path(), + UUID_A, + &[meta(Some(UUID_A)), token_count(json!(reset)), limit_turn()], + ); + let mut codex = Codex::with_home(home.path().to_path_buf()); + assert!(codex.poll(&ctx(1), watch_now()).is_some()); + append(&path, &[token_count(json!(reset + RESET_AHEAD_SECS))]); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); +} + +#[test] +fn two_rollout_files_and_no_binding_stay_ambiguous_so_resume_holds() { + let home = TempDir::new().expect("temp home"); + let lines = [meta(Some(UUID_A)), limit_turn()]; + write_rollout(home.path(), UUID_A, &lines); + write_rollout(home.path(), UUID_B, &lines); + let mut codex = Codex::with_home(home.path().to_path_buf()); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + // Still ambiguous on a later tick: binding late is no safer than binding now. + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + let limit = LimitEvent::usage(None, None, USAGE_LIMIT_ERROR_INFO); + assert_eq!( + codex.resume(&ctx(1), &limit, false), + Some(ResumePlan::Hold(HOLD_NO_ID)) + ); +} + +#[test] +fn a_single_rollout_file_binds_and_resume_asks_for_exactly_resume_and_the_id() { + let home = TempDir::new().expect("temp home"); + write_rollout(home.path(), UUID_A, &[meta(Some(UUID_A)), limit_turn()]); + let mut codex = Codex::with_home(home.path().to_path_buf()); + let event = codex + .poll(&ctx(1), watch_now()) + .expect("a usage limit event"); + assert_eq!(event.session_id.as_deref(), Some(UUID_A)); + assert_eq!( + codex.resume(&ctx(1), &event, false), + Some(ResumePlan::Relaunch(vec![ + "resume".to_string(), + UUID_A.to_string() + ])) + ); +} + +#[test] +fn a_session_meta_without_an_id_falls_back_to_the_filename_uuid() { + let (_home, event) = poll_once(&[meta(None), limit_turn()]); + let event = event.expect("a usage limit event"); + assert_eq!(event.session_id.as_deref(), Some(UUID_A)); +} + +#[test] +fn appended_lines_are_read_incrementally_and_a_consumed_record_does_not_fire_twice() { + let reset = now() + RESET_AHEAD_SECS; + let home = TempDir::new().expect("temp home"); + let path = write_rollout( + home.path(), + UUID_A, + &[meta(Some(UUID_A)), token_count(json!(reset))], + ); + let mut codex = Codex::with_home(home.path().to_path_buf()); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + append(&path, &[limit_turn()]); + let event = codex + .poll(&ctx(1), watch_now()) + .expect("a usage limit event"); + assert_eq!(event.resets_at, Some(reset)); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); +} + +#[test] +fn a_truncated_rollout_file_is_re_read_from_the_start() { + let home = TempDir::new().expect("temp home"); + let path = write_rollout( + home.path(), + UUID_A, + &[meta(Some(UUID_A)), token_count(json!(null)), limit_turn()], + ); + let mut codex = Codex::with_home(home.path().to_path_buf()); + assert!(codex.poll(&ctx(1), watch_now()).is_some()); + std::fs::write(&path, joined(&[limit_turn()])).expect("truncate rollout"); + let event = codex + .poll(&ctx(1), watch_now()) + .expect("the rewritten record fires again"); + assert_eq!(event.session_id.as_deref(), Some(UUID_A)); +} + +#[test] +fn a_partial_trailing_line_is_only_applied_once_it_is_complete() { + let home = TempDir::new().expect("temp home"); + let path = write_rollout(home.path(), UUID_A, &[meta(Some(UUID_A))]); + let mut codex = Codex::with_home(home.path().to_path_buf()); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + let whole = limit_turn(); + let (head, tail) = whole.split_at(whole.len() / 2); + let mut file = std::fs::OpenOptions::new() + .append(true) + .open(&path) + .expect("open rollout for append"); + file.write_all(head.as_bytes()).expect("write head"); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + file.write_all(format!("{tail}\n").as_bytes()) + .expect("write tail"); + assert!(codex.poll(&ctx(1), watch_now()).is_some()); +} + +#[test] +fn a_missing_codex_home_never_fires_and_never_panics() { + let home = TempDir::new().expect("temp home"); + let mut codex = Codex::with_home(home.path().join("never-created")); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); + assert_eq!(codex.poll(&ctx(1), watch_now()), None); +} + +#[test] +fn a_rollout_older_than_the_watch_start_is_not_bound() { + let home = TempDir::new().expect("temp home"); + write_rollout(home.path(), UUID_A, &[meta(Some(UUID_A)), limit_turn()]); + let mut codex = Codex::with_home(home.path().to_path_buf()); + // Watching starts in the future, so nothing already on disk qualifies. + assert_eq!(codex.poll(&ctx(1), now() + RESET_AHEAD_SECS), None); +} diff --git a/plugins/nightcrow-recovery/src/provider/mod.rs b/plugins/nightcrow-recovery/src/provider/mod.rs new file mode 100644 index 00000000..b7d9b8f4 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/mod.rs @@ -0,0 +1,209 @@ +//! What every provider adapter must be able to answer. +//! +//! The state machine knows nothing about any particular CLI: it asks an adapter +//! "did this pane just hit a usage limit, and when does that limit reset", and +//! later "how do I get this session going again". Everything provider-specific — +//! which hook fires, which file to tail, which flag resumes a session — lives in +//! one file per adapter. + +use crate::protocol::{PaneGeneration, PaneToken}; +use serde_json::Value; + +pub mod claude; +pub mod codex; +pub mod opencode; + +/// Which pane an adapter is being asked about. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaneContext { + pub token: PaneToken, + pub generation: PaneGeneration, + pub cwd: String, + pub command: Option, +} + +/// Why a provider stopped. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LimitKind { + /// A plan/usage limit that clears at a known or estimated time. Worth + /// waiting for. + UsageLimit, + /// An overload or server error. Worth a short backoff, not a long wait. + Transient, + /// Auth, billing, or a bad request. No amount of waiting fixes it, so the + /// machine stops and says so instead of retrying. + NeedsHuman, +} + +/// An adapter's report that a pane's provider stopped for a limit-like reason. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LimitEvent { + /// The provider's own session identifier, when it is known exactly. Without + /// it a resume can only be refused: resuming "the last session" could pick + /// up a different pane's work. + pub session_id: Option, + /// Absolute unix seconds at which the limit clears, when the provider said + /// so. `None` means the machine must fall back to bounded backoff. + pub resets_at: Option, + pub kind: LimitKind, + /// Short, non-sensitive explanation for the host's status line. Never + /// carries transcript text or a raw payload. + pub detail: String, +} + +impl LimitEvent { + pub fn usage(session_id: Option, resets_at: Option, detail: &str) -> Self { + Self { + session_id, + resets_at, + kind: LimitKind::UsageLimit, + detail: detail.to_string(), + } + } +} + +/// A signal that did not come through the pane's terminal output. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalKind { + /// Claude Code's `StopFailure` hook payload. + StopFailure, + /// The `rate_limits` object from Claude Code's statusline payload. + RateLimits, +} + +impl SignalKind { + /// The wire name used on the IPC socket. Anything else is rejected there. + pub fn as_wire(&self) -> &'static str { + match self { + Self::StopFailure => "stop_failure", + Self::RateLimits => "rate_limits", + } + } + + pub fn from_wire(s: &str) -> Option { + match s { + "stop_failure" => Some(Self::StopFailure), + "rate_limits" => Some(Self::RateLimits), + _ => None, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OutOfBand { + pub kind: SignalKind, + pub payload: Value, +} + +/// How to get a stopped session going again. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResumePlan { + /// Append these args to the pane's original command. The host supplies the + /// program, so this can never name a different binary. + Relaunch(Vec), + /// Type this into a pane whose process is still alive and idle. + Input(String), + /// The adapter knows this pane must not be touched yet, and why. + Hold(&'static str), +} + +#[allow(unused_variables)] +pub trait Provider { + /// Stable adapter name, reported in `status` and used in logs. + fn name(&self) -> &'static str; + + /// Terminal text the pane produced. This is the least reliable source — + /// wording changes between releases — so an adapter uses it only as a + /// documented fallback. + fn on_output(&mut self, ctx: &PaneContext, text: &str, now_epoch: i64) -> Option { + None + } + + /// A signal that arrived over the IPC socket. + fn on_signal( + &mut self, + ctx: &PaneContext, + signal: &OutOfBand, + now_epoch: i64, + ) -> Option { + None + } + + /// Called on the plugin's timer, for an adapter that has to look somewhere + /// itself (a rollout file, an HTTP endpoint). + fn poll(&mut self, ctx: &PaneContext, now_epoch: i64) -> Option { + None + } + + /// The pane's process has ended. Adapters that must not act while a + /// provider is retrying internally use this as their gate. + fn on_exit(&mut self, ctx: &PaneContext) {} + + /// How to resume, or `None` when this adapter cannot say safely. + /// + /// `alive` is the host's word that the pane's process is still running; an + /// adapter must only answer [`ResumePlan::Input`] when it is true. + fn resume(&self, ctx: &PaneContext, limit: &LimitEvent, alive: bool) -> Option; +} + +/// Pick an adapter from the pane's command line, or `None` when the pane runs +/// something this plugin knows nothing about — in which case the plugin stays +/// out of the pane entirely. +pub fn detect(command: Option<&str>) -> Option> { + let program = first_word(command?)?; + match program { + "claude" => Some(Box::new(claude::Claude::default())), + "codex" => Some(Box::new(codex::Codex::default())), + "opencode" => Some(Box::new(opencode::OpenCode::default())), + _ => None, + } +} + +/// The command's program name without its directory, so `/usr/local/bin/claude` +/// and `claude --foo` both resolve to `claude`. +fn first_word(command: &str) -> Option<&str> { + let first = command.split_whitespace().next()?; + let base = first.rsplit('/').next().unwrap_or(first); + (!base.is_empty()).then_some(base) +} + +/// Furthest ahead a reported reset time is believed. +/// +/// Claude's longest documented window is seven days, so anything beyond eight is +/// either a different unit or a corrupt value; treating it as absent keeps a +/// bogus number from parking a pane for months. +pub const MAX_RESET_HORIZON_SECS: i64 = 8 * 24 * 60 * 60; + +/// Earliest plausible unix second for a reset time (2020-01-01). Below this a +/// value is a duration, a millisecond count that lost precision, or garbage. +const MIN_PLAUSIBLE_EPOCH_SECS: i64 = 1_577_836_800; + +/// Read a reset time from provider JSON as absolute unix seconds. +/// +/// Returns `None` for a missing field, a non-number, a non-integer, a value +/// outside the plausible band, and a time already far in the past — every one of +/// which must degrade to bounded backoff rather than to a wait of the wrong +/// length. +pub fn reset_epoch_from_json(payload: &Value, path: &[&str], now_epoch: i64) -> Option { + let mut node = payload; + for key in path { + node = node.get(key)?; + } + let secs = node.as_i64()?; + plausible_reset(secs, now_epoch) +} + +/// Accept a reset time only if it could really be one. +pub fn plausible_reset(secs: i64, now_epoch: i64) -> Option { + if secs < MIN_PLAUSIBLE_EPOCH_SECS { + return None; + } + if secs > now_epoch.saturating_add(MAX_RESET_HORIZON_SECS) { + return None; + } + Some(secs) +} + +#[cfg(test)] +#[path = "mod_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/provider/mod_tests.rs b/plugins/nightcrow-recovery/src/provider/mod_tests.rs new file mode 100644 index 00000000..9b52ebb6 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/mod_tests.rs @@ -0,0 +1,103 @@ +use super::*; + +/// A readable fixed "now": 2026-01-01T00:00:00Z. +const NOW: i64 = 1_767_225_600; + +#[test] +fn a_reset_time_in_the_documented_shape_is_read_as_unix_seconds() { + let payload = serde_json::json!({"five_hour": {"resets_at": NOW + 3600}}); + assert_eq!( + reset_epoch_from_json(&payload, &["five_hour", "resets_at"], NOW), + Some(NOW + 3600) + ); +} + +#[test] +fn a_reset_time_that_is_absent_is_not_invented() { + let payload = serde_json::json!({"five_hour": {}}); + assert_eq!( + reset_epoch_from_json(&payload, &["five_hour", "resets_at"], NOW), + None + ); + assert_eq!(reset_epoch_from_json(&payload, &["seven_day"], NOW), None); +} + +#[test] +fn a_reset_time_of_the_wrong_type_is_refused_rather_than_coerced() { + for value in [ + serde_json::json!("1767225600"), + serde_json::json!(null), + serde_json::json!(1.5), + serde_json::json!([NOW]), + serde_json::json!({"secs": NOW}), + ] { + let payload = serde_json::json!({"w": {"resets_at": value}}); + assert_eq!( + reset_epoch_from_json(&payload, &["w", "resets_at"], NOW), + None, + "{value:?} is not a unix second" + ); + } +} + +#[test] +fn a_reset_time_outside_the_plausible_band_is_treated_as_unknown() { + assert_eq!(plausible_reset(0, NOW), None); + assert_eq!(plausible_reset(-1, NOW), None); + // A millisecond timestamp is far past the horizon, not a reset time. + assert_eq!(plausible_reset(NOW * 1000, NOW), None); + assert_eq!(plausible_reset(NOW + MAX_RESET_HORIZON_SECS + 1, NOW), None); + assert_eq!( + plausible_reset(NOW + MAX_RESET_HORIZON_SECS, NOW), + Some(NOW + MAX_RESET_HORIZON_SECS) + ); +} + +#[test] +fn a_reset_time_already_in_the_past_is_still_a_reset_time() { + // Stale, not implausible: the state machine's minimum wait handles it, and + // discarding it here would lose the only deadline we were given. + assert_eq!(plausible_reset(NOW - 60, NOW), Some(NOW - 60)); +} + +#[test] +fn each_known_provider_is_recognised_from_its_command_line() { + for (command, name) in [ + ("claude", "claude"), + ("claude --model opus", "claude"), + ("/usr/local/bin/claude", "claude"), + ("codex", "codex"), + ("codex resume --last", "codex"), + ("opencode", "opencode"), + (" opencode --port 5000", "opencode"), + ] { + let provider = detect(Some(command)).unwrap_or_else(|| panic!("{command} is known")); + assert_eq!(provider.name(), name); + } +} + +#[test] +fn a_pane_running_something_else_is_not_watched_at_all() { + for command in [ + None, + Some(""), + Some(" "), + Some("bash"), + Some("zsh -l"), + Some("claudette"), + ] { + assert!( + detect(command).is_none(), + "{command:?} must not be adopted by any adapter" + ); + } +} + +#[test] +fn a_signal_kind_round_trips_through_its_wire_name() { + for kind in [SignalKind::StopFailure, SignalKind::RateLimits] { + assert_eq!(SignalKind::from_wire(kind.as_wire()), Some(kind)); + } + assert_eq!(SignalKind::from_wire("transcript"), None); + assert_eq!(SignalKind::from_wire(""), None); +} diff --git a/plugins/nightcrow-recovery/src/provider/opencode.rs b/plugins/nightcrow-recovery/src/provider/opencode.rs new file mode 100644 index 00000000..76022285 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/opencode.rs @@ -0,0 +1,296 @@ +//! OpenCode adapter — deliberately observe-only. +//! +//! OpenCode retries a retryable API error *without bound*: there is no +//! max-attempt constant, the backoff starts at 2 s and doubles, and the 30 s cap +//! applies only when the response carried no `retry-after` header — with one the +//! cap is ~24.8 days. So "wait for the retries to run out" is a state this +//! adapter can never reach, and a pane in `retry` is hands off: no input, no +//! relaunch, no abort. It only reports, and only once the retry is demonstrably +//! over — the session went `idle`, or the process exited. +//! +//! State comes from the local server's `GET /session/status`, which is +//! first-class server state rather than screen scraping. Terminal text is not +//! consulted at all; see [`Provider::on_output`] below for why. + +use super::{LimitEvent, PaneContext, Provider, ResumePlan}; +use crate::protocol::PaneGeneration; +use std::time::Duration; + +#[path = "opencode_http.rs"] +mod http; + +pub use http::{ + SessionStatus, StatusKind, StatusSource, http_get, interpret_next, parse_status_body, +}; + +/// Port OpenCode's local server binds unless `--port` says otherwise. +pub const DEFAULT_PORT: u16 = 4096; + +/// Override for a user who always runs the server elsewhere. A `--port` on the +/// pane's own command line wins over it: that is the truth about *this* process. +const PORT_ENV: &str = "NIGHTCROW_OPENCODE_PORT"; + +/// Snapshot of every session the server knows about. +const STATUS_PATH: &str = "/session/status"; + +/// Flags that carry the server port on a command line. +const PORT_FLAGS: &[&str] = &["--port", "-p"]; + +/// Shortest gap between two status requests. The host's timer can tick far +/// faster than this, and a snapshot is not worth a request per tick. +const MIN_POLL_INTERVAL_SECS: i64 = 5; + +/// Socket budget for one status request. Loopback, so anything slower means the +/// server is wedged and waiting longer would only stall the plugin's loop. +const HTTP_TIMEOUT: Duration = Duration::from_millis(1_500); + +/// A session id is handed back as a command-line argument, so it is bounded. +/// Loose but finite: real ids are short, and an over-long value is a bug. +const MAX_SESSION_ID_BYTES: usize = 64; + +/// Resumes a named session. Never `--continue`/`-c`, which resumes *the last* +/// session and would happily pick up another pane's work. +const RESUME_FLAG: &str = "--session"; + +const RETRY_ENDED_DETAIL: &str = "opencode stopped retrying without producing a result"; +const ALIVE_HOLD: &str = "opencode retries internally without bound; never interrupt a retry"; +const NO_SESSION_HOLD: &str = + "no opencode session id; --continue could resume another pane's session"; + +/// A retry this adapter saw, and what it managed to learn from it. +#[derive(Debug, Clone, Default)] +struct Retrying { + session_id: Option, + resets_at: Option, +} + +/// Adapter state for one pane. Nothing here is written to disk, and no message +/// text from a status payload is retained. +#[derive(Debug, Default)] +pub struct OpenCode { + /// `None` until something states a port, so the env fallback stays lazy and + /// an explicit `--port` is distinguishable from the default. + port: Option, + /// Injected snapshot source. `None` means the HTTP source, resolved per poll + /// so [`Self::observe_command`] can still change the port. + source: Option>, + /// Generation this state belongs to; a change re-arms everything. + generation: Option, + retrying: Option, + exited: bool, + fired: bool, + last_poll: Option, +} + +impl OpenCode { + /// Testing seam: replace the status source. The HTTP source is the default. + #[cfg(test)] + pub fn with_status_source(source: Box) -> Self { + Self { + source: Some(source), + ..Self::default() + } + } + + /// Port this adapter would query. + pub fn port(&self) -> u16 { + self.port.or_else(env_port).unwrap_or(DEFAULT_PORT) + } + + /// Learn the port from the pane's own command line (`--port N` / `-p N`). An + /// absent flag or an unparsable value leaves the port as it was. + pub fn observe_command(&mut self, command: &str) { + if let Some(port) = port_from_command(command) { + self.port = Some(port); + } + } + + fn sync_generation(&mut self, ctx: &PaneContext) { + if let Some(command) = &ctx.command { + self.observe_command(command); + } + if self.generation == Some(ctx.generation) { + return; + } + self.generation = Some(ctx.generation); + // A respawn is a different process and a different session: nothing + // learned about the old one may be reported against the new one. + self.retrying = None; + self.exited = false; + self.fired = false; + self.last_poll = None; + } + + fn fetch_status(&mut self) -> anyhow::Result { + match &mut self.source { + Some(source) => source.fetch(), + None => http_get(self.port(), STATUS_PATH, HTTP_TIMEOUT), + } + } + + fn due(&self, now_epoch: i64) -> bool { + match self.last_poll { + None => true, + // A clock that moved backwards also falls outside the window, which + // re-arms polling instead of stalling until the clock catches up. + Some(last) => !(last..last.saturating_add(MIN_POLL_INTERVAL_SECS)).contains(&now_epoch), + } + } + + fn remember_retry(&mut self, status: &SessionStatus, now_epoch: i64) { + let resets_at = match status.kind { + StatusKind::Retry { + next: Some(next), .. + } => interpret_next(next, now_epoch), + _ => None, + }; + let known = self.retrying.get_or_insert_default(); + // A later snapshot may omit what an earlier one told us, so only ever + // fill a gap — never overwrite a known value with None. + if status.session_id.is_some() { + known.session_id = status.session_id.clone(); + } + if resets_at.is_some() { + known.resets_at = resets_at; + } + } + + /// Did the session we watched retrying go idle in this snapshot? + fn went_idle(&self, statuses: &[SessionStatus]) -> bool { + let watched = self.retrying.as_ref().and_then(|r| r.session_id.as_deref()); + statuses.iter().any(|s| { + matches!(s.kind, StatusKind::Idle) + && match (watched, s.session_id.as_deref()) { + (Some(want), Some(got)) => want == got, + // With an id missing on either side the snapshot cannot be + // attributed; counting it is the only alternative to never + // reporting at all. + _ => true, + } + }) + } + + fn emit(&mut self, now_epoch: i64) -> Option { + let retrying = self.retrying.clone()?; + self.fired = true; + Some(LimitEvent::usage( + retrying + .session_id + .as_deref() + .and_then(validated_session_id), + // A deadline already past is worse than none: it would tell the + // machine to resume immediately, into the same limit. + retrying.resets_at.filter(|at| *at > now_epoch), + RETRY_ENDED_DETAIL, + )) + } +} + +impl Provider for OpenCode { + fn name(&self) -> &'static str { + "opencode" + } + + fn on_output( + &mut self, + _ctx: &PaneContext, + _text: &str, + _now_epoch: i64, + ) -> Option { + // Intentionally blind: OpenCode's TUI retry format string is unverified, + // so any needle list here would be a guess, and a wrong guess parks a + // healthy pane. The status endpoint is authoritative; the screen is not. + None + } + + fn poll(&mut self, ctx: &PaneContext, now_epoch: i64) -> Option { + self.sync_generation(ctx); + if self.fired { + return None; + } + // The process is gone, so the last thing we saw is final and there is + // nothing left on the server worth asking about. + if self.exited { + return self.emit(now_epoch); + } + if !self.due(now_epoch) { + return None; + } + self.last_poll = Some(now_epoch); + // No server, a non-200, or an unreadable body is ordinary — the user need + // not be running the server at all. Swallow it, and let the interval keep + // the next attempt from becoming a tight loop. + let statuses = parse_status_body(&self.fetch_status().ok()?); + if let Some(status) = statuses + .iter() + .find(|s| matches!(s.kind, StatusKind::Retry { .. })) + { + self.remember_retry(status, now_epoch); + return None; + } + if self.retrying.is_some() && self.went_idle(&statuses) { + return self.emit(now_epoch); + } + None + } + + fn on_exit(&mut self, ctx: &PaneContext) { + self.sync_generation(ctx); + self.exited = true; + } + + fn resume(&self, _ctx: &PaneContext, limit: &LimitEvent, alive: bool) -> Option { + // Never `ResumePlan::Input`: a live pane may be mid-retry, and typing at + // one is exactly what this adapter exists to avoid. + if alive { + return Some(ResumePlan::Hold(ALIVE_HOLD)); + } + let Some(id) = limit.session_id.as_deref().and_then(validated_session_id) else { + return Some(ResumePlan::Hold(NO_SESSION_HOLD)); + }; + Some(ResumePlan::Relaunch(vec![RESUME_FLAG.to_string(), id])) + } +} + +/// First `--port`/`-p` value on a command line, in either `--port N` or +/// `--port=N` form. `None` for an absent flag, a non-numeric value, or port 0, +/// which nothing can be reached at. +fn port_from_command(command: &str) -> Option { + let mut tokens = command.split_whitespace(); + while let Some(token) = tokens.next() { + let value = match token.split_once('=') { + Some((flag, inline)) if PORT_FLAGS.contains(&flag) => inline, + _ if PORT_FLAGS.contains(&token) => tokens.next()?, + _ => continue, + }; + return value.parse().ok().filter(|p| *p != 0); + } + None +} + +fn env_port() -> Option { + std::env::var(PORT_ENV) + .ok()? + .parse() + .ok() + .filter(|p| *p != 0) +} + +/// Accept a session id only if it is safe to hand back as a command-line +/// argument: non-empty, bounded, and made of ASCII alphanumerics, `-`, or `_`. +fn validated_session_id(raw: &str) -> Option { + if raw.is_empty() || raw.len() > MAX_SESSION_ID_BYTES { + return None; + } + if !raw + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return None; + } + Some(raw.to_string()) +} + +#[cfg(test)] +#[path = "opencode_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/provider/opencode_http.rs b/plugins/nightcrow-recovery/src/provider/opencode_http.rs new file mode 100644 index 00000000..151c59e7 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/opencode_http.rs @@ -0,0 +1,228 @@ +//! The OpenCode server's wire side: how a status snapshot is fetched, and how +//! the payload is read once it arrives. +//! +//! Split out of `opencode.rs` so the adapter's state machine reads as state +//! transitions only. Everything here is either a pure function over bytes or a +//! single loopback request, and nothing here decides anything about a pane. + +use crate::provider::{MAX_RESET_HORIZON_SECS, plausible_reset}; +use anyhow::Context; +use serde_json::Value; +use std::io::{Read, Write}; +use std::net::{Ipv4Addr, SocketAddr, TcpStream}; +use std::time::Duration; + +/// Ceiling on one whole response, headers included. A status snapshot is a few +/// KiB; the cap exists so a wedged or hostile socket cannot make this process +/// allocate without bound. +const MAX_RESPONSE_BYTES: usize = 256 * 1024; + +/// Boundary between an HTTP head and its body. +const HEAD_BODY_SEPARATOR: &str = "\r\n\r\n"; + +/// The only status code whose body is worth parsing. +const OK_STATUS: &str = "200"; + +/// Divisor between the millisecond and second readings of an ambiguous `next`. +const MILLIS_PER_SEC: i64 = 1_000; + +/// Longest `next` accepted as a *relative* delay, in milliseconds — OpenCode +/// states its own backoff in milliseconds. Bounded by the same horizon the rest +/// of the plugin trusts, so a relative delay cannot park a pane for longer than +/// an absolute reset time could. +const MAX_RELATIVE_NEXT_MILLIS: i64 = MAX_RESET_HORIZON_SECS * MILLIS_PER_SEC; + +/// Keys an entry may carry its session id under. The envelope's schema is +/// unverified, so every plausible spelling is accepted. +const SESSION_ID_KEYS: &[&str] = &["sessionID", "sessionId", "id"]; + +/// Where a status snapshot comes from. Exists so the state logic is testable +/// without a listening socket. +pub trait StatusSource: std::fmt::Debug { + fn fetch(&mut self) -> anyhow::Result; +} + +/// One session's status, as far as we could understand it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SessionStatus { + pub session_id: Option, + pub kind: StatusKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StatusKind { + Retry { + attempt: u32, + next: Option, + }, + Busy, + Idle, + /// A `type` we do not model. Kept rather than dropped so a future status + /// value reads as "not a retry" instead of as "no session here". + Unknown, +} + +/// Read a status snapshot, tolerating both envelope shapes seen in the wild: an +/// object keyed by session id, and an array of per-session entries. +/// +/// Never errors. Malformed JSON, a bare scalar, or an entry shaped a third way +/// yields no statuses, because one unreadable poll must not fail the plugin. +pub fn parse_status_body(body: &str) -> Vec { + let Ok(root) = serde_json::from_str::(body) else { + return Vec::new(); + }; + match &root { + Value::Object(map) => map + .iter() + .filter_map(|(key, entry)| read_entry(entry, Some(key.as_str()))) + .collect(), + Value::Array(items) => items.iter().filter_map(|e| read_entry(e, None)).collect(), + _ => Vec::new(), + } +} + +/// Turn one entry into a status, or `None` when it holds no status object. +fn read_entry(entry: &Value, key: Option<&str>) -> Option { + let status = status_object(entry)?; + let session_id = SESSION_ID_KEYS + .iter() + .find_map(|k| entry.get(k).and_then(Value::as_str)) + .or(key) + .map(str::to_string); + Some(SessionStatus { + session_id, + kind: status_kind(status), + }) +} + +/// The entry either *is* the status object or wraps one under `status`. +fn status_object(entry: &Value) -> Option<&Value> { + if entry.get("type").is_some() { + return Some(entry); + } + let nested = entry.get("status")?; + nested.get("type").map(|_| nested) +} + +fn status_kind(status: &Value) -> StatusKind { + match status.get("type").and_then(Value::as_str) { + Some("retry") => StatusKind::Retry { + // Informational only — nothing is decided from the attempt number — + // so a missing one is not a parse failure. + attempt: status + .get("attempt") + .and_then(Value::as_u64) + .and_then(|n| u32::try_from(n).ok()) + .unwrap_or(0), + next: status.get("next").and_then(Value::as_i64), + }, + Some("busy") => StatusKind::Busy, + Some("idle") => StatusKind::Idle, + _ => StatusKind::Unknown, + } +} + +/// Resolve the ambiguous `next` field to an absolute unix time in **seconds**. +/// +/// Whether OpenCode reports an absolute epoch (in which unit) or a relative +/// delay is unverified, so all three readings are tried. The order is by safety +/// rather than by likelihood: absolute readings come first, because over-waiting +/// only costs time while firing early walks straight back into the limit. `None` +/// means "no deadline", which degrades to the machine's own bounded backoff. +pub fn interpret_next(next: i64, now_epoch: i64) -> Option { + // Zero or negative is "now" or a corrupt value; both would fire immediately, + // so neither is accepted as a deadline. + if next <= 0 { + return None; + } + if let Some(at) = plausible_reset(next, now_epoch) { + return Some(at); + } + if let Some(at) = plausible_reset(next / MILLIS_PER_SEC, now_epoch) { + return Some(at); + } + if next <= MAX_RELATIVE_NEXT_MILLIS { + return Some(now_epoch.saturating_add(next / MILLIS_PER_SEC)); + } + None +} + +/// `GET` one loopback path and return the response body. +/// +/// Loopback only, by construction: the host is not a parameter, so this can +/// never be pointed at a remote server. `timeout` bounds the connect, the write +/// and the read separately, so no single stage can hang the plugin's loop. +/// +/// Deliberately no transfer-encoding handling: a chunked answer comes back with +/// its framing intact, [`parse_status_body`] then finds no statuses in it, and +/// the poll degrades to "nothing to report" — the same outcome as no server at +/// all. That is the right failure for an adapter that must never guess. +pub fn http_get(port: u16, path: &str, timeout: Duration) -> anyhow::Result { + anyhow::ensure!( + is_safe_path(path), + "refusing opencode request path {path:?}: only /, -, _ and ASCII alphanumerics may reach \ + the request line" + ); + let addr = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); + let mut stream = TcpStream::connect_timeout(&addr, timeout) + .map_err(|e| anyhow::anyhow!("cannot reach the opencode server at {addr}: {e}"))?; + stream + .set_read_timeout(Some(timeout)) + .and_then(|()| stream.set_write_timeout(Some(timeout))) + .context("cannot bound the opencode socket's read and write time")?; + let request = format!( + "GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nAccept: application/json\r\n\ + Connection: close\r\n\r\n" + ); + stream + .write_all(request.as_bytes()) + .and_then(|()| stream.flush()) + .with_context(|| format!("cannot send GET {path} to the opencode server"))?; + let raw = read_capped(&mut stream).with_context(|| format!("reading GET {path}"))?; + let text = String::from_utf8(raw) + .map_err(|_| anyhow::anyhow!("opencode server answered with non-UTF-8 bytes"))?; + let (head, body) = text + .split_once(HEAD_BODY_SEPARATOR) + .ok_or_else(|| anyhow::anyhow!("opencode server answer has no header/body boundary"))?; + let code = head + .lines() + .next() + .unwrap_or_default() + .split_whitespace() + .nth(1) + .unwrap_or_default(); + anyhow::ensure!( + code == OK_STATUS, + "opencode server answered status {code:?}, not {OK_STATUS}" + ); + Ok(body.to_string()) +} + +/// Read at most [`MAX_RESPONSE_BYTES`], and refuse rather than truncate. +/// +/// Reads one byte past the cap so an over-long answer is *detected* — silently +/// truncating would hand the parser a body that only looks malformed. +fn read_capped(source: impl Read) -> anyhow::Result> { + let mut buf = Vec::new(); + let mut limited = source.take(MAX_RESPONSE_BYTES as u64 + 1); + limited.read_to_end(&mut buf)?; + anyhow::ensure!( + buf.len() <= MAX_RESPONSE_BYTES, + "opencode server answer exceeds the {MAX_RESPONSE_BYTES}-byte cap" + ); + Ok(buf) +} + +/// An absolute path made only of characters that cannot alter the request line. +/// Whitespace, CR and LF are the ones that matter: any of them would let a +/// caller append headers or a second request. +fn is_safe_path(path: &str) -> bool { + path.starts_with('/') + && path + .chars() + .all(|c| c.is_ascii_alphanumeric() || matches!(c, '/' | '-' | '_')) +} + +#[cfg(test)] +#[path = "opencode_http_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/provider/opencode_http_tests.rs b/plugins/nightcrow-recovery/src/provider/opencode_http_tests.rs new file mode 100644 index 00000000..d2167cfe --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/opencode_http_tests.rs @@ -0,0 +1,215 @@ +use super::*; + +/// Fixed "now" well inside the plausible epoch band, so every case below is +/// decided by the value under test rather than by the clock. +const NOW: i64 = 1_800_000_000; + +/// Port used by the path-rejection tests. They must fail before any connect is +/// attempted, so nothing is expected to be listening here. +const UNUSED_PORT: u16 = 4096; + +fn refusal(path: &str) -> String { + http_get(UNUSED_PORT, path, Duration::from_millis(1)) + .unwrap_err() + .to_string() +} + +#[test] +fn an_absolute_epoch_in_seconds_is_taken_as_the_reset_time() { + assert_eq!(interpret_next(NOW + 3600, NOW), Some(NOW + 3600)); +} + +#[test] +fn an_absolute_epoch_in_milliseconds_is_divided_down_to_seconds() { + assert_eq!(interpret_next((NOW + 3600) * 1_000, NOW), Some(NOW + 3600)); +} + +#[test] +fn a_small_millisecond_delay_is_taken_as_relative_to_now() { + assert_eq!(interpret_next(30_000, NOW), Some(NOW + 30)); +} + +#[test] +fn a_sub_second_relative_delay_resolves_to_now() { + assert_eq!(interpret_next(500, NOW), Some(NOW)); +} + +#[test] +fn a_next_of_zero_is_not_a_deadline() { + assert_eq!(interpret_next(0, NOW), None); +} + +#[test] +fn a_negative_next_is_not_a_deadline() { + assert_eq!(interpret_next(-5_000, NOW), None); + assert_eq!(interpret_next(i64::MIN, NOW), None); +} + +#[test] +fn an_absurdly_large_next_is_not_a_deadline() { + assert_eq!(interpret_next(i64::MAX, NOW), None); +} + +#[test] +fn an_absolute_next_just_past_the_reset_horizon_is_not_a_deadline() { + let past = NOW + MAX_RESET_HORIZON_SECS + 1; + assert_eq!(interpret_next(past, NOW), None); + assert_eq!( + interpret_next(NOW + MAX_RESET_HORIZON_SECS, NOW), + Some(past - 1) + ); +} + +#[test] +fn a_relative_delay_is_accepted_up_to_the_horizon_and_refused_past_it() { + assert_eq!( + interpret_next(MAX_RELATIVE_NEXT_MILLIS, NOW), + Some(NOW + MAX_RESET_HORIZON_SECS) + ); + assert_eq!(interpret_next(MAX_RELATIVE_NEXT_MILLIS + 1, NOW), None); +} + +#[test] +fn an_object_keyed_by_session_id_yields_one_status_per_key() { + let statuses = parse_status_body(r#"{"ses_abc":{"type":"retry","attempt":3,"next":4000}}"#); + assert_eq!( + statuses, + vec![SessionStatus { + session_id: Some("ses_abc".to_string()), + kind: StatusKind::Retry { + attempt: 3, + next: Some(4000) + }, + }] + ); +} + +#[test] +fn an_array_of_entries_yields_one_status_per_element() { + let statuses = parse_status_body( + r#"[{"sessionID":"a","status":{"type":"busy"}},{"id":"b","type":"idle"}]"#, + ); + assert_eq!(statuses.len(), 2); + assert_eq!(statuses[0].session_id.as_deref(), Some("a")); + assert_eq!(statuses[0].kind, StatusKind::Busy); + assert_eq!(statuses[1].session_id.as_deref(), Some("b")); + assert_eq!(statuses[1].kind, StatusKind::Idle); +} + +#[test] +fn a_retry_without_next_parses_with_no_deadline() { + let statuses = parse_status_body(r#"{"s":{"type":"retry","attempt":1}}"#); + assert_eq!( + statuses[0].kind, + StatusKind::Retry { + attempt: 1, + next: None + } + ); +} + +#[test] +fn a_retry_with_an_unusable_attempt_number_parses_as_attempt_zero() { + for body in [ + r#"{"s":{"type":"retry","attempt":-1}}"#, + r#"{"s":{"type":"retry","attempt":"many"}}"#, + r#"{"s":{"type":"retry"}}"#, + ] { + assert_eq!( + parse_status_body(body)[0].kind, + StatusKind::Retry { + attempt: 0, + next: None + }, + "body {body}" + ); + } +} + +#[test] +fn a_busy_status_parses_as_busy() { + assert_eq!( + parse_status_body(r#"{"s":{"type":"busy"}}"#)[0].kind, + StatusKind::Busy + ); +} + +#[test] +fn an_idle_status_parses_as_idle() { + assert_eq!( + parse_status_body(r#"{"s":{"type":"idle"}}"#)[0].kind, + StatusKind::Idle + ); +} + +#[test] +fn an_unrecognised_type_parses_as_unknown() { + for body in [ + r#"{"s":{"type":"queued"}}"#, + r#"{"s":{"type":42}}"#, + r#"{"s":{"type":null}}"#, + ] { + assert_eq!( + parse_status_body(body)[0].kind, + StatusKind::Unknown, + "{body}" + ); + } +} + +#[test] +fn an_empty_body_yields_no_statuses() { + assert!(parse_status_body("").is_empty()); + assert!(parse_status_body(" ").is_empty()); +} + +#[test] +fn malformed_json_yields_no_statuses() { + assert!(parse_status_body("{not json").is_empty()); + assert!(parse_status_body(r#"{"s":{"type":"idle"}"#).is_empty()); +} + +#[test] +fn a_json_scalar_instead_of_a_container_yields_no_statuses() { + for body in ["42", "null", "true", r#""idle""#] { + assert!(parse_status_body(body).is_empty(), "{body}"); + } +} + +#[test] +fn entries_holding_no_status_object_are_ignored() { + assert!(parse_status_body(r#"{"a":1,"b":{"foo":2},"c":null}"#).is_empty()); + assert!(parse_status_body(r#"[1,"x",{"status":{}}]"#).is_empty()); +} + +#[test] +fn a_request_path_containing_a_space_is_refused_before_connecting() { + assert!(refusal("/session status").contains("refusing")); +} + +#[test] +fn a_request_path_containing_a_carriage_return_or_newline_is_refused() { + assert!(refusal("/a\r\nX-Evil: 1").contains("refusing")); + assert!(refusal("/a\nb").contains("refusing")); + assert!(refusal("/a\rb").contains("refusing")); +} + +#[test] +fn a_path_that_is_not_an_absolute_ascii_word_path_is_refused() { + for path in [ + "", + "session/status", + "/session?x=1", + "/session/../etc", + "/sé", + ] { + assert!(!is_safe_path(path), "{path}"); + } +} + +#[test] +fn the_status_path_shape_passes_the_path_filter() { + for path in ["/session/status", "/event", "/a-b_c/9"] { + assert!(is_safe_path(path), "{path}"); + } +} diff --git a/plugins/nightcrow-recovery/src/provider/opencode_tests.rs b/plugins/nightcrow-recovery/src/provider/opencode_tests.rs new file mode 100644 index 00000000..78ec7d05 --- /dev/null +++ b/plugins/nightcrow-recovery/src/provider/opencode_tests.rs @@ -0,0 +1,286 @@ +use super::*; +use crate::provider::LimitKind; +use serde_json::Value; + +/// Fixed "now", well inside the plausible epoch band. +const NOW: i64 = 1_800_000_000; + +/// Session id used wherever the id itself is not what a test is about. +const SESSION: &str = "ses_1"; + +/// Status bodies handed out one per fetch; the last one repeats forever, so a +/// test can keep polling without restating the snapshot. +#[derive(Debug)] +struct Scripted { + bodies: Vec, + at: usize, +} + +impl Scripted { + fn new(bodies: Vec) -> Box { + Box::new(Self { bodies, at: 0 }) + } +} + +impl StatusSource for Scripted { + fn fetch(&mut self) -> anyhow::Result { + let body = self + .bodies + .get(self.at) + .or_else(|| self.bodies.last()) + .cloned() + .unwrap_or_default(); + self.at += 1; + Ok(body) + } +} + +/// Stands in for "the user is not running the server". +#[derive(Debug)] +struct Unreachable; + +impl StatusSource for Unreachable { + fn fetch(&mut self) -> anyhow::Result { + anyhow::bail!("connection refused") + } +} + +fn ctx(generation: PaneGeneration) -> PaneContext { + PaneContext { + token: "t0".to_string(), + generation, + cwd: "/repo".to_string(), + command: Some("opencode".to_string()), + } +} + +fn wrap(id: &str, status: Value) -> String { + let mut map = serde_json::Map::new(); + map.insert(id.to_string(), status); + Value::Object(map).to_string() +} + +fn retry_body(id: &str, next: Option) -> String { + let mut status = serde_json::json!({"type": "retry", "attempt": 2}); + if let Some(next) = next { + status["next"] = next.into(); + } + wrap(id, status) +} + +fn idle_body(id: &str) -> String { + wrap(id, serde_json::json!({"type": "idle"})) +} + +fn adapter(bodies: Vec) -> OpenCode { + OpenCode::with_status_source(Scripted::new(bodies)) +} + +#[test] +fn the_adapter_reports_itself_as_opencode() { + assert_eq!(adapter(vec![]).name(), "opencode"); +} + +#[test] +fn a_retry_status_never_produces_an_event_no_matter_how_many_polls() { + let mut oc = adapter(vec![retry_body(SESSION, Some(30_000))]); + for tick in 0..6 { + let at = NOW + tick * MIN_POLL_INTERVAL_SECS; + assert_eq!(oc.poll(&ctx(1), at), None, "poll at +{tick} intervals"); + } +} + +#[test] +fn a_retry_going_idle_produces_exactly_one_usage_limit_event() { + let mut oc = adapter(vec![retry_body(SESSION, None), idle_body(SESSION)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + let event = oc.poll(&ctx(1), NOW + 5).expect("idle after retry reports"); + assert_eq!(event.kind, LimitKind::UsageLimit); + assert_eq!(event.session_id.as_deref(), Some(SESSION)); + assert_eq!(oc.poll(&ctx(1), NOW + 10), None, "second event suppressed"); +} + +#[test] +fn a_retry_then_a_process_exit_produces_exactly_one_event() { + let mut oc = adapter(vec![retry_body(SESSION, None)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + oc.on_exit(&ctx(1)); + let event = oc.poll(&ctx(1), NOW + 5).expect("exit after retry reports"); + assert_eq!(event.session_id.as_deref(), Some(SESSION)); + assert_eq!(oc.poll(&ctx(1), NOW + 10), None); +} + +#[test] +fn an_exit_without_a_retry_ever_seen_produces_no_event() { + let mut oc = adapter(vec![wrap(SESSION, serde_json::json!({"type": "busy"}))]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + oc.on_exit(&ctx(1)); + assert_eq!(oc.poll(&ctx(1), NOW + 5), None); + assert_eq!(oc.poll(&ctx(1), NOW + 10), None); +} + +#[test] +fn an_event_carries_a_deadline_only_when_the_resolved_next_is_in_the_future() { + let future = (NOW + 3600) * 1_000; + let mut ahead = adapter(vec![retry_body(SESSION, Some(future)), idle_body(SESSION)]); + assert_eq!(ahead.poll(&ctx(1), NOW), None); + let event = ahead.poll(&ctx(1), NOW + 5).expect("reports"); + assert_eq!(event.resets_at, Some(NOW + 3600)); + + // 500 ms resolves to `NOW`, which is already past by the time it is emitted. + let mut elapsed = adapter(vec![retry_body(SESSION, Some(500)), idle_body(SESSION)]); + assert_eq!(elapsed.poll(&ctx(1), NOW), None); + let event = elapsed.poll(&ctx(1), NOW + 5).expect("reports"); + assert_eq!(event.resets_at, None); +} + +#[test] +fn an_event_carries_no_deadline_when_the_status_gave_no_next() { + let mut oc = adapter(vec![retry_body(SESSION, None), idle_body(SESSION)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + assert_eq!(oc.poll(&ctx(1), NOW + 5).expect("reports").resets_at, None); +} + +#[test] +fn a_session_id_that_is_not_command_line_safe_is_dropped_and_resume_holds() { + let long = "a".repeat(MAX_SESSION_ID_BYTES + 1); + for id in ["ses 1", "ses;rm", "ses$(id)", "../ses", &long] { + let mut oc = adapter(vec![retry_body(id, None), idle_body(id)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + let event = oc.poll(&ctx(1), NOW + 5).expect("reports"); + assert_eq!(event.session_id, None, "id {id:?} must not survive"); + assert_eq!( + oc.resume(&ctx(1), &event, false), + Some(ResumePlan::Hold(NO_SESSION_HOLD)) + ); + } +} + +#[test] +fn a_generation_change_re_arms_the_once_per_generation_latch() { + let mut oc = adapter(vec![ + retry_body(SESSION, None), + idle_body(SESSION), + retry_body(SESSION, None), + idle_body(SESSION), + ]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + assert!(oc.poll(&ctx(1), NOW + 5).is_some()); + assert_eq!(oc.poll(&ctx(2), NOW + 10), None); + assert!(oc.poll(&ctx(2), NOW + 15).is_some(), "latch re-armed"); +} + +#[test] +fn a_generation_change_forgets_a_retry_seen_before_it() { + let mut oc = adapter(vec![retry_body(SESSION, None), idle_body(SESSION)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + // The retry belonged to the old process, so the new generation's idle + // snapshot is not the end of anything this adapter watched. + assert_eq!(oc.poll(&ctx(2), NOW + 5), None); + assert_eq!(oc.poll(&ctx(2), NOW + 10), None); +} + +#[test] +fn an_exit_recorded_against_an_older_generation_does_not_report() { + let mut oc = adapter(vec![retry_body(SESSION, None)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + oc.on_exit(&ctx(2)); + assert_eq!(oc.poll(&ctx(2), NOW + 5), None); +} + +#[test] +fn polls_closer_together_than_the_minimum_interval_do_not_reach_the_source() { + let mut oc = adapter(vec![retry_body(SESSION, None), idle_body(SESSION)]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + for early in 1..MIN_POLL_INTERVAL_SECS { + assert_eq!(oc.poll(&ctx(1), NOW + early), None, "+{early}s is too soon"); + } + assert!(oc.poll(&ctx(1), NOW + MIN_POLL_INTERVAL_SECS).is_some()); +} + +#[test] +fn an_unreachable_server_produces_no_event() { + let mut oc = OpenCode::with_status_source(Box::new(Unreachable)); + for tick in 0..4 { + let at = NOW + tick * MIN_POLL_INTERVAL_SECS; + assert_eq!(oc.poll(&ctx(1), at), None); + } + oc.on_exit(&ctx(1)); + assert_eq!(oc.poll(&ctx(1), NOW + 100), None); +} + +#[test] +fn an_unreadable_status_body_produces_no_event() { + let mut oc = adapter(vec!["not json".to_string(), "[]".to_string()]); + assert_eq!(oc.poll(&ctx(1), NOW), None); + assert_eq!(oc.poll(&ctx(1), NOW + 5), None); +} + +#[test] +fn terminal_output_never_produces_an_event() { + let mut oc = adapter(vec![]); + for text in ["retrying in 2s", "usage limit reached", "rate_limit_error"] { + assert_eq!(oc.on_output(&ctx(1), text, NOW), None, "{text}"); + } +} + +#[test] +fn resume_holds_while_the_pane_is_still_alive() { + let event = LimitEvent::usage(Some(SESSION.to_string()), None, "d"); + let plan = adapter(vec![]).resume(&ctx(1), &event, true); + assert_eq!(plan, Some(ResumePlan::Hold(ALIVE_HOLD))); +} + +#[test] +fn resume_relaunches_with_the_session_flag_once_the_pane_has_exited() { + let event = LimitEvent::usage(Some(SESSION.to_string()), None, "d"); + let plan = adapter(vec![]).resume(&ctx(1), &event, false); + let want = vec!["--session".to_string(), SESSION.to_string()]; + assert_eq!(plan, Some(ResumePlan::Relaunch(want))); +} + +#[test] +fn resume_holds_when_no_session_id_is_known() { + let event = LimitEvent::usage(None, None, "d"); + let plan = adapter(vec![]).resume(&ctx(1), &event, false); + assert_eq!(plan, Some(ResumePlan::Hold(NO_SESSION_HOLD))); +} + +#[test] +fn observe_command_takes_the_port_from_a_port_flag() { + for (command, want) in [ + ("opencode --port 5000", 5000), + ("opencode -p 6000", 6000), + ("opencode --port=7000", 7000), + ("opencode run --model x --port 5173", 5173), + ] { + let mut oc = adapter(vec![]); + oc.observe_command(command); + assert_eq!(oc.port(), want, "{command}"); + } +} + +#[test] +fn observe_command_ignores_a_port_it_cannot_use() { + let mut oc = adapter(vec![]); + oc.observe_command("opencode --port 5000"); + for command in [ + "opencode --port abc", + "opencode --port 99999", + "opencode --port 0", + "opencode --port", + "opencode --port=", + ] { + oc.observe_command(command); + assert_eq!(oc.port(), 5000, "{command} must not change the port"); + } +} + +#[test] +fn observe_command_leaves_the_port_alone_when_no_flag_is_present() { + let mut oc = adapter(vec![]); + let before = oc.port(); + oc.observe_command("opencode run --model anthropic/claude --print"); + assert_eq!(oc.port(), before); + assert_eq!(port_from_command("opencode"), None); +} diff --git a/plugins/nightcrow-recovery/src/runloop.rs b/plugins/nightcrow-recovery/src/runloop.rs new file mode 100644 index 00000000..94a3ea6f --- /dev/null +++ b/plugins/nightcrow-recovery/src/runloop.rs @@ -0,0 +1,289 @@ +//! The plugin's main loop: NDJSON on stdin/stdout, plus the IPC socket. +//! +//! Three sources have to be watched at once — the host's stdin, the socket, and +//! a clock — and this process deliberately has no async runtime, so each of the +//! first two gets a thread that forwards into one channel and the main thread +//! blocks on that channel with a timeout. The timeout *is* the clock: every +//! wakeup, expired or not, ticks the state machines. +//! +//! Everything the plugin says goes out from this one thread, so the NDJSON +//! stream cannot interleave two half-written lines. + +use crate::ipc::{Ipc, IpcMessage, socket_path}; +use crate::protocol::{LogLevel, PluginCommand, PluginEvent, decode_event, encode_command, log}; +use crate::provider::{PaneContext, Provider, detect}; +use crate::state::{PaneRecovery, RecoveryState}; +use crate::wait::now_epoch; +use anyhow::Result; +use std::collections::HashMap; +use std::io::{BufRead, BufReader, Write}; +use std::sync::mpsc::{Sender, channel}; +use std::time::{Duration, Instant}; + +/// How often the state machines are advanced when nothing else happens. +/// +/// Waits last minutes to hours, so a second of granularity is far finer than +/// anything that depends on it; it is small enough that a pane looks responsive +/// and large enough that an idle plugin costs nothing measurable. +const TICK: Duration = Duration::from_secs(1); + +/// Most pane slots tracked at once. +/// +/// A session holds a handful of panes. The cap exists so a host that somehow +/// announced panes without ever closing them cannot grow this process without +/// bound; reaching it means dropping the *new* pane, which fails closed. +const MAX_TRACKED_PANES: usize = 64; + +/// What the main thread waits on. +enum Message { + Host(PluginEvent), + /// A line from the host that could not be understood. Reported and skipped: + /// one bad line is not a reason to abandon a session's panes. + HostGarbage(String), + Signal(IpcMessage), + /// stdin ended. The host is gone, so there is nothing left to serve. + HostGone, +} + +/// One tracked pane: its recovery progress and the adapter watching it. +struct Watch { + recovery: PaneRecovery, + provider: Box, + ctx: PaneContext, +} + +pub fn run() -> Result<()> { + let (tx, rx) = channel::(); + // Bound the socket's lifetime to this function: dropping it unlinks the + // socket file, so a normal exit leaves nothing for the next run to clear. + let ipc = match Ipc::bind(socket_path()?) { + Ok(ipc) => Some(ipc), + Err(e) => { + // Without the socket the plugin still works from terminal output and + // from the providers it can poll, so this is degraded, not fatal. + emit(&log( + LogLevel::Warn, + format!("recovery ipc unavailable, falling back to output watching: {e}"), + ))?; + None + } + }; + if let Some(ipc) = &ipc { + emit(&log( + LogLevel::Debug, + format!("recovery ipc listening on {}", ipc.path().display()), + ))?; + let signals = tx.clone(); + ipc.serve(move |msg| signals.send(Message::Signal(msg)).is_ok())?; + } + spawn_stdin_reader(tx); + + let mut panes: HashMap = HashMap::new(); + emit(&log(LogLevel::Info, "nightcrow-recovery watching panes"))?; + loop { + let message = match rx.recv_timeout(TICK) { + Ok(message) => Some(message), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => None, + // Every sender is gone, which only happens once the reader thread + // has ended; treat it as the host having gone away. + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Some(Message::HostGone), + }; + match message { + Some(Message::HostGone) | Some(Message::Host(PluginEvent::Shutdown { .. })) => { + return farewell(&panes); + } + Some(Message::Host(event)) => on_host_event(&mut panes, &event)?, + Some(Message::HostGarbage(reason)) => { + emit(&log(LogLevel::Warn, reason))?; + } + Some(Message::Signal(msg)) => on_signal(&mut panes, msg)?, + None => {} + } + tick(&mut panes)?; + } +} + +/// Say what was left unfinished. A pane parked on a reset hours away simply +/// stops being watched when the host goes away, and a user who comes back to a +/// pane that never resumed deserves to find out why from the log. +fn farewell(panes: &HashMap) -> Result<()> { + let unfinished = panes + .values() + .filter(|w| w.recovery.state() != RecoveryState::Idle) + .count(); + if unfinished > 0 { + let attempts: u32 = panes.values().map(|w| w.recovery.attempt()).sum(); + emit(&log( + LogLevel::Info, + format!( + "stopping with {unfinished} pane(s) mid-recovery after {attempts} resume attempt(s)" + ), + ))?; + } + Ok(()) +} + +fn spawn_stdin_reader(tx: Sender) { + std::thread::spawn(move || { + let reader = BufReader::new(std::io::stdin()); + for line in reader.lines() { + let Ok(line) = line else { + break; + }; + if line.trim().is_empty() { + continue; + } + let message = match decode_event(&line) { + Ok(event) => Message::Host(event), + Err(e) => Message::HostGarbage(format!("ignoring a host line: {e}")), + }; + if tx.send(message).is_err() { + return; + } + } + let _ = tx.send(Message::HostGone); + }); +} + +fn on_host_event(panes: &mut HashMap, event: &PluginEvent) -> Result<()> { + let Some(token) = event.token().cloned() else { + return Ok(()); + }; + let now = now_epoch(); + if let PluginEvent::PaneOpened { + generation, + command, + cwd, + .. + } = event + { + open_pane(panes, &token, *generation, command.as_deref(), cwd)?; + } + let Some(watch) = panes.get_mut(&token) else { + return Ok(()); + }; + // Housekeeping first: the adapter's answer below depends on whether the host + // has just said this pane is alive or idle. + let Some(commands) = watch.recovery.on_event(event) else { + return Ok(()); // a generation this pane has already moved past + }; + emit_all(&commands)?; + watch.ctx.generation = watch.recovery.generation(); + let limit = match event { + PluginEvent::PaneOutput { text, .. } => watch.provider.on_output(&watch.ctx, text, now), + PluginEvent::PaneExited { .. } => { + watch.provider.on_exit(&watch.ctx); + None + } + _ => None, + }; + if let Some(limit) = limit { + let commands = watch.recovery.note_limit(limit, now, Instant::now()); + emit_all(&commands)?; + } + if matches!(event, PluginEvent::PaneClosed { .. }) { + panes.remove(&token); + } + Ok(()) +} + +fn open_pane( + panes: &mut HashMap, + token: &str, + generation: u32, + command: Option<&str>, + cwd: &str, +) -> Result<()> { + let ctx = PaneContext { + token: token.to_string(), + generation, + cwd: cwd.to_string(), + command: command.map(str::to_string), + }; + if let Some(watch) = panes.get_mut(token) { + // A relaunch reopens the same slot. The recovery state survives, so an + // attempt budget cannot be reset by relaunching into the same limit. + watch.ctx = ctx; + return Ok(()); + } + let Some(provider) = detect(command) else { + // A pane running something this plugin knows nothing about is not + // watched at all, which is the cheapest way to stay out of it. + return Ok(()); + }; + if panes.len() >= MAX_TRACKED_PANES { + emit(&log( + LogLevel::Warn, + format!("already watching {MAX_TRACKED_PANES} panes; not watching another"), + ))?; + return Ok(()); + } + emit(&log( + LogLevel::Info, + format!("watching a {} pane", provider.name()), + ))?; + panes.insert( + token.to_string(), + Watch { + recovery: PaneRecovery::new(token.to_string(), generation), + provider, + ctx, + }, + ); + Ok(()) +} + +fn on_signal(panes: &mut HashMap, msg: IpcMessage) -> Result<()> { + let (token, signal) = msg.into_signal(); + // A signal for a pane we do not track is ordinary: a provider helper from + // another nightcrow session, or a pane whose command we do not watch. + let Some(watch) = panes.get_mut(&token) else { + return Ok(()); + }; + let now = now_epoch(); + if let Some(limit) = watch.provider.on_signal(&watch.ctx, &signal, now) { + let commands = watch.recovery.note_limit(limit, now, Instant::now()); + emit_all(&commands)?; + } + Ok(()) +} + +fn tick(panes: &mut HashMap) -> Result<()> { + let epoch = now_epoch(); + let now = Instant::now(); + for watch in panes.values_mut() { + if let Some(limit) = watch.provider.poll(&watch.ctx, epoch) { + let commands = watch.recovery.note_limit(limit, epoch, now); + emit_all(&commands)?; + } + let commands = watch + .recovery + .tick(watch.provider.as_ref(), &watch.ctx, epoch, now); + emit_all(&commands)?; + } + Ok(()) +} + +fn emit_all(commands: &[PluginCommand]) -> Result<()> { + for command in commands { + emit(command)?; + } + Ok(()) +} + +/// Write one command as one NDJSON line. A command that cannot be framed is +/// dropped with a complaint rather than corrupting the stream. +fn emit(command: &PluginCommand) -> Result<()> { + let line = match encode_command(command) { + Ok(line) => line, + Err(e) => encode_command(&log( + LogLevel::Error, + format!("dropped an unencodable command: {e}"), + ))?, + }; + let mut out = std::io::stdout().lock(); + out.write_all(line.as_bytes())?; + out.write_all(b"\n")?; + out.flush()?; + Ok(()) +} diff --git a/plugins/nightcrow-recovery/src/state.rs b/plugins/nightcrow-recovery/src/state.rs new file mode 100644 index 00000000..844461fa --- /dev/null +++ b/plugins/nightcrow-recovery/src/state.rs @@ -0,0 +1,277 @@ +//! The recovery state machine for one pane slot. +//! +//! Deliberately provider-agnostic: an adapter says "this pane hit a limit and it +//! clears at T" and "here is how to resume", and everything about *when* to act, +//! how many times, and when to stop lives here. The machine holds no IO and no +//! clock of its own — every entry point takes the current time — so the awkward +//! cases (a stale generation, a clock jump, an exhausted attempt budget) are +//! ordinary unit tests rather than something only reproducible by waiting. +//! +//! Safety posture: this machine never decides that a pane is alive or idle. It +//! only ever repeats back what the host told it, and it refuses to ask for input +//! unless the host has said both. The host judges every request again anyway. + +use crate::protocol::{PROTOCOL_VERSION, PaneGeneration, PaneToken, PluginCommand, PluginEvent}; +use crate::provider::{LimitEvent, LimitKind}; +use crate::wait::ResetWait; +use std::time::Instant; + +/// Resume attempts allowed per limit episode before the pane is handed back to +/// its human. +/// +/// Four is enough to ride out a reset time that was slightly optimistic plus a +/// couple of transient failures. Beyond that the cause is not something waiting +/// fixes, and a fifth automatic attempt is just noise in someone's terminal. +pub const MAX_RESUME_ATTEMPTS: u32 = 4; + +/// How long a resume has to show some sign of life before it is treated as +/// failed. +/// +/// A relaunch reports back as a new generation within milliseconds and typed +/// input echoes almost as fast, so this only has to cover a slow provider +/// start-up; a minute and a half is generous and still bounded. +pub const RESUME_CONFIRM_SECS: u64 = 90; + +/// Where a pane is in its recovery. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecoveryState { + /// Nothing to do. The steady state, and where every cancellation lands. + Idle, + /// A limit was reported; the machine has not yet decided how to wait. + LimitDetected, + /// Waiting for a known reset time. + WaitingForReset, + /// The wait is over; waiting on the pane to be in a state we may touch. + ReadyToResume, + /// A resume was asked of the host; waiting for a sign it landed. + Resuming, + /// No reset time, or a resume that did not land: waiting out a backoff step. + Backoff, + /// Given up. Only a human clears this. + NeedsAttention, +} + +impl RecoveryState { + /// The name the host displays. Stable: it is part of what a user reads. + pub fn as_str(self) -> &'static str { + match self { + Self::Idle => "idle", + Self::LimitDetected => "limit_detected", + Self::WaitingForReset => "waiting_for_reset", + Self::ReadyToResume => "ready_to_resume", + Self::Resuming => "resuming", + Self::Backoff => "backoff", + Self::NeedsAttention => "needs_attention", + } + } +} + +/// One pane slot's recovery progress. +#[derive(Debug)] +pub struct PaneRecovery { + token: PaneToken, + generation: PaneGeneration, + state: RecoveryState, + limit: Option, + wait: Option, + attempt: u32, + /// The host's word on the pane's process, never this machine's guess. + alive: bool, + idle: bool, + detail: Option, + resumed_at: Option, +} + +impl PaneRecovery { + pub fn new(token: PaneToken, generation: PaneGeneration) -> Self { + Self { + token, + generation, + state: RecoveryState::Idle, + limit: None, + wait: None, + attempt: 0, + alive: true, + idle: false, + detail: None, + resumed_at: None, + } + } + + pub fn state(&self) -> RecoveryState { + self.state + } + + pub fn attempt(&self) -> u32 { + self.attempt + } + + pub fn generation(&self) -> PaneGeneration { + self.generation + } + + #[cfg(test)] + pub fn session_id(&self) -> Option<&str> { + self.limit.as_ref()?.session_id.as_deref() + } + + pub fn deadline_epoch(&self) -> Option { + self.wait.as_ref().map(ResetWait::deadline_epoch) + } + + #[cfg(test)] + pub fn alive(&self) -> bool { + self.alive + } + + /// Fold in one host event. + /// + /// Returns `None` when the event names a generation this machine has already + /// moved past — a decision about a dead process must never land on its + /// successor — and otherwise the commands the transition produced. + pub fn on_event(&mut self, event: &PluginEvent) -> Option> { + let generation = event.generation()?; + if generation < self.generation { + return None; + } + let mut out = Vec::new(); + if generation > self.generation { + // A new spawn of the slot voids everything decided about the previous + // process. Landing in `Idle` either way; the only difference is that + // a relaunch we asked for counts as a resume that worked, while a + // respawn we did not ask for is a plain cancellation. + self.generation = generation; + self.alive = true; + self.idle = false; + out.extend(if self.state == RecoveryState::Resuming { + self.confirm_resume() + } else { + self.cancel() + }); + } + match event { + PluginEvent::PaneOpened { .. } => { + self.alive = true; + self.idle = false; + } + PluginEvent::PaneOutput { .. } => { + self.idle = false; + out.extend(self.confirm_resume()); + } + PluginEvent::PaneIdle { .. } => { + self.idle = true; + out.extend(self.confirm_resume()); + } + PluginEvent::PaneExited { .. } => { + self.alive = false; + self.idle = false; + } + PluginEvent::PaneClosed { .. } | PluginEvent::UserInput { .. } => { + // The slot is gone, or its human took it back. Either way this + // machine has no business acting on it again, and the attempt + // budget resets because the next episode is a fresh one. + self.attempt = 0; + out.extend(self.cancel()); + } + PluginEvent::Shutdown { .. } => {} + } + Some(out) + } + + /// Record an adapter's limit report. Idempotent: the same episode reported + /// twice changes nothing. + pub fn note_limit( + &mut self, + limit: LimitEvent, + now_epoch: i64, + now: Instant, + ) -> Vec { + if self.state == RecoveryState::NeedsAttention || self.is_same_episode(&limit) { + return Vec::new(); + } + self.detail = Some(limit.detail.clone()); + let kind = limit.kind; + self.limit = Some(limit); + let mut out = self.goto(RecoveryState::LimitDetected); + if kind == LimitKind::NeedsHuman { + out.extend(self.goto(RecoveryState::NeedsAttention)); + return out; + } + out.extend(self.arm_wait(now_epoch, now)); + out + } + + /// A limit episode is the triple (token, generation, provider session id) + /// plus the reset time. Anything else is a new episode and re-arms the wait. + fn is_same_episode(&self, limit: &LimitEvent) -> bool { + let Some(current) = &self.limit else { + return false; + }; + self.state != RecoveryState::Idle + && current.session_id == limit.session_id + && current.resets_at == limit.resets_at + && current.kind == limit.kind + } + + /// A sign that a resume landed. Only meaningful while [`RecoveryState::Resuming`]. + fn confirm_resume(&mut self) -> Vec { + if self.state != RecoveryState::Resuming { + return Vec::new(); + } + // The attempt budget is refunded only for an episode that had a real + // reset time to wait for. Those are bounded by the provider's own + // window, so refunding cannot spin. An episode with no known reset time + // keeps its count, which is what stops a pane that resumes cleanly and + // then immediately fails again from retrying forever. + if self.limit.as_ref().and_then(|l| l.resets_at).is_some() { + self.attempt = 0; + } + self.resumed_at = None; + self.limit = None; + self.wait = None; + self.detail = Some("resumed".to_string()); + self.goto(RecoveryState::Idle) + } + + fn cancel(&mut self) -> Vec { + self.wait = None; + self.limit = None; + self.resumed_at = None; + if self.state == RecoveryState::Idle { + return Vec::new(); + } + self.detail = Some("cancelled".to_string()); + self.goto(RecoveryState::Idle) + } + + fn goto(&mut self, state: RecoveryState) -> Vec { + if self.state == state { + return Vec::new(); + } + self.state = state; + + vec![self.status()] + } + + fn status(&self) -> PluginCommand { + PluginCommand::Status { + v: PROTOCOL_VERSION, + token: self.token.clone(), + generation: self.generation, + state: self.state.as_str().to_string(), + detail: self.detail.clone(), + deadline_epoch: self.deadline_epoch(), + attempt: self.attempt, + } + } +} + +#[path = "state_clock.rs"] +mod clock; + +#[path = "state_resume.rs"] +mod resume; + +#[cfg(test)] +#[path = "state_tests/mod.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/state_clock.rs b/plugins/nightcrow-recovery/src/state_clock.rs new file mode 100644 index 00000000..517c2536 --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_clock.rs @@ -0,0 +1,86 @@ +//! Waiting: the half of the machine driven by the clock rather than by an event. +//! +//! Split out of `state.rs` for readability. Nothing here decides *what* to do +//! about a limit; it decides only how long to sit still first, and it is the one +//! place that can end a recovery by running out of attempts. + +use super::{MAX_RESUME_ATTEMPTS, PaneRecovery, RESUME_CONFIRM_SECS, RecoveryState}; +use crate::protocol::PluginCommand; +use crate::provider::{LimitKind, PaneContext, Provider}; +use crate::wait::ResetWait; +use std::time::{Duration, Instant}; + +impl PaneRecovery { + /// Advance anything that is only a function of time. + pub fn tick( + &mut self, + provider: &dyn Provider, + ctx: &PaneContext, + now_epoch: i64, + now: Instant, + ) -> Vec { + let mut out = Vec::new(); + match self.state { + RecoveryState::WaitingForReset | RecoveryState::Backoff => { + let elapsed = self.wait.as_mut().is_some_and(|w| w.poll(now_epoch, now)); + if elapsed { + self.wait = None; + out.extend(self.goto(RecoveryState::ReadyToResume)); + } + } + RecoveryState::Resuming => { + let stale = self.resumed_at.is_some_and(|at| { + now.duration_since(at) >= Duration::from_secs(RESUME_CONFIRM_SECS) + }); + if stale { + self.resumed_at = None; + out.extend(self.arm_wait_after_failure(now_epoch, now)); + } + } + _ => {} + } + if self.state == RecoveryState::ReadyToResume { + out.extend(self.try_resume(provider, ctx, now_epoch, now)); + } + out + } + + pub(super) fn arm_wait(&mut self, now_epoch: i64, now: Instant) -> Vec { + // A known reset time is waited out exactly once and does not spend an + // attempt: nothing has been tried yet. + let reset = self + .limit + .as_ref() + .filter(|l| l.kind == LimitKind::UsageLimit) + .and_then(|l| l.resets_at); + if let Some(reset) = reset { + self.wait = Some(ResetWait::until(reset, now_epoch, now)); + return self.goto(RecoveryState::WaitingForReset); + } + self.arm_backoff(now_epoch, now) + } + + /// Re-arm after a resume that did not land. Distinct from [`Self::arm_wait`] + /// because a failed resume must never go back to waiting on the same reset + /// time — that time has passed. + pub(super) fn arm_wait_after_failure( + &mut self, + now_epoch: i64, + now: Instant, + ) -> Vec { + self.detail = Some("resume produced no sign of life".to_string()); + self.arm_backoff(now_epoch, now) + } + + fn arm_backoff(&mut self, now_epoch: i64, now: Instant) -> Vec { + if self.attempt >= MAX_RESUME_ATTEMPTS { + self.wait = None; + self.detail = Some(format!( + "gave up after {MAX_RESUME_ATTEMPTS} resume attempts" + )); + return self.goto(RecoveryState::NeedsAttention); + } + self.wait = Some(ResetWait::backoff(self.attempt + 1, now_epoch, now)); + self.goto(RecoveryState::Backoff) + } +} diff --git a/plugins/nightcrow-recovery/src/state_resume.rs b/plugins/nightcrow-recovery/src/state_resume.rs new file mode 100644 index 00000000..07131c5d --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_resume.rs @@ -0,0 +1,130 @@ +//! Turning "the wait is over" into a single request to the host. +//! +//! Split out of `state.rs` to keep each file readable: `state.rs` owns time and +//! transitions, this owns the one moment the plugin actually asks for something. +//! +//! Everything here is written on the assumption that the host will refuse. A +//! refusal costs an attempt and nothing else, so the checks below exist to avoid +//! wasting attempts on requests that are obviously going to be rejected — not to +//! be the safety boundary. That boundary is the host's. + +use super::{MAX_RESUME_ATTEMPTS, PaneRecovery, RecoveryState}; +use crate::protocol::{MAX_INPUT_BYTES, PROTOCOL_VERSION, PluginCommand}; +use crate::provider::{PaneContext, Provider, ResumePlan}; +use std::time::Instant; + +/// Most arguments a resume may consist of, matching the host's own cap: a resume +/// is a flag and an identifier, and anything longer is not a resume. +const MAX_RESUME_ARGS: usize = 6; + +/// Longest single resume argument, matching the host's cap. Past a UUID or a +/// session name with room to spare. +const MAX_RESUME_ARG_LEN: usize = 256; + +/// Characters a resume argument may consist of, matching the host's rule. +/// +/// The host appends the argument to a command line a login shell parses, so it +/// refuses anything carrying a space, quote, backtick, `$` or `;`. Checking the +/// same rule here means a session id picked up from a provider's file cannot +/// spend an attempt on a request that was never going to be accepted. +fn is_safe_arg_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':' | '/' | '=' | '@' | '+') +} + +fn args_are_safe(args: &[String]) -> bool { + !args.is_empty() + && args.len() <= MAX_RESUME_ARGS + && args.iter().all(|a| { + !a.is_empty() && a.len() <= MAX_RESUME_ARG_LEN && a.chars().all(is_safe_arg_char) + }) +} + +impl PaneRecovery { + /// Ask the adapter how to resume and, if the pane is in a state we may + /// touch, send exactly one request. + /// + /// Staying in [`RecoveryState::ReadyToResume`] with nothing emitted is the + /// normal answer while the pane is not yet touchable: a relaunch needs the + /// process gone, typed input needs it alive *and* idle, and both of those + /// facts arrive as later host events. + pub(super) fn try_resume( + &mut self, + provider: &dyn Provider, + ctx: &PaneContext, + now_epoch: i64, + now: Instant, + ) -> Vec { + let Some(limit) = self.limit.clone() else { + // Reaching a resume with no episode means the episode was cancelled + // between the wait ending and this tick. Nothing to do. + return self.cancel(); + }; + let Some(plan) = provider.resume(ctx, &limit, self.alive) else { + return self.arm_wait_after_failure(now_epoch, now); + }; + match plan { + ResumePlan::Hold(reason) => { + self.detail = Some(reason.to_string()); + self.goto(RecoveryState::NeedsAttention) + } + ResumePlan::Input(data) => self.send_input(data, now_epoch, now), + ResumePlan::Relaunch(args) => self.relaunch(args, now), + } + } + + fn send_input(&mut self, data: String, now_epoch: i64, now: Instant) -> Vec { + if !self.alive { + // The adapter offered typed input for a process that has since + // exited. Do not invent a relaunch on its behalf. + return self.arm_wait_after_failure(now_epoch, now); + } + if !self.idle { + return Vec::new(); + } + if data.is_empty() || data.len() > MAX_INPUT_BYTES { + self.detail = Some("adapter offered input the host would refuse".to_string()); + return self.goto(RecoveryState::NeedsAttention); + } + let command = PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: self.token.clone(), + generation: self.generation, + data, + }; + self.spend_attempt(command, now) + } + + fn relaunch(&mut self, args: Vec, now: Instant) -> Vec { + if self.alive { + // The host refuses a relaunch of a live pane, so wait for the exit + // rather than spending an attempt learning that again. + return Vec::new(); + } + if !args_are_safe(&args) { + self.detail = Some("adapter offered resume args the host would refuse".to_string()); + return self.goto(RecoveryState::NeedsAttention); + } + let command = PluginCommand::Relaunch { + v: PROTOCOL_VERSION, + token: self.token.clone(), + generation: self.generation, + resume_args: args, + }; + self.spend_attempt(command, now) + } + + /// Count the attempt, emit the request, and start the confirmation timer. + fn spend_attempt(&mut self, command: PluginCommand, now: Instant) -> Vec { + if self.attempt >= MAX_RESUME_ATTEMPTS { + self.detail = Some(format!( + "gave up after {MAX_RESUME_ATTEMPTS} resume attempts" + )); + return self.goto(RecoveryState::NeedsAttention); + } + self.attempt += 1; + self.resumed_at = Some(now); + let mut out = self.goto(RecoveryState::Resuming); + out.push(command); + out + } +} diff --git a/plugins/nightcrow-recovery/src/state_tests/cancel.rs b/plugins/nightcrow-recovery/src/state_tests/cancel.rs new file mode 100644 index 00000000..3909b0c5 --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_tests/cancel.rs @@ -0,0 +1,156 @@ +//! The three ways a recovery is called off, and what each one leaves behind. + +use super::*; + +#[test] +fn a_human_typing_into_the_pane_cancels_the_wait() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let out = rec.on_event(&user_input(1)).expect("current generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(rec.deadline_epoch(), None); + assert_eq!(rec.session_id(), None); + assert_eq!(states(&out), vec!["idle"]); +} + +#[test] +fn the_pane_slot_closing_cancels_the_wait() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let out = rec.on_event(&closed(1)).expect("current generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(states(&out), vec!["idle"]); +} + +#[test] +fn a_respawn_the_plugin_did_not_ask_for_cancels_the_wait() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let out = rec.on_event(&opened(2)).expect("a newer generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(rec.generation(), 2); + assert_eq!(rec.deadline_epoch(), None); + assert_eq!(states(&out), vec!["idle"]); +} + +#[test] +fn cancelling_an_already_idle_pane_reports_nothing() { + let mut rec = recovery(); + let first = rec.on_event(&user_input(1)).expect("current generation"); + let second = rec.on_event(&user_input(1)).expect("current generation"); + assert!(first.is_empty(), "the pane was already idle"); + assert!(second.is_empty(), "and cancelling again is the same no-op"); + assert_eq!(rec.state(), RecoveryState::Idle); +} + +#[test] +fn cancelling_the_same_wait_twice_reports_the_change_once() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let first = rec.on_event(&user_input(1)).expect("current generation"); + let second = rec.on_event(&user_input(1)).expect("current generation"); + assert_eq!(states(&first), vec!["idle"]); + assert!(second.is_empty()); +} + +#[test] +fn a_human_taking_the_pane_back_refunds_the_attempt_budget() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.on_event(&exited(1)).expect("current generation"); + rec.note_limit(usage(Some(SESSION), None), T0, mono); + let ready = BACKOFF_BASE_SECS; + tick_at( + &mut rec, + &provider, + T0 + ready, + mono + Duration::from_secs(ready as u64), + ); + assert_eq!(rec.attempt(), 1); + rec.on_event(&user_input(2)).expect("a newer generation"); + assert_eq!(rec.attempt(), 0); + assert_eq!(rec.state(), RecoveryState::Idle); +} + +#[test] +fn a_cancelled_pane_can_start_a_new_episode() { + let mut rec = recovery(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&user_input(1)).expect("current generation"); + let out = rec.note_limit(usage(Some(SESSION), Some(RESET)), T0 + 10, mono); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + assert_eq!(states(&out), vec!["limit_detected", "waiting_for_reset"]); +} + +#[test] +fn a_pane_needing_attention_is_cleared_by_its_human() { + let mut rec = recovery(); + rec.note_limit(needs_human(), T0, Instant::now()); + let out = rec.on_event(&user_input(1)).expect("current generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(states(&out), vec!["idle"]); +} + +#[test] +fn two_panes_in_one_repo_recover_their_own_sessions_independently() { + let mut first = PaneRecovery::new(TOKEN.to_string(), 1); + let mut second = PaneRecovery::new(OTHER_TOKEN.to_string(), 1); + let mono = Instant::now(); + let out = first.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + second.note_limit(usage(Some(OTHER_SESSION), Some(RESET + 600)), T0, mono); + + assert_eq!(first.session_id(), Some(SESSION)); + assert_eq!(second.session_id(), Some(OTHER_SESSION)); + assert_eq!(first.deadline_epoch(), Some(RESET + RESET_GRACE_SECS)); + assert_eq!( + second.deadline_epoch(), + Some(RESET + 600 + RESET_GRACE_SECS) + ); + + // A status names the pane it belongs to, which is how the host tells them + // apart when both panes run in the same repository. + match out.last().expect("a status") { + PluginCommand::Status { token, .. } => assert_eq!(token, TOKEN), + other => panic!("expected a status, got {other:?}"), + } + + // Cancelling one leaves the other alone. + first.on_event(&user_input(1)).expect("current generation"); + assert_eq!(first.state(), RecoveryState::Idle); + assert_eq!(second.state(), RecoveryState::WaitingForReset); +} + +#[test] +fn a_shutdown_is_not_a_pane_event_and_moves_nothing() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let out = rec.on_event(&PluginEvent::Shutdown { + v: PROTOCOL_VERSION, + }); + assert!(out.is_none(), "a shutdown names no pane"); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); +} + +#[test] +fn the_wait_is_dropped_when_a_cancelled_pane_reaches_a_resume() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.on_event(&exited(1)).expect("current generation"); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + // The wait ends, but the episode was cancelled in between, so there is + // nothing left to resume and nothing is asked of the host. + rec.on_event(&user_input(1)).expect("current generation"); + let elapsed = RESET - T0 + RESET_GRACE_SECS; + let out = tick_at( + &mut rec, + &provider, + T0 + elapsed, + mono + Duration::from_secs(elapsed as u64), + ); + assert!(out.is_empty()); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(rec.attempt(), 0); +} diff --git a/plugins/nightcrow-recovery/src/state_tests/mod.rs b/plugins/nightcrow-recovery/src/state_tests/mod.rs new file mode 100644 index 00000000..c5e15064 --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_tests/mod.rs @@ -0,0 +1,183 @@ +//! The recovery state machine, one module per group of behaviours, sharing the +//! fixtures below. A fake adapter stands in for a provider: the machine's +//! contract with an adapter is `resume`, so that is all the fake implements. + +use super::*; +use crate::protocol::PROTOCOL_VERSION; +use crate::provider::{LimitEvent, LimitKind, PaneContext, Provider, ResumePlan}; +use crate::wait::{BACKOFF_BASE_SECS, RESET_GRACE_SECS}; +use std::time::Duration; + +mod cancel; +mod resume; +mod transitions; + +pub(super) const TOKEN: &str = "0123456789abcdef0123456789abcdef"; +pub(super) const OTHER_TOKEN: &str = "ffffffffffffffffffffffffffffffff"; +pub(super) const SESSION: &str = "11111111-2222-3333-4444-555555555555"; +pub(super) const OTHER_SESSION: &str = "99999999-8888-7777-6666-555555555555"; +/// A readable fixed "now": 2026-01-01T00:00:00Z. +pub(super) const T0: i64 = 1_767_225_600; +/// A reset an hour out, so a wait is unambiguously in the future. +pub(super) const RESET: i64 = T0 + 3600; + +/// An adapter that answers whatever the test told it to, so a test spoils +/// exactly one thing. +#[derive(Debug)] +pub(super) struct FakeProvider { + pub alive_plan: Option, + pub exited_plan: Option, +} + +impl Default for FakeProvider { + fn default() -> Self { + Self { + alive_plan: Some(ResumePlan::Input("continue\r".to_string())), + exited_plan: Some(ResumePlan::Relaunch(vec![ + "--resume".to_string(), + SESSION.to_string(), + ])), + } + } +} + +impl FakeProvider { + /// An adapter that will only ever relaunch, which is the codex/opencode + /// shape: those providers exit when they hit a limit. + pub fn relaunch_only() -> Self { + Self { + alive_plan: Some(ResumePlan::Hold("still running")), + ..Self::default() + } + } +} + +impl Provider for FakeProvider { + fn name(&self) -> &'static str { + "fake" + } + + fn resume(&self, _ctx: &PaneContext, _limit: &LimitEvent, alive: bool) -> Option { + if alive { + self.alive_plan.clone() + } else { + self.exited_plan.clone() + } + } +} + +pub(super) fn ctx() -> PaneContext { + PaneContext { + token: TOKEN.to_string(), + generation: 1, + cwd: "/w/repo".to_string(), + command: Some("claude".to_string()), + } +} + +pub(super) fn recovery() -> PaneRecovery { + PaneRecovery::new(TOKEN.to_string(), 1) +} + +pub(super) fn usage(session: Option<&str>, resets_at: Option) -> LimitEvent { + LimitEvent::usage(session.map(str::to_string), resets_at, "test limit") +} + +pub(super) fn transient() -> LimitEvent { + LimitEvent { + session_id: Some(SESSION.to_string()), + resets_at: Some(RESET), + kind: LimitKind::Transient, + detail: "overloaded".to_string(), + } +} + +pub(super) fn needs_human() -> LimitEvent { + LimitEvent { + session_id: Some(SESSION.to_string()), + resets_at: None, + kind: LimitKind::NeedsHuman, + detail: "billing_error".to_string(), + } +} + +pub(super) fn opened(generation: PaneGeneration) -> PluginEvent { + PluginEvent::PaneOpened { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + title: None, + command: Some("claude".to_string()), + cwd: "/w/repo".to_string(), + } +} + +pub(super) fn output(generation: PaneGeneration) -> PluginEvent { + PluginEvent::PaneOutput { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + text: "thinking".to_string(), + } +} + +pub(super) fn went_idle(generation: PaneGeneration) -> PluginEvent { + PluginEvent::PaneIdle { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + idle_ms: 30_000, + } +} + +pub(super) fn exited(generation: PaneGeneration) -> PluginEvent { + PluginEvent::PaneExited { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + } +} + +pub(super) fn closed(generation: PaneGeneration) -> PluginEvent { + PluginEvent::PaneClosed { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + } +} + +pub(super) fn user_input(generation: PaneGeneration) -> PluginEvent { + PluginEvent::UserInput { + v: PROTOCOL_VERSION, + token: TOKEN.to_string(), + generation, + } +} + +/// Advance the machine as the run loop would: once, at this moment. +pub(super) fn tick_at( + rec: &mut PaneRecovery, + provider: &dyn Provider, + epoch: i64, + mono: Instant, +) -> Vec { + rec.tick(provider, &ctx(), epoch, mono) +} + +/// The one command a test cares about, or `None` if the machine only reported +/// status. +pub(super) fn action(commands: &[PluginCommand]) -> Option<&PluginCommand> { + commands + .iter() + .find(|c| !matches!(c, PluginCommand::Status { .. })) +} + +pub(super) fn states(commands: &[PluginCommand]) -> Vec { + commands + .iter() + .filter_map(|c| match c { + PluginCommand::Status { state, .. } => Some(state.clone()), + _ => None, + }) + .collect() +} diff --git a/plugins/nightcrow-recovery/src/state_tests/resume.rs b/plugins/nightcrow-recovery/src/state_tests/resume.rs new file mode 100644 index 00000000..cae3d61a --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_tests/resume.rs @@ -0,0 +1,200 @@ +//! What the machine asks of the host once a wait is over, and the bound on how +//! often it will ask. + +use super::*; +use crate::wait::BACKOFF_MAX_SECS; + +/// The monotonic instant matching `T0 + delta` on the wall clock, so both clocks +/// advance together and no jump is detected. +fn at(base: Instant, delta: i64) -> Instant { + base + Duration::from_secs(delta as u64) +} + +/// Wall seconds from `T0` at which a wait for [`RESET`] is over. +const AFTER_RESET: i64 = RESET - T0 + RESET_GRACE_SECS; + +#[test] +fn a_wait_that_ends_relaunches_a_pane_whose_process_is_gone() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + let out = tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::Resuming); + assert_eq!(rec.attempt(), 1); + assert_eq!(states(&out), vec!["ready_to_resume", "resuming"]); + match action(&out) { + Some(PluginCommand::Relaunch { resume_args, .. }) => { + assert_eq!(resume_args, &["--resume".to_string(), SESSION.to_string()]); + } + other => panic!("expected a relaunch, got {other:?}"), + } +} + +#[test] +fn a_pane_whose_process_is_still_running_is_not_relaunched() { + let mut rec = recovery(); + let provider = FakeProvider { + alive_plan: Some(ResumePlan::Relaunch(vec![ + "--resume".to_string(), + SESSION.to_string(), + ])), + ..FakeProvider::default() + }; + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + let out = tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::ReadyToResume); + assert!(action(&out).is_none(), "nothing is asked of a live pane"); +} + +#[test] +fn a_live_pane_is_not_typed_into_until_the_host_says_it_is_idle() { + let mut rec = recovery(); + let provider = FakeProvider::default(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + let out = tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::ReadyToResume); + assert!(action(&out).is_none()); + + rec.on_event(&went_idle(1)).expect("current generation"); + let out = tick_at( + &mut rec, + &provider, + T0 + AFTER_RESET + 1, + at(mono, AFTER_RESET + 1), + ); + assert_eq!(rec.state(), RecoveryState::Resuming); + match action(&out) { + Some(PluginCommand::SendInput { data, .. }) => assert_eq!(data, "continue\r"), + other => panic!("expected typed input, got {other:?}"), + } +} + +#[test] +fn a_relaunch_that_lands_as_a_new_generation_confirms_the_resume() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + let out = rec.on_event(&opened(2)).expect("a newer generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(rec.generation(), 2); + assert_eq!(rec.attempt(), 0, "a resume that landed refunds the budget"); + assert_eq!(states(&out), vec!["idle"]); +} + +#[test] +fn output_after_typed_input_confirms_the_resume() { + let mut rec = recovery(); + let provider = FakeProvider::default(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&went_idle(1)).expect("current generation"); + tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::Resuming); + rec.on_event(&output(1)).expect("current generation"); + assert_eq!(rec.state(), RecoveryState::Idle); + assert_eq!(rec.attempt(), 0); +} + +#[test] +fn a_resume_showing_no_sign_of_life_backs_off_and_tries_again() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.on_event(&exited(1)).expect("current generation"); + rec.note_limit(usage(Some(SESSION), None), T0, mono); + let ready = BACKOFF_BASE_SECS; + tick_at(&mut rec, &provider, T0 + ready, at(mono, ready)); + assert_eq!(rec.state(), RecoveryState::Resuming); + + let gave_up = ready + RESUME_CONFIRM_SECS as i64; + tick_at(&mut rec, &provider, T0 + gave_up, at(mono, gave_up)); + assert_eq!(rec.state(), RecoveryState::Backoff); + assert_eq!(rec.attempt(), 1); + assert_eq!( + rec.deadline_epoch(), + Some(T0 + gave_up + BACKOFF_BASE_SECS * 2) + ); +} + +#[test] +fn attempts_stop_at_the_maximum_and_land_in_needs_attention() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.on_event(&exited(1)).expect("current generation"); + rec.note_limit(usage(Some(SESSION), None), T0, mono); + let mut elapsed = 0i64; + for attempt in 1..=MAX_RESUME_ATTEMPTS { + // Past any backoff step, so the wait is certainly over. + elapsed += BACKOFF_MAX_SECS; + let out = tick_at(&mut rec, &provider, T0 + elapsed, at(mono, elapsed)); + assert_eq!(rec.state(), RecoveryState::Resuming, "attempt {attempt}"); + assert_eq!(rec.attempt(), attempt); + assert!(matches!(action(&out), Some(PluginCommand::Relaunch { .. }))); + elapsed += RESUME_CONFIRM_SECS as i64; + tick_at(&mut rec, &provider, T0 + elapsed, at(mono, elapsed)); + } + assert_eq!(rec.state(), RecoveryState::NeedsAttention); + assert_eq!(rec.attempt(), MAX_RESUME_ATTEMPTS); + + // And it stays there rather than trying a fifth time. + elapsed += BACKOFF_MAX_SECS; + let out = tick_at(&mut rec, &provider, T0 + elapsed, at(mono, elapsed)); + assert!(out.is_empty()); + assert_eq!(rec.state(), RecoveryState::NeedsAttention); +} + +#[test] +fn an_adapter_that_holds_hands_the_pane_to_its_human() { + let mut rec = recovery(); + let provider = FakeProvider { + exited_plan: Some(ResumePlan::Hold("no session id")), + ..FakeProvider::default() + }; + let mono = Instant::now(); + rec.note_limit(usage(None, Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + let out = tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::NeedsAttention); + assert!(action(&out).is_none()); + assert_eq!(rec.attempt(), 0, "a hold is not an attempt"); +} + +#[test] +fn an_adapter_offering_unsafe_resume_args_is_refused_without_asking_the_host() { + let mut rec = recovery(); + let provider = FakeProvider { + exited_plan: Some(ResumePlan::Relaunch(vec![ + "--resume".to_string(), + "abc; rm -rf /".to_string(), + ])), + ..FakeProvider::default() + }; + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + let out = tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::NeedsAttention); + assert!(action(&out).is_none()); +} + +#[test] +fn an_adapter_with_nothing_to_say_costs_one_attempt_and_backs_off() { + let mut rec = recovery(); + let provider = FakeProvider { + exited_plan: None, + ..FakeProvider::default() + }; + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + tick_at(&mut rec, &provider, T0 + AFTER_RESET, at(mono, AFTER_RESET)); + assert_eq!(rec.state(), RecoveryState::Backoff); +} diff --git a/plugins/nightcrow-recovery/src/state_tests/transitions.rs b/plugins/nightcrow-recovery/src/state_tests/transitions.rs new file mode 100644 index 00000000..ca0cb61e --- /dev/null +++ b/plugins/nightcrow-recovery/src/state_tests/transitions.rs @@ -0,0 +1,148 @@ +//! Every transition of the machine, and the bound on how many times it will try. + +use super::*; + +/// The monotonic instant matching `T0 + delta` on the wall clock, so both clocks +/// advance together and no jump is detected. +fn at(base: Instant, delta: i64) -> Instant { + base + Duration::from_secs(delta as u64) +} + +#[test] +fn a_usage_limit_with_a_known_reset_waits_until_that_reset() { + let mut rec = recovery(); + let out = rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + assert_eq!(rec.deadline_epoch(), Some(RESET + RESET_GRACE_SECS)); + assert_eq!(states(&out), vec!["limit_detected", "waiting_for_reset"]); + assert!(action(&out).is_none(), "a wait asks the host for nothing"); +} + +#[test] +fn the_same_limit_reported_twice_is_one_episode_and_changes_nothing() { + let mut rec = recovery(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + let again = rec.note_limit(usage(Some(SESSION), Some(RESET)), T0 + 1, mono); + assert!(again.is_empty()); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + assert_eq!(rec.deadline_epoch(), Some(RESET + RESET_GRACE_SECS)); +} + +#[test] +fn a_limit_naming_a_different_session_starts_a_new_episode() { + let mut rec = recovery(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + let out = rec.note_limit(usage(Some(OTHER_SESSION), Some(RESET + 60)), T0, mono); + assert_eq!(rec.session_id(), Some(OTHER_SESSION)); + assert_eq!(rec.deadline_epoch(), Some(RESET + 60 + RESET_GRACE_SECS)); + assert!(!out.is_empty()); +} + +#[test] +fn a_transient_failure_backs_off_instead_of_waiting_for_a_usage_window() { + let mut rec = recovery(); + let out = rec.note_limit(transient(), T0, Instant::now()); + assert_eq!(rec.state(), RecoveryState::Backoff); + assert_eq!(rec.deadline_epoch(), Some(T0 + BACKOFF_BASE_SECS)); + assert_eq!(states(&out), vec!["limit_detected", "backoff"]); +} + +#[test] +fn a_limit_with_no_known_reset_backs_off() { + let mut rec = recovery(); + rec.note_limit(usage(Some(SESSION), None), T0, Instant::now()); + assert_eq!(rec.state(), RecoveryState::Backoff); + assert_eq!(rec.deadline_epoch(), Some(T0 + BACKOFF_BASE_SECS)); +} + +#[test] +fn a_failure_only_a_human_can_fix_goes_straight_to_needs_attention() { + let mut rec = recovery(); + let out = rec.note_limit(needs_human(), T0, Instant::now()); + assert_eq!(rec.state(), RecoveryState::NeedsAttention); + assert_eq!(states(&out), vec!["limit_detected", "needs_attention"]); + assert!(action(&out).is_none()); +} + +#[test] +fn needs_attention_ignores_every_later_limit_report() { + let mut rec = recovery(); + let mono = Instant::now(); + rec.note_limit(needs_human(), T0, mono); + let out = rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + assert!(out.is_empty()); + assert_eq!(rec.state(), RecoveryState::NeedsAttention); +} + +#[test] +fn a_stale_generation_cannot_drive_a_transition() { + let mut rec = recovery(); + let mono = Instant::now(); + rec.on_event(&opened(2)).expect("a newer generation"); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + + for stale in [exited(1), closed(1), user_input(1), went_idle(1)] { + assert!(rec.on_event(&stale).is_none(), "{stale:?} is stale"); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + assert_eq!(rec.generation(), 2); + assert!(rec.alive(), "a stale exit does not mark the pane dead"); + } +} + +#[test] +fn a_clock_pushed_forward_during_a_wait_does_not_resume_early() { + let mut rec = recovery(); + let provider = FakeProvider::relaunch_only(); + let mono = Instant::now(); + rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, mono); + rec.on_event(&exited(1)).expect("current generation"); + // A minute of real time, then the wall clock leaps a day ahead. + tick_at(&mut rec, &provider, T0 + 60, at(mono, 60)); + let out = tick_at(&mut rec, &provider, T0 + 60 + 86_400, at(mono, 61)); + assert_eq!(rec.state(), RecoveryState::WaitingForReset); + assert!(action(&out).is_none()); +} + +#[test] +fn every_state_reports_the_name_the_host_displays() { + for (state, name) in [ + (RecoveryState::Idle, "idle"), + (RecoveryState::LimitDetected, "limit_detected"), + (RecoveryState::WaitingForReset, "waiting_for_reset"), + (RecoveryState::ReadyToResume, "ready_to_resume"), + (RecoveryState::Resuming, "resuming"), + (RecoveryState::Backoff, "backoff"), + (RecoveryState::NeedsAttention, "needs_attention"), + ] { + assert_eq!(state.as_str(), name); + } +} + +#[test] +fn a_status_carries_the_pane_identity_the_deadline_and_the_attempt() { + let mut rec = recovery(); + let out = rec.note_limit(usage(Some(SESSION), Some(RESET)), T0, Instant::now()); + let last = out.last().expect("a status was reported"); + match last { + PluginCommand::Status { + v, + token, + generation, + state, + deadline_epoch, + attempt, + .. + } => { + assert_eq!(*v, PROTOCOL_VERSION); + assert_eq!(token, TOKEN); + assert_eq!(*generation, 1); + assert_eq!(state, "waiting_for_reset"); + assert_eq!(*deadline_epoch, Some(RESET + RESET_GRACE_SECS)); + assert_eq!(*attempt, 0); + } + other => panic!("expected a status, got {other:?}"), + } +} diff --git a/plugins/nightcrow-recovery/src/wait.rs b/plugins/nightcrow-recovery/src/wait.rs new file mode 100644 index 00000000..d6699589 --- /dev/null +++ b/plugins/nightcrow-recovery/src/wait.rs @@ -0,0 +1,143 @@ +//! Waiting for a usage limit to reset, without trusting either clock alone. +//! +//! A reset time arrives as an absolute unix second, which is the only form a +//! provider reports and the only form worth showing a human. But the wall clock +//! can be changed underneath a wait that lasts hours — an NTP correction, a +//! laptop returning from suspend, a user fixing their timezone — and a wait +//! driven purely by wall time would then either fire immediately (resuming into +//! a limit that has not cleared, burning an attempt) or never fire at all +//! (stranding the pane). +//! +//! So a wait keeps both: the absolute deadline, and a monotonic countdown of the +//! same length. Between two polls the two clocks must advance together; when +//! they disagree by more than [`JUMP_TOLERANCE_SECS`] the wall clock moved, and +//! the deadline is shifted by that amount so it stays fixed to the *new* clock. +//! The monotonic countdown then still has to elapse before the wait is over, so +//! a jump can neither shorten nor lengthen the real time spent waiting. + +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +/// Disagreement between the wall and monotonic clocks that counts as the wall +/// clock having been changed. +/// +/// Poll-to-poll deltas are compared at whole-second resolution, so a couple of +/// seconds of rounding is normal; five is comfortably above that and far below +/// any correction a human or NTP would make worth reacting to. +pub const JUMP_TOLERANCE_SECS: i64 = 5; + +/// Shortest wait, applied even when the reported reset time is already past. +/// +/// A reset time can be stale by the time it reaches us. Resuming in the same +/// breath as noticing the limit would spend an attempt on a limit that has not +/// actually cleared, so every wait has a floor. +pub const MIN_WAIT_SECS: i64 = 15; + +/// Longest wait. Claude's longest documented window is seven days; eight days +/// is past anything legitimate, so a deadline that would exceed it is clamped +/// rather than parking a pane indefinitely. +/// Must stay under the host's `PENDING_RELAUNCH_TTL` (nine days): the host +/// retires an exited pane's slot at that point, and a wait outlasting it would +/// end with nothing left to resume. +pub const MAX_WAIT_SECS: i64 = 8 * 24 * 60 * 60; + +/// Added to a reported reset time before resuming. +/// +/// Providers report the second a window rolls over, and a request landing on +/// that exact second is still liable to be refused. Half a minute costs nothing +/// against a multi-hour window and avoids spending an attempt on the boundary. +pub const RESET_GRACE_SECS: i64 = 30; + +/// First backoff step, used when no reset time is known. +/// +/// Long enough that a provider's own transient failure has passed, short enough +/// that a human watching the pane sees progress. +pub const BACKOFF_BASE_SECS: i64 = 30; + +/// Ceiling on the doubling. Past half an hour a blind retry is no more likely to +/// succeed, and the machine's attempt bound will end the sequence anyway. +pub const BACKOFF_MAX_SECS: i64 = 30 * 60; + +/// Current unix second, or 0 if the system clock predates the epoch — a value +/// every reset-time check treats as implausible, which is the safe reading. +pub fn now_epoch() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +/// A pending wait. Created once, polled on the plugin's timer. +#[derive(Debug, Clone)] +pub struct ResetWait { + /// The deadline as this process now believes it, in unix seconds. Shifted + /// when a wall-clock jump is detected. + deadline_epoch: i64, + /// How long the wait should really last, fixed when the wait was made. + planned: Duration, + started: Instant, + last_epoch: i64, + last_mono: Instant, +} + +impl ResetWait { + /// Wait until `deadline_epoch` (plus [`RESET_GRACE_SECS`]), clamped to + /// [`MIN_WAIT_SECS`]..=[`MAX_WAIT_SECS`]. + pub fn until(deadline_epoch: i64, now_epoch: i64, now: Instant) -> Self { + let target = deadline_epoch.saturating_add(RESET_GRACE_SECS); + let planned = (target - now_epoch).clamp(MIN_WAIT_SECS, MAX_WAIT_SECS); + Self { + deadline_epoch: now_epoch.saturating_add(planned), + planned: Duration::from_secs(planned as u64), + started: now, + last_epoch: now_epoch, + last_mono: now, + } + } + + /// Wait out an exponential backoff step, for when no reset time is known. + /// `attempt` counts from 1. + pub fn backoff(attempt: u32, now_epoch: i64, now: Instant) -> Self { + // `attempt - 1` doublings, saturating so a large attempt count cannot + // overflow the shift; the cap makes anything past a few steps identical. + let shift = attempt.saturating_sub(1).min(u32::BITS - 2); + let secs = BACKOFF_BASE_SECS + .saturating_mul(1i64 << shift) + .clamp(MIN_WAIT_SECS, BACKOFF_MAX_SECS); + // Built directly rather than through `until`: a backoff is a duration we + // chose, so it needs no reset grace and no provider-reported time. + let deadline = now_epoch.saturating_add(secs); + Self { + deadline_epoch: deadline, + planned: Duration::from_secs(secs as u64), + started: now, + last_epoch: now_epoch, + last_mono: now, + } + } + + /// When this wait expects to end, in unix seconds on the clock as currently + /// understood. This is what the host displays. + pub fn deadline_epoch(&self) -> i64 { + self.deadline_epoch + } + + /// Whether the wait is over. Also absorbs any wall-clock jump since the last + /// call, so this must be called on a timer rather than only when the answer + /// is wanted. + pub fn poll(&mut self, now_epoch: i64, now: Instant) -> bool { + let mono_delta = now.duration_since(self.last_mono).as_secs() as i64; + let jump = (now_epoch - self.last_epoch) - mono_delta; + if jump.abs() >= JUMP_TOLERANCE_SECS { + // Keep the deadline pinned to real elapsed time rather than to the + // number the wall clock now shows. + self.deadline_epoch = self.deadline_epoch.saturating_add(jump); + } + self.last_epoch = now_epoch; + self.last_mono = now; + now_epoch >= self.deadline_epoch && now.duration_since(self.started) >= self.planned + } +} + +#[cfg(test)] +#[path = "wait_tests.rs"] +mod tests; diff --git a/plugins/nightcrow-recovery/src/wait_tests.rs b/plugins/nightcrow-recovery/src/wait_tests.rs new file mode 100644 index 00000000..0fd36319 --- /dev/null +++ b/plugins/nightcrow-recovery/src/wait_tests.rs @@ -0,0 +1,107 @@ +use super::*; + +/// A fixed "now" so a test's arithmetic is readable. 2026-01-01T00:00:00Z. +const T0: i64 = 1_767_225_600; + +fn start() -> Instant { + Instant::now() +} + +fn after(base: Instant, secs: u64) -> Instant { + base + Duration::from_secs(secs) +} + +#[test] +fn a_wait_ends_only_once_both_clocks_have_passed_the_deadline() { + let mono = start(); + let mut wait = ResetWait::until(T0 + 600, T0, mono); + let planned = 600 + RESET_GRACE_SECS; + assert!(!wait.poll(T0 + planned - 1, after(mono, planned as u64 - 1))); + assert!(wait.poll(T0 + planned, after(mono, planned as u64))); +} + +#[test] +fn a_reset_time_already_in_the_past_still_waits_the_floor() { + let mono = start(); + let mut wait = ResetWait::until(T0 - 10_000, T0, mono); + assert_eq!(wait.deadline_epoch(), T0 + MIN_WAIT_SECS); + assert!(!wait.poll( + T0 + MIN_WAIT_SECS - 1, + after(mono, MIN_WAIT_SECS as u64 - 1) + )); + assert!(wait.poll(T0 + MIN_WAIT_SECS, after(mono, MIN_WAIT_SECS as u64))); +} + +#[test] +fn a_reset_time_beyond_the_maximum_wait_is_clamped() { + let mono = start(); + let wait = ResetWait::until(T0 + MAX_WAIT_SECS * 10, T0, mono); + assert_eq!(wait.deadline_epoch(), T0 + MAX_WAIT_SECS); +} + +#[test] +fn a_wait_ends_a_grace_period_after_the_reported_reset() { + let wait = ResetWait::until(T0 + 600, T0, start()); + assert_eq!(wait.deadline_epoch(), T0 + 600 + RESET_GRACE_SECS); +} + +#[test] +fn a_wall_clock_jumped_forward_does_not_fire_a_wait_early() { + let mono = start(); + let mut wait = ResetWait::until(T0 + 3600, T0, mono); + // One minute of real time passes, then the wall clock leaps two hours ahead. + assert!(!wait.poll(T0 + 60, after(mono, 60))); + assert!(!wait.poll(T0 + 60 + 7200, after(mono, 61))); + // Even far past the original wall-clock deadline, the countdown has not run. + assert!(!wait.poll(T0 + 60 + 7200 + 5, after(mono, 66))); +} + +#[test] +fn a_wall_clock_jumped_backwards_does_not_strand_a_wait() { + let mono = start(); + let planned = 600 + RESET_GRACE_SECS; + let mut wait = ResetWait::until(T0 + 600, T0, mono); + // The clock is corrected back by a day after a minute of real time. + assert!(!wait.poll(T0 + 60, after(mono, 60))); + let shifted = T0 + 60 - 86_400; + assert!(!wait.poll(shifted, after(mono, 61))); + // The wait still ends after its planned real duration, on the new clock. + let end_mono = after(mono, planned as u64 + 1); + assert!(wait.poll(shifted + planned - 60, end_mono)); +} + +#[test] +fn small_clock_jitter_does_not_shift_the_deadline() { + let mono = start(); + let mut wait = ResetWait::until(T0 + 600, T0, mono); + let before = wait.deadline_epoch(); + // A second of skew is rounding, not a jump. + assert!(!wait.poll(T0 + 61, after(mono, 60))); + assert_eq!(wait.deadline_epoch(), before); +} + +#[test] +fn backoff_doubles_each_attempt_and_stops_at_the_cap() { + let mono = start(); + let step = |attempt| ResetWait::backoff(attempt, T0, mono).deadline_epoch() - T0; + assert_eq!(step(1), BACKOFF_BASE_SECS); + assert_eq!(step(2), BACKOFF_BASE_SECS * 2); + assert_eq!(step(3), BACKOFF_BASE_SECS * 4); + assert_eq!(step(20), BACKOFF_MAX_SECS); + assert_eq!(step(u32::MAX), BACKOFF_MAX_SECS); +} + +#[test] +fn a_backoff_wait_ends_after_its_own_duration() { + let mono = start(); + let mut wait = ResetWait::backoff(1, T0, mono); + let secs = BACKOFF_BASE_SECS as u64; + assert!(!wait.poll(T0 + secs as i64 - 1, after(mono, secs - 1))); + assert!(wait.poll(T0 + secs as i64, after(mono, secs))); +} + +#[test] +fn now_epoch_reports_a_plausible_current_time() { + // Any real clock is past 2020 and this test's own compile date. + assert!(now_epoch() > 1_577_836_800); +} diff --git a/src/app/terminal_ctrl.rs b/src/app/terminal_ctrl.rs index a11cf27c..2208fed5 100644 --- a/src/app/terminal_ctrl.rs +++ b/src/app/terminal_ctrl.rs @@ -57,6 +57,19 @@ impl App { !self.terminal.owns_size } + /// Give up on the recovery a plugin has pending, releasing the slot it was + /// holding. The session decides and tells every client; nothing is assumed + /// here (see `TerminalState::cancel_recovery`). + pub fn cancel_pane_recovery(&mut self) { + self.terminal.cancel_recovery(); + } + + /// Whether the cancel key would do anything — the hint rows advertise it only + /// where it would, since a hint for a no-op lies. + pub fn can_cancel_recovery(&self) -> bool { + self.terminal.can_cancel_recovery() + } + /// Ask the session to close the active pane. Nothing is re-clamped here: /// the pane goes when its exit arrives, and `poll_terminal` clamps focus and /// fullscreen then — the same path a pane another client closed takes. diff --git a/src/application/input/dispatch.rs b/src/application/input/dispatch.rs index 79a6446d..287de284 100644 --- a/src/application/input/dispatch.rs +++ b/src/application/input/dispatch.rs @@ -131,6 +131,12 @@ pub(super) fn handle_global_action(app: &mut App, action: Action) -> Option { + app.cancel_pane_recovery(); + Some(KeyOutcome::Continue) + } Action::ClosePane => { // Scoped by `can_close_pane` (terminal focus — the close target // is invisible without it). The key is still consumed so it diff --git a/src/application/tests/terminal.rs b/src/application/tests/terminal.rs index 804bbe65..74763c8b 100644 --- a/src/application/tests/terminal.rs +++ b/src/application/tests/terminal.rs @@ -36,3 +36,46 @@ fn handle_key_terminal_ctrl_app_keys_all_pass_through() { ); } } + +#[test] +fn handle_key_leader_then_c_cancels_the_pane_recovery() { + // The fake backend answers a cancel the way a session does — by reporting the + // pane `cancelled` — so the badge clearing on the next poll is proof the + // request actually went out. + let mut app = app_with_terminal_pane(); + let pane = app.terminal.panes[0].id; + app.terminal.recovery.insert( + pane, + crate::runtime::terminal::PaneRecovery { + state: "waiting_for_reset".to_string(), + detail: None, + deadline_epoch: Some(1_700_000_000), + attempt: 1, + }, + ); + assert!(app.can_cancel_recovery(), "the hint must be advertised"); + + let _ = handle_key(&mut app, leader()); + let _ = handle_key(&mut app, press(KeyCode::Char('c'), KeyModifiers::NONE)); + app.poll_terminal(); + + assert!( + !app.can_cancel_recovery(), + "` c` must reach the session" + ); + assert!( + backend_payloads(&app).is_empty(), + "a leader follow-up must never leak into the PTY" + ); +} + +#[test] +fn handle_key_bare_c_in_a_terminal_pane_reaches_the_program() { + // The cancel binding is leader-prefixed precisely so a bare `c` is still + // ordinary typing in whatever the pane is running. + let mut app = app_with_terminal_pane(); + + let _ = handle_key(&mut app, press(KeyCode::Char('c'), KeyModifiers::NONE)); + + assert_eq!(backend_payloads(&app), vec![b"c".to_vec()]); +} diff --git a/src/backend/hub.rs b/src/backend/hub.rs index 61efddb6..b90bf8dd 100644 --- a/src/backend/hub.rs +++ b/src/backend/hub.rs @@ -105,6 +105,15 @@ impl TerminalBackend for HubBackend { } } + fn cancel_recovery(&mut self, id: PaneId) { + if let Err(err) = self + .link + .send(HubClientMessage::CancelRecovery { pane: id }) + { + tracing::warn!(%err, pane = id, "could not cancel a pane's recovery"); + } + } + fn drain_events(&mut self) -> Vec { let mut events = Vec::new(); for message in self.link.drain() { @@ -140,6 +149,19 @@ impl TerminalBackend for HubBackend { TerminalMessage::Event(HubServerMessage::Reordered { order }) => { events.push(BackendEvent::Reordered { order }) } + TerminalMessage::Event(HubServerMessage::Recovery { + pane, + state, + detail, + deadline_epoch, + attempt, + }) => events.push(BackendEvent::Recovery { + pane, + state, + detail, + deadline_epoch, + attempt, + }), // Refusals do not come this way — they are not about a pane, so // the client keeps them on the queue that reaches its notices. TerminalMessage::Event(HubServerMessage::Error { message }) => { diff --git a/src/backend/identity.rs b/src/backend/identity.rs new file mode 100644 index 00000000..5a8939c9 --- /dev/null +++ b/src/backend/identity.rs @@ -0,0 +1,88 @@ +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Serialize}; + +/// Env var carrying the pane token into the pane's child process. +/// +/// A provider CLI spawns its own helper processes (hooks, statusline commands) +/// and those inherit this, which is what lets an out-of-process observer say +/// which pane an event came from. The working directory cannot answer that — +/// nightcrow deliberately allows several panes on one repository, so cwd +/// identifies the project rather than the pane. +pub const PANE_TOKEN_ENV: &str = "NIGHTCROW_PANE_TOKEN"; + +/// Entropy behind a pane token, matching the viewer's session tokens at half +/// the width: a session holds a handful of panes, not thousands. +const TOKEN_BYTES: usize = 16; + +/// Opaque name for a pane slot, stable for as long as the slot exists. +/// +/// [`PaneId`](super::PaneId) cannot serve this purpose outside the process that +/// owns the panes: it is a per-backend counter that restarts at 1 whenever a +/// backend is rebuilt, so the same number means different panes across two +/// runs. The token is random instead, and it deliberately outlives the process +/// occupying the slot — an observer tracking a slot keeps its state when the +/// slot's process is replaced. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct PaneToken(String); + +impl PaneToken { + /// Mint a token from OS entropy. + /// + /// Fallible rather than silently weakened: a predictable token would let + /// anything that can guess one address a pane it was never given. + pub fn new() -> Result { + let mut bytes = [0u8; TOKEN_BYTES]; + getrandom::fill(&mut bytes) + .map_err(|e| anyhow!("OS RNG unavailable for pane token: {e}"))?; + let mut hex = String::with_capacity(TOKEN_BYTES * 2); + for b in bytes { + hex.push_str(&format!("{b:02x}")); + } + Ok(Self(hex)) + } + + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Which spawn of a pane slot something refers to. +/// +/// Starts at [`FIRST_GENERATION`] and rises every time the slot's process is +/// replaced. An out-of-process observer decides what to do asynchronously, so +/// by the time it asks for something the process it watched may already be +/// gone; carrying the generation is what makes that detectable instead of +/// letting a decision about one process land on its successor. +pub type PaneGeneration = u32; + +pub const FIRST_GENERATION: PaneGeneration = 1; + +/// A pane slot's identity: which slot, and which spawn within it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaneIdentity { + pub token: PaneToken, + pub generation: PaneGeneration, +} + +impl PaneIdentity { + pub fn new() -> Result { + Ok(Self { + token: PaneToken::new()?, + generation: FIRST_GENERATION, + }) + } + + /// Advance to the next spawn of the same slot. + /// + /// Saturating rather than wrapping: a wrapped generation would make a stale + /// command look current again, which is the one thing the counter exists to + /// prevent. A slot that somehow reached `u32::MAX` relaunches stops being + /// able to distinguish spawns, and refusing to move is the safe end state. + pub fn advance(&mut self) { + self.generation = self.generation.saturating_add(1); + } +} + +#[cfg(test)] +#[path = "identity_tests.rs"] +mod tests; diff --git a/src/backend/identity_tests.rs b/src/backend/identity_tests.rs new file mode 100644 index 00000000..bad9fdfb --- /dev/null +++ b/src/backend/identity_tests.rs @@ -0,0 +1,47 @@ +use super::*; + +#[test] +fn minting_two_tokens_yields_distinct_values() { + let a = PaneToken::new().expect("OS RNG"); + let b = PaneToken::new().expect("OS RNG"); + assert_ne!(a, b); + assert_eq!(a.as_str().len(), TOKEN_BYTES * 2); + assert!(a.as_str().chars().all(|c| c.is_ascii_hexdigit())); +} + +#[test] +fn a_new_identity_starts_at_the_first_generation() { + let id = PaneIdentity::new().expect("OS RNG"); + assert_eq!(id.generation, FIRST_GENERATION); +} + +#[test] +fn advancing_an_identity_keeps_the_token_and_raises_the_generation() { + let mut id = PaneIdentity::new().expect("OS RNG"); + let token = id.token.clone(); + id.advance(); + // The slot is the same slot; only the spawn inside it changed. + assert_eq!(id.token, token); + assert_eq!(id.generation, FIRST_GENERATION + 1); +} + +#[test] +fn advancing_at_the_counter_ceiling_refuses_to_wrap() { + let mut id = PaneIdentity::new().expect("OS RNG"); + id.generation = PaneGeneration::MAX; + id.advance(); + // Wrapping would make a stale generation compare equal to the live one. + assert_eq!(id.generation, PaneGeneration::MAX); +} + +#[test] +fn a_token_serialises_as_a_plain_string() { + // The plugin protocol carries tokens as JSON; a newtype that serialised as + // an object or array would break every out-of-process reader. + let token = PaneToken::new().expect("OS RNG"); + let json = serde_json::to_string(&token).expect("serialise"); + assert_eq!(json, format!("\"{}\"", token.as_str())); + + let back: PaneToken = serde_json::from_str(&json).expect("deserialise"); + assert_eq!(back, token); +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index de0df8e7..5947e38d 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,7 +1,10 @@ pub mod hub; +pub mod identity; pub mod pty; +pub mod slot; pub use hub::HubBackend; +pub use identity::{PaneGeneration, PaneToken}; pub use pty::PtyBackend; use anyhow::Result; @@ -64,6 +67,22 @@ pub enum BackendEvent { SizeOwnership { owned: bool, }, + /// What a plugin driving `pane` reports about getting it running again. + /// + /// Only a backend serving a shared session reports this: the plugins run + /// beside the session's panes, not beside this client. Pane metadata, not + /// screen content — nothing here reaches an emulator. + Recovery { + pane: PaneId, + /// The plugin's own short label. Uninterpreted here; the one value with a + /// meaning is `"cancelled"`, which ends the report. + state: String, + detail: Option, + /// When the wait ends, in unix epoch seconds, or `None` when no clock is + /// involved. + deadline_epoch: Option, + attempt: u32, + }, } pub trait TerminalBackend { @@ -96,6 +115,15 @@ pub trait TerminalBackend { /// shared session has anything to ask. fn claim_size(&mut self) {} + /// Ask the session to give up on a pane's pending recovery. + /// + /// A no-op by default: a backend that owns its PTYs has no plugin nursing one + /// back, so there is nothing pending to abandon. The session answers with a + /// [`BackendEvent::Recovery`] whose state is `"cancelled"`. + fn cancel_recovery(&mut self, pane: PaneId) { + let _ = pane; + } + /// Test hook: byte payloads recorded by a recording backend. Real /// backends return `None`; the in-memory test `FakeBackend` overrides /// this so input tests can assert exact PTY pass-through bytes. diff --git a/src/backend/pty.rs b/src/backend/pty.rs index e56bea2b..fae5fda3 100644 --- a/src/backend/pty.rs +++ b/src/backend/pty.rs @@ -1,13 +1,17 @@ +use super::slot::{PaneSlot, PaneSlots}; use super::{BackendEvent, PaneId, TerminalBackend}; use crate::platform::threading::try_timed_join; use anyhow::Result; -use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; +use portable_pty::PtySize; use std::collections::BTreeMap; -use std::io::{Read, Write}; +use std::io::Write; use std::path::{Path, PathBuf}; -use std::sync::mpsc::{self, Receiver}; +use std::sync::mpsc::Receiver; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; + +#[path = "pty_spawn.rs"] +mod spawn; /// Reap window for PTY reader / wait threads. Bigger than the commit-log /// REAP_TIMEOUT because `read()` on a PTY master can stay blocked if a @@ -21,21 +25,21 @@ const PTY_REAP_TIMEOUT: Duration = Duration::from_millis(50); /// the round-robin cap bounds the work per pane to a small constant. const PER_PANE_DRAIN_BUDGET: usize = 64; -enum PtyEvent { +pub(super) enum PtyEvent { Output(Vec), Exited, } -struct PtyPane { +pub(super) struct PtyPane { // master/writer are wrapped in Option so `Drop` can release them // before joining the reader thread — the reader blocks in `read()` // and only unblocks when both sides of the PTY are closed. - master: Option>, - writer: Option>, - killer: Box, - rx: Receiver, - reader_handle: Option>, - wait_handle: Option>, + pub(super) master: Option>, + pub(super) writer: Option>, + pub(super) killer: Box, + pub(super) rx: Receiver, + pub(super) reader_handle: Option>, + pub(super) wait_handle: Option>, } impl Drop for PtyPane { @@ -68,11 +72,15 @@ pub struct PtyBackend { // and stays deterministic across runs. HashMap iteration was random // per process, which made inter-pane event ordering unobservable // and could mask fairness regressions in tests. - panes: BTreeMap, - next_id: PaneId, + pub(super) panes: BTreeMap, + /// Slot bookkeeping — identity, launch, idle clock — kept beside `panes` + /// rather than inside `PtyPane` because a relaunch replaces the `PtyPane` + /// while the slot has to survive it. + pub(super) slots: PaneSlots, + pub(super) next_id: PaneId, // Each new pane spawns the shell here so its cwd matches the repo // nightcrow is tracking, even when the binary was launched elsewhere. - cwd: PathBuf, + pub(super) cwd: PathBuf, /// Panes created since the last drain, waiting to be reported. /// /// This backend knows the id the moment it makes the pane, but the trait @@ -85,94 +93,44 @@ impl PtyBackend { pub fn new(cwd: impl AsRef) -> Self { Self { panes: BTreeMap::new(), + slots: PaneSlots::default(), next_id: 1, cwd: cwd.as_ref().to_path_buf(), created: Vec::new(), } } -} -impl PtyBackend { - /// Open a pane and say which one it is. - /// - /// The trait reports panes as events, because a backend serving a shared - /// session cannot answer on the spot. This one can, and the terminal hub — - /// which owns a `PtyBackend` outright rather than through the trait — needs - /// the id to register the pane before anything else happens to it. - pub fn open_pane(&mut self, rows: u16, cols: u16, command: Option<&str>) -> Result { - // Reserve the next id only after every fallible PTY/spawn step succeeds, - // so a failure here does not consume an id slot. - let pty_system = NativePtySystem::default(); - let pair = pty_system.openpty(PtySize { - rows, - cols, - pixel_width: 0, - pixel_height: 0, - })?; - - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); - let mut cmd = CommandBuilder::new(&shell); - // A reserved startup command runs through the login shell's `-lc`: - // the command text is passed as a single argv item, so the shell — - // not us — handles its quoting/word-splitting. This avoids the race - // of spawning a shell and later injecting `command\r`, and avoids any - // string interpolation into a wrapper on our side. - if let Some(command) = command { - cmd.arg("-lc"); - cmd.arg(command); - } - cmd.env("TERM", "xterm-256color"); - // Only set cwd if the directory actually exists; otherwise inherit - // ours so spawn does not fail outright (matters for unit tests that - // pass placeholder paths). - if let Ok(canonical) = self.cwd.canonicalize() { - cmd.cwd(canonical); - } - let mut child = pair.slave.spawn_command(cmd)?; - let killer = child.clone_killer(); - drop(pair.slave); - - let mut reader = pair.master.try_clone_reader()?; - let writer = pair.master.take_writer()?; + /// The slot behind a live pane, or `None` once the pane is gone. + pub fn slot(&self, id: PaneId) -> Option<&PaneSlot> { + self.slots.get(id) + } - let id = self.next_id; - let next = id - .checked_add(1) - .ok_or_else(|| anyhow::anyhow!("pane id counter overflow"))?; - self.next_id = next; + /// Which live pane a token names. + pub fn pane_for_token(&self, token: &super::PaneToken) -> Option { + self.slots.find_by_token(token) + } - let (tx, rx) = mpsc::channel(); - let reader_handle = thread::spawn(move || { - let mut buf = [0u8; 4096]; - loop { - match reader.read(&mut buf) { - Ok(0) | Err(_) => break, - Ok(n) => { - if tx.send(PtyEvent::Output(buf[..n].to_vec())).is_err() { - break; - } - } - } - } - let _ = tx.send(PtyEvent::Exited); - }); + /// Whether the pane still has a running process. + pub fn is_process_alive(&self, id: PaneId) -> bool { + self.panes.contains_key(&id) + } - let wait_handle = thread::spawn(move || { - let _ = child.wait(); - }); + /// Let go of a pane's process while keeping its slot. + /// + /// Splitting this out of `destroy_pane` is what makes waiting for a reset + /// affordable: a wait can run for hours, and holding the dead child's fds + /// and threads open for that long to preserve the token would be pure + /// waste. The slot is small, and it is the only part a relaunch needs. + pub fn release_process(&mut self, id: PaneId) { + self.panes.remove(&id); + } - self.panes.insert( - id, - PtyPane { - master: Some(pair.master), - writer: Some(writer), - killer, - rx, - reader_handle: Some(reader_handle), - wait_handle: Some(wait_handle), - }, - ); - Ok(id) + /// Drop a slot for good, retiring its token. + /// + /// Called when nothing more is expected of the pane — the wait was + /// abandoned, the pane was closed, or the session is going away. + pub fn retire_slot(&mut self, id: PaneId) { + self.slots.remove(id); } } @@ -198,6 +156,9 @@ impl TerminalBackend for PtyBackend { // Removing the pane drops it, which runs PtyPane::drop: kill, // release master/writer, join reader/wait threads. self.panes.remove(&id); + // The slot goes with it, retiring its token. A relaunch keeps the slot + // by going through `relaunch_pane` instead of destroy-then-open. + self.slots.remove(id); } fn send_input(&mut self, id: PaneId, data: &[u8]) -> Result<()> { @@ -249,11 +210,17 @@ impl TerminalBackend for PtyBackend { // Ahead of any output: a pane has to exist before bytes can be routed // to it, and both can be queued before the same drain. let mut events: Vec = std::mem::take(&mut self.created); + let now = Instant::now(); for (id, pane) in &self.panes { let mut budget = PER_PANE_DRAIN_BUDGET; while budget > 0 { match pane.rx.try_recv() { Ok(PtyEvent::Output(data)) => { + // One timestamp for the whole drain: the bytes arrived + // between this tick and the last, and a per-event clock + // read would claim a precision the 8 ms poll does not + // have. + self.slots.mark_output(*id, now); events.push(BackendEvent::Output { pane: *id, data }); } Ok(PtyEvent::Exited) => { diff --git a/src/backend/pty_spawn.rs b/src/backend/pty_spawn.rs new file mode 100644 index 00000000..7238cd05 --- /dev/null +++ b/src/backend/pty_spawn.rs @@ -0,0 +1,153 @@ +use super::{PtyBackend, PtyEvent, PtyPane}; +use crate::backend::PaneId; +use crate::backend::identity::{PANE_TOKEN_ENV, PaneIdentity}; +use crate::backend::slot::{PaneLaunch, resume_command_line}; +use anyhow::Result; +use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem}; +use std::io::Read; +use std::sync::mpsc; +use std::thread; +use std::time::Instant; + +impl PtyBackend { + /// Open a pane and say which one it is. + /// + /// The trait reports panes as events, because a backend serving a shared + /// session cannot answer on the spot. This one can, and the terminal hub — + /// which owns a `PtyBackend` outright rather than through the trait — needs + /// the id to register the pane before anything else happens to it. + pub fn open_pane(&mut self, rows: u16, cols: u16, command: Option<&str>) -> Result { + let identity = PaneIdentity::new()?; + let launch = PaneLaunch { + command: command.map(str::to_string), + }; + self.spawn_pane(rows, cols, command, identity, launch) + } + + /// Replace an exited pane's process, keeping the slot it ran in. + /// + /// A new `PaneId` is unavoidable: ids are monotonic and every client treats + /// `Exited` as final for one. The slot's token is what carries over, so an + /// observer that has been tracking this pane keeps its place, and the + /// generation moves so decisions made about the old process cannot land on + /// the new one. + /// + /// The composed command line is checked before anything is torn down, so a + /// refused relaunch leaves the pane exactly as it was. + pub fn relaunch_pane( + &mut self, + id: PaneId, + rows: u16, + cols: u16, + resume_args: &[String], + allowed_flags: &[String], + ) -> Result { + let slot = self + .slots + .get(id) + .ok_or_else(|| anyhow::anyhow!("pane {id} has no slot to relaunch"))?; + let launch = slot.launch.clone(); + let mut identity = slot.identity.clone(); + let line = resume_command_line(launch.command.as_deref(), resume_args, allowed_flags)?; + + identity.advance(); + // Retire the old process first: two children writing one slot's PTY + // would interleave, and the reader thread has to be let go before the + // replacement's is started. + self.panes.remove(&id); + self.slots.remove(id); + + // The retained launch stays the *original* invocation. Carrying the + // composed line forward instead would accumulate resume arguments on + // every further relaunch. + self.spawn_pane(rows, cols, Some(line.as_str()), identity, launch) + } + + fn spawn_pane( + &mut self, + rows: u16, + cols: u16, + command: Option<&str>, + identity: PaneIdentity, + launch: PaneLaunch, + ) -> Result { + // Reserve the next id only after every fallible PTY/spawn step succeeds, + // so a failure here does not consume an id slot. + let pty_system = NativePtySystem::default(); + let pair = pty_system.openpty(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + })?; + + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string()); + let mut cmd = CommandBuilder::new(&shell); + // A reserved startup command runs through the login shell's `-lc`: + // the command text is passed as a single argv item, so the shell — + // not us — handles its quoting/word-splitting. This avoids the race + // of spawning a shell and later injecting `command\r`, and avoids any + // string interpolation into a wrapper on our side. + if let Some(command) = command { + cmd.arg("-lc"); + cmd.arg(command); + } + cmd.env("TERM", "xterm-256color"); + // Set at spawn time because a child cannot be told afterwards, and the + // provider's own helper processes inherit it — that inheritance is what + // lets an out-of-process observer name the pane an event came from. + cmd.env(PANE_TOKEN_ENV, identity.token.as_str()); + // Only set cwd if the directory actually exists; otherwise inherit + // ours so spawn does not fail outright (matters for unit tests that + // pass placeholder paths). + if let Ok(canonical) = self.cwd.canonicalize() { + cmd.cwd(canonical); + } + let mut child = pair.slave.spawn_command(cmd)?; + let killer = child.clone_killer(); + drop(pair.slave); + + let mut reader = pair.master.try_clone_reader()?; + let writer = pair.master.take_writer()?; + + let id = self.next_id; + let next = id + .checked_add(1) + .ok_or_else(|| anyhow::anyhow!("pane id counter overflow"))?; + self.next_id = next; + + let (tx, rx) = mpsc::channel(); + let reader_handle = thread::spawn(move || { + let mut buf = [0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(n) => { + if tx.send(PtyEvent::Output(buf[..n].to_vec())).is_err() { + break; + } + } + } + } + let _ = tx.send(PtyEvent::Exited); + }); + + let wait_handle = thread::spawn(move || { + let _ = child.wait(); + }); + + self.panes.insert( + id, + PtyPane { + master: Some(pair.master), + writer: Some(writer), + killer, + rx, + reader_handle: Some(reader_handle), + wait_handle: Some(wait_handle), + }, + ); + self.slots.insert(id, identity, launch, Instant::now()); + Ok(id) + } +} diff --git a/src/backend/pty_tests.rs b/src/backend/pty_tests.rs index c1362396..4f36a50c 100644 --- a/src/backend/pty_tests.rs +++ b/src/backend/pty_tests.rs @@ -1,6 +1,10 @@ use super::*; +use crate::backend::identity::FIRST_GENERATION; use std::time::{Duration, Instant}; +/// Long enough that a `printf` and its exit are certainly drained. +const RELAUNCH_MARKER: &str = "nightcrow-relaunched"; + #[test] fn pty_backend_create_and_destroy_pane() { let mut backend = PtyBackend::new("."); @@ -53,6 +57,205 @@ fn pty_backend_drains_output_before_exit_event() { ); } +#[test] +fn opening_a_pane_gives_it_a_token_at_the_first_generation() { + let mut backend = PtyBackend::new("."); + let id = backend.open_pane(24, 80, None).expect("open_pane failed"); + + let identity = backend.slot(id).expect("pane has a slot").identity.clone(); + assert_eq!(identity.generation, FIRST_GENERATION); + assert!(!identity.token.as_str().is_empty()); + + backend.destroy_pane(id); +} + +#[test] +fn a_token_resolves_to_the_pane_holding_it() { + let mut backend = PtyBackend::new("."); + let id = backend.open_pane(24, 80, None).expect("open_pane failed"); + let token = backend.slot(id).expect("slot").identity.token.clone(); + + assert_eq!(backend.pane_for_token(&token), Some(id)); + backend.destroy_pane(id); + assert_eq!(backend.pane_for_token(&token), None); +} + +#[test] +fn relaunching_a_pane_keeps_the_token_and_advances_the_generation() { + let mut backend = PtyBackend::new("."); + let first = backend + .open_pane(24, 80, Some("printf first; exit")) + .expect("open_pane failed"); + let token = backend.slot(first).expect("slot").identity.token.clone(); + + let second = backend + .relaunch_pane(first, 24, 80, &[], &[]) + .expect("relaunch failed"); + + // A new id is unavoidable — ids are monotonic and clients treat `Exited` as + // final — so the token is what carries the slot's identity across. + assert_ne!(second, first); + let slot = backend.slot(second).expect("relaunched slot"); + assert_eq!(slot.identity.token, token); + assert_eq!(slot.identity.generation, FIRST_GENERATION + 1); + // The old id stops resolving, so a decision made about it cannot land here. + assert!(backend.slot(first).is_none()); + assert_eq!(backend.pane_for_token(&token), Some(second)); +} + +#[test] +fn a_relaunch_reproduces_the_original_command() { + let mut backend = PtyBackend::new("."); + let first = backend + .open_pane(24, 80, Some(&format!("printf {RELAUNCH_MARKER}; exit"))) + .expect("open_pane failed"); + let second = backend + .relaunch_pane(first, 24, 80, &[], &[]) + .expect("relaunch failed"); + + let deadline = Instant::now() + PTY_TEST_DEADLINE; + let mut output = Vec::new(); + let mut saw_exit = false; + while Instant::now() < deadline { + for event in backend.drain_events() { + match event { + BackendEvent::Output { pane, data } if pane == second => output.extend(data), + BackendEvent::Exited { pane } if pane == second => saw_exit = true, + _ => {} + } + } + if saw_exit { + break; + } + thread::sleep(Duration::from_millis(10)); + } + + assert!( + String::from_utf8_lossy(&output).contains(RELAUNCH_MARKER), + "relaunch did not re-run the original command" + ); +} + +#[test] +fn a_relaunch_keeps_the_original_command_rather_than_accumulating_resume_args() { + let mut backend = PtyBackend::new("."); + let allowed = vec!["--flag".to_string()]; + let args = vec!["--flag".to_string()]; + let first = backend + .open_pane(24, 80, Some("true")) + .expect("open_pane failed"); + + let second = backend + .relaunch_pane(first, 24, 80, &args, &allowed) + .expect("first relaunch"); + // The retained launch is the original invocation, so a second relaunch does + // not stack another copy of the resume arguments onto it. + assert_eq!( + backend + .slot(second) + .expect("slot") + .launch + .command + .as_deref(), + Some("true") + ); + + let third = backend + .relaunch_pane(second, 24, 80, &args, &allowed) + .expect("second relaunch"); + assert_eq!( + backend.slot(third).expect("slot").launch.command.as_deref(), + Some("true") + ); +} + +#[test] +fn relaunching_a_pane_that_is_gone_is_refused() { + let mut backend = PtyBackend::new("."); + let id = backend.open_pane(24, 80, Some("true")).expect("open_pane"); + backend.destroy_pane(id); + + let err = backend.relaunch_pane(id, 24, 80, &[], &[]).unwrap_err(); + assert!(err.to_string().contains("no slot"), "{err}"); +} + +#[test] +fn a_refused_relaunch_leaves_the_pane_running() { + let mut backend = PtyBackend::new("."); + let id = backend + .open_pane(24, 80, Some("sleep 30")) + .expect("open_pane"); + let token = backend.slot(id).expect("slot").identity.token.clone(); + + // Not in the allowlist, so the command line is refused before anything is + // torn down. + let args = vec!["--nope".to_string()]; + assert!(backend.relaunch_pane(id, 24, 80, &args, &[]).is_err()); + + assert_eq!(backend.pane_for_token(&token), Some(id)); + assert!(backend.panes.contains_key(&id)); + backend.destroy_pane(id); +} + +#[test] +fn two_panes_get_distinct_tokens() { + let mut backend = PtyBackend::new("."); + let a = backend.open_pane(24, 80, None).expect("open_pane failed"); + let b = backend.open_pane(24, 80, None).expect("open_pane failed"); + + // Two panes on one repository is a supported layout, so the token — not the + // working directory — is what tells them apart. + let ta = backend.slot(a).expect("a").identity.token.clone(); + let tb = backend.slot(b).expect("b").identity.token.clone(); + assert_ne!(ta, tb); + + backend.destroy_pane(a); + backend.destroy_pane(b); +} + +#[test] +fn destroying_a_pane_retires_its_token() { + let mut backend = PtyBackend::new("."); + let id = backend.open_pane(24, 80, None).expect("open_pane failed"); + backend.destroy_pane(id); + + // A held token must stop resolving, or it would address whatever id lands + // in this slot next. + assert!(backend.slot(id).is_none()); +} + +#[test] +fn a_panes_child_process_sees_its_token_in_the_environment() { + let mut backend = PtyBackend::new("."); + let id = backend + .open_pane(24, 80, Some("printf %s \"$NIGHTCROW_PANE_TOKEN\"; exit")) + .expect("open_pane failed"); + let token = backend.slot(id).expect("slot").identity.token.clone(); + + let deadline = Instant::now() + PTY_TEST_DEADLINE; + let mut output = Vec::new(); + let mut saw_exit = false; + while Instant::now() < deadline { + for event in backend.drain_events() { + match event { + BackendEvent::Output { data, .. } => output.extend(data), + BackendEvent::Exited { pane } if pane == id => saw_exit = true, + _ => {} + } + } + if saw_exit { + break; + } + thread::sleep(Duration::from_millis(10)); + } + + // This is the correlation path a provider's own hook processes inherit. + assert!( + String::from_utf8_lossy(&output).contains(token.as_str()), + "pane token was not exported to the child environment" + ); +} + #[test] fn pty_backend_runs_startup_command() { let mut backend = PtyBackend::new("."); diff --git a/src/backend/slot.rs b/src/backend/slot.rs new file mode 100644 index 00000000..66837b56 --- /dev/null +++ b/src/backend/slot.rs @@ -0,0 +1,180 @@ +use super::PaneId; +use super::identity::{PaneIdentity, PaneToken}; +use anyhow::{Result, bail}; +use std::collections::BTreeMap; +use std::time::{Duration, Instant}; + +/// What it takes to put a pane's process back after it exits. +/// +/// The hub discards the startup command once a pane is spawned, which is fine +/// until something wants to replace the process rather than the pane: a shell +/// cannot be asked what it was told to run. Keeping the text here is what makes +/// a relaunch reproduce the original launch instead of guessing at it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaneLaunch { + /// Startup command exactly as configured, or `None` for a bare shell. + pub command: Option, +} + +/// A pane slot: who it is, what it was launched as, and when it last spoke. +#[derive(Debug)] +pub struct PaneSlot { + pub identity: PaneIdentity, + pub launch: PaneLaunch, + last_output: Instant, +} + +impl PaneSlot { + /// How long the pane has been quiet. + /// + /// Measured from the last byte the child produced, not from the last thing + /// written to it: a CLI that is mid-answer keeps emitting, and typing into + /// it then would interleave with what it is drawing. + pub fn idle_for(&self, now: Instant) -> Duration { + now.saturating_duration_since(self.last_output) + } +} + +/// Per-pane slot bookkeeping, held beside the live PTYs. +/// +/// Separate from the PTY map because the two have different lifetimes: a +/// relaunch replaces the PTY while the slot — and so the token an outside +/// observer holds — has to survive it. +#[derive(Debug, Default)] +pub struct PaneSlots(BTreeMap); + +impl PaneSlots { + pub fn insert(&mut self, id: PaneId, identity: PaneIdentity, launch: PaneLaunch, now: Instant) { + self.0.insert( + id, + PaneSlot { + identity, + launch, + // A pane that has said nothing yet counts as quiet since it + // opened, so a plugin does not have to wait for first output + // before a freshly opened pane can be considered idle. + last_output: now, + }, + ); + } + + pub fn remove(&mut self, id: PaneId) -> Option { + self.0.remove(&id) + } + + pub fn get(&self, id: PaneId) -> Option<&PaneSlot> { + self.0.get(&id) + } + + /// Note that the pane produced output. + pub fn mark_output(&mut self, id: PaneId, now: Instant) { + if let Some(slot) = self.0.get_mut(&id) { + slot.last_output = now; + } + } + + /// Find the pane a token names, if it still exists. + /// + /// Linear over a handful of panes; a reverse index would be more state to + /// keep consistent than the scan costs. + pub fn find_by_token(&self, token: &PaneToken) -> Option { + self.0 + .iter() + .find(|(_, slot)| &slot.identity.token == token) + .map(|(id, _)| *id) + } +} + +/// Longest resume argument list a plugin may append. +/// +/// A resume invocation is a flag and an id; anything longer is not a resume. +const MAX_RESUME_ARGS: usize = 6; + +/// Longest single resume argument. Comfortably past a UUID or a session name. +const MAX_RESUME_ARG_LEN: usize = 256; + +/// Characters a resume argument may consist of. +/// +/// Deliberately narrower than "anything the shell can be made to swallow": the +/// argument is appended to a command line that a login shell parses, so a value +/// carrying a space, quote, backtick, `$`, or `;` is refused outright rather +/// than trusted to survive quoting. Quoting is applied as well — this is the +/// belt to that braces. +fn is_safe_arg_char(c: char) -> bool { + c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':' | '/' | '=' | '@' | '+') +} + +/// Build the command line for a relaunch. +/// +/// `allowed_flags` is the plugin's declared list from config. Anything that +/// looks like a flag must appear there, which is how the core refuses to weaken +/// a CLI's permission posture without knowing what any particular CLI's +/// permission flags are called — the user names what a plugin may pass, and a +/// flag they did not name cannot be smuggled in. +pub fn resume_command_line( + base: Option<&str>, + resume_args: &[String], + allowed_flags: &[String], +) -> Result { + let Some(base) = base else { + // Nothing was configured to run, so there is no session to resume and + // no original invocation to preserve. + bail!("pane has no startup command to relaunch"); + }; + if resume_args.is_empty() { + return Ok(base.to_string()); + } + if resume_args.len() > MAX_RESUME_ARGS { + bail!( + "relaunch passed {} arguments, at most {MAX_RESUME_ARGS} allowed", + resume_args.len() + ); + } + + let mut line = String::from(base); + for arg in resume_args { + if arg.is_empty() { + bail!("relaunch argument must not be empty"); + } + if arg.len() > MAX_RESUME_ARG_LEN { + bail!( + "relaunch argument is {} bytes, at most {MAX_RESUME_ARG_LEN} allowed", + arg.len() + ); + } + if !arg.chars().all(is_safe_arg_char) { + bail!("relaunch argument {arg:?} holds characters that are not allowed"); + } + if arg.starts_with('-') && !allowed_flags.iter().any(|f| f == arg) { + bail!( + "relaunch flag {arg:?} is not in the plugin's allowed_resume_flags; \ + add it there if the plugin is meant to pass it" + ); + } + line.push(' '); + line.push_str(&shell_quote(arg)); + } + Ok(line) +} + +/// Wrap a value so a login shell reads it as one literal word. +/// +/// Single quotes suspend every expansion the shell would otherwise perform, so +/// the only character needing care is the quote itself. +fn shell_quote(arg: &str) -> String { + let mut out = String::with_capacity(arg.len() + 2); + out.push('\''); + for c in arg.chars() { + if c == '\'' { + out.push_str("'\\''"); + } else { + out.push(c); + } + } + out.push('\''); + out +} + +#[cfg(test)] +#[path = "slot_tests.rs"] +mod tests; diff --git a/src/backend/slot_tests.rs b/src/backend/slot_tests.rs new file mode 100644 index 00000000..d4801a12 --- /dev/null +++ b/src/backend/slot_tests.rs @@ -0,0 +1,157 @@ +use super::*; +use crate::backend::identity::FIRST_GENERATION; + +fn launch(command: &str) -> PaneLaunch { + PaneLaunch { + command: Some(command.to_string()), + } +} + +fn slots_with_one(now: Instant) -> (PaneSlots, PaneId, PaneToken) { + let mut slots = PaneSlots::default(); + let identity = PaneIdentity::new().expect("OS RNG"); + let token = identity.token.clone(); + slots.insert(7, identity, launch("agent"), now); + (slots, 7, token) +} + +#[test] +fn a_freshly_opened_pane_counts_as_quiet_since_it_opened() { + let now = Instant::now(); + let (slots, id, _) = slots_with_one(now); + // Otherwise a pane that has printed nothing could never be called idle. + assert_eq!(slots.get(id).expect("slot").idle_for(now), Duration::ZERO); +} + +#[test] +fn marking_output_restarts_the_idle_clock() { + let start = Instant::now(); + let (mut slots, id, _) = slots_with_one(start); + let later = start + Duration::from_secs(5); + slots.mark_output(id, later); + + assert_eq!(slots.get(id).expect("slot").idle_for(later), Duration::ZERO); + assert_eq!( + slots + .get(id) + .expect("slot") + .idle_for(later + Duration::from_secs(2)), + Duration::from_secs(2) + ); +} + +#[test] +fn marking_output_on_a_pane_that_is_gone_is_ignored() { + let now = Instant::now(); + let (mut slots, id, _) = slots_with_one(now); + slots.remove(id); + slots.mark_output(id, now); + assert!(slots.get(id).is_none()); +} + +#[test] +fn a_token_resolves_to_its_pane_and_stops_resolving_once_removed() { + let now = Instant::now(); + let (mut slots, id, token) = slots_with_one(now); + assert_eq!(slots.find_by_token(&token), Some(id)); + + slots.remove(id); + // A held token must not address whatever occupies the slot next. + assert_eq!(slots.find_by_token(&token), None); +} + +#[test] +fn a_slot_keeps_the_launch_command_so_it_can_be_reproduced() { + let now = Instant::now(); + let (slots, id, _) = slots_with_one(now); + assert_eq!( + slots.get(id).expect("slot").launch.command.as_deref(), + Some("agent") + ); + assert_eq!( + slots.get(id).expect("slot").identity.generation, + FIRST_GENERATION + ); +} + +#[test] +fn no_resume_arguments_leaves_the_original_command_untouched() { + let line = resume_command_line(Some("agent --model x"), &[], &[]).expect("no args is fine"); + assert_eq!(line, "agent --model x"); +} + +#[test] +fn an_allowed_flag_and_its_value_are_appended_quoted() { + let args = vec!["--resume".to_string(), "abc-123".to_string()]; + let allowed = vec!["--resume".to_string()]; + let line = resume_command_line(Some("agent"), &args, &allowed).expect("allowed"); + assert_eq!(line, "agent '--resume' 'abc-123'"); +} + +#[test] +fn a_flag_the_plugin_was_not_allowed_to_pass_is_refused() { + // The whole point: a plugin cannot weaken a CLI's permission posture unless + // the user named that flag themselves. + let args = vec!["--dangerously-skip-permissions".to_string()]; + let allowed = vec!["--resume".to_string()]; + let err = resume_command_line(Some("agent"), &args, &allowed).unwrap_err(); + assert!( + err.to_string().contains("allowed_resume_flags"), + "error should point at the allowlist: {err}" + ); +} + +#[test] +fn an_argument_holding_shell_metacharacters_is_refused() { + let allowed: Vec = Vec::new(); + for hostile in [ + "a; rm -rf /", + "$(id)", + "`id`", + "a b", + "a'b", + "a\nb", + "a|b", + "a>b", + "a&b", + ] { + let args = vec![hostile.to_string()]; + assert!( + resume_command_line(Some("agent"), &args, &allowed).is_err(), + "should refuse {hostile:?}" + ); + } +} + +#[test] +fn too_many_or_too_long_arguments_are_refused() { + let allowed: Vec = Vec::new(); + let many: Vec = (0..MAX_RESUME_ARGS + 1).map(|i| format!("v{i}")).collect(); + assert!(resume_command_line(Some("agent"), &many, &allowed).is_err()); + + let long = vec!["v".repeat(MAX_RESUME_ARG_LEN + 1)]; + assert!(resume_command_line(Some("agent"), &long, &allowed).is_err()); +} + +#[test] +fn an_empty_argument_is_refused() { + let allowed: Vec = Vec::new(); + let args = vec![String::new()]; + assert!(resume_command_line(Some("agent"), &args, &allowed).is_err()); +} + +#[test] +fn a_pane_with_no_startup_command_cannot_be_relaunched() { + let args = vec!["--resume".to_string()]; + let allowed = vec!["--resume".to_string()]; + // A bare shell has no session to resume, and nothing to reproduce. + let err = resume_command_line(None, &args, &allowed).unwrap_err(); + assert!(err.to_string().contains("no startup command"), "{err}"); +} + +#[test] +fn quoting_neutralises_an_embedded_single_quote() { + // Reached only if the charset check is ever loosened; the quoting has to be + // correct on its own rather than relying on that check. + assert_eq!(shell_quote("a'b"), "'a'\\''b'"); +} diff --git a/src/cli.rs b/src/cli.rs index 91777956..ec345d59 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,8 @@ use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +pub(crate) mod plugin_cmd; + /// nightcrow — session daemon for agentic coding /// /// Run with no subcommand to start the session: a git diff viewer and @@ -51,6 +53,14 @@ pub(crate) enum Commands { /// are opened from inside, with the leader chord's open dialog or the /// browser's folder picker. Leaving does not end the session. Attach, + /// Manage plugin executables in ~/.nightcrow/plugins. + /// + /// Installing one only puts the binary in place; it stays inert until + /// config.toml declares it and a startup pane opts in by name. + Plugin { + #[command(subcommand)] + command: plugin_cmd::PluginCommands, + }, } /// Run the session daemon in the foreground until it is stopped. @@ -136,6 +146,7 @@ pub(crate) fn run_daemon( &paths, true, startup, + cfg.plugins.clone(), )?; if paths.is_empty() { // An empty catalog is a legitimate state — the same one the TUI starts diff --git a/src/cli/plugin_cmd.rs b/src/cli/plugin_cmd.rs new file mode 100644 index 00000000..64d3660a --- /dev/null +++ b/src/cli/plugin_cmd.rs @@ -0,0 +1,160 @@ +//! `nightcrow plugin` — argument parsing and reporting only. Every filesystem +//! decision lives in [`crate::plugin::registry`]. + +use anyhow::Result; +use clap::Subcommand; +use std::path::{Path, PathBuf}; + +use crate::plugin::registry::{self, InstallOutcome, RemoveOutcome}; + +#[derive(Subcommand)] +pub(crate) enum PluginCommands { + /// Copy an executable into ~/.nightcrow/plugins + Install { + /// Path to the plugin executable + #[arg(value_name = "PATH")] + path: PathBuf, + /// Install under this name instead of the source file stem + #[arg(long, value_name = "NAME")] + name: Option, + /// Replace an already-installed plugin of the same name + #[arg(long)] + force: bool, + }, + /// List installed plugins and how config.toml refers to them + List, + /// Delete an installed plugin + Remove { + #[arg(value_name = "NAME")] + name: String, + }, +} + +pub(crate) fn run_plugin(command: PluginCommands) -> Result<()> { + let dir = registry::default_plugins_dir()?; + match command { + PluginCommands::Install { path, name, force } => { + run_install(&dir, &path, name.as_deref(), force) + } + PluginCommands::List => run_list(&dir), + PluginCommands::Remove { name } => run_remove(&dir, &name), + } +} + +fn run_install(dir: &Path, path: &Path, name: Option<&str>, force: bool) -> Result<()> { + let installed = match registry::install(dir, path, name, force)? { + InstallOutcome::Created(installed) => { + println!("Installed {}", installed.display()); + installed + } + InstallOutcome::Replaced(installed) => { + println!("Replaced {}", installed.display()); + installed + } + InstallOutcome::AlreadyExists(installed) => { + println!( + "A plugin is already installed at {} — left untouched (pass --force to replace).", + installed.display() + ); + return Ok(()); + } + }; + let name = installed + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into_owned(); + println!(); + println!("Add this to ~/.nightcrow/config.toml to declare it:"); + println!(); + print!("{}", registry::config_snippet(&name, &installed)); + println!(); + // Deliberately not written for the user: a plugin that is enabled and + // opted into can drive that pane's terminal, so both switches stay an + // explicit, reviewed edit. + println!( + "Nothing was written to your config — flip `enabled` yourself, and add \ + `plugin = \"{name}\"` to a [[startup_command]] to opt that pane in. \ + A plugin with no opted-in pane never sees anything." + ); + Ok(()) +} + +fn run_list(dir: &Path) -> Result<()> { + let names = registry::list(dir)?; + if names.is_empty() { + println!("No plugins installed in {}", dir.display()); + return Ok(()); + } + // A broken config must not hide what is on disk — listing files is the + // thing this command is for, and the config line is extra detail. + let cfg = match crate::config::load_config() { + Ok(cfg) => Some(cfg), + Err(e) => { + println!("warning: config.toml could not be read ({e:#})"); + println!("Listing installed files only."); + None + } + }; + println!("Installed plugins in {}:", dir.display()); + for name in names { + match cfg.as_ref() { + Some(cfg) => println!(" {name} — {}", describe(®istry::status(cfg, &name))), + None => println!(" {name}"), + } + } + Ok(()) +} + +fn describe(status: ®istry::PluginStatus) -> String { + if !status.declared { + return "not declared in config.toml".to_string(); + } + format!( + "declared, {}, {} startup pane{} opted in", + if status.enabled { + "enabled" + } else { + "disabled" + }, + status.opt_ins, + if status.opt_ins == 1 { "" } else { "s" } + ) +} + +fn run_remove(dir: &Path, name: &str) -> Result<()> { + let references = crate::config::load_config() + .ok() + .map(|cfg| registry::status(&cfg, name)); + match registry::remove(dir, name)? { + RemoveOutcome::Removed(path) => println!("Removed {}", path.display()), + RemoveOutcome::NotInstalled(name) => { + println!( + "No plugin named \"{name}\" is installed in {}", + dir.display() + ); + } + } + // A `plugin =` opt-in naming a plugin that is no longer declared is a hard + // config error, so a dangling reference stops the next startup outright. + if let Some(status) = references.filter(|s| s.declared || s.opt_ins > 0) { + println!(); + println!("WARNING: config.toml still refers to \"{name}\"."); + if status.declared { + println!(" - remove its [[plugin]] table (name = \"{name}\")"); + } + if status.opt_ins > 0 { + println!( + " - remove `plugin = \"{name}\"` from {} [[startup_command]] entr{}", + status.opt_ins, + if status.opt_ins == 1 { "y" } else { "ies" } + ); + } + println!( + "Left as they are, the next `nightcrow` start tries to launch a plugin that is \ + no longer installed — and a `plugin =` opt-in whose [[plugin]] table is gone \ + fails config validation outright." + ); + } + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index d3ba7549..006f59d8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; mod layout; mod log; mod panels; +mod plugin; mod web; pub use layout::{Accent, InputConfig, LayoutConfig, StartupCommand, ThemeConfig, parse_leader}; @@ -12,6 +13,7 @@ pub use layout::{Accent, InputConfig, LayoutConfig, StartupCommand, ThemeConfig, pub use log::LogLevel; pub use log::{LogConfig, LogRotation}; pub use panels::{AgentIndicatorConfig, MouseConfig, TreeConfig}; +pub use plugin::PluginConfig; #[cfg(test)] pub use web::generate_password; pub use web::{WebViewerConfig, ensure_web_viewer_password}; @@ -25,6 +27,12 @@ pub use web::{WebViewerConfig, ensure_web_viewer_password}; /// are reachable by focus cycling (`Shift+←/→`) rather than a jump key. pub const MAX_STARTUP_COMMANDS: usize = 8; +/// Upper bound on `[[plugin]]` entries. Each one is a child process the host +/// keeps alive for the whole session, and a pane opts into exactly one of them, +/// so the bound tracks `MAX_STARTUP_COMMANDS` rather than being independently +/// generous. +pub const MAX_PLUGINS: usize = 8; + #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default)] pub struct Config { @@ -41,6 +49,11 @@ pub struct Config { /// default, which preserves the single empty-shell startup behaviour. #[serde(rename = "startup_command")] pub startup_commands: Vec, + /// External plugin processes, from TOML `[[plugin]]`. Empty by default, and + /// every entry is additionally off until it sets `enabled = true`, so no + /// plugin runs unless the user asked for it twice over. + #[serde(rename = "plugin")] + pub plugins: Vec, } fn default_config_path() -> Option { @@ -157,6 +170,7 @@ pub fn validate_config(cfg: &Config) -> Result<()> { "startup_command[{i}].command must not be empty" ); } + plugin::validate_plugins(cfg)?; anyhow::ensure!( (1..=1024).contains(&cfg.tree.max_depth), "tree.max_depth must be between 1 and 1024" @@ -195,6 +209,8 @@ pub fn resolve_startup_commands(cfg: &Config, cli_exec: &[String]) -> Result, /// Shell command run in the pane immediately on launch. pub command: String, + /// Name of the `[[plugin]]` that may act on this pane. `None` — the default — + /// means no plugin ever receives this pane's events or can act on it. + #[serde(default)] + pub plugin: Option, } diff --git a/src/config/plugin.rs b/src/config/plugin.rs new file mode 100644 index 00000000..99ae0882 --- /dev/null +++ b/src/config/plugin.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; + +/// An external plugin process (`[[plugin]]`). nightcrow itself knows nothing +/// about what a plugin does: it launches the executable and speaks its protocol. +/// A plugin only ever sees a pane whose `[[startup_command]]` opted in by name, +/// so adding a plugin here does not hand it the whole session. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct PluginConfig { + /// Name a startup pane refers to in its `plugin =` field. + pub name: String, + /// Executable to run. Resolved against PATH and the plugin dir by the host. + pub command: String, + /// Arguments passed verbatim. + #[serde(default)] + pub args: Vec, + /// Extra environment for the plugin process only (NOT for panes). + #[serde(default)] + pub env: BTreeMap, + /// Flags this plugin may append when relaunching a pane's command. + /// + /// Empty by default, which refuses every relaunch that passes a flag. The + /// core has no idea what any CLI's flags mean, so the decision of which + /// ones a plugin may add is the user's: a flag that is not listed here + /// cannot be smuggled into the pane's command line, which is what keeps a + /// plugin from quietly weakening a CLI's permission posture. + #[serde(default)] + pub allowed_resume_flags: Vec, + /// Off unless explicitly turned on. + #[serde(default)] + pub enabled: bool, +} + +/// Check the `[[plugin]]` list and every startup pane's opt-in against it. +/// +/// An opt-in naming a plugin that does not exist is an error, because the only +/// way to reach that state is a typo and the pane would silently never be +/// watched. Naming a plugin that exists but is disabled is allowed: `enabled` +/// is the switch for turning a plugin off without unpicking every pane that +/// refers to it, and "off" already means nothing happens. +pub(super) fn validate_plugins(cfg: &super::Config) -> Result<()> { + anyhow::ensure!( + cfg.plugins.len() <= super::MAX_PLUGINS, + "at most {} [[plugin]] entries are allowed, found {}", + super::MAX_PLUGINS, + cfg.plugins.len() + ); + let mut seen: BTreeSet<&str> = BTreeSet::new(); + for (i, p) in cfg.plugins.iter().enumerate() { + anyhow::ensure!( + !p.name.trim().is_empty(), + "plugin[{i}].name must not be empty" + ); + anyhow::ensure!( + !p.command.trim().is_empty(), + "plugin[{i}].command must not be empty" + ); + anyhow::ensure!( + seen.insert(p.name.as_str()), + "duplicate [[plugin]] name \"{}\"; plugin names must be unique so \ + a startup_command's plugin = \"...\" is unambiguous", + p.name + ); + } + for (i, sc) in cfg.startup_commands.iter().enumerate() { + let Some(name) = sc.plugin.as_deref() else { + continue; + }; + anyhow::ensure!( + cfg.plugins.iter().any(|p| p.name == name), + "startup_command[{i}].plugin \"{name}\" does not name any [[plugin]]" + ); + } + Ok(()) +} diff --git a/src/config/tests/config_core.rs b/src/config/tests/config_core.rs index 6091283f..511c29aa 100644 --- a/src/config/tests/config_core.rs +++ b/src/config/tests/config_core.rs @@ -81,6 +81,7 @@ fn startup_command_validation_rejects_empty_command() { cfg.startup_commands.push(StartupCommand { name: Some("blank".into()), command: " ".into(), + plugin: None, }); assert!(validate_config(&cfg).is_err()); } @@ -92,6 +93,7 @@ fn startup_command_validation_rejects_too_many() { cfg.startup_commands.push(StartupCommand { name: None, command: format!("echo {i}"), + plugin: None, }); } assert!(validate_config(&cfg).is_err()); @@ -104,6 +106,7 @@ fn startup_command_validation_accepts_max() { cfg.startup_commands.push(StartupCommand { name: None, command: format!("echo {i}"), + plugin: None, }); } assert!(validate_config(&cfg).is_ok()); diff --git a/src/config/tests/mod.rs b/src/config/tests/mod.rs index f9a9575d..8d34b226 100644 --- a/src/config/tests/mod.rs +++ b/src/config/tests/mod.rs @@ -2,6 +2,7 @@ mod config_core; mod input; mod log; mod panels; +mod plugin; mod startup; mod theme; mod tree; diff --git a/src/config/tests/plugin.rs b/src/config/tests/plugin.rs new file mode 100644 index 00000000..99e44b1b --- /dev/null +++ b/src/config/tests/plugin.rs @@ -0,0 +1,138 @@ +use crate::config::{Config, MAX_PLUGINS, PluginConfig, StartupCommand, validate_config}; + +fn plugin(name: &str, enabled: bool) -> PluginConfig { + PluginConfig { + name: name.to_string(), + command: "nightcrow-recovery".to_string(), + enabled, + ..PluginConfig::default() + } +} + +fn pane_using(plugin: Option<&str>) -> StartupCommand { + StartupCommand { + name: None, + command: "claude".to_string(), + plugin: plugin.map(str::to_string), + } +} + +#[test] +fn plugin_section_parses_and_stays_disabled_unless_enabled_is_set() { + let toml = r#" +[[plugin]] +name = "recovery" +command = "nightcrow-recovery" +args = ["--verbose"] + +[plugin.env] +NIGHTCROW_RECOVERY_LOG = "info" +"#; + let cfg: Config = toml::from_str(toml).unwrap(); + assert_eq!(cfg.plugins.len(), 1); + assert_eq!(cfg.plugins[0].name, "recovery"); + assert_eq!(cfg.plugins[0].command, "nightcrow-recovery"); + assert_eq!(cfg.plugins[0].args, vec!["--verbose".to_string()]); + assert_eq!( + cfg.plugins[0].env.get("NIGHTCROW_RECOVERY_LOG").unwrap(), + "info" + ); + assert!(!cfg.plugins[0].enabled, "plugins must be off by default"); + validate_config(&cfg).unwrap(); +} + +#[test] +fn plugin_args_and_env_default_to_empty_when_omitted() { + let cfg: Config = + toml::from_str("[[plugin]]\nname = \"recovery\"\ncommand = \"run-me\"\n").unwrap(); + assert!(cfg.plugins[0].args.is_empty()); + assert!(cfg.plugins[0].env.is_empty()); +} + +#[test] +fn a_plugin_without_name_or_command_fails_to_deserialize() { + assert!(toml::from_str::("[[plugin]]\ncommand = \"run-me\"\n").is_err()); + assert!(toml::from_str::("[[plugin]]\nname = \"recovery\"\n").is_err()); +} + +#[test] +fn absent_plugin_section_defaults_to_empty_and_validates() { + assert!(Config::default().plugins.is_empty()); + let cfg: Config = toml::from_str("[layout]\nupper_pct = 50\n").unwrap(); + assert!(cfg.plugins.is_empty()); + validate_config(&cfg).unwrap(); +} + +#[test] +fn a_startup_command_without_a_plugin_field_defaults_to_no_plugin() { + let cfg: Config = toml::from_str("[[startup_command]]\ncommand = \"claude\"\n").unwrap(); + assert_eq!(cfg.startup_commands[0].plugin, None); + validate_config(&cfg).unwrap(); +} + +#[test] +fn a_startup_command_naming_an_enabled_plugin_validates() { + let mut cfg = Config::default(); + cfg.plugins.push(plugin("recovery", true)); + cfg.startup_commands.push(pane_using(Some("recovery"))); + validate_config(&cfg).unwrap(); +} + +#[test] +fn a_startup_command_naming_an_unknown_plugin_is_rejected() { + let mut cfg = Config::default(); + cfg.plugins.push(plugin("recovery", true)); + cfg.startup_commands.push(pane_using(Some("typo"))); + let err = validate_config(&cfg).unwrap_err().to_string(); + assert!(err.contains("typo"), "error should name the opt-in: {err}"); +} + +#[test] +fn a_startup_command_naming_a_disabled_plugin_still_validates() { + // `enabled = false` is how a plugin gets switched off; requiring every + // opt-in to be unpicked first would make that a two-place edit. Off simply + // means the pane is never handed to anyone. + let mut cfg = Config::default(); + cfg.plugins.push(plugin("recovery", false)); + cfg.startup_commands.push(pane_using(Some("recovery"))); + validate_config(&cfg).expect("a disabled plugin is a valid target"); +} + +#[test] +fn duplicate_plugin_names_are_rejected() { + let mut cfg = Config::default(); + cfg.plugins.push(plugin("recovery", true)); + cfg.plugins.push(plugin("recovery", true)); + let err = validate_config(&cfg).unwrap_err().to_string(); + assert!( + err.contains("recovery"), + "error should name the clash: {err}" + ); +} + +#[test] +fn a_blank_plugin_name_is_rejected() { + let mut cfg = Config::default(); + cfg.plugins.push(plugin(" ", true)); + assert!(validate_config(&cfg).is_err()); +} + +#[test] +fn a_blank_plugin_command_is_rejected() { + let mut cfg = Config::default(); + let mut p = plugin("recovery", true); + p.command = " ".to_string(); + cfg.plugins.push(p); + assert!(validate_config(&cfg).is_err()); +} + +#[test] +fn exceeding_the_plugin_cap_is_rejected_but_the_cap_itself_validates() { + let mut cfg = Config::default(); + for i in 0..MAX_PLUGINS { + cfg.plugins.push(plugin(&format!("p{i}"), true)); + } + validate_config(&cfg).unwrap(); + cfg.plugins.push(plugin("one-too-many", true)); + assert!(validate_config(&cfg).is_err()); +} diff --git a/src/config/tests/startup.rs b/src/config/tests/startup.rs index 5316671e..9db9807b 100644 --- a/src/config/tests/startup.rs +++ b/src/config/tests/startup.rs @@ -37,6 +37,7 @@ fn resolve_startup_commands_appends_cli_exec_after_config() { cfg.startup_commands.push(StartupCommand { name: Some("Claude".into()), command: "claude".into(), + plugin: None, }); let resolved = resolve_startup_commands(&cfg, &["codex".to_string(), "vim".to_string()]).unwrap(); @@ -68,6 +69,7 @@ fn resolve_startup_commands_caps_combined_total() { cfg.startup_commands.push(StartupCommand { name: None, command: format!("echo {i}"), + plugin: None, }); } // 4 config + 5 CLI = 9 > MAX_STARTUP_COMMANDS (8). diff --git a/src/daemon/terminals.rs b/src/daemon/terminals.rs index 43e33b47..ab436508 100644 --- a/src/daemon/terminals.rs +++ b/src/daemon/terminals.rs @@ -197,3 +197,7 @@ impl Drop for Bridge { } } } + +#[cfg(test)] +#[path = "terminals_tests.rs"] +mod tests; diff --git a/src/daemon/terminals_tests.rs b/src/daemon/terminals_tests.rs new file mode 100644 index 00000000..4af3e46e --- /dev/null +++ b/src/daemon/terminals_tests.rs @@ -0,0 +1,69 @@ +//! What the relay is allowed to change on its way through. +//! +//! `Created` is the one event whose requester is rewritten into the attach +//! socket's id space; everything else has to arrive at the client exactly as the +//! hub said it, and a recovery report in particular — a rewritten deadline or a +//! dropped detail would be a silent lie about a pane nobody can see. + +use super::rewrite_requester; +use crate::daemon::protocol::ServerMessage; +use crate::web::viewer::terminal::frame::ServerMessage as HubServerMessage; + +const HUB_CLIENT: u64 = 4; +const ATTACHED_CLIENT: u64 = 91; + +fn recovery() -> HubServerMessage { + HubServerMessage::Recovery { + pane: 6, + state: "waiting_for_reset".to_string(), + detail: Some("provider window closed".to_string()), + deadline_epoch: Some(1_700_000_000), + attempt: 2, + } +} + +#[test] +fn a_recovery_report_is_relayed_unchanged() { + assert_eq!( + rewrite_requester(recovery(), HUB_CLIENT, ATTACHED_CLIENT), + recovery() + ); +} + +#[test] +fn a_recovery_report_survives_the_attach_envelope_intact() { + // The relay parses a hub control frame and re-encodes it under a repository + // tag, so the round trip is part of the delivery path rather than a test + // convenience. + let tagged = ServerMessage::Terminal { + repo: "repo-a".to_string(), + event: rewrite_requester(recovery(), HUB_CLIENT, ATTACHED_CLIENT), + }; + let json = serde_json::to_string(&tagged).unwrap(); + let back: ServerMessage = serde_json::from_str(&json).unwrap(); + + match back { + ServerMessage::Terminal { repo, event } => { + assert_eq!(repo, "repo-a"); + assert_eq!(event, recovery()); + } + other => panic!("the envelope changed shape: {other:?}"), + } +} + +#[test] +fn a_recovery_report_with_no_deadline_or_detail_relays_as_absent_not_as_zero() { + let bare = HubServerMessage::Recovery { + pane: 6, + state: "cancelled".to_string(), + detail: None, + deadline_epoch: None, + attempt: 0, + }; + let relayed = rewrite_requester(bare.clone(), HUB_CLIENT, ATTACHED_CLIENT); + assert_eq!(relayed, bare); + + let json = serde_json::to_string(&relayed).unwrap(); + assert!(!json.contains("deadline_epoch"), "{json}"); + assert!(!json.contains("detail"), "{json}"); +} diff --git a/src/input/mod.rs b/src/input/mod.rs index 3ad453d4..860699ac 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -15,6 +15,11 @@ pub enum Action { /// render that grid; this asks for it. Inert when this client already has /// it, and when its panes are its own. ClaimPaneSizing, + /// Give up on the recovery a plugin has pending for a pane. + /// + /// Inert when nothing is pending. Behind the leader like every other app + /// command: a bare key in a terminal pane belongs to the program in it. + CancelRecovery, /// Arm swap mode: the next digit picks the pane to swap the active pane /// with. Emitted by the ` s` follow-up; the digit is resolved in a /// separate tick (see `handle_swap_target_followup` in `main`). diff --git a/src/input/routing.rs b/src/input/routing.rs index 994e9aad..bd4a2320 100644 --- a/src/input/routing.rs +++ b/src/input/routing.rs @@ -56,6 +56,10 @@ pub fn prefix_action(event: KeyEvent) -> Action { 'f' => Action::ToggleFullscreen, 's' => Action::SwapPanePrompt, 'z' => Action::ClaimPaneSizing, + // `c` for cancel. Bare, not `ctrl+c`: the follow-up handler treats + // `ctrl+c` as the universal prefix cancel, and it has to keep doing + // so whatever leader is configured. + 'c' => Action::CancelRecovery, 'o' => Action::OpenProject, 'x' => Action::CloseProject, 'p' => Action::CycleTheme, diff --git a/src/main.rs b/src/main.rs index e02dc4d4..0dc64d42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod daemon; mod git; mod input; mod platform; +pub mod plugin; mod runtime; #[cfg(test)] mod test_util; @@ -30,6 +31,7 @@ fn main() -> Result<()> { match cli.command { Some(Commands::Init { force }) => run_init(force), Some(Commands::Attach) => application::attach::run_attach(), + Some(Commands::Plugin { command }) => cli::plugin_cmd::run_plugin(command), None => run_daemon(cli.exec, cli.port, cli.bind, cli.detach), } } diff --git a/src/plugin/guard.rs b/src/plugin/guard.rs new file mode 100644 index 00000000..b268fe4b --- /dev/null +++ b/src/plugin/guard.rs @@ -0,0 +1,283 @@ +//! The trust boundary between an untrusted plugin and the panes. +//! +//! [`decode_command`](super::protocol::decode_command) checked shape and bounds; +//! it never checked authority. Everything a plugin asks for passes through +//! [`Guard::judge`], which returns either an [`Approved`] action naming a real +//! [`PaneId`] or a [`Refused`] saying why not. There is no other way through: a +//! [`PluginCommand`] carries a token, and only the guard turns one into a pane. + +use super::guard_budget::{Budgets, RateAction, RateLimits}; +use super::guard_refusal::Refused; +use super::guard_text::{is_forbidden_control, truncate_message}; +use super::protocol::{LogLevel, MAX_INPUT_BYTES, PluginCommand}; +use crate::backend::slot::resume_command_line; +use crate::backend::{PaneGeneration, PaneId, PaneToken}; +use std::time::{Duration, Instant}; + +/// What the guard needs to know about the pane a command names. +/// +/// Supplied by the caller rather than looked up here, so every rule is testable +/// without a PTY and the guard holds no reference to the backend. +#[derive(Debug, Clone)] +pub struct PaneFacts { + pub pane: PaneId, + pub generation: PaneGeneration, + /// The pane's `[[startup_command]]` named *this* plugin. + pub opted_in: bool, + /// The pane's process is still running. + pub alive: bool, + /// Since the pane last produced output. + pub idle: Duration, + /// The pane's configured startup command, or `None` for a bare shell. + /// + /// Needed because a relaunch is validated by calling the real + /// [`resume_command_line`], which builds the line from the original + /// invocation and is also what refuses a pane that has no command to resume. + pub launch_command: Option, +} + +/// A command that passed every rule, addressed to a pane by id. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Approved { + SendInput { + pane: PaneId, + data: Vec, + }, + Relaunch { + pane: PaneId, + resume_args: Vec, + /// The line the guard already validated and built. Carried so the + /// caller runs exactly what was approved instead of rebuilding it. + command_line: String, + }, + Status { + pane: PaneId, + state: String, + detail: Option, + deadline_epoch: Option, + attempt: u32, + }, + Log { + level: LogLevel, + message: String, + }, +} + +pub struct Guard { + min_idle: Duration, + limits: RateLimits, + budgets: Budgets, +} + +impl Guard { + /// `min_idle` is how long a pane must have been quiet before input may be + /// typed into it. + pub fn new(min_idle: Duration, limits: RateLimits) -> Self { + Self { + min_idle, + limits, + budgets: Budgets::default(), + } + } + + /// Forget everything held for the slot `token` names. + /// + /// Called when a human types into the pane, when the pane closes, and when + /// the session is replaced: in all three the plugin's picture of the pane is + /// void, and its spent budget belongs to a situation that no longer exists. + /// Keyed by token because the budget is, so this must be called while the + /// slot still exists — before the slot is retired, not after. + pub fn cancel(&mut self, token: &PaneToken) { + self.budgets.clear(token); + } + + /// Decide one command. Never panics. + /// + /// `facts` is what the caller knows about the pane `cmd`'s token resolves + /// to, or `None` if it resolves to nothing. `allowed_resume_flags` is the + /// plugin's configured list. + pub fn judge( + &mut self, + cmd: PluginCommand, + facts: Option<&PaneFacts>, + allowed_resume_flags: &[String], + now: Instant, + ) -> Result { + match cmd { + // Not pane-scoped, so none of the pane rules can apply to it. + PluginCommand::Log { level, message, .. } => Ok(Approved::Log { + level, + message: truncate_message(message), + }), + PluginCommand::SendInput { + token, + generation, + data, + .. + } => { + let facts = pane_facts(&token, generation, facts)?; + self.judge_send_input(&token, facts, data, now) + } + PluginCommand::Relaunch { + token, + generation, + resume_args, + .. + } => { + let facts = pane_facts(&token, generation, facts)?; + self.judge_relaunch(&token, facts, resume_args, allowed_resume_flags, now) + } + PluginCommand::Status { + token, + generation, + state, + detail, + deadline_epoch, + attempt, + .. + } => { + let facts = pane_facts(&token, generation, facts)?; + // Observability only: nothing happens to the pane, so there is + // no effect to rate-limit. + Ok(Approved::Status { + pane: facts.pane, + state, + detail, + deadline_epoch, + attempt, + }) + } + } + } + + fn judge_send_input( + &mut self, + token: &PaneToken, + facts: &PaneFacts, + data: String, + now: Instant, + ) -> Result { + let pane = facts.pane; + if !facts.alive { + // The slot outlives its process, so typing here would reach + // whatever occupies it next rather than what the plugin watched. + return Err(Refused::PaneNotRunning { pane }); + } + if facts.idle < self.min_idle { + return Err(Refused::PaneBusy { + pane, + idle: facts.idle, + min_idle: self.min_idle, + }); + } + if data.len() > MAX_INPUT_BYTES { + return Err(Refused::InputTooLarge { + pane, + bytes: data.len(), + limit: MAX_INPUT_BYTES, + }); + } + if let Some(bad) = data.chars().find(|c| is_forbidden_control(*c)) { + return Err(Refused::ControlCharacter { + pane, + code: bad as u32, + }); + } + self.spend(token, pane, RateAction::SendInput, now)?; + Ok(Approved::SendInput { + pane, + data: data.into_bytes(), + }) + } + + fn judge_relaunch( + &mut self, + token: &PaneToken, + facts: &PaneFacts, + resume_args: Vec, + allowed_resume_flags: &[String], + now: Instant, + ) -> Result { + let pane = facts.pane; + if facts.alive { + // Half of the guarantee that one incident is handled once: input + // recovery acts on live panes, relaunch only on exited ones. + return Err(Refused::PaneStillRunning { pane }); + } + let command_line = resume_command_line( + facts.launch_command.as_deref(), + &resume_args, + allowed_resume_flags, + ) + .map_err(|e| Refused::ResumeArgsRejected { + pane, + reason: e.to_string(), + })?; + self.spend(token, pane, RateAction::Relaunch, now)?; + Ok(Approved::Relaunch { + pane, + resume_args, + command_line, + }) + } + + /// Charge the budget, as the last check before approval. + /// + /// Only approvals spend, deliberately: the budget bounds what a plugin + /// *does* to a pane, and a refused command did nothing. Charging refusals + /// would let noise — a stale generation the plugin could not have known + /// about, a flag config forbids — eat the budget a legitimate action needs, + /// losing the pane's one real attempt to a race. Spam is bounded elsewhere + /// and more cheaply: the outbound queue drops and every refusal is logged. + fn spend( + &mut self, + token: &PaneToken, + pane: PaneId, + action: RateAction, + now: Instant, + ) -> Result<(), Refused> { + if self.budgets.try_spend(token, action, &self.limits, now) { + return Ok(()); + } + Err(Refused::RateLimited { + pane, + action, + limit: match action { + RateAction::SendInput => self.limits.max_sends_per_window, + RateAction::Relaunch => self.limits.max_relaunches_per_window, + }, + window: self.limits.window, + }) + } +} + +/// Rules 2, 3 and 4: the pane must exist, have opted in, and be the same spawn. +fn pane_facts<'a>( + token: &PaneToken, + generation: PaneGeneration, + facts: Option<&'a PaneFacts>, +) -> Result<&'a PaneFacts, Refused> { + let Some(facts) = facts else { + return Err(Refused::UnknownPane { + token: token.clone(), + }); + }; + if !facts.opted_in { + return Err(Refused::NotOptedIn { + pane: facts.pane, + token: token.clone(), + }); + } + if generation != facts.generation { + return Err(Refused::StaleGeneration { + pane: facts.pane, + claimed: generation, + current: facts.generation, + }); + } + Ok(facts) +} + +#[cfg(test)] +#[path = "guard_tests/mod.rs"] +mod tests; diff --git a/src/plugin/guard_budget.rs b/src/plugin/guard_budget.rs new file mode 100644 index 00000000..c17c1b27 --- /dev/null +++ b/src/plugin/guard_budget.rs @@ -0,0 +1,123 @@ +//! How much a plugin may actually do to a pane, per pane, per window. +//! +//! The point is that a plugin's attempts are bounded and its failures visible: +//! a recovery flow that is not working must stop trying rather than type into a +//! pane forever. +//! +//! Counted per *slot*, keyed by [`PaneToken`], not per `PaneId`. A relaunch +//! always mints a new id, so an id-keyed budget would hand a fresh allowance to +//! every relaunch — a command that exits at once plus a plugin that relaunches +//! on every exit would then loop with nothing to stop it. The token is what +//! survives a relaunch, so it is what the ceiling has to be attached to. + +use crate::backend::PaneToken; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +/// Window over which a pane's actions are counted. +pub const DEFAULT_RATE_WINDOW: Duration = Duration::from_secs(60); + +/// Approved inputs per pane per window by default. +/// +/// A recovery exchange is a prompt and perhaps a confirmation. More than a +/// handful in a minute is a loop, not a conversation. +pub const DEFAULT_MAX_SENDS: u32 = 5; + +/// Approved relaunches per slot per window by default. Fewer than sends: a +/// relaunch that did not take is unlikely to take on the fourth try either. +pub const DEFAULT_MAX_RELAUNCHES: u32 = 3; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RateLimits { + pub max_sends_per_window: u32, + pub max_relaunches_per_window: u32, + pub window: Duration, +} + +impl Default for RateLimits { + fn default() -> Self { + Self { + max_sends_per_window: DEFAULT_MAX_SENDS, + max_relaunches_per_window: DEFAULT_MAX_RELAUNCHES, + window: DEFAULT_RATE_WINDOW, + } + } +} + +/// Which counter an action draws on. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RateAction { + SendInput, + Relaunch, +} + +impl RateAction { + pub fn as_str(self) -> &'static str { + match self { + Self::SendInput => "send_input", + Self::Relaunch => "relaunch", + } + } +} + +#[derive(Debug, Default)] +struct PaneBudget { + sends: Vec, + relaunches: Vec, +} + +/// When each slot's approved actions happened, pruned as it is read. +#[derive(Debug, Default)] +pub(super) struct Budgets(HashMap); + +impl Budgets { + /// Charge one `action` against the slot's budget, if there is room. + /// + /// Nothing is recorded when there is not, so a refusal here costs the slot + /// nothing beyond the refusal itself. Each list is capped at its limit by + /// construction, so this stays O(limit). + pub(super) fn try_spend( + &mut self, + token: &PaneToken, + action: RateAction, + limits: &RateLimits, + now: Instant, + ) -> bool { + let budget = self.0.entry(token.clone()).or_default(); + let (stamps, max) = match action { + RateAction::SendInput => (&mut budget.sends, limits.max_sends_per_window), + RateAction::Relaunch => (&mut budget.relaunches, limits.max_relaunches_per_window), + }; + stamps.retain(|at| now.saturating_duration_since(*at) < limits.window); + if stamps.len() as u32 >= max { + return false; + } + stamps.push(now); + true + } + + pub(super) fn clear(&mut self, token: &PaneToken) { + self.0.remove(token); + } + + /// How much of `action`'s budget the slot has spent inside the window. Lets + /// a test assert on the budget itself rather than on the next refusal. + #[cfg(test)] + pub(super) fn spent( + &mut self, + token: &PaneToken, + action: RateAction, + limits: &RateLimits, + now: Instant, + ) -> u32 { + let Some(budget) = self.0.get_mut(token) else { + return 0; + }; + let stamps = match action { + RateAction::SendInput => &mut budget.sends, + RateAction::Relaunch => &mut budget.relaunches, + }; + stamps.retain(|at| now.saturating_duration_since(*at) < limits.window); + stamps.len() as u32 + } +} diff --git a/src/plugin/guard_refusal.rs b/src/plugin/guard_refusal.rs new file mode 100644 index 00000000..5729e0f5 --- /dev/null +++ b/src/plugin/guard_refusal.rs @@ -0,0 +1,109 @@ +//! Why a plugin's command was not carried out. +//! +//! Each variant carries what a log line needs to be actionable without the +//! reader having to correlate it with anything else: which pane, and the values +//! the rule compared. + +use super::guard_budget::RateAction; +use crate::backend::{PaneGeneration, PaneId, PaneToken}; +use std::fmt; +use std::time::Duration; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Refused { + /// The token names no pane the host still has. + UnknownPane { token: PaneToken }, + /// The pane exists but its `[[startup_command]]` did not name this plugin. + NotOptedIn { pane: PaneId, token: PaneToken }, + /// The command is about a process that has already been replaced. + StaleGeneration { + pane: PaneId, + claimed: PaneGeneration, + current: PaneGeneration, + }, + /// Input for a pane whose process has exited. + PaneNotRunning { pane: PaneId }, + /// Input for a pane that is still producing output. + PaneBusy { + pane: PaneId, + idle: Duration, + min_idle: Duration, + }, + InputTooLarge { + pane: PaneId, + bytes: usize, + limit: usize, + }, + /// Input holding a control character that is not `\r`, `\n`, or `\t`. + ControlCharacter { pane: PaneId, code: u32 }, + /// A relaunch for a pane whose process is still running. + PaneStillRunning { pane: PaneId }, + /// The resume arguments did not survive the command-line rules. + ResumeArgsRejected { pane: PaneId, reason: String }, + RateLimited { + pane: PaneId, + action: RateAction, + limit: u32, + window: Duration, + }, +} + +impl fmt::Display for Refused { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownPane { token } => { + write!(f, "no live pane for token {}", token.as_str()) + } + Self::NotOptedIn { pane, token } => write!( + f, + "pane {pane} (token {}) did not opt in to this plugin", + token.as_str() + ), + Self::StaleGeneration { + pane, + claimed, + current, + } => write!( + f, + "pane {pane} is on generation {current}, command claims {claimed}" + ), + Self::PaneNotRunning { pane } => { + write!(f, "pane {pane} has no running process to type into") + } + Self::PaneBusy { + pane, + idle, + min_idle, + } => write!( + f, + "pane {pane} has been idle {idle:?}, under the {min_idle:?} required" + ), + Self::InputTooLarge { pane, bytes, limit } => { + write!(f, "input for pane {pane} is {bytes} bytes, over {limit}") + } + Self::ControlCharacter { pane, code } => write!( + f, + "input for pane {pane} holds control character U+{code:04X}" + ), + Self::PaneStillRunning { pane } => { + write!( + f, + "pane {pane} is still running, so it cannot be relaunched" + ) + } + Self::ResumeArgsRejected { pane, reason } => { + write!(f, "relaunch of pane {pane} refused: {reason}") + } + Self::RateLimited { + pane, + action, + limit, + window, + } => write!( + f, + "pane {pane} already used its {limit} {} per {window:?}", + action.as_str() + ), + } + } +} diff --git a/src/plugin/guard_tests/budget.rs b/src/plugin/guard_tests/budget.rs new file mode 100644 index 00000000..5a3e4197 --- /dev/null +++ b/src/plugin/guard_tests/budget.rs @@ -0,0 +1,194 @@ +//! Rules 10-11 and `cancel`: how much a plugin may do, and what it costs. +//! +//! Every test here binds ONE token and reuses it. The budget is keyed by the +//! slot's token, so a fresh token per call would each get its own allowance and +//! the ceiling these tests exist to pin would never be reached. + +use super::*; + +fn limits(sends: u32, relaunches: u32) -> RateLimits { + RateLimits { + max_sends_per_window: sends, + max_relaunches_per_window: relaunches, + ..RateLimits::default() + } +} + +#[test] +fn sends_beyond_the_window_limit_are_refused() { + let limits = limits(2, 1); + let mut g = Guard::new(MIN_IDLE, limits); + let now = Instant::now(); + let t = token(); + for _ in 0..limits.max_sends_per_window { + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); + } + assert_eq!( + g.judge(send(&t, "hi\n"), Some(&facts()), &[], now) + .expect_err("refused"), + Refused::RateLimited { + pane: PANE, + action: RateAction::SendInput, + limit: 2, + window: limits.window + } + ); +} + +#[test] +fn relaunches_beyond_the_window_limit_are_refused() { + let mut g = Guard::new(MIN_IDLE, limits(2, 1)); + let now = Instant::now(); + let t = token(); + assert!( + g.judge(relaunch(&t, &[]), Some(&exited_facts()), &[], now) + .is_ok() + ); + assert!(matches!( + g.judge(relaunch(&t, &[]), Some(&exited_facts()), &[], now), + Err(Refused::RateLimited { + action: RateAction::Relaunch, + .. + }) + )); +} + +#[test] +fn a_relaunch_budget_is_not_refreshed_by_the_new_pane_id_it_produces() { + // The loop this closes: a relaunch always mints a new `PaneId`, so an + // id-keyed budget handed out a fresh allowance every time and a plugin + // answering every exit with another relaunch never hit the ceiling. The + // token is what a relaunch preserves, so it is what the ceiling counts. + let mut g = Guard::new(MIN_IDLE, limits(1, 1)); + let now = Instant::now(); + let t = token(); + + assert!( + g.judge(relaunch(&t, &[]), Some(&exited_facts()), &[], now) + .is_ok() + ); + + // The replacement process: same slot and so the same token, a different id, + // and the next generation. + let replacement = PaneFacts { + pane: PANE + 1, + generation: GENERATION + 1, + ..exited_facts() + }; + let mut cmd = relaunch(&t, &[]); + if let PluginCommand::Relaunch { generation, .. } = &mut cmd { + *generation = GENERATION + 1; + } + assert!(matches!( + g.judge(cmd, Some(&replacement), &[], now), + Err(Refused::RateLimited { + action: RateAction::Relaunch, + .. + }) + )); +} + +#[test] +fn two_panes_have_budgets_of_their_own() { + // Two opted-in panes in one repository is a supported layout, so one pane + // exhausting its allowance must not silence the other. + let mut g = Guard::new(MIN_IDLE, limits(1, 1)); + let now = Instant::now(); + let a = token(); + let b = token(); + + assert!(g.judge(send(&a, "hi\n"), Some(&facts()), &[], now).is_ok()); + assert!(g.judge(send(&a, "hi\n"), Some(&facts()), &[], now).is_err()); + assert!(g.judge(send(&b, "hi\n"), Some(&facts()), &[], now).is_ok()); +} + +#[test] +fn the_two_budgets_are_counted_separately() { + let mut g = Guard::new(MIN_IDLE, limits(1, 1)); + let now = Instant::now(); + let t = token(); + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); + // A spent send budget must not block the relaunch the pane is owed. + assert!( + g.judge(relaunch(&t, &[]), Some(&exited_facts()), &[], now) + .is_ok() + ); +} + +#[test] +fn a_zero_limit_refuses_every_attempt() { + let mut g = Guard::new(MIN_IDLE, limits(0, 0)); + assert!(matches!( + g.judge(send(&token(), "hi\n"), Some(&facts()), &[], Instant::now()), + Err(Refused::RateLimited { .. }) + )); +} + +#[test] +fn a_status_command_spends_no_budget() { + let mut g = Guard::new(MIN_IDLE, limits(1, 1)); + let now = Instant::now(); + let t = token(); + for _ in 0..5 { + assert!(g.judge(status(&t), Some(&facts()), &[], now).is_ok()); + } + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); +} + +#[test] +fn budget_spent_before_the_window_elapsed_is_available_again_after_it() { + let limits = limits(1, 1); + let mut g = Guard::new(MIN_IDLE, limits); + let start = Instant::now(); + let t = token(); + assert!( + g.judge(send(&t, "hi\n"), Some(&facts()), &[], start) + .is_ok() + ); + assert!( + g.judge(send(&t, "hi\n"), Some(&facts()), &[], start + limits.window) + .is_ok() + ); +} + +#[test] +fn a_refused_command_does_not_consume_budget() { + let limits = limits(1, 1); + let mut g = Guard::new(MIN_IDLE, limits); + let now = Instant::now(); + let t = token(); + let busy = PaneFacts { + idle: Duration::ZERO, + ..facts() + }; + // Refusals are what a plugin produces by racing or by being wrong, and they + // must not cost the pane the one legitimate attempt it is owed. + for _ in 0..5 { + assert!(g.judge(send(&t, "hi\n"), Some(&busy), &[], now).is_err()); + } + assert_eq!(g.budgets.spent(&t, RateAction::SendInput, &limits, now), 0); + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); +} + +#[test] +fn cancelling_a_pane_clears_its_spent_budget() { + let limits = limits(1, 1); + let mut g = Guard::new(MIN_IDLE, limits); + let now = Instant::now(); + let t = token(); + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_err()); + + g.cancel(&t); + + assert_eq!(g.budgets.spent(&t, RateAction::SendInput, &limits, now), 0); + assert!(g.judge(send(&t, "hi\n"), Some(&facts()), &[], now).is_ok()); +} + +#[test] +fn cancelling_a_pane_that_holds_no_state_is_harmless() { + let mut g = guard(); + let t = token(); + g.cancel(&t); + g.cancel(&t); +} diff --git a/src/plugin/guard_tests/identity.rs b/src/plugin/guard_tests/identity.rs new file mode 100644 index 00000000..5aa08b28 --- /dev/null +++ b/src/plugin/guard_tests/identity.rs @@ -0,0 +1,126 @@ +//! Rules 1-4: what a command must prove before any of its own rules run. + +use super::*; + +#[test] +fn a_log_command_is_always_allowed_without_a_pane() { + let mut g = guard(); + let cmd = PluginCommand::Log { + v: PROTOCOL_VERSION, + level: LogLevel::Info, + message: "watching".to_string(), + }; + assert_eq!( + g.judge(cmd, None, &[], Instant::now()).expect("allowed"), + Approved::Log { + level: LogLevel::Info, + message: "watching".to_string() + } + ); +} + +#[test] +fn an_over_long_log_message_is_truncated_rather_than_refused() { + let mut g = guard(); + let cmd = PluginCommand::Log { + v: PROTOCOL_VERSION, + level: LogLevel::Warn, + // Multi-byte so a naive truncation would split a character. + message: "가".repeat(MAX_LOG_MESSAGE_BYTES), + }; + let Approved::Log { message, .. } = g.judge(cmd, None, &[], Instant::now()).expect("allowed") + else { + panic!("expected a log approval"); + }; + assert!(message.len() <= MAX_LOG_MESSAGE_BYTES); + assert!(message.chars().all(|c| c == '가')); +} + +#[test] +fn a_command_for_a_token_with_no_live_pane_is_refused() { + let mut g = guard(); + let t = token(); + assert_eq!( + g.judge(send(&t, "hi\n"), None, &[], Instant::now()) + .expect_err("refused"), + Refused::UnknownPane { token: t } + ); +} + +#[test] +fn a_command_for_a_pane_that_did_not_opt_in_is_refused() { + let mut g = guard(); + let t = token(); + let facts = PaneFacts { + opted_in: false, + ..facts() + }; + assert_eq!( + g.judge(send(&t, "hi\n"), Some(&facts), &[], Instant::now()) + .expect_err("refused"), + Refused::NotOptedIn { + pane: PANE, + token: t + } + ); +} + +#[test] +fn a_status_command_for_a_pane_that_did_not_opt_in_is_refused_too() { + let mut g = guard(); + let facts = PaneFacts { + opted_in: false, + ..facts() + }; + assert!(matches!( + g.judge(status(&token()), Some(&facts), &[], Instant::now()), + Err(Refused::NotOptedIn { .. }) + )); +} + +#[test] +fn a_stale_generation_command_is_refused() { + let mut g = guard(); + let facts = PaneFacts { + generation: GENERATION + 1, + ..facts() + }; + assert_eq!( + g.judge(send(&token(), "hi\n"), Some(&facts), &[], Instant::now()) + .expect_err("refused"), + Refused::StaleGeneration { + pane: PANE, + claimed: GENERATION, + current: GENERATION + 1 + } + ); +} + +#[test] +fn a_stale_generation_is_refused_for_a_relaunch_too() { + let mut g = guard(); + let facts = PaneFacts { + generation: GENERATION + 3, + ..exited_facts() + }; + assert!(matches!( + g.judge(relaunch(&token(), &[]), Some(&facts), &[], Instant::now()), + Err(Refused::StaleGeneration { .. }) + )); +} + +#[test] +fn a_status_command_is_approved_with_its_fields_intact() { + let mut g = guard(); + assert_eq!( + g.judge(status(&token()), Some(&facts()), &[], Instant::now()) + .expect("allowed"), + Approved::Status { + pane: PANE, + state: "waiting".to_string(), + detail: None, + deadline_epoch: Some(42), + attempt: 2 + } + ); +} diff --git a/src/plugin/guard_tests/input.rs b/src/plugin/guard_tests/input.rs new file mode 100644 index 00000000..d3dd3930 --- /dev/null +++ b/src/plugin/guard_tests/input.rs @@ -0,0 +1,112 @@ +//! Rules 5-7: when input may be typed into a pane, and what it may contain. + +use super::*; + +#[test] +fn sending_input_to_a_pane_whose_process_exited_is_refused() { + let mut g = guard(); + assert_eq!( + g.judge( + send(&token(), "hi\n"), + Some(&exited_facts()), + &[], + Instant::now() + ) + .expect_err("refused"), + Refused::PaneNotRunning { pane: PANE } + ); +} + +#[test] +fn sending_input_to_a_pane_that_is_still_talking_is_refused() { + let mut g = guard(); + let facts = PaneFacts { + idle: MIN_IDLE - Duration::from_millis(1), + ..facts() + }; + assert!(matches!( + g.judge(send(&token(), "hi\n"), Some(&facts), &[], Instant::now()), + Err(Refused::PaneBusy { pane: PANE, .. }) + )); +} + +#[test] +fn input_exactly_at_the_idle_threshold_is_allowed() { + let mut g = guard(); + // The threshold is inclusive: a pane quiet for exactly min_idle qualifies. + assert_eq!( + g.judge(send(&token(), "hi\n"), Some(&facts()), &[], Instant::now()) + .expect("allowed"), + Approved::SendInput { + pane: PANE, + data: b"hi\n".to_vec() + } + ); +} + +#[test] +fn oversized_input_is_refused() { + let mut g = guard(); + let data = "a".repeat(MAX_INPUT_BYTES + 1); + assert_eq!( + g.judge(send(&token(), &data), Some(&facts()), &[], Instant::now()) + .expect_err("refused"), + Refused::InputTooLarge { + pane: PANE, + bytes: MAX_INPUT_BYTES + 1, + limit: MAX_INPUT_BYTES + } + ); +} + +#[test] +fn input_exactly_at_the_size_limit_is_allowed() { + let mut g = guard(); + let data = "a".repeat(MAX_INPUT_BYTES); + assert!( + g.judge(send(&token(), &data), Some(&facts()), &[], Instant::now()) + .is_ok() + ); +} + +#[test] +fn empty_input_is_allowed() { + let mut g = guard(); + assert!( + g.judge(send(&token(), ""), Some(&facts()), &[], Instant::now()) + .is_ok() + ); +} + +#[test] +fn only_carriage_return_newline_and_tab_are_accepted_as_control_characters() { + let cases: &[(&str, bool)] = &[ + ("plain text", true), + ("line\n", true), + ("line\r\n", true), + ("col\tcol", true), + ("한글도 통과한다\n", true), + ("\u{1b}[2J", false), + ("bell\u{7}", false), + ("nul\u{0}", false), + ("del\u{7f}", false), + ("csi\u{9b}31m", false), + ("back\u{8}space", false), + ("vertical\u{b}tab", false), + ]; + for (data, allowed) in cases { + let mut g = guard(); + let verdict = g.judge(send(&token(), data), Some(&facts()), &[], Instant::now()); + assert_eq!( + verdict.is_ok(), + *allowed, + "input {data:?} should be allowed={allowed}" + ); + if !allowed { + assert!(matches!( + verdict.expect_err("refused"), + Refused::ControlCharacter { pane: PANE, .. } + )); + } + } +} diff --git a/src/plugin/guard_tests/mod.rs b/src/plugin/guard_tests/mod.rs new file mode 100644 index 00000000..e939b633 --- /dev/null +++ b/src/plugin/guard_tests/mod.rs @@ -0,0 +1,77 @@ +//! Guard rules, one module per group of rules, sharing the fixtures below. + +use super::*; +use crate::plugin::guard_text::MAX_LOG_MESSAGE_BYTES; +use crate::plugin::protocol::PROTOCOL_VERSION; + +mod budget; +mod identity; +mod input; +mod relaunch; + +pub(super) const PANE: PaneId = 4; +pub(super) const GENERATION: PaneGeneration = 9; +pub(super) const MIN_IDLE: Duration = Duration::from_secs(10); +pub(super) const LAUNCH: &str = "provider-cli"; + +pub(super) fn token() -> PaneToken { + PaneToken::new().expect("OS RNG") +} + +pub(super) fn guard() -> Guard { + Guard::new(MIN_IDLE, RateLimits::default()) +} + +/// A pane that passes every precondition, so each test spoils exactly one. +pub(super) fn facts() -> PaneFacts { + PaneFacts { + pane: PANE, + generation: GENERATION, + opted_in: true, + alive: true, + idle: MIN_IDLE, + launch_command: Some(LAUNCH.to_string()), + } +} + +/// The same pane after its process ended, which is what a relaunch needs. +pub(super) fn exited_facts() -> PaneFacts { + PaneFacts { + alive: false, + ..facts() + } +} + +pub(super) fn send(token: &PaneToken, data: &str) -> PluginCommand { + PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token.clone(), + generation: GENERATION, + data: data.to_string(), + } +} + +pub(super) fn relaunch(token: &PaneToken, args: &[&str]) -> PluginCommand { + PluginCommand::Relaunch { + v: PROTOCOL_VERSION, + token: token.clone(), + generation: GENERATION, + resume_args: args.iter().map(|a| a.to_string()).collect(), + } +} + +pub(super) fn status(token: &PaneToken) -> PluginCommand { + PluginCommand::Status { + v: PROTOCOL_VERSION, + token: token.clone(), + generation: GENERATION, + state: "waiting".to_string(), + detail: None, + deadline_epoch: Some(42), + attempt: 2, + } +} + +pub(super) fn flags(names: &[&str]) -> Vec { + names.iter().map(|n| n.to_string()).collect() +} diff --git a/src/plugin/guard_tests/relaunch.rs b/src/plugin/guard_tests/relaunch.rs new file mode 100644 index 00000000..f62e461c --- /dev/null +++ b/src/plugin/guard_tests/relaunch.rs @@ -0,0 +1,96 @@ +//! Rules 8-9: when a pane's process may be replaced, and with what. + +use super::*; + +#[test] +fn relaunching_a_pane_whose_process_is_still_running_is_refused() { + let mut g = guard(); + assert_eq!( + g.judge(relaunch(&token(), &[]), Some(&facts()), &[], Instant::now()) + .expect_err("refused"), + Refused::PaneStillRunning { pane: PANE } + ); +} + +#[test] +fn a_relaunch_with_no_arguments_reproduces_the_original_command() { + let mut g = guard(); + let approved = g + .judge( + relaunch(&token(), &[]), + Some(&exited_facts()), + &[], + Instant::now(), + ) + .expect("allowed"); + assert_eq!( + approved, + Approved::Relaunch { + pane: PANE, + resume_args: Vec::new(), + command_line: LAUNCH.to_string() + } + ); +} + +#[test] +fn a_relaunch_flag_outside_the_allowed_list_is_refused() { + let mut g = guard(); + assert!(matches!( + g.judge( + relaunch(&token(), &["--continue"]), + Some(&exited_facts()), + &flags(&["--resume"]), + Instant::now() + ), + Err(Refused::ResumeArgsRejected { pane: PANE, .. }) + )); +} + +#[test] +fn a_relaunch_argument_holding_shell_metacharacters_is_refused() { + let mut g = guard(); + assert!(matches!( + g.judge( + relaunch(&token(), &["a; rm -rf /"]), + Some(&exited_facts()), + &[], + Instant::now() + ), + Err(Refused::ResumeArgsRejected { pane: PANE, .. }) + )); +} + +#[test] +fn a_relaunch_with_an_allowed_flag_carries_the_built_command_line() { + let mut g = guard(); + let approved = g + .judge( + relaunch(&token(), &["--resume", "abc123"]), + Some(&exited_facts()), + &flags(&["--resume"]), + Instant::now(), + ) + .expect("allowed"); + assert_eq!( + approved, + Approved::Relaunch { + pane: PANE, + resume_args: vec!["--resume".to_string(), "abc123".to_string()], + command_line: format!("{LAUNCH} '--resume' 'abc123'"), + } + ); +} + +#[test] +fn relaunching_a_pane_with_no_configured_command_is_refused() { + let mut g = guard(); + let facts = PaneFacts { + launch_command: None, + ..exited_facts() + }; + assert!(matches!( + g.judge(relaunch(&token(), &[]), Some(&facts), &[], Instant::now()), + Err(Refused::ResumeArgsRejected { pane: PANE, .. }) + )); +} diff --git a/src/plugin/guard_text.rs b/src/plugin/guard_text.rs new file mode 100644 index 00000000..60831837 --- /dev/null +++ b/src/plugin/guard_text.rs @@ -0,0 +1,32 @@ +//! Bounding and sanitising the text a plugin sends. + +/// Longest log message the host will keep from a plugin. +/// +/// A log line is unbounded text from an untrusted process heading for a file +/// that rotates on size, so truncating is what keeps a chatty plugin from being +/// a way to fill a disk. +pub const MAX_LOG_MESSAGE_BYTES: usize = 2 * 1024; + +/// Whether a character may not appear in input typed into a pane. +/// +/// Input stands in for a human at a keyboard, and a keyboard produces `\r`, +/// `\n` and `\t` and no other control characters. Anything else is an escape +/// sequence: a plugin that could send ESC could reprogram the emulator, move the +/// cursor over what the user is reading, or drive the running CLI's own key +/// bindings. +pub(super) fn is_forbidden_control(c: char) -> bool { + c.is_control() && !matches!(c, '\r' | '\n' | '\t') +} + +/// Cut a message to [`MAX_LOG_MESSAGE_BYTES`], on a character boundary. +pub(super) fn truncate_message(mut message: String) -> String { + if message.len() <= MAX_LOG_MESSAGE_BYTES { + return message; + } + let mut end = MAX_LOG_MESSAGE_BYTES; + while end > 0 && !message.is_char_boundary(end) { + end -= 1; + } + message.truncate(end); + message +} diff --git a/src/plugin/host.rs b/src/plugin/host.rs new file mode 100644 index 00000000..56c2f63e --- /dev/null +++ b/src/plugin/host.rs @@ -0,0 +1,264 @@ +//! One long-lived child process per enabled plugin. +//! +//! The host owns the process and three pump threads (see +//! [`host_pump`](super::host_pump)) and exposes only two non-blocking +//! operations: queue an event, take a decoded command. Nothing a plugin does can +//! make either of those block, because the terminal hub calls them on the thread +//! that also serves every pane. +//! +//! A plugin child is not one of `PtyBackend`'s panes, so [`PluginHost`] is the +//! only place it is ever reaped — hence `shutdown` running from `Drop` too. + +use super::host_pump; +use super::protocol::{PROTOCOL_VERSION, PluginCommand, PluginEvent, encode_event}; +use crate::config::PluginConfig; +use crate::platform::threading::{REAP_TIMEOUT, try_timed_join}; +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::mpsc::{self, Receiver, SyncSender, TrySendError}; +use std::sync::{Arc, Mutex}; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +/// Events that may sit unwritten before further ones are dropped. +/// +/// Bounded on purpose: the queue exists to absorb a plugin that is briefly busy, +/// not to buffer one that has stopped reading. Deep enough to cover a burst of +/// output from every pane at once, shallow enough that a wedged plugin's backlog +/// is bounded memory rather than a growing one. +pub const OUTBOUND_QUEUE_DEPTH: usize = 256; + +/// Commands that may sit undrained before the reader stops accepting more. +/// +/// Bounded for the same reason the outbound queue is, from the other direction: +/// the hub drains only a handful per tick, so an unbounded inbound queue let a +/// plugin writing faster than that grow the host's memory without limit. +/// +/// Unlike the outbound side this blocks rather than drops — the reader thread +/// stalls, the plugin's stdout pipe fills, and the plugin blocks writing. That +/// is backpressure onto whoever is being too loud, and it loses no command a +/// well-behaved plugin sent. Shutdown still ends the thread, because dropping +/// the receiver makes the blocked send fail. +const INBOUND_QUEUE_DEPTH: usize = 256; + +/// How long a plugin gets to exit on its own after being told to. +const CHILD_EXIT_GRACE: Duration = Duration::from_millis(200); + +/// Gap between `try_wait` polls while waiting out [`CHILD_EXIT_GRACE`]. +const CHILD_EXIT_POLL: Duration = Duration::from_millis(5); + +pub struct PluginHost { + name: String, + /// Behind a mutex so [`Self::is_alive`] can ask the OS without `&mut`: the + /// hub holds hosts immutably while dispatching events. + child: Mutex, + /// Held in an `Option` so shutdown can drop it, which is what ends the + /// writer thread and closes the plugin's stdin. + events: Option>, + commands: Receiver, + dropped: Arc, + writer: Option>, + reader: Option>, + stderr: Option>, + shut_down: bool, +} + +impl PluginHost { + /// Launch `cfg.command` and start pumping. + /// + /// Resolution order for the program: a `cfg.command` containing a path + /// separator is taken as a path and used as given; otherwise `plugin_dir` is + /// searched first, so an installed plugin wins over a same-named binary on + /// the user's `PATH`, and only if it is not there is the bare name handed to + /// the OS to resolve against `PATH`. + /// + /// No pane token is passed in the environment. A plugin learns which panes + /// exist only from the events it is sent, which is what keeps a plugin from + /// addressing a pane that never opted in to it. + pub fn spawn(cfg: &PluginConfig, plugin_dir: Option<&Path>) -> Result { + Self::spawn_with_queue_depth(cfg, plugin_dir, OUTBOUND_QUEUE_DEPTH) + } + + fn spawn_with_queue_depth( + cfg: &PluginConfig, + plugin_dir: Option<&Path>, + depth: usize, + ) -> Result { + let program = resolve_program(&cfg.command, plugin_dir); + let mut child = Command::new(&program) + .args(&cfg.args) + .envs(&cfg.env) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .with_context(|| { + format!( + "cannot launch plugin \"{}\" from {}", + cfg.name, + program.display() + ) + })?; + + // Every pipe was requested above, so these are present; the context + // still says which one is missing rather than unwrapping blind. + let stdin = child.stdin.take().context("plugin stdin pipe missing")?; + let stdout = child.stdout.take().context("plugin stdout pipe missing")?; + let stderr = child.stderr.take().context("plugin stderr pipe missing")?; + + let (events_tx, events_rx) = mpsc::sync_channel::(depth); + let (commands_tx, commands_rx) = mpsc::sync_channel::(INBOUND_QUEUE_DEPTH); + let name = cfg.name.clone(); + + let writer_name = name.clone(); + let reader_name = name.clone(); + let stderr_name = name.clone(); + Ok(PluginHost { + name, + child: Mutex::new(child), + events: Some(events_tx), + commands: commands_rx, + dropped: Arc::new(AtomicU64::new(0)), + writer: Some(thread::spawn(move || { + host_pump::write_events(stdin, events_rx, writer_name) + })), + reader: Some(thread::spawn(move || { + host_pump::read_commands(stdout, commands_tx, reader_name) + })), + stderr: Some(thread::spawn(move || { + host_pump::drain_stderr(stderr, stderr_name) + })), + shut_down: false, + }) + } + + pub fn name(&self) -> &str { + &self.name + } + + /// Queue one event. `false` means it was not queued — the plugin is behind, + /// gone, or the event could not be encoded — and the caller carries on + /// regardless: a slow plugin must never stall the pane it is watching. + pub fn send(&self, ev: &PluginEvent) -> bool { + let line = match encode_event(ev) { + Ok(line) => line, + Err(error) => { + tracing::warn!(plugin = %self.name, %error, "dropping unencodable plugin event"); + self.dropped.fetch_add(1, Ordering::Relaxed); + return false; + } + }; + let Some(events) = self.events.as_ref() else { + return false; + }; + match events.try_send(line) { + Ok(()) => true, + Err(TrySendError::Full(_)) => { + self.dropped.fetch_add(1, Ordering::Relaxed); + false + } + Err(TrySendError::Disconnected(_)) => false, + } + } + + /// Take one decoded command, if the plugin has sent one. Never blocks. + pub fn try_recv(&self) -> Option { + self.commands.try_recv().ok() + } + + /// How many events were thrown away rather than queued. Rises whenever the + /// plugin cannot keep up, so it is the signal that a plugin is wedged. + pub fn dropped_events(&self) -> u64 { + self.dropped.load(Ordering::Relaxed) + } + + pub fn is_alive(&self) -> bool { + matches!(self.locked_child().try_wait(), Ok(None)) + } + + /// Tell the plugin to stop, then make sure it has. Idempotent. + pub fn shutdown(&mut self) { + if self.shut_down { + return; + } + self.shut_down = true; + + self.send(&PluginEvent::Shutdown { + v: PROTOCOL_VERSION, + }); + // Ends the writer thread, which drops the plugin's stdin: the plugin + // sees EOF even if it never looked at the shutdown event. + self.events = None; + if let Some(writer) = self.writer.take() { + try_timed_join(writer, REAP_TIMEOUT); + } + + if !self.wait_for_exit() { + let mut child = self.locked_child(); + if let Err(error) = child.kill() { + tracing::warn!(plugin = %self.name, %error, "cannot kill plugin process"); + } + // Reaps the zombie; the kill above only delivers the signal. + if let Err(error) = child.wait() { + tracing::warn!(plugin = %self.name, %error, "cannot reap plugin process"); + } + } + + // Both read a pipe the dead child owned, so both are at EOF by now. + for handle in [self.reader.take(), self.stderr.take()] + .into_iter() + .flatten() + { + try_timed_join(handle, REAP_TIMEOUT); + } + } + + /// Poll for a clean exit within [`CHILD_EXIT_GRACE`]. `true` if it happened. + fn wait_for_exit(&self) -> bool { + let deadline = Instant::now() + CHILD_EXIT_GRACE; + loop { + match self.locked_child().try_wait() { + Ok(Some(_)) => return true, + // Cannot be asked about, so waiting longer will not help. + Err(_) => return true, + Ok(None) => {} + } + if Instant::now() >= deadline { + return false; + } + thread::sleep(CHILD_EXIT_POLL); + } + } + + /// The child, recovering from a poisoned lock rather than panicking: a + /// panic while holding it would otherwise make the process unreapable. + fn locked_child(&self) -> std::sync::MutexGuard<'_, Child> { + self.child.lock().unwrap_or_else(|e| e.into_inner()) + } +} + +impl Drop for PluginHost { + fn drop(&mut self) { + self.shutdown(); + } +} + +/// See [`PluginHost::spawn`] for the order and why it is that way. +fn resolve_program(command: &str, plugin_dir: Option<&Path>) -> PathBuf { + if command.contains(std::path::MAIN_SEPARATOR) || command.contains('/') { + return PathBuf::from(command); + } + if let Some(dir) = plugin_dir { + let candidate = dir.join(command); + if candidate.is_file() { + return candidate; + } + } + PathBuf::from(command) +} + +#[cfg(all(test, unix))] +#[path = "host_tests.rs"] +mod tests; diff --git a/src/plugin/host_pump.rs b/src/plugin/host_pump.rs new file mode 100644 index 00000000..e26033e3 --- /dev/null +++ b/src/plugin/host_pump.rs @@ -0,0 +1,161 @@ +//! The three pump threads behind a [`PluginHost`](super::host::PluginHost): +//! events out, commands in, and diagnostics off stderr. +//! +//! Every loop here ends on its own when the pipe it holds reaches EOF or the +//! peer it talks to is gone, so shutdown never has to interrupt one — it closes +//! a pipe and reaps. + +use super::protocol::{MAX_LINE_BYTES, PluginCommand, decode_command, is_blank_line}; +use std::io::{BufRead, BufReader, Write}; +use std::process::{ChildStderr, ChildStdin, ChildStdout}; +use std::sync::mpsc::{Receiver, SyncSender}; + +/// What one read off a plugin's stdout produced. +enum Pulled { + Line(String), + /// Over [`MAX_LINE_BYTES`]; the rest of the line was thrown away so the + /// next newline resynchronises the stream. + TooLong(usize), + NotUtf8, + Eof, +} + +/// Write encoded event lines to the plugin's stdin until the queue closes. +/// +/// Dropping `stdin` on return is what tells the plugin the host is done, so +/// this returning is a meaningful signal and not only a thread ending. +pub(super) fn write_events(mut stdin: ChildStdin, rx: Receiver, name: String) { + for line in rx { + let written = stdin + .write_all(line.as_bytes()) + .and_then(|()| stdin.write_all(b"\n")) + .and_then(|()| stdin.flush()); + if let Err(error) = written { + tracing::warn!(plugin = %name, %error, "plugin stdin unusable; event writer stopping"); + return; + } + } +} + +/// Decode commands off the plugin's stdout until EOF or nobody is listening. +pub(super) fn read_commands(stdout: ChildStdout, tx: SyncSender, name: String) { + let mut reader = BufReader::new(stdout); + loop { + match pull_line(&mut reader) { + Ok(Pulled::Line(line)) => { + if is_blank_line(&line) { + continue; + } + match decode_command(&line) { + Ok(command) => { + if tx.send(command).is_err() { + return; + } + } + Err(error) => { + tracing::warn!(plugin = %name, %error, "refused a line from plugin") + } + } + } + Ok(Pulled::TooLong(bytes)) => tracing::warn!( + plugin = %name, + bytes, + limit = MAX_LINE_BYTES, + "discarded an over-long line from plugin" + ), + Ok(Pulled::NotUtf8) => { + tracing::warn!(plugin = %name, "discarded a line from plugin that is not UTF-8") + } + Ok(Pulled::Eof) => return, + Err(error) => { + tracing::warn!(plugin = %name, %error, "plugin stdout unusable; reader stopping"); + return; + } + } + } +} + +/// Relay the plugin's stderr into the host's log. +/// +/// Drained rather than ignored for two reasons: a plugin that dies needs to be +/// diagnosable, and an undrained stderr pipe fills and blocks the plugin inside +/// its own `write`, which looks exactly like a hang. +pub(super) fn drain_stderr(stderr: ChildStderr, name: String) { + let mut reader = BufReader::new(stderr); + loop { + match pull_line(&mut reader) { + Ok(Pulled::Line(line)) => { + if !is_blank_line(&line) { + tracing::warn!(plugin = %name, "{line}"); + } + } + Ok(Pulled::TooLong(bytes)) => { + tracing::warn!(plugin = %name, bytes, "discarded an over-long stderr line") + } + Ok(Pulled::NotUtf8) => { + tracing::warn!(plugin = %name, "discarded a stderr line that is not UTF-8") + } + Ok(Pulled::Eof) => return, + Err(error) => { + tracing::warn!(plugin = %name, %error, "plugin stderr unusable; drain stopping"); + return; + } + } + } +} + +/// Read one line, or say why there is none to decode. +fn pull_line(reader: &mut impl BufRead) -> std::io::Result { + let mut buf = Vec::new(); + let (seen, terminated) = read_capped(reader, &mut buf)?; + if seen == 0 { + return Ok(Pulled::Eof); + } + let line_bytes = seen - usize::from(terminated); + if line_bytes > MAX_LINE_BYTES { + // Everything up to the newline was consumed and only the cap's worth + // kept, so the stream is already resynchronised. + return Ok(Pulled::TooLong(line_bytes)); + } + if buf.last() == Some(&b'\r') { + buf.pop(); + } + match String::from_utf8(buf) { + Ok(line) => Ok(Pulled::Line(line)), + Err(_) => Ok(Pulled::NotUtf8), + } +} + +/// Consume bytes up to and including the next newline, keeping at most +/// [`MAX_LINE_BYTES`] of them in `buf` (the newline is never kept). +/// +/// Returns how many bytes the line actually spanned and whether a newline ended +/// it. The cap is applied while reading rather than left to +/// [`decode_command`]'s own length check: by the time that runs the host has +/// already allocated whatever the plugin chose to send, which is the thing worth +/// preventing. +fn read_capped(reader: &mut impl BufRead, buf: &mut Vec) -> std::io::Result<(usize, bool)> { + let mut seen = 0; + loop { + let available = match reader.fill_buf() { + Ok(available) => available, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + if available.is_empty() { + return Ok((seen, false)); + } + let (take, terminated) = match available.iter().position(|b| *b == b'\n') { + Some(at) => (at, true), + None => (available.len(), false), + }; + let room = MAX_LINE_BYTES.saturating_sub(buf.len()); + buf.extend_from_slice(&available[..take.min(room)]); + let consumed = take + usize::from(terminated); + reader.consume(consumed); + seen += consumed; + if terminated { + return Ok((seen, true)); + } + } +} diff --git a/src/plugin/host_tests.rs b/src/plugin/host_tests.rs new file mode 100644 index 00000000..81f08962 --- /dev/null +++ b/src/plugin/host_tests.rs @@ -0,0 +1,181 @@ +//! Round-trip tests against a real `/bin/sh` child, which is guaranteed present +//! wherever these run. + +use super::*; +use crate::backend::PaneToken; +use crate::backend::identity::FIRST_GENERATION; +use crate::plugin::protocol::{LogLevel, MAX_LINE_BYTES}; + +/// Generous like the PTY tests': spawning processes under a parallel test run is +/// slow, and the point of every deadline here is only to fail rather than hang. +const HOST_TEST_DEADLINE: Duration = Duration::from_secs(15); + +const POLL: Duration = Duration::from_millis(10); + +/// Env var each script reads the command it should echo back from. +const CANNED_ENV: &str = "CANNED"; + +const CANNED_MESSAGE: &str = "round trip"; + +fn canned_line() -> String { + serde_json::to_string(&PluginCommand::Log { + v: PROTOCOL_VERSION, + level: LogLevel::Info, + message: CANNED_MESSAGE.to_string(), + }) + .expect("encode a command") +} + +fn shell_plugin(script: &str) -> PluginConfig { + PluginConfig { + name: "test-plugin".to_string(), + command: "/bin/sh".to_string(), + args: vec!["-c".to_string(), script.to_string()], + env: [(CANNED_ENV.to_string(), canned_line())] + .into_iter() + .collect(), + enabled: true, + ..PluginConfig::default() + } +} + +fn pane_opened() -> PluginEvent { + PluginEvent::PaneOpened { + v: PROTOCOL_VERSION, + token: PaneToken::new().expect("OS RNG"), + generation: FIRST_GENERATION, + title: None, + command: None, + cwd: "/".to_string(), + } +} + +fn wait_for_command(host: &PluginHost) -> Option { + let deadline = Instant::now() + HOST_TEST_DEADLINE; + while Instant::now() < deadline { + if let Some(cmd) = host.try_recv() { + return Some(cmd); + } + thread::sleep(POLL); + } + None +} + +fn assert_is_canned(cmd: Option) { + match cmd { + Some(PluginCommand::Log { message, .. }) => assert_eq!(message, CANNED_MESSAGE), + other => panic!("expected the canned log command, got {other:?}"), + } +} + +#[test] +fn an_event_reaches_the_plugin_and_the_command_it_answers_with_is_decoded() { + // The command only comes back if the line the plugin read was the event, so + // receiving it proves both directions. + let cfg = + shell_plugin(r#"read line; case "$line" in *pane_opened*) printf '%s\n' "$CANNED";; esac"#); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + assert!(host.send(&pane_opened())); + assert_is_canned(wait_for_command(&host)); + host.shutdown(); + assert_eq!(host.dropped_events(), 0); +} + +#[test] +fn a_blank_line_from_the_plugin_is_skipped_rather_than_ending_the_stream() { + let cfg = shell_plugin(r#"printf '\n \n%s\n' "$CANNED""#); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + assert_is_canned(wait_for_command(&host)); + host.shutdown(); +} + +#[test] +fn a_line_the_host_cannot_decode_does_not_stop_later_commands() { + let cfg = shell_plugin(r#"printf 'not json\n{"cmd":"nope"}\n%s\n' "$CANNED""#); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + assert_is_canned(wait_for_command(&host)); + host.shutdown(); +} + +#[test] +fn an_over_long_line_is_discarded_and_the_stream_resynchronises() { + // 70 KiB in 1000-byte chunks, past MAX_LINE_BYTES, then a valid command: + // the host must survive the first and still deliver the second. + const CHUNKS: usize = 70; + const CHUNK_BYTES: usize = 1000; + const { assert!(CHUNKS * CHUNK_BYTES > MAX_LINE_BYTES) }; + let cfg = shell_plugin( + r#"i=0; while [ $i -lt 70 ]; do printf '%01000d' 0; i=$((i+1)); done; printf '\n'; printf '%s\n' "$CANNED""#, + ); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + assert_is_canned(wait_for_command(&host)); + host.shutdown(); +} + +#[test] +fn a_plugin_that_exits_at_once_is_reported_not_alive_and_shuts_down_cleanly() { + let cfg = shell_plugin("exit 0"); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + let deadline = Instant::now() + HOST_TEST_DEADLINE; + while host.is_alive() && Instant::now() < deadline { + thread::sleep(POLL); + } + assert!(!host.is_alive(), "plugin should have exited"); + host.shutdown(); + assert!(!host.is_alive()); +} + +#[test] +fn shutting_down_twice_is_safe() { + let cfg = shell_plugin("cat"); + let mut host = PluginHost::spawn(&cfg, None).expect("spawn"); + assert!(host.is_alive()); + host.shutdown(); + host.shutdown(); + assert!(!host.is_alive()); + // The second call must not have queued anything either. + assert!(!host.send(&pane_opened())); +} + +#[test] +fn a_full_outbound_queue_drops_events_instead_of_blocking() { + // Depth 1 against a plugin that never reads: the pipe fills, the writer + // blocks inside its write, and every further event has nowhere to go. + let cfg = shell_plugin("sleep 30"); + let mut host = PluginHost::spawn_with_queue_depth(&cfg, None, 1).expect("spawn"); + let bulky = PluginEvent::PaneOutput { + v: PROTOCOL_VERSION, + token: PaneToken::new().expect("OS RNG"), + generation: FIRST_GENERATION, + text: "a".repeat(8 * 1024), + }; + + let deadline = Instant::now() + HOST_TEST_DEADLINE; + let mut refused = false; + while !refused && Instant::now() < deadline { + refused = !host.send(&bulky); + } + + assert!(refused, "a full queue should have refused an event"); + assert!(host.dropped_events() > 0); + host.shutdown(); +} + +#[test] +fn a_command_named_by_path_is_launched_from_that_path() { + // "/bin/sh" holds a separator, so it is used as given rather than searched + // for in the plugin directory. + let cfg = shell_plugin("exit 0"); + let host = PluginHost::spawn(&cfg, Some(Path::new("/nonexistent"))).expect("spawn"); + assert_eq!(host.name(), "test-plugin"); +} + +#[test] +fn a_plugin_that_cannot_be_launched_is_reported_rather_than_ignored() { + let mut cfg = shell_plugin("exit 0"); + cfg.command = "/nonexistent/plugin-binary".to_string(); + let Err(err) = PluginHost::spawn(&cfg, None) else { + panic!("launching a missing binary should have failed"); + }; + assert!(err.to_string().contains("cannot launch plugin")); +} diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs new file mode 100644 index 00000000..aa087264 --- /dev/null +++ b/src/plugin/mod.rs @@ -0,0 +1,32 @@ +//! The wire contract for an out-of-process plugin. +//! +//! Trust posture: a plugin is a separate program the host launches and speaks +//! NDJSON to, so everything arriving from it is untrusted input. Every command +//! is validated by the host — version, line length, payload bounds, and the +//! pane identity it names — before anything acts on it. This module is types +//! and parsing only: no IO, no process spawning, no threads. +//! +//! The contract is deliberately provider-agnostic. It speaks of panes, output, +//! idleness and relaunching, and never of any particular tool; which program a +//! pane runs stays the host's knowledge, which is what keeps +//! [`protocol::PluginCommand::Relaunch`] from being arbitrary execution. + +//! The layering is deliberate: `protocol` is types only, `host` moves bytes and +//! knows nothing about authority, and `guard` decides authority and touches no +//! IO. Nothing acts on a plugin's command without passing through `guard`. + +pub mod guard; +pub mod host; +pub mod protocol; +pub mod registry; + +mod guard_budget; +mod guard_refusal; +mod guard_text; +mod host_pump; + +pub use guard::{Approved, Guard, PaneFacts}; +pub use guard_budget::{RateAction, RateLimits}; +pub use guard_refusal::Refused; +pub use guard_text::MAX_LOG_MESSAGE_BYTES; +pub use host::PluginHost; diff --git a/src/plugin/protocol.rs b/src/plugin/protocol.rs new file mode 100644 index 00000000..558a2611 --- /dev/null +++ b/src/plugin/protocol.rs @@ -0,0 +1,247 @@ +//! What the host and an out-of-process plugin say to each other. +//! +//! Newline-delimited JSON over the plugin child's stdin and stdout: the host +//! writes one [`PluginEvent`] per line, the plugin writes one +//! [`PluginCommand`] per line. A line is the framing, so nothing here may emit +//! an embedded newline — [`encode_event`] refuses rather than corrupt the +//! stream. +//! +//! Unlike the daemon's control protocol, the two sides here are separate +//! builds: a plugin is written against a version of this contract and shipped +//! independently. That makes [`PROTOCOL_VERSION`] a real negotiation, and a +//! mismatch is refused rather than half-understood. + +use crate::backend::{PaneGeneration, PaneToken}; +use anyhow::{Result, anyhow, bail}; +use serde::{Deserialize, Serialize}; + +/// Bumped when a field changes meaning. A plugin built against a version the +/// host does not speak is refused rather than half-understood. +pub const PROTOCOL_VERSION: u32 = 1; + +/// Longest line the host will read from a plugin. +/// +/// The plugin is untrusted input on a stream with no length prefix, so without +/// a cap a plugin that never writes a newline makes the host's reader allocate +/// without bound. 64 KiB is far above any legitimate command — the largest is a +/// [`PluginCommand::SendInput`] bounded by [`MAX_INPUT_BYTES`] plus JSON +/// escaping and a token — while still being a single small allocation. +pub const MAX_LINE_BYTES: usize = 64 * 1024; + +/// Longest `data` a single [`PluginCommand::SendInput`] may carry. +/// +/// Typed input stands in for a human at a keyboard: a prompt, a confirmation, +/// a pasted snippet. 8 KiB covers that with room to spare and keeps one command +/// from filling a PTY's input buffer, which would block the writer and stall +/// every other pane behind it. +pub const MAX_INPUT_BYTES: usize = 8 * 1024; + +/// Sentinel [`decode_command`] answers a blank line with. Prefer +/// [`is_blank_line`] over matching this text. +const BLANK_LINE_MESSAGE: &str = "blank line carries no command"; + +/// Something that happened, sent host to plugin. +/// +/// Every pane-scoped variant carries both the slot's token and the generation +/// within it: a plugin decides asynchronously, so the spawn it is reacting to +/// may already be gone by the time it answers. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "event", rename_all = "snake_case")] +pub enum PluginEvent { + /// A pane slot began a spawn. `title` and `command` are absent when the + /// host has none to report. + PaneOpened { + v: u32, + token: PaneToken, + generation: PaneGeneration, + title: Option, + command: Option, + cwd: String, + }, + /// Plain text the pane produced, already escape-stripped. + PaneOutput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + text: String, + }, + /// The pane has produced nothing for this long. + PaneIdle { + v: u32, + token: PaneToken, + generation: PaneGeneration, + idle_ms: u64, + }, + /// The pane's process ended. The slot survives, so a relaunch is possible. + PaneExited { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + /// The slot itself is gone. No relaunch is possible; drop any state held + /// for this token. + PaneClosed { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + /// A human typed into the pane. A plugin must treat this as a cancellation + /// signal: the person has taken the pane back. + UserInput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + }, + /// The host is going away. Exit cleanly. + Shutdown { v: u32 }, +} + +/// A request, sent plugin to host. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "cmd", rename_all = "snake_case")] +pub enum PluginCommand { + /// Text to type into a live pane, bounded by [`MAX_INPUT_BYTES`]. + SendInput { + v: u32, + token: PaneToken, + generation: PaneGeneration, + data: String, + }, + /// Replace an exited pane's process, appending these args to the original + /// command. The plugin does not name the program: which binary a pane runs + /// is the host's knowledge, and letting a plugin choose it would make this + /// command arbitrary execution. + Relaunch { + v: u32, + token: PaneToken, + generation: PaneGeneration, + resume_args: Vec, + }, + /// What the plugin believes about a pane. Observability only — the host + /// displays it and acts on nothing. + Status { + v: u32, + token: PaneToken, + generation: PaneGeneration, + state: String, + detail: Option, + deadline_epoch: Option, + attempt: u32, + }, + /// A line for the host's log. + Log { + v: u32, + level: LogLevel, + message: String, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, +} + +impl PluginCommand { + /// The protocol version the plugin claims to speak. + pub fn version(&self) -> u32 { + match self { + Self::SendInput { v, .. } + | Self::Relaunch { v, .. } + | Self::Status { v, .. } + | Self::Log { v, .. } => *v, + } + } + + /// Which slot this addresses, or `None` for [`Self::Log`], which is not + /// pane-scoped. Lets the guard layer check identity without matching every + /// variant. + pub fn token(&self) -> Option<&PaneToken> { + match self { + Self::SendInput { token, .. } + | Self::Relaunch { token, .. } + | Self::Status { token, .. } => Some(token), + Self::Log { .. } => None, + } + } + + /// Which spawn of the slot this addresses, paired with [`Self::token`]. + pub fn generation(&self) -> Option { + match self { + Self::SendInput { generation, .. } + | Self::Relaunch { generation, .. } + | Self::Status { generation, .. } => Some(*generation), + Self::Log { .. } => None, + } + } + + /// Check the bounds serde cannot express. Called by [`decode_command`], and + /// separately callable by anything that builds a command in-process. + pub fn validate(&self) -> Result<()> { + if let Self::SendInput { data, .. } = self + && data.len() > MAX_INPUT_BYTES + { + bail!( + "send_input data is {} bytes, over the {MAX_INPUT_BYTES}-byte limit", + data.len() + ); + } + Ok(()) + } +} + +/// Serialise one event as exactly one NDJSON line, without its terminator. +/// +/// `serde_json::to_string` never emits a newline — it escapes them inside +/// strings — but the framing depends on that, so it is checked rather than +/// assumed. +pub fn encode_event(ev: &PluginEvent) -> Result { + let line = serde_json::to_string(ev).map_err(|e| anyhow!("cannot encode plugin event: {e}"))?; + if line.contains('\n') { + bail!("encoded plugin event contains a newline, which would split the frame"); + } + Ok(line) +} + +/// Whether a line carries no command and should be skipped. +/// +/// A plugin's writer may flush a bare newline, so a blank line is expected +/// traffic rather than an error. [`decode_command`] still refuses it — it has +/// no command to return — so a reader loop tests this first. +pub fn is_blank_line(line: &str) -> bool { + line.trim().is_empty() +} + +/// Parse one line from a plugin. +/// +/// Refuses an over-long line, a version this host does not speak, an unknown +/// `cmd`, and a command that fails [`PluginCommand::validate`]. A blank line is +/// refused too, with [`BLANK_LINE_MESSAGE`]; see [`is_blank_line`]. +pub fn decode_command(line: &str) -> Result { + if line.len() > MAX_LINE_BYTES { + bail!( + "plugin line is {} bytes, over the {MAX_LINE_BYTES}-byte limit", + line.len() + ); + } + if is_blank_line(line) { + bail!("{BLANK_LINE_MESSAGE}"); + } + let command: PluginCommand = serde_json::from_str(line) + .map_err(|e| anyhow!("cannot parse plugin command: {e}; unknown or malformed cmd"))?; + if command.version() != PROTOCOL_VERSION { + bail!( + "plugin speaks protocol version {}, host speaks {PROTOCOL_VERSION}", + command.version() + ); + } + command.validate()?; + Ok(command) +} + +#[cfg(test)] +#[path = "protocol_tests.rs"] +mod tests; diff --git a/src/plugin/protocol_tests.rs b/src/plugin/protocol_tests.rs new file mode 100644 index 00000000..f32bd984 --- /dev/null +++ b/src/plugin/protocol_tests.rs @@ -0,0 +1,277 @@ +use super::{ + LogLevel, MAX_INPUT_BYTES, MAX_LINE_BYTES, PROTOCOL_VERSION, PluginCommand, PluginEvent, + decode_command, encode_event, is_blank_line, +}; +use crate::backend::{PaneGeneration, PaneToken}; + +const TOKEN: &str = "0123456789abcdef0123456789abcdef"; +const GENERATION: PaneGeneration = 2; + +fn token() -> PaneToken { + serde_json::from_str(&format!("\"{TOKEN}\"")).expect("a token is a JSON string") +} + +fn every_event() -> Vec { + vec![ + PluginEvent::PaneOpened { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + title: Some("worker".into()), + command: None, + cwd: "/w/repo".into(), + }, + PluginEvent::PaneOutput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + text: "first\nsecond".into(), + }, + PluginEvent::PaneIdle { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + idle_ms: 30_000, + }, + PluginEvent::PaneExited { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + }, + PluginEvent::PaneClosed { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + }, + PluginEvent::UserInput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + }, + PluginEvent::Shutdown { + v: PROTOCOL_VERSION, + }, + ] +} + +fn every_command() -> Vec { + vec![ + PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + data: "continue\r".into(), + }, + PluginCommand::Relaunch { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + resume_args: vec!["--resume".into(), "last".into()], + }, + PluginCommand::Status { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + state: "waiting".into(), + detail: None, + deadline_epoch: Some(1_700_000_000), + attempt: 1, + }, + PluginCommand::Log { + v: PROTOCOL_VERSION, + level: LogLevel::Warn, + message: "retrying".into(), + }, + ] +} + +#[test] +fn encoding_an_event_yields_one_line_tagged_with_its_name() { + let expected = [ + "pane_opened", + "pane_output", + "pane_idle", + "pane_exited", + "pane_closed", + "user_input", + "shutdown", + ]; + for (event, tag) in every_event().iter().zip(expected) { + let line = encode_event(event).expect("encodes"); + assert!(!line.contains('\n'), "{tag} split the frame: {line}"); + assert!( + line.starts_with(&format!(r#"{{"event":"{tag}""#)), + "{tag} is not the tag of {line}" + ); + } +} + +#[test] +fn text_holding_a_newline_is_escaped_rather_than_splitting_the_line() { + let event = PluginEvent::PaneOutput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + text: "first\nsecond".into(), + }; + let line = encode_event(&event).expect("encodes"); + assert!(!line.contains('\n')); + assert!(line.contains(r"first\nsecond")); +} + +#[test] +fn every_command_survives_the_round_trip() { + for command in &every_command() { + let line = serde_json::to_string(command).expect("encodes"); + assert_eq!(&decode_command(&line).expect("decodes"), command); + } +} + +#[test] +fn an_events_json_shape_is_the_wire_contract() { + // Pinned literally: this is what an independently built plugin parses, so + // an accidental rename must fail here rather than in the field. + let event = PluginEvent::PaneIdle { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + idle_ms: 30_000, + }; + assert_eq!( + encode_event(&event).unwrap(), + format!( + r#"{{"event":"pane_idle","v":1,"token":"{TOKEN}","generation":2,"idle_ms":30000}}"# + ) + ); +} + +#[test] +fn a_commands_json_shape_is_the_wire_contract() { + let command = PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + data: "go".into(), + }; + assert_eq!( + serde_json::to_string(&command).unwrap(), + format!(r#"{{"cmd":"send_input","v":1,"token":"{TOKEN}","generation":2,"data":"go"}}"#) + ); +} + +#[test] +fn a_log_levels_json_shape_is_lowercase() { + assert_eq!( + serde_json::to_string(&LogLevel::Error).unwrap(), + r#""error""# + ); + assert_eq!(serde_json::to_string(&LogLevel::Warn).unwrap(), r#""warn""#); + assert_eq!(serde_json::to_string(&LogLevel::Info).unwrap(), r#""info""#); + assert_eq!( + serde_json::to_string(&LogLevel::Debug).unwrap(), + r#""debug""# + ); +} + +#[test] +fn a_command_from_another_protocol_version_is_refused_and_both_versions_named() { + let line = format!( + r#"{{"cmd":"log","v":{},"level":"info","message":"hi"}}"#, + PROTOCOL_VERSION + 1 + ); + let err = decode_command(&line).expect_err("refused"); + let message = err.to_string(); + assert!( + message.contains(&(PROTOCOL_VERSION + 1).to_string()) + && message.contains(&PROTOCOL_VERSION.to_string()), + "message names only one version: {message}" + ); +} + +#[test] +fn a_line_over_the_length_limit_is_refused_before_it_is_parsed() { + // Refused on length alone, so nothing here depends on the payload being + // valid JSON — an unbounded line must not become an unbounded parse. + let line = "x".repeat(MAX_LINE_BYTES + 1); + let message = decode_command(&line).expect_err("refused").to_string(); + assert!(message.contains(&MAX_LINE_BYTES.to_string()), "{message}"); +} + +#[test] +fn a_line_at_the_length_limit_is_parsed() { + let padding = "p".repeat(MAX_INPUT_BYTES); + let line = format!(r#"{{"cmd":"log","v":1,"level":"debug","message":"{padding}"}}"#); + assert!(line.len() <= MAX_LINE_BYTES); + assert!(decode_command(&line).is_ok()); +} + +#[test] +fn an_unknown_cmd_is_refused_rather_than_guessed() { + let message = decode_command(r#"{"cmd":"drop_everything","v":1}"#) + .expect_err("refused") + .to_string(); + assert!(message.contains("drop_everything"), "{message}"); + assert!(message.contains("cmd"), "{message}"); +} + +#[test] +fn a_command_missing_a_required_field_is_refused() { + assert!(decode_command(r#"{"cmd":"send_input","v":1}"#).is_err()); +} + +#[test] +fn send_input_data_over_the_input_limit_is_refused() { + let command = PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + data: "y".repeat(MAX_INPUT_BYTES + 1), + }; + let message = command.validate().expect_err("refused").to_string(); + assert!(message.contains(&MAX_INPUT_BYTES.to_string()), "{message}"); + + let line = serde_json::to_string(&command).unwrap(); + assert!( + line.len() <= MAX_LINE_BYTES, + "the length cap would mask this" + ); + assert!(decode_command(&line).is_err()); +} + +#[test] +fn send_input_data_at_the_input_limit_is_accepted() { + let command = PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token(), + generation: GENERATION, + data: "y".repeat(MAX_INPUT_BYTES), + }; + assert!(command.validate().is_ok()); +} + +#[test] +fn a_blank_line_is_recognised_as_blank_and_carries_no_command() { + for line in ["", " ", "\t", "\r\n"] { + assert!(is_blank_line(line), "not seen as blank: {line:?}"); + assert!(decode_command(line).is_err()); + } + assert!(!is_blank_line( + r#"{"cmd":"log","v":1,"level":"info","message":"x"}"# + )); +} + +#[test] +fn only_a_pane_scoped_command_reports_an_identity() { + for command in &every_command() { + match command { + PluginCommand::Log { .. } => { + assert_eq!(command.token(), None); + assert_eq!(command.generation(), None); + } + _ => { + assert_eq!(command.token(), Some(&token())); + assert_eq!(command.generation(), Some(GENERATION)); + } + } + } +} diff --git a/src/plugin/registry.rs b/src/plugin/registry.rs new file mode 100644 index 00000000..12964b66 --- /dev/null +++ b/src/plugin/registry.rs @@ -0,0 +1,271 @@ +//! The on-disk set of installed plugin executables (`~/.nightcrow/plugins`). +//! +//! Installing a binary here does not switch it on: the host only ever launches +//! a plugin that `config.toml` declares in a `[[plugin]]` table *and* that a +//! `[[startup_command]]` opted a pane into by name. That edit is left to the +//! user on purpose — a plugin can drive a pane's terminal, so the file that +//! grants it that has to be one a person read. +//! +//! Every function takes the plugins directory as a parameter so the filesystem +//! behaviour is testable against a temp directory. + +use anyhow::{Context, Result}; +use std::path::{Path, PathBuf}; + +/// Subdirectory of `~/.nightcrow` holding installed plugin executables. +const PLUGINS_SUBDIR: &str = "plugins"; +/// Owner-only read/write/execute for an installed plugin. Least privilege, and +/// the same posture the config file's 0600 takes: nothing else on the machine +/// has business running a binary nightcrow will attach to a terminal. +#[cfg(unix)] +const PLUGIN_MODE: u32 = 0o700; +/// Longest accepted plugin name. +const MAX_NAME_LEN: usize = 64; + +/// `~/.nightcrow/plugins`, resolved whether or not it exists yet. Errors only +/// when the home directory cannot be determined. +pub fn default_plugins_dir() -> Result { + let home = dirs::home_dir().context("cannot determine the home directory")?; + Ok(home.join(".nightcrow").join(PLUGINS_SUBDIR)) +} + +/// Result of [`install`], so the caller can report exactly what was touched. +#[derive(Debug)] +pub enum InstallOutcome { + Created(PathBuf), + Replaced(PathBuf), + /// A plugin of that name was already installed and `force` was not set. + AlreadyExists(PathBuf), +} + +/// Result of [`remove`]. Removing something absent is a report, not a failure. +#[derive(Debug)] +pub enum RemoveOutcome { + Removed(PathBuf), + NotInstalled(String), +} + +/// How `config.toml` currently refers to an installed plugin. +#[derive(Debug, PartialEq, Eq)] +pub struct PluginStatus { + pub declared: bool, + pub enabled: bool, + /// `[[startup_command]]` entries whose `plugin =` names this plugin. + pub opt_ins: usize, +} + +/// Accept only a safe single filename. +/// +/// This is the path-traversal boundary: the name is joined onto the plugins +/// directory and then written to and deleted, so anything that could escape +/// that directory — a separator, `.`/`..` — is refused here rather than +/// sanitised. A leading `-` is refused too, because such a file name is read as +/// a flag by every command the user might later point at it. +pub fn validate_name(name: &str) -> Result<()> { + anyhow::ensure!(!name.is_empty(), "a plugin name must not be empty"); + anyhow::ensure!( + name.len() <= MAX_NAME_LEN, + "plugin name \"{name}\" is longer than {MAX_NAME_LEN} characters" + ); + anyhow::ensure!( + !name.contains('/') && !name.contains('\\'), + "plugin name \"{name}\" must be a single file name, not a path" + ); + anyhow::ensure!( + name != "." && name != "..", + "plugin name \"{name}\" is a directory reference, not a file name" + ); + anyhow::ensure!( + !name.starts_with('-'), + "plugin name \"{name}\" must not start with '-'; such a name is read as a flag" + ); + anyhow::ensure!( + name.bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-'), + "plugin name \"{name}\" may only contain letters, digits, '.', '_' and '-'" + ); + Ok(()) +} + +/// Copy the executable at `source` into `base` under `name` (defaulting to the +/// source file stem) and restrict it to owner-only. +pub fn install( + base: &Path, + source: &Path, + name: Option<&str>, + force: bool, +) -> Result { + let name = match name { + Some(name) => name.to_string(), + None => derive_name(source)?, + }; + validate_name(&name)?; + check_source(source)?; + + let dest = base.join(&name); + let installed = dest.symlink_metadata().is_ok(); + if installed && !force { + return Ok(InstallOutcome::AlreadyExists(dest)); + } + std::fs::create_dir_all(base) + .with_context(|| format!("creating plugin directory {}", base.display()))?; + // Unlink rather than copy over: truncating a binary that is currently + // running fails with ETXTBSY, and a fresh inode leaves any live process + // on the old file. + if installed { + std::fs::remove_file(&dest) + .with_context(|| format!("replacing installed plugin {}", dest.display()))?; + } + std::fs::copy(source, &dest) + .with_context(|| format!("copying plugin {} to {}", source.display(), dest.display()))?; + restrict_permissions(&dest)?; + Ok(if installed { + InstallOutcome::Replaced(dest) + } else { + InstallOutcome::Created(dest) + }) +} + +/// Installed plugin names, sorted. A missing directory lists as empty — that is +/// the state before the first install, not an error. +pub fn list(base: &Path) -> Result> { + let entries = match std::fs::read_dir(base) { + Ok(entries) => entries, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(e) => { + return Err(e).with_context(|| format!("reading plugin directory {}", base.display())); + } + }; + let mut names = Vec::new(); + for entry in entries { + let entry = + entry.with_context(|| format!("reading plugin directory {}", base.display()))?; + // Follows symlinks, so a hand-linked plugin still lists. + if !std::fs::metadata(entry.path()).is_ok_and(|m| m.is_file()) { + continue; + } + names.push(entry.file_name().to_string_lossy().into_owned()); + } + names.sort(); + Ok(names) +} + +/// Delete an installed plugin. +pub fn remove(base: &Path, name: &str) -> Result { + validate_name(name)?; + let path = base.join(name); + if path.symlink_metadata().is_err() { + return Ok(RemoveOutcome::NotInstalled(name.to_string())); + } + std::fs::remove_file(&path) + .with_context(|| format!("removing installed plugin {}", path.display()))?; + Ok(RemoveOutcome::Removed(path)) +} + +/// What the loaded config says about `name`. +pub fn status(cfg: &crate::config::Config, name: &str) -> PluginStatus { + let declared = cfg.plugins.iter().find(|p| p.name == name); + PluginStatus { + declared: declared.is_some(), + enabled: declared.is_some_and(|p| p.enabled), + opt_ins: cfg + .startup_commands + .iter() + .filter(|sc| sc.plugin.as_deref() == Some(name)) + .count(), + } +} + +/// The `[[plugin]]` block to paste into `config.toml` after an install. +/// +/// Printed, never written: enabling a plugin hands it a pane's terminal, so the +/// opt-in stays an explicit edit the user reviews. `enabled = false` and an +/// empty `allowed_resume_flags` are the off positions of both switches. +pub fn config_snippet(name: &str, command: &Path) -> String { + format!( + "[[plugin]]\n\ + name = \"{name}\"\n\ + command = \"{}\"\n\ + args = []\n\ + allowed_resume_flags = []\n\ + enabled = false\n", + toml_escape(&command.display().to_string()) + ) +} + +fn toml_escape(value: &str) -> String { + value.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn derive_name(source: &Path) -> Result { + source + .file_stem() + .map(|stem| stem.to_string_lossy().into_owned()) + .ok_or_else(|| { + anyhow::anyhow!( + "cannot derive a plugin name from {}; pass --name", + source.display() + ) + }) +} + +fn check_source(source: &Path) -> Result<()> { + let meta = std::fs::symlink_metadata(source).with_context(|| { + format!( + "plugin source {} cannot be read; it must be an existing executable file", + source.display() + ) + })?; + let meta = if meta.file_type().is_symlink() { + std::fs::metadata(source) + .with_context(|| format!("plugin source {} is a broken symlink", source.display()))? + } else { + meta + }; + anyhow::ensure!( + meta.is_file(), + "plugin source {} is not a regular file", + source.display() + ); + anyhow::ensure!( + is_executable(source), + "plugin source {} is not executable by the current user; chmod +x it first", + source.display() + ); + Ok(()) +} + +#[cfg(unix)] +fn is_executable(path: &Path) -> bool { + use std::os::unix::ffi::OsStrExt; + let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else { + return false; + }; + // access(2) rather than a permission-bit test: it answers the question that + // matters — whether *this* user may execute it — for owner, group and other + // in one call. + unsafe { libc::access(c_path.as_ptr(), libc::X_OK) == 0 } +} + +#[cfg(not(unix))] +fn is_executable(_path: &Path) -> bool { + true +} + +fn restrict_permissions(path: &Path) -> Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(PLUGIN_MODE)) + .with_context(|| format!("restricting permissions on {}", path.display()))?; + } + #[cfg(not(unix))] + { + let _ = path; + } + Ok(()) +} + +#[cfg(test)] +#[path = "registry_tests.rs"] +mod tests; diff --git a/src/plugin/registry_tests.rs b/src/plugin/registry_tests.rs new file mode 100644 index 00000000..060be259 --- /dev/null +++ b/src/plugin/registry_tests.rs @@ -0,0 +1,262 @@ +use super::*; +use std::os::unix::fs::PermissionsExt; +use tempfile::TempDir; + +/// Source executable plus the plugins directory it installs into, both inside +/// one temp dir that is removed when the returned handle drops. +fn workspace() -> (TempDir, PathBuf, PathBuf) { + let root = TempDir::new().expect("a temp dir"); + let source = root.path().join("watcher"); + std::fs::write(&source, b"#!/bin/sh\nexit 0\n").unwrap(); + std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o755)).unwrap(); + let base = root.path().join("plugins"); + (root, source, base) +} + +fn mode_of(path: &Path) -> u32 { + std::fs::metadata(path).unwrap().permissions().mode() & 0o777 +} + +#[test] +fn installing_copies_the_file_sets_owner_only_mode_and_reports_created() { + let (_root, source, base) = workspace(); + + let outcome = install(&base, &source, Some("watcher"), false).expect("install to succeed"); + + let InstallOutcome::Created(path) = outcome else { + panic!("expected Created, got {outcome:?}"); + }; + assert_eq!(path, base.join("watcher")); + assert_eq!( + std::fs::read(&path).unwrap(), + std::fs::read(&source).unwrap() + ); + assert_eq!(mode_of(&path), 0o700); +} + +#[test] +fn installing_over_an_existing_name_without_force_is_refused_and_keeps_the_original_bytes() { + let (_root, source, base) = workspace(); + install(&base, &source, Some("watcher"), false).unwrap(); + let other = source.parent().unwrap().join("other"); + std::fs::write(&other, b"#!/bin/sh\nexit 1\n").unwrap(); + std::fs::set_permissions(&other, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let outcome = install(&base, &other, Some("watcher"), false).expect("no error, a report"); + + let InstallOutcome::AlreadyExists(path) = outcome else { + panic!("expected AlreadyExists, got {outcome:?}"); + }; + assert_eq!( + std::fs::read(&path).unwrap(), + std::fs::read(&source).unwrap() + ); +} + +#[test] +fn installing_with_force_replaces_the_installed_plugin() { + let (_root, source, base) = workspace(); + install(&base, &source, Some("watcher"), false).unwrap(); + let other = source.parent().unwrap().join("other"); + std::fs::write(&other, b"#!/bin/sh\nexit 1\n").unwrap(); + std::fs::set_permissions(&other, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let outcome = install(&base, &other, Some("watcher"), true).expect("install to succeed"); + + let InstallOutcome::Replaced(path) = outcome else { + panic!("expected Replaced, got {outcome:?}"); + }; + assert_eq!( + std::fs::read(&path).unwrap(), + std::fs::read(&other).unwrap() + ); + assert_eq!(mode_of(&path), 0o700); +} + +#[test] +fn a_name_that_is_not_a_safe_single_filename_is_rejected() { + let unsafe_names = [ + "../escape", + "..", + ".", + "sub/dir", + "back\\slash", + "-rf", + "has space", + "semi;colon", + "quote\"d", + "null\0byte", + "", + ]; + + for name in unsafe_names { + assert!( + validate_name(name).is_err(), + "expected {name:?} to be rejected" + ); + } +} + +#[test] +fn an_unsafe_name_is_rejected_by_install_and_remove_before_any_filesystem_write() { + let (_root, source, base) = workspace(); + + assert!(install(&base, &source, Some("../escape"), false).is_err()); + assert!(remove(&base, "../escape").is_err()); + assert!(!base.exists(), "nothing should have been created"); +} + +#[test] +fn a_source_that_does_not_exist_is_rejected() { + let (_root, source, base) = workspace(); + let missing = source.parent().unwrap().join("nope"); + + let err = install(&base, &missing, None, false).expect_err("a missing source is an error"); + + assert!( + format!("{err:#}").contains("cannot be read"), + "unexpected error: {err:#}" + ); +} + +#[test] +fn a_source_that_is_a_directory_is_rejected() { + let (_root, source, base) = workspace(); + let dir = source.parent().unwrap().join("adir"); + std::fs::create_dir(&dir).unwrap(); + + let err = install(&base, &dir, None, false).expect_err("a directory source is an error"); + + assert!( + format!("{err:#}").contains("not a regular file"), + "unexpected error: {err:#}" + ); +} + +#[test] +fn a_source_that_is_not_executable_is_rejected() { + let (_root, source, base) = workspace(); + std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o644)).unwrap(); + + let err = + install(&base, &source, None, false).expect_err("a non-executable source is an error"); + + assert!( + format!("{err:#}").contains("not executable"), + "unexpected error: {err:#}" + ); +} + +#[test] +fn listing_an_empty_directory_yields_nothing_and_does_not_error() { + let (_root, _source, base) = workspace(); + std::fs::create_dir_all(&base).unwrap(); + + assert_eq!( + list(&base).expect("an empty list, not an error"), + Vec::::new() + ); +} + +#[test] +fn listing_a_directory_that_does_not_exist_yet_yields_nothing() { + let (_root, _source, base) = workspace(); + + assert_eq!( + list(&base).expect("an empty list, not an error"), + Vec::::new() + ); +} + +#[test] +fn listing_reports_installed_names_in_sorted_order() { + let (_root, source, base) = workspace(); + install(&base, &source, Some("zulu"), false).unwrap(); + install(&base, &source, Some("alpha"), false).unwrap(); + std::fs::create_dir(base.join("subdir")).unwrap(); + + assert_eq!(list(&base).unwrap(), vec!["alpha", "zulu"]); +} + +#[test] +fn removing_an_installed_plugin_deletes_the_file_and_reports_removed() { + let (_root, source, base) = workspace(); + install(&base, &source, Some("watcher"), false).unwrap(); + + let outcome = remove(&base, "watcher").expect("remove to succeed"); + + let RemoveOutcome::Removed(path) = outcome else { + panic!("expected Removed, got {outcome:?}"); + }; + assert!(!path.exists()); +} + +#[test] +fn removing_a_name_that_is_not_installed_reports_not_installed() { + let (_root, _source, base) = workspace(); + std::fs::create_dir_all(&base).unwrap(); + + let outcome = remove(&base, "watcher").expect("no error, a report"); + + let RemoveOutcome::NotInstalled(name) = outcome else { + panic!("expected NotInstalled, got {outcome:?}"); + }; + assert_eq!(name, "watcher"); +} + +#[test] +fn the_default_name_is_derived_from_the_source_file_stem() { + let (_root, source, base) = workspace(); + let stemmed = source.parent().unwrap().join("my-plugin.sh"); + std::fs::copy(&source, &stemmed).unwrap(); + std::fs::set_permissions(&stemmed, std::fs::Permissions::from_mode(0o755)).unwrap(); + + install(&base, &stemmed, None, false).expect("install to succeed"); + + assert_eq!(list(&base).unwrap(), vec!["my-plugin"]); +} + +#[test] +fn status_reports_declaration_enablement_and_the_number_of_opted_in_panes() { + let mut cfg = crate::config::Config::default(); + cfg.plugins.push(crate::config::PluginConfig { + name: "watcher".into(), + command: "watcher".into(), + enabled: true, + ..Default::default() + }); + for _ in 0..2 { + cfg.startup_commands.push(crate::config::StartupCommand { + name: None, + command: "bash".into(), + plugin: Some("watcher".into()), + }); + } + + assert_eq!( + status(&cfg, "watcher"), + PluginStatus { + declared: true, + enabled: true, + opt_ins: 2, + } + ); + assert_eq!( + status(&cfg, "absent"), + PluginStatus { + declared: false, + enabled: false, + opt_ins: 0, + } + ); +} + +#[test] +fn the_printed_config_snippet_leaves_the_plugin_off_and_grants_no_resume_flags() { + let snippet = config_snippet("watcher", Path::new("/home/u/.nightcrow/plugins/watcher")); + + assert!(snippet.contains("name = \"watcher\"")); + assert!(snippet.contains("command = \"/home/u/.nightcrow/plugins/watcher\"")); + assert!(snippet.contains("allowed_resume_flags = []")); + assert!(snippet.contains("enabled = false")); +} diff --git a/src/runtime/snapshot_tests.rs b/src/runtime/snapshot_tests.rs index 50f357d1..e4b104d8 100644 --- a/src/runtime/snapshot_tests.rs +++ b/src/runtime/snapshot_tests.rs @@ -29,9 +29,10 @@ fn next_read(channel: &SnapshotChannel) -> SnapshotMsg { /// Drain until the reader has been quiet for several times its rate limit. /// -/// For a test whose claim is "this change caused a read": a read already owed -/// when the test acts — starting the second watch owes one — would arrive on its -/// own and be indistinguishable from the one being asserted. +/// For a test whose claim is about which reads happen: a read already owed when +/// the test acts — starting a watch owes one — would arrive on its own, and is +/// indistinguishable from the read being asserted, or from one the test says +/// must not happen at all. /// /// The window is what makes that hold, so it is wide. An owed read is taken /// within `MIN_READ_INTERVAL` of the one before it *plus* however long the @@ -107,6 +108,11 @@ fn a_repository_nothing_happens_in_is_not_read_again() { // every second spent finding nothing. let (dir, path) = make_repo(); let channel = watched(&path); + // Starting the watch owes a read for the gap it was not up for, and that + // read lands a second or more later — inside the window below, where it + // would read as the walk this test says must not happen. Wait for silence + // rather than for a duration, so the count is only of idleness. + quiesce(&channel); let reads = reads_during(&channel, SETTLE); @@ -222,7 +228,11 @@ fn changes_git_ignores_do_not_cause_a_read() { std::fs::write(Path::new(&path).join(".gitignore"), "out/\n").unwrap(); std::fs::create_dir_all(Path::new(&path).join("out")).unwrap(); let channel = watched(&path); - reads_during(&channel, Duration::from_millis(300)); + // The read the watch owes arrives well after a fixed 300 ms pause would + // have returned, and it cannot be told apart from a walk the ignored writes + // caused. Waiting for the reader to go quiet first leaves the loop below as + // the only thing that could break the silence. + quiesce(&channel); for i in 0..200 { std::fs::write( diff --git a/src/runtime/snapshot_watch.rs b/src/runtime/snapshot_watch.rs index 277407a2..f914fc0a 100644 --- a/src/runtime/snapshot_watch.rs +++ b/src/runtime/snapshot_watch.rs @@ -13,7 +13,8 @@ //! install is not fatal: the reader falls back to the fixed interval it used //! before this existed. -use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher}; +use notify::event::{AccessKind, AccessMode}; +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::path::{Path, PathBuf}; use std::sync::mpsc::Sender; @@ -37,12 +38,8 @@ pub(super) enum Wake { /// same way and log the same line. pub(super) fn install(root: &Path, wake: Sender) -> Option { let mut watcher = match notify::recommended_watcher(move |event: notify::Result| { - let paths = match event { - Ok(event) => event.paths, - Err(err) => { - tracing::debug!(%err, "filesystem watcher error; reading anyway"); - Vec::new() - } + let Some(paths) = changed_paths(event) else { + return; }; // The worker is gone once this fails, which is not this thread's // business to report. @@ -67,6 +64,40 @@ pub(super) fn install(root: &Path, wake: Sender) -> Option) -> Option> { + let event = match event { + Ok(event) => event, + Err(err) => { + tracing::debug!(%err, "filesystem watcher error; reading anyway"); + // It may have missed events, and naming none of them is how that is + // said — see [`Wake::Changed`]. + return Some(Vec::new()); + } + }; + match event.kind { + // The one access that is not somebody looking: the file was open for + // writing and is now finished, which is `IN_CLOSE_WRITE` and a change. + EventKind::Access(AccessKind::Close(AccessMode::Write)) => Some(event.paths), + EventKind::Access(_) => None, + // Everything else as before, [`EventKind::Other`] included: an inotify + // queue overflow arrives as `Other` naming no paths, and that empty list + // is what forces the re-read a dropped event calls for. + _ => Some(event.paths), + } +} + /// A directory as given, and as the filesystem reports it. /// /// Both, because macOS resolves symlinks in the paths it hands back: a repository diff --git a/src/runtime/snapshot_watch_tests.rs b/src/runtime/snapshot_watch_tests.rs index 6350a098..4caea0ff 100644 --- a/src/runtime/snapshot_watch_tests.rs +++ b/src/runtime/snapshot_watch_tests.rs @@ -1,11 +1,24 @@ -use super::{Roots, any_matters, external_git_dir}; +use super::{Roots, any_matters, changed_paths, external_git_dir}; use crate::test_util::{make_linked_worktree, make_repo, run_git}; +use notify::event::{AccessKind, AccessMode, CreateKind, Event, EventKind, Flag, ModifyKind}; use std::path::{Path, PathBuf}; fn under(root: &str, relative: &str) -> Vec { vec![Path::new(root).join(relative)] } +/// The whole gate the watcher applies: the kind decides whether the event is +/// forwarded at all, and the paths then decide whether it is worth a read. +fn wakes_the_reader(root: &str, event: notify::Result) -> bool { + let repo = crate::test_util::open_repo(root); + changed_paths(event) + .is_some_and(|paths| any_matters(Some(&repo), &Roots::of(Path::new(root)), &paths)) +} + +fn at(kind: EventKind, root: &str, relative: &str) -> notify::Result { + Ok(Event::new(kind).add_path(Path::new(root).join(relative))) +} + #[test] fn a_source_file_matters() { let (dir, path) = make_repo(); @@ -204,3 +217,67 @@ fn a_path_outside_the_tree_matters() { )); drop(dir); } + +#[test] +fn an_open_caused_by_the_read_itself_does_not_wake_the_reader() { + // The loop this closes. A status read opens `HEAD`, the branch ref and the + // work-tree root, inotify reports each of those as an open, and every one of + // them clears the filter above — so the reader kept re-reading at the rate + // limit for as long as a repository was on screen. + let (dir, path) = make_repo(); + + for opened in [".git/HEAD", ".git/refs/heads/main", "src/main.rs", ""] { + let event = at( + EventKind::Access(AccessKind::Open(AccessMode::Any)), + &path, + opened, + ); + assert!( + !wakes_the_reader(&path, event), + "opening {opened} is a read, not a change" + ); + } + drop(dir); +} + +#[test] +fn a_finished_write_wakes_the_reader() { + // `IN_CLOSE_WRITE` is the one access that is not somebody looking, so it + // cannot go with the rest: dropping it would lose real changes on Linux. + let (dir, path) = make_repo(); + let closed = EventKind::Access(AccessKind::Close(AccessMode::Write)); + + assert!(wakes_the_reader(&path, at(closed, &path, ".git/HEAD"))); + assert!(wakes_the_reader(&path, at(closed, &path, "src/main.rs"))); + drop(dir); +} + +#[test] +fn an_ordinary_write_or_creation_wakes_the_reader() { + // Nothing outside `Access` was touched; this is what says so. + let (dir, path) = make_repo(); + + for kind in [ + EventKind::Modify(ModifyKind::Any), + EventKind::Create(CreateKind::File), + ] { + assert!( + wakes_the_reader(&path, at(kind, &path, "src/main.rs")), + "{kind:?} changes what a status says" + ); + } + drop(dir); +} + +#[test] +fn a_dropped_events_signal_wakes_the_reader() { + // An inotify queue overflow arrives as `Other` with the rescan flag and no + // paths at all, and a watcher error names nothing either. Both mean events + // were missed, so both must still reach the worker. + let (dir, path) = make_repo(); + + let overflowed = Ok(Event::new(EventKind::Other).set_flag(Flag::Rescan)); + assert!(wakes_the_reader(&path, overflowed)); + assert!(wakes_the_reader(&path, Err(notify::Error::generic("boom")))); + drop(dir); +} diff --git a/src/runtime/terminal/lifecycle.rs b/src/runtime/terminal/lifecycle.rs index 90eb64ff..609f5f9c 100644 --- a/src/runtime/terminal/lifecycle.rs +++ b/src/runtime/terminal/lifecycle.rs @@ -62,6 +62,13 @@ impl TerminalState { } } BackendEvent::Reordered { order } => self.apply_order(&order), + BackendEvent::Recovery { + pane, + state, + detail, + deadline_epoch, + attempt, + } => self.apply_recovery(pane, state, detail, deadline_epoch, attempt), BackendEvent::SizeOwnership { owned } => { // Gaining it means the panes are this client's layout to // set, and they are currently at someone else's sizes — so diff --git a/src/runtime/terminal/mod.rs b/src/runtime/terminal/mod.rs index f17b70e2..2a9a2ee8 100644 --- a/src/runtime/terminal/mod.rs +++ b/src/runtime/terminal/mod.rs @@ -5,11 +5,13 @@ use std::collections::HashMap; mod escape; mod input; mod lifecycle; +mod recovery; mod scroll; mod session_panes; mod state; pub(crate) use escape::strip_escape_sequences; +pub use recovery::PaneRecovery; /// Upper bound on a pane's in-flight prompt buffer before further chars are /// dropped. Prevents unbounded growth when a program writes a stream of bytes @@ -120,6 +122,12 @@ pub struct TerminalState { /// this client owns follow its layout; panes it does not follow /// [`BackendEvent::Resized`](crate::backend::BackendEvent::Resized). pub owns_size: bool, + /// What each pane's plugin last reported about recovering it, for the panes + /// any has spoken about. Deliberately outlives a pane's process: the report + /// that matters most arrives while the pane is gone and its slot is held for + /// a relaunch. Cleared only by a `cancelled` report (see + /// [`recovery::RECOVERY_CANCELLED`]). + pub(crate) recovery: HashMap, /// Index of the first pane in the visible split-view window. pub visible_start: usize, pub max_visible_normal: usize, @@ -143,6 +151,7 @@ impl TerminalState { fullscreen: TerminalFullscreen::Off, last_content_size: HashMap::new(), owns_size: true, + recovery: HashMap::new(), visible_start: 0, max_visible_normal: MAX_VISIBLE_NORMAL, max_visible_fullscreen: MAX_VISIBLE_FULLSCREEN, diff --git a/src/runtime/terminal/recovery.rs b/src/runtime/terminal/recovery.rs new file mode 100644 index 00000000..355451a0 --- /dev/null +++ b/src/runtime/terminal/recovery.rs @@ -0,0 +1,104 @@ +//! What a pane's plugin last said about getting it running again. +//! +//! Held per pane and nowhere near the emulators: this is metadata the session +//! reports, not screen content, so it never touches a grid and a pane without a +//! report costs one absent map entry. + +use crate::backend::PaneId; + +use super::TerminalState; + +/// The `state` a session sends once a pane's recovery is over without having +/// succeeded. Mirrors the hub's own constant; the wire is the contract between +/// them, so the value is asserted rather than shared. +pub const RECOVERY_CANCELLED: &str = "cancelled"; + +/// One pane's latest recovery report. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaneRecovery { + /// The plugin's own short label, e.g. `waiting_for_reset`. + pub state: String, + /// A short human line, when the plugin gave one. + pub detail: Option, + /// When the wait ends, in unix epoch seconds. `None` when the plugin is not + /// waiting on a clock — the renderer then shows no time at all rather than + /// inventing one. + pub deadline_epoch: Option, + pub attempt: u32, +} + +impl TerminalState { + /// Record what the session reports about `pane`. + /// + /// A `cancelled` report *clears* the entry instead of storing it: there is + /// nothing left to wait for, and keeping the label would leave a badge on a + /// pane whose recovery is over. + pub(super) fn apply_recovery( + &mut self, + pane: PaneId, + state: String, + detail: Option, + deadline_epoch: Option, + attempt: u32, + ) { + if state == RECOVERY_CANCELLED { + self.recovery.remove(&pane); + return; + } + self.recovery.insert( + pane, + PaneRecovery { + state, + detail, + deadline_epoch, + attempt, + }, + ); + } + + /// What `pane`'s plugin last reported, if anything. + pub fn recovery_for(&self, pane: PaneId) -> Option<&PaneRecovery> { + self.recovery.get(&pane) + } + + /// The one report a person is looking at, and the one the cancel key acts on. + /// + /// The focused pane's own report comes first. Failing that, a report for a + /// pane this client no longer lists — a pane whose process has ended while + /// its slot is held for a relaunch. That pane cannot be focused, and it is + /// exactly the one someone would want to release, so it must still be + /// reachable. Lowest id wins so the display and the key can never disagree + /// about which one that is. + pub fn recovery_focus(&self) -> Option<(PaneId, &PaneRecovery)> { + if let Some(pane) = self.active_pane_id() + && let Some(report) = self.recovery.get(&pane) + { + return Some((pane, report)); + } + self.recovery + .iter() + .filter(|(pane, _)| !self.panes.iter().any(|p| p.id == **pane)) + .min_by_key(|(pane, _)| **pane) + .map(|(pane, report)| (*pane, report)) + } + + /// Whether the cancel key would do anything. Single source for the key gate + /// and the hint row, so a hint can never advertise a no-op. + pub fn can_cancel_recovery(&self) -> bool { + self.recovery_focus().is_some() + } + + /// Ask the session to give up on the recovery a person is looking at. + /// + /// Nothing is cleared here: the entry goes when the session broadcasts + /// `cancelled`, which is also what tells every other client. Assuming it + /// locally would hide a cancellation the session refused. + pub fn cancel_recovery(&mut self) { + let Some((pane, _)) = self.recovery_focus() else { + return; + }; + if let Some(backend) = &mut self.backend { + backend.cancel_recovery(pane); + } + } +} diff --git a/src/runtime/terminal/tests/mod.rs b/src/runtime/terminal/tests/mod.rs index 3fa9e879..c7438c6c 100644 --- a/src/runtime/terminal/tests/mod.rs +++ b/src/runtime/terminal/tests/mod.rs @@ -3,6 +3,7 @@ use super::*; mod common; mod lifecycle_tests; mod poll_tests; +mod recovery_tests; mod scroll_tests; mod session_panes_tests; mod size_owner_tests; diff --git a/src/runtime/terminal/tests/recovery_tests.rs b/src/runtime/terminal/tests/recovery_tests.rs new file mode 100644 index 00000000..65d7e423 --- /dev/null +++ b/src/runtime/terminal/tests/recovery_tests.rs @@ -0,0 +1,147 @@ +use super::common::state_with_event_queue; +use crate::backend::BackendEvent; +use crate::runtime::terminal::recovery::RECOVERY_CANCELLED; + +const STATE: &str = "waiting_for_reset"; +const DEADLINE: i64 = 1_700_000_000; + +fn report(pane: crate::backend::PaneId, state: &str, attempt: u32) -> BackendEvent { + BackendEvent::Recovery { + pane, + state: state.to_string(), + detail: Some("provider window closed".to_string()), + deadline_epoch: Some(DEADLINE), + attempt, + } +} + +#[test] +fn a_reported_state_is_kept_verbatim_for_the_pane_it_names() { + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + let pane = state.panes[0].id; + + events.borrow_mut().push(report(pane, STATE, 2)); + state.poll(); + + let held = state.recovery_for(pane).expect("the report was dropped"); + assert_eq!(held.state, STATE); + assert_eq!(held.deadline_epoch, Some(DEADLINE)); + assert_eq!(held.attempt, 2); + assert_eq!(held.detail.as_deref(), Some("provider window closed")); +} + +#[test] +fn a_later_report_replaces_the_earlier_one_rather_than_accumulating() { + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + let pane = state.panes[0].id; + + events.borrow_mut().push(report(pane, STATE, 1)); + state.poll(); + events.borrow_mut().push(report(pane, "backoff", 3)); + state.poll(); + + let held = state.recovery_for(pane).expect("the report was dropped"); + assert_eq!(held.state, "backoff"); + assert_eq!(held.attempt, 3, "the newest report is the whole picture"); +} + +#[test] +fn a_cancelled_report_clears_the_pane_instead_of_being_stored() { + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + let pane = state.panes[0].id; + + events.borrow_mut().push(report(pane, STATE, 1)); + state.poll(); + events + .borrow_mut() + .push(report(pane, RECOVERY_CANCELLED, 0)); + state.poll(); + + assert!( + state.recovery_for(pane).is_none(), + "a finished recovery must leave no badge behind" + ); + assert!(!state.can_cancel_recovery()); +} + +#[test] +fn a_report_survives_the_pane_it_names_going_away() { + // The report that matters most arrives while the pane is gone and its slot is + // held for a relaunch, so an exit must not take it with it. + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + let pane = state.panes[0].id; + + events.borrow_mut().push(report(pane, STATE, 1)); + events.borrow_mut().push(BackendEvent::Exited { pane }); + state.poll(); + + assert!(state.panes.is_empty(), "the pane must be gone"); + assert_eq!( + state.recovery_focus().map(|(id, _)| id), + Some(pane), + "a pane with no tab must still be reachable" + ); +} + +#[test] +fn the_focused_pane_outranks_a_report_for_a_pane_that_is_gone() { + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + state.create_pane_now().unwrap(); + let (first, second) = (state.panes[0].id, state.panes[1].id); + state.active = 1; + + events.borrow_mut().push(report(first, STATE, 1)); + events.borrow_mut().push(report(second, "resuming", 0)); + events + .borrow_mut() + .push(BackendEvent::Exited { pane: first }); + state.poll(); + state.active = 0; + + // `poll` removed the exited pane, so the surviving one is index 0 now. + assert_eq!(state.panes.len(), 1); + assert_eq!( + state.recovery_focus().map(|(id, _)| id), + Some(second), + "the pane a person is looking at comes first" + ); +} + +#[test] +fn cancelling_with_nothing_reported_asks_the_session_for_nothing() { + let (mut state, _events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + + state.cancel_recovery(); + state.poll(); + + assert!( + state.recovery_focus().is_none(), + "there was nothing to cancel and nothing to show" + ); +} + +#[test] +fn cancelling_asks_the_session_and_clears_only_on_its_answer() { + let (mut state, events) = state_with_event_queue(); + state.create_pane_now().unwrap(); + let pane = state.panes[0].id; + events.borrow_mut().push(report(pane, STATE, 1)); + state.poll(); + + state.cancel_recovery(); + + // Nothing is assumed locally: the badge is still there until the session + // confirms, which is what keeps a refused cancellation visible. + assert!(state.recovery_for(pane).is_some()); + state.poll(); + assert!( + state.recovery_for(pane).is_none(), + "the session's answer must clear the pane" + ); +} diff --git a/src/test_util.rs b/src/test_util.rs index 96e81533..04a54a83 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -136,6 +136,21 @@ impl crate::backend::TerminalBackend for FakeBackend { }); } + /// Echoed back like `destroy_pane` and `reorder`: a cancel is a request, and + /// the report a session broadcasts is what actually clears the pane's state. + /// A test therefore proves the request was made by polling for the answer. + fn cancel_recovery(&mut self, pane: crate::backend::PaneId) { + self.pending_events + .borrow_mut() + .push(crate::backend::BackendEvent::Recovery { + pane, + state: "cancelled".to_string(), + detail: None, + deadline_epoch: None, + attempt: 0, + }); + } + fn send_input(&mut self, _id: crate::backend::PaneId, data: &[u8]) -> anyhow::Result<()> { self.sent.push(data.to_vec()); Ok(()) diff --git a/src/ui/hint_text.rs b/src/ui/hint_text.rs index fab43e31..fee30810 100644 --- a/src/ui/hint_text.rs +++ b/src/ui/hint_text.rs @@ -33,6 +33,14 @@ pub(crate) fn prefix_armed_hint_text(app: &App) -> String { } else { "" }; + // Only while a plugin actually has a recovery pending, which is rare — an + // always-present hint for it would spend a scarce row on a key that is + // usually inert. + let cancel = if app.can_cancel_recovery() { + "c: cancel recovery | " + } else { + "" + }; // The view toggles name their destination from the current mode. let (log_toggle, tree_toggle) = match app.mode { ViewMode::Log => ("l: status view", "b: tree view"), @@ -43,7 +51,7 @@ pub(crate) fn prefix_armed_hint_text(app: &App) -> String { // reports why on the notice row, so the key always produces a visible // result. format!( - " t: new pane | {close}{swap}{resize}{log_toggle} | {tree_toggle} | f: fullscreen | o: open project | x: close project | p: theme | r: redraw | q: detach | {digits} | esc: cancel" + " t: new pane | {close}{swap}{resize}{cancel}{log_toggle} | {tree_toggle} | f: fullscreen | o: open project | x: close project | p: theme | r: redraw | q: detach | {digits} | esc: cancel" ) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2ffdd619..1ac66444 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -23,6 +23,7 @@ mod hit_test; mod notice; #[cfg(test)] mod tests; +mod wall_clock; pub(crate) use chrome::{Chrome, chrome_rows, main_content_constraints}; pub(crate) use helpers::{ diff --git a/src/ui/notice.rs b/src/ui/notice.rs index 2f985876..6059a4b1 100644 --- a/src/ui/notice.rs +++ b/src/ui/notice.rs @@ -1,5 +1,6 @@ use crate::app::{App, Notice}; use crate::ui::status_view::RepoInput; +use crate::ui::wall_clock::local_hour_minute; use ratatui::{ style::{Color, Modifier, Style}, text::{Line, Span}, @@ -103,9 +104,41 @@ pub(crate) fn render_repo_header<'a>(app: &'a App, accent: Color) -> Paragraph<' Style::default().fg(Color::Cyan), )); } + if let Some(chip) = recovery_chip(app) { + spans.push(Span::styled( + chip, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); + } Paragraph::new(Line::from(spans)) } +/// The full recovery report as one chip: which pane, the plugin's state, the +/// deadline as a local wall-clock time, the attempts spent, and the detail line. +/// +/// On this row rather than in a row or overlay of its own for the reason the +/// notices are: a row that appears and disappears resizes every open PTY. It is +/// the last chip, so an actual notice still covers the whole line — a rejected +/// action needs explaining more than a wait does. The pane it describes is the +/// one ` c` would cancel (see `TerminalState::recovery_focus`). +fn recovery_chip(app: &App) -> Option { + let (pane, report) = app.terminal.recovery_focus()?; + let mut chip = format!(" pane {pane}: {}", report.state); + if let Some(at) = report.deadline_epoch.and_then(local_hour_minute) { + chip.push_str(&format!(" until {at}")); + } + if report.attempt > 0 { + chip.push_str(&format!(" (attempt {})", report.attempt)); + } + if let Some(detail) = report.detail.as_deref() { + chip.push_str(&format!(" — {detail}")); + } + chip.push(' '); + Some(chip) +} + pub(crate) fn home_relative_path(path: &str) -> String { let trimmed = path.trim_end_matches('/'); if let Some(home) = dirs::home_dir() diff --git a/src/ui/terminal_tab/mod.rs b/src/ui/terminal_tab/mod.rs index a7ae2afc..17c14aae 100644 --- a/src/ui/terminal_tab/mod.rs +++ b/src/ui/terminal_tab/mod.rs @@ -1,5 +1,6 @@ mod cells; mod layout; +mod recovery; mod screen; mod tab_bar; #[cfg(test)] @@ -11,9 +12,8 @@ pub(crate) use tab_bar::tab_target_at; use crate::app::{App, Focus}; use crate::runtime::terminal::visible_range; use crate::ui::terminal_tab::cells::visible_pane_cells; -use crate::ui::terminal_tab::layout::{ - TAB_TITLE_MAX_CHARS, TERMINAL_BORDERS, terminal_layout, truncate_tab_title, -}; +use crate::ui::terminal_tab::layout::{TERMINAL_BORDERS, terminal_layout}; +use crate::ui::terminal_tab::recovery::pane_label; use crate::ui::terminal_tab::screen::{build_screen_lines, render_cursor}; use crate::ui::terminal_tab::tab_bar::render_tab_bar; use ratatui::{ @@ -83,12 +83,7 @@ pub fn render(frame: &mut Frame, app: &App, area: Rect, accent: Color) { } else { Style::default().fg(Color::DarkGray) }; - let pane_title = app - .terminal - .panes - .get(i) - .map(|p| truncate_tab_title(&p.title, TAB_TITLE_MAX_CHARS)) - .unwrap_or_default(); + let pane_title = pane_label(app, i); let cell_block = Block::default() .borders(Borders::ALL) .border_style(pane_border_style) diff --git a/src/ui/terminal_tab/recovery.rs b/src/ui/terminal_tab/recovery.rs new file mode 100644 index 00000000..9a7a55df --- /dev/null +++ b/src/ui/terminal_tab/recovery.rs @@ -0,0 +1,115 @@ +//! The recovery marker a pane's tab label carries. +//! +//! Deliberately a *suffix on an existing label* rather than a row or an overlay: +//! adding or removing a layout row resizes every open PTY (see the Layout and +//! Notice Row sections of `docs/architecture.md`), and a badge that comes and +//! goes would do that every time a plugin changed its mind. The full report — +//! state, deadline, attempt and detail — is on the notice row; this is only the +//! "which pane" pointer, so it has to stay short. + +use crate::app::App; +use crate::runtime::terminal::PaneRecovery; +use crate::ui::terminal_tab::layout::{TAB_TITLE_MAX_CHARS, truncate_tab_title}; +use crate::ui::wall_clock::local_hour_minute; + +/// Chars of the pane title kept when a marker rides along. +/// +/// Well under [`TAB_TITLE_MAX_CHARS`](super::layout::TAB_TITLE_MAX_CHARS): the +/// title is truncated to make room *before* the marker is appended, so a narrow +/// pane loses title characters rather than the marker. Losing the marker is the +/// one degradation that defeats the point of having it. +pub(crate) const RECOVERY_TITLE_MAX_CHARS: usize = 8; + +/// A wait with a known end. +const WAITING_GLYPH: char = '⏳'; +/// Attempts already spent, which is what a person judges "is this going +/// anywhere" by. +const ATTENTION_GLYPH: char = '⚠'; + +/// The label for the pane at `index`: its title, plus a recovery marker when its +/// plugin has reported one. Single source for the tab bar and the pane's own +/// border title, so the two cannot disagree about which pane is waiting. +pub(crate) fn pane_label(app: &App, index: usize) -> String { + let Some(pane) = app.terminal.panes.get(index) else { + return String::new(); + }; + match app.terminal.recovery_for(pane.id).map(recovery_marker) { + Some(marker) => format!( + "{} {marker}", + truncate_tab_title(&pane.title, RECOVERY_TITLE_MAX_CHARS) + ), + None => truncate_tab_title(&pane.title, TAB_TITLE_MAX_CHARS), + } +} + +/// The marker for one pane's report: the deadline as a local wall-clock time +/// when there is one, and the attempt count when any have been spent. +/// +/// A report with neither is still marked, with the bare hourglass — a pane its +/// plugin is doing something about must be distinguishable from one it is not, +/// even when there is no number to show. +pub(crate) fn recovery_marker(report: &PaneRecovery) -> String { + let mut marker = String::new(); + if let Some(at) = report.deadline_epoch.and_then(local_hour_minute) { + marker.push(WAITING_GLYPH); + marker.push_str(&at); + } + if report.attempt > 0 { + marker.push(ATTENTION_GLYPH); + marker.push_str(&report.attempt.to_string()); + } + if marker.is_empty() { + marker.push(WAITING_GLYPH); + } + marker +} + +#[cfg(test)] +mod tests { + use super::{ATTENTION_GLYPH, WAITING_GLYPH, recovery_marker}; + use crate::runtime::terminal::PaneRecovery; + + fn report(deadline_epoch: Option, attempt: u32) -> PaneRecovery { + PaneRecovery { + state: "waiting_for_reset".to_string(), + detail: None, + deadline_epoch, + attempt, + } + } + + #[test] + fn a_report_with_a_deadline_marks_it_as_a_wall_clock_time() { + let marker = recovery_marker(&report(Some(1_700_000_000), 0)); + assert!(marker.starts_with(WAITING_GLYPH), "{marker}"); + assert_eq!(marker.chars().count(), 6, "{marker}"); + } + + #[test] + fn a_report_with_no_deadline_shows_no_time_at_all() { + let marker = recovery_marker(&report(None, 3)); + assert_eq!(marker, format!("{ATTENTION_GLYPH}3")); + assert!(!marker.contains(':'), "no deadline must mean no clock time"); + } + + #[test] + fn a_report_with_both_carries_the_deadline_and_the_attempt_count() { + let marker = recovery_marker(&report(Some(1_700_000_000), 2)); + assert!(marker.contains(':'), "{marker}"); + assert!(marker.ends_with(&format!("{ATTENTION_GLYPH}2")), "{marker}"); + } + + #[test] + fn a_report_with_neither_is_still_marked() { + assert_eq!(recovery_marker(&report(None, 0)), WAITING_GLYPH.to_string()); + } + + #[test] + fn a_deadline_no_clock_can_place_falls_back_to_the_bare_marker() { + assert_eq!( + recovery_marker(&report(Some(i64::MIN), 0)), + WAITING_GLYPH.to_string(), + "an unplaceable deadline must not print a wrong time" + ); + } +} diff --git a/src/ui/terminal_tab/tab_bar.rs b/src/ui/terminal_tab/tab_bar.rs index 922f1a97..3a87aec2 100644 --- a/src/ui/terminal_tab/tab_bar.rs +++ b/src/ui/terminal_tab/tab_bar.rs @@ -1,8 +1,7 @@ use crate::app::App; use crate::runtime::terminal::visible_range; -use crate::ui::terminal_tab::layout::{ - JUMP_KEY_PANE_COUNT, TAB_TITLE_MAX_CHARS, terminal_layout, truncate_tab_title, -}; +use crate::ui::terminal_tab::layout::{JUMP_KEY_PANE_COUNT, terminal_layout}; +use crate::ui::terminal_tab::recovery::pane_label; use ratatui::{ Frame, layout::{Position, Rect}, @@ -64,14 +63,16 @@ pub(crate) fn tab_segments( )); } segments.extend(app.terminal.panes[visible.clone()].iter().enumerate().map( - |(offset, pane)| { + |(offset, _pane)| { let i = visible.start + offset; // Panes 0..=7 carry a jump key: ` 1..8` in fullscreen, // ` 3..9,0` in the split view (the digit row is // layout-aware). Panes past the 8th have no jump key, so they // carry no hint to avoid implying an unbound shortcut. The bare // F-keys are NOT advertised here: they select project tabs. - let title = truncate_tab_title(&pane.title, TAB_TITLE_MAX_CHARS); + // Carries the recovery marker when the pane has one, so a pane its + // plugin is nursing back is visible without leaving the tab row. + let title = pane_label(app, i); let label = if i < JUMP_KEY_PANE_COUNT { // Split view runs 3,4..9 then wraps to 0 for the eighth pane. let digit = if fullscreen { diff --git a/src/ui/terminal_tab/tests/tab_tests.rs b/src/ui/terminal_tab/tests/tab_tests.rs index 9b311546..226d182e 100644 --- a/src/ui/terminal_tab/tests/tab_tests.rs +++ b/src/ui/terminal_tab/tests/tab_tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::runtime::terminal::PaneRecovery; use crate::ui::terminal_tab::layout::terminal_layout; use crate::ui::terminal_tab::render; use crate::ui::terminal_tab::tab_bar::tab_target_at; @@ -185,3 +186,61 @@ fn tab_bar_labels_panes_with_digits_in_fullscreen() { "fullscreen must not show the split-view F-key legend, got: {text}" ); } + +#[test] +fn a_tab_label_carries_a_recovery_marker_only_while_one_is_reported() { + let mut app = crate::app::tests::app_with_fake_backend(); + app.terminal + .create_pane_with_now(None, Some("agent")) + .unwrap(); + let pane = app.terminal.panes[0].id; + let visible = 0..1; + + let plain = tab_segments(&app, visible.clone())[0].0.clone(); + assert!(plain.contains("agent"), "{plain}"); + assert!( + !plain.contains('⏳'), + "an unwatched pane must carry no marker" + ); + + app.terminal.recovery.insert( + pane, + PaneRecovery { + state: "waiting_for_reset".to_string(), + detail: Some("provider window closed".to_string()), + deadline_epoch: Some(1_700_000_000), + attempt: 3, + }, + ); + + let marked = tab_segments(&app, visible)[0].0.clone(); + assert!(marked.contains("agent"), "the title must survive: {marked}"); + assert!(marked.contains('⏳'), "{marked}"); + assert!(marked.contains("⚠3"), "{marked}"); + // The deadline is a wall-clock time, so the label carries a `HH:MM` colon + // that the plain label did not. + assert!(marked.contains(':'), "{marked}"); +} + +#[test] +fn a_long_title_loses_characters_before_a_recovery_marker_does() { + let mut app = crate::app::tests::app_with_fake_backend(); + let long = "a-very-long-program-title-indeed"; + app.terminal.create_pane_with_now(None, Some(long)).unwrap(); + let pane = app.terminal.panes[0].id; + app.terminal.recovery.insert( + pane, + PaneRecovery { + state: "backoff".to_string(), + detail: None, + deadline_epoch: None, + attempt: 7, + }, + ); + + let label = tab_segments(&app, 0..1)[0].0.clone(); + + assert!(label.contains("⚠7"), "the marker must survive: {label}"); + assert!(label.contains('…'), "the title must be cut: {label}"); + assert!(!label.contains(long), "{label}"); +} diff --git a/src/ui/wall_clock.rs b/src/ui/wall_clock.rs new file mode 100644 index 00000000..2b74d680 --- /dev/null +++ b/src/ui/wall_clock.rs @@ -0,0 +1,94 @@ +//! Turning a unix epoch second into the `HH:MM` a person reads off their own +//! clock. +//! +//! Hand-rolled because nightcrow has no date crate and a deadline is the only +//! absolute time it ever renders: adding `chrono`/`time` for two integers would +//! buy a dependency and its transitive tree for one format string. + +#[cfg(any(not(unix), test))] +const SECS_PER_MINUTE: i64 = 60; +#[cfg(any(not(unix), test))] +const SECS_PER_HOUR: i64 = 3_600; +#[cfg(any(not(unix), test))] +const SECS_PER_DAY: i64 = 86_400; + +/// `HH:MM` in the machine's local zone, or `None` when the timestamp is one the +/// platform cannot place. +/// +/// `None` rather than a fallback on purpose: a wrong wall-clock time reads as +/// fact, and the caller is expected to show nothing instead. +pub(crate) fn local_hour_minute(epoch: i64) -> Option { + let (hour, minute) = local_hm(epoch)?; + Some(format!("{hour:02}:{minute:02}")) +} + +// The conversion is identity where `time_t` is 64-bit and a real narrowing check +// where it is 32-bit, so it has to stay even though it looks redundant here. +#[allow(clippy::useless_conversion)] +#[cfg(unix)] +fn local_hm(epoch: i64) -> Option<(u32, u32)> { + let seconds: libc::time_t = epoch.try_into().ok()?; + let mut parts: libc::tm = unsafe { std::mem::zeroed() }; + // SAFETY: `seconds` is a live `time_t` and `parts` a live `tm` for the whole + // call; `localtime_r` reads the first and writes only into the second, and is + // the reentrant form precisely so it needs no shared state. + let filled = unsafe { libc::localtime_r(&seconds, &mut parts) }; + if filled.is_null() { + return None; + } + Some(( + u32::try_from(parts.tm_hour).ok()?, + u32::try_from(parts.tm_min).ok()?, + )) +} + +/// UTC on platforms with no `localtime_r`. The zone database is the OS's to +/// expose, and guessing an offset would be worse than being explicit about the +/// one this falls back to. +#[cfg(not(unix))] +fn local_hm(epoch: i64) -> Option<(u32, u32)> { + utc_hm(epoch) +} + +/// `HH:MM` in UTC, which is what the epoch already counts. +/// +/// `epoch.rem_euclid` rather than `%` so a pre-1970 timestamp lands on the right +/// side of midnight instead of producing a negative hour. +#[cfg(any(not(unix), test))] +fn utc_hm(epoch: i64) -> Option<(u32, u32)> { + let into_day = epoch.rem_euclid(SECS_PER_DAY); + Some(( + u32::try_from(into_day / SECS_PER_HOUR).ok()?, + u32::try_from((into_day % SECS_PER_HOUR) / SECS_PER_MINUTE).ok()?, + )) +} + +#[cfg(test)] +mod tests { + use super::{local_hour_minute, utc_hm}; + + #[test] + fn the_epoch_itself_is_midnight_utc() { + assert_eq!(utc_hm(0), Some((0, 0))); + } + + #[test] + fn a_timestamp_before_the_epoch_stays_inside_the_day() { + // One minute before 1970 is 23:59, not a negative hour. + assert_eq!(utc_hm(-60), Some((23, 59))); + } + + #[test] + fn a_deadline_renders_as_two_padded_fields() { + let rendered = local_hour_minute(1_700_000_000).expect("a plausible deadline must render"); + assert_eq!(rendered.len(), 5, "{rendered}"); + let (hour, minute) = rendered.split_once(':').expect("HH:MM"); + assert!(hour.parse::().unwrap() < 24, "{rendered}"); + assert!(minute.parse::().unwrap() < 60, "{rendered}"); + } + + #[test] + fn a_timestamp_no_platform_clock_can_place_renders_nothing() { + assert_eq!(local_hour_minute(i64::MIN), None); + } +} diff --git a/src/web/viewer/catalog/mod.rs b/src/web/viewer/catalog/mod.rs index 9e0715c5..1e7ce7c4 100644 --- a/src/web/viewer/catalog/mod.rs +++ b/src/web/viewer/catalog/mod.rs @@ -44,6 +44,10 @@ pub struct Catalog { /// first client connect (empty = one bare shell). Applied to every hub the /// catalog spawns. startup_commands: Vec, + /// The `[[plugin]]` table, handed to every hub the catalog spawns. A hub only + /// launches the ones its own startup commands opted into, so an entry here is + /// an offer rather than a process. + plugins: Vec, } impl Catalog { @@ -60,6 +64,24 @@ impl Catalog { } } + /// Like [`Catalog::with_startup`], and every hub is also given the + /// `[[plugin]]` table its startup commands may name. + /// + /// Paired with the startup commands rather than set separately, because the + /// two are one decision: a plugin is only ever reachable through a startup + /// command's `plugin =`, so a catalog with one and not the other is a + /// half-configured session. + pub fn with_startup_and_plugins( + startup_commands: Vec, + plugins: Vec, + ) -> Self { + Self { + startup_commands, + plugins, + ..Self::default() + } + } + /// Replace the base served set — CLI `--repo` args or the TUI workspace's /// open tabs. Browser-opened repositories ([`Catalog::add_path`]) survive /// this, so a workspace change does not close a tab a viewer opened. @@ -160,7 +182,11 @@ impl Catalog { name: repo_name(&path), display_path: display_path(&path), runtime: RepoRuntime::spawn(&path), - terminals: TerminalHub::spawn(&path, self.startup_commands.clone()), + terminals: TerminalHub::spawn( + &path, + self.startup_commands.clone(), + self.plugins.clone(), + ), id, path, })), diff --git a/src/web/viewer/server/mod.rs b/src/web/viewer/server/mod.rs index 8b940bec..2cfab08c 100644 --- a/src/web/viewer/server/mod.rs +++ b/src/web/viewer/server/mod.rs @@ -118,6 +118,17 @@ impl ViewerState { /// or not a browser listener does: the daemon socket serves this same /// state, and a test drives it without taking a port. pub fn new(options: ViewerOptions) -> Self { + Self::with_plugins(options, Vec::new()) + } + + /// Like [`ViewerState::new`], with the `[[plugin]]` table the session's + /// startup panes may hand themselves to. + /// + /// Taken here rather than as a [`ViewerOptions`] field because it has to + /// reach the catalog before [`crate::web::viewer::catalog::Catalog::set_paths`] + /// spawns the first hub — a plugin association is decided when a pane is + /// created, not afterwards. + pub fn with_plugins(options: ViewerOptions, plugins: Vec) -> Self { let ViewerOptions { bind, port: _, @@ -129,7 +140,10 @@ impl ViewerState { prefs, } = options; let state = Self { - catalog: crate::web::viewer::catalog::Catalog::with_startup(startup_commands), + catalog: crate::web::viewer::catalog::Catalog::with_startup_and_plugins( + startup_commands, + plugins, + ), bound_loopback: bind.is_loopback(), auth, sessions: SessionStore::new(), @@ -149,6 +163,10 @@ impl ViewerState { impl ViewerServer { /// Bind and start from `[web_viewer]`, building the password verifier from /// either `hashed_password` or `password`. + /// + /// `plugins` is `config.toml`'s `[[plugin]]` table; an empty list means no + /// pane can be plugin-managed, which is the default. + #[allow(clippy::too_many_arguments)] pub fn start_from_config( viewer: &crate::config::WebViewerConfig, agent_indicator: &crate::config::AgentIndicatorConfig, @@ -156,6 +174,7 @@ impl ViewerServer { paths: &[String], persist: bool, startup_commands: Vec, + plugins: Vec, ) -> Result { let auth = if let Some(hash) = viewer.hashed_password.as_deref() { Auth::from_hashed(hash)? @@ -170,23 +189,34 @@ impl ViewerServer { viewer.bind ) })?; - Self::start(ViewerOptions { - bind, - port: viewer.port, - auth, - repos: paths.to_vec(), - persist, - startup_commands, - hot: agent_indicator.clone(), - // The session's accent outlives any one config edit, so `[theme]` - // only names the colour a session with no stored choice starts in. - prefs: PrefsStore::load_seeded(theme.preset_index()), - }) + Self::start_with_plugins( + ViewerOptions { + bind, + port: viewer.port, + auth, + repos: paths.to_vec(), + persist, + startup_commands, + hot: agent_indicator.clone(), + // The session's accent outlives any one config edit, so `[theme]` + // only names the colour a session with no stored choice starts in. + prefs: PrefsStore::load_seeded(theme.preset_index()), + }, + plugins, + ) } /// Bind and start accepting. The seeded repositories may be replaced later /// through [`ViewerServer::set_repos`]. pub fn start(options: ViewerOptions) -> Result { + Self::start_with_plugins(options, Vec::new()) + } + + /// Like [`ViewerServer::start`], with the `[[plugin]]` table. + pub fn start_with_plugins( + options: ViewerOptions, + plugins: Vec, + ) -> Result { // Copied out before the options are consumed: the listener is bound // first so a port conflict fails before any repository is opened. let (bind, port) = (options.bind, options.port); @@ -196,7 +226,7 @@ impl ViewerServer { .local_addr() .unwrap_or_else(|_| SocketAddr::new(bind, port)); - let state = Arc::new(ViewerState::new(options)); + let state = Arc::new(ViewerState::with_plugins(options, plugins)); let accept_state = Arc::clone(&state); std::thread::Builder::new() diff --git a/src/web/viewer/terminal/frame.rs b/src/web/viewer/terminal/frame.rs index 01869146..6f10e9e3 100644 --- a/src/web/viewer/terminal/frame.rs +++ b/src/web/viewer/terminal/frame.rs @@ -40,6 +40,16 @@ pub enum ClientMessage { /// glancing at a phone repaint everybody's screen. #[serde(rename = "claim_size")] ClaimSize, + /// Give up on whatever recovery is pending for `pane`. + /// + /// A person deciding the wait is over outranks the plugin still waiting: the + /// hold on the pane's slot is dropped and the slot retired, so nothing can + /// be relaunched into it afterwards. Harmless for a pane with no recovery in + /// flight — a client can be a beat behind the hold expiring. + #[serde(rename = "cancel_recovery")] + CancelRecovery { + pane: PaneId, + }, /// The sizes to give the startup terminals, answering [`ServerMessage::Pending`]. /// /// One entry per pending pane, in the order they will be created. A short @@ -159,6 +169,29 @@ pub enum ServerMessage { Pending { count: usize, }, + /// What a plugin reports about a pane it is nursing back, relayed verbatim. + /// + /// Pane metadata rather than screen content: nothing here is drawn into a + /// terminal grid, and a client that ignores it renders exactly as before. + /// `state` is the plugin's own short label; the hub neither interprets it nor + /// keeps it, so this is a broadcast of the latest word and not a state + /// machine. The one label the hub itself sends is + /// [`RECOVERY_CANCELLED`](super::hub_recovery::RECOVERY_CANCELLED), which a + /// client treats as "there is nothing pending any more". + Recovery { + pane: PaneId, + state: String, + /// A short human line, absent when the plugin gave none. Never carries + /// transcript or payload text. + #[serde(default, skip_serializing_if = "Option::is_none")] + detail: Option, + /// When the wait ends, in **unix epoch seconds**, absent when the plugin + /// is not waiting on a clock. A client renders it in its own local zone; + /// an absent one must render nothing rather than a guess. + #[serde(default, skip_serializing_if = "Option::is_none")] + deadline_epoch: Option, + attempt: u32, + }, } /// One frame queued for a connected client. diff --git a/src/web/viewer/terminal/hub_events.rs b/src/web/viewer/terminal/hub_events.rs new file mode 100644 index 00000000..a609e3ad --- /dev/null +++ b/src/web/viewer/terminal/hub_events.rs @@ -0,0 +1,201 @@ +//! Turning pane activity into plugin events, and plugin commands into judged +//! actions. +//! +//! Every function here is a no-op for a pane no plugin owns, so the ordinary +//! terminal path pays one hash lookup and nothing else. + +use super::hub_plugins::{MAX_COMMANDS_PER_TICK, PANE_IDLE_THRESHOLD, Plugins}; +use crate::backend::{PaneGeneration, PaneId, PaneToken, PtyBackend}; +use crate::plugin::protocol::{PROTOCOL_VERSION, PluginCommand, PluginEvent}; +use crate::plugin::{Approved, PaneFacts, Refused}; +use crate::runtime::terminal::strip_escape_sequences; +use std::time::Instant; + +impl Plugins { + /// Send one pane-scoped event to whichever plugin owns `pane`, stamped with + /// the pane's current identity. + /// + /// A dropped event is logged and nothing else: a plugin that cannot keep up + /// must never stall the pane it is watching, which is why + /// [`crate::plugin::PluginHost::send`] does not block. + fn send_for( + &self, + backend: &PtyBackend, + pane: PaneId, + build: impl FnOnce(PaneToken, PaneGeneration) -> PluginEvent, + ) { + let Some(name) = self.owners.get(&pane) else { + return; + }; + let Some(host) = self.hosts.get(name) else { + return; + }; + let Some(slot) = backend.slot(pane) else { + return; + }; + let event = build(slot.identity.token.clone(), slot.identity.generation); + if !host.send(&event) { + tracing::debug!(plugin = %name, pane, "viewer: plugin event dropped"); + } + } + + /// Announce a pane a plugin has just been given, including one that a + /// relaunch has just put back — the generation is what tells them apart. + pub(super) fn pane_opened(&self, backend: &PtyBackend, pane: PaneId, title: Option<&str>) { + let command = backend + .slot(pane) + .and_then(|slot| slot.launch.command.clone()); + let (title, cwd) = (title.map(str::to_string), self.cwd.clone()); + self.send_for(backend, pane, |token, generation| PluginEvent::PaneOpened { + v: PROTOCOL_VERSION, + token, + generation, + title, + command, + cwd, + }); + } + + /// Feed a pane's output to its plugin as plain text. + /// + /// Stripped per chunk, which makes it best-effort: an escape sequence split + /// across two reads survives in fragments. Acceptable because output text is + /// only ever a fallback signal — nothing happens to a pane unless the plugin + /// asks, and every ask goes through the guard. + pub(super) fn pane_output(&mut self, backend: &PtyBackend, pane: PaneId, data: &[u8]) { + if !self.owners.contains_key(&pane) { + return; + } + // Fresh bytes end the quiet period, so the next one is announced again. + self.idle_announced.remove(&pane); + let text = strip_escape_sequences(data); + if text.is_empty() { + return; + } + self.send_for(backend, pane, |token, generation| PluginEvent::PaneOutput { + v: PROTOCOL_VERSION, + token, + generation, + text, + }); + } + + /// The pane's process ended, but its slot is being held: a relaunch is + /// possible until the hold expires. + pub(super) fn pane_exited(&self, backend: &PtyBackend, pane: PaneId) { + self.send_for(backend, pane, |token, generation| PluginEvent::PaneExited { + v: PROTOCOL_VERSION, + token, + generation, + }); + } + + /// The slot itself is going away, so nothing more can be done with it. + /// Must be sent before the slot is retired — the event carries its identity. + pub(super) fn pane_closed(&self, backend: &PtyBackend, pane: PaneId) { + self.send_for(backend, pane, |token, generation| PluginEvent::PaneClosed { + v: PROTOCOL_VERSION, + token, + generation, + }); + } + + /// A human typed into the pane. Both halves matter: the plugin is told it + /// has been taken over, and whatever it had already spent on this pane is + /// dropped so its picture of the pane cannot outlive the person's. + pub(super) fn user_input(&mut self, backend: &PtyBackend, pane: PaneId) { + if !self.owners.contains_key(&pane) { + return; + } + self.send_for(backend, pane, |token, generation| PluginEvent::UserInput { + v: PROTOCOL_VERSION, + token, + generation, + }); + if let Some(slot) = backend.slot(pane) { + self.guard.cancel(&slot.identity.token.clone()); + } + } + + /// Tell each plugin about any of its panes that has just gone quiet, once + /// per quiet period. + pub(super) fn notify_idle(&mut self, backend: &PtyBackend, now: Instant) { + if self.owners.is_empty() { + return; + } + let due: Vec<(PaneId, u64)> = self + .owners + .keys() + .copied() + .filter(|pane| !self.idle_announced.contains(pane)) + // An exited pane is quiet by definition; its plugin already had + // `PaneExited`, which is the stronger signal. + .filter(|pane| backend.is_process_alive(*pane)) + .filter_map(|pane| { + let idle = backend.slot(pane)?.idle_for(now); + (idle >= PANE_IDLE_THRESHOLD).then_some((pane, idle.as_millis() as u64)) + }) + .collect(); + for (pane, idle_ms) in due { + self.send_for(backend, pane, |token, generation| PluginEvent::PaneIdle { + v: PROTOCOL_VERSION, + token, + generation, + idle_ms, + }); + self.idle_announced.insert(pane); + } + } + + /// Take what every plugin has asked for since the last tick, at most + /// [`MAX_COMMANDS_PER_TICK`] each so a chatty plugin cannot starve the panes. + pub(super) fn take_commands(&self) -> Vec<(String, PluginCommand)> { + let mut taken = Vec::new(); + for (name, host) in &self.hosts { + for _ in 0..MAX_COMMANDS_PER_TICK { + match host.try_recv() { + Some(command) => taken.push((name.clone(), command)), + None => break, + } + } + } + taken + } + + /// Put one command through the guard. + /// + /// The pane the command names is resolved from its token — a plugin never + /// sees a [`PaneId`] — and every fact the rules need is read from the + /// backend here, so the guard itself holds no reference to it. + pub(super) fn judge( + &mut self, + plugin: &str, + command: PluginCommand, + backend: &PtyBackend, + now: Instant, + ) -> Result { + let facts = command + .token() + .and_then(|token| backend.pane_for_token(token)) + .and_then(|pane| { + let slot = backend.slot(pane)?; + Some(PaneFacts { + pane, + generation: slot.identity.generation, + // This plugin's own claim on the pane, not any plugin's: + // one plugin must not be able to act through another's + // opt-in. + opted_in: self.owners.get(&pane).is_some_and(|name| name == plugin), + alive: backend.is_process_alive(pane), + idle: slot.idle_for(now), + launch_command: slot.launch.command.clone(), + }) + }); + let allowed = self + .allowed_flags + .get(plugin) + .map(Vec::as_slice) + .unwrap_or_default(); + self.guard.judge(command, facts.as_ref(), allowed, now) + } +} diff --git a/src/web/viewer/terminal/hub_helpers.rs b/src/web/viewer/terminal/hub_helpers.rs index 574dd0f4..30ec8256 100644 --- a/src/web/viewer/terminal/hub_helpers.rs +++ b/src/web/viewer/terminal/hub_helpers.rs @@ -58,6 +58,12 @@ pub enum Command { Reorder { order: Vec, }, + /// Abandon a pane's pending relaunch. On the worker queue because carrying it + /// out needs the backend and the plugin bookkeeping, both of which are + /// worker-local. + CancelRecovery { + pane: PaneId, + }, } /// One startup terminal: the command to run, at the size a client measured, under @@ -66,6 +72,11 @@ pub struct StartupPane { pub(super) size: crate::web::viewer::terminal::frame::PaneSize, pub(super) command: Option, pub(super) title: Option, + /// The `[[plugin]]` this pane's configuration handed it to, if any. Carried + /// only as far as the worker, which records it in a map of its own — it is + /// deliberately absent from [`PaneState`] and from every `ServerMessage`, so + /// no client learns which panes a plugin can act on. + pub(super) plugin: Option, } /// A live terminal and the recent raw bytes it has produced, kept so a client diff --git a/src/web/viewer/terminal/hub_layout.rs b/src/web/viewer/terminal/hub_layout.rs new file mode 100644 index 00000000..2b62eb6c --- /dev/null +++ b/src/web/viewer/terminal/hub_layout.rs @@ -0,0 +1,90 @@ +//! Pane geometry and ordering: the two worker operations that change how the +//! panes are arranged rather than what is in them. +//! +//! Split out of `hub_run.rs` so the worker loop stays readable; the behaviour is +//! unchanged. + +use super::TerminalHub; +use super::frame::{ServerMessage, TerminalFrame}; +use super::hub_helpers::{broadcast_locked, canonical_order}; +use crate::backend::{PaneId, PtyBackend, TerminalBackend}; + +impl TerminalHub { + /// Resize a live pane's PTY at the sizing owner's request, record the size it + /// is now set to, and tell every client what it is. + /// + /// All under one lock, and the liveness check with them. `connect` reports + /// each pane's size from this record and the client caches it as "already + /// applied"; a client that slipped between the two would be told the old + /// size for a PTY that has the new one, and would then skip the resize that + /// would have corrected it. The `resize` itself is an ioctl on the master — + /// far cheaper than the broadcast this lock already covers. + pub(super) fn resize_pane( + &self, + backend: &mut PtyBackend, + pane: PaneId, + rows: u16, + cols: u16, + client: u64, + ) { + let mut state = self.state.lock().expect("terminal state poisoned"); + // Not this client's to set. Dropped rather than refused: a client can + // lose the sizing between laying out a frame and this arriving, which is + // ordinary rather than an error worth interrupting anyone over. + if state.size_owner != Some(client) { + return; + } + // An unknown pane is ignored rather than errored: a client racing a + // pane exit is normal, not an attack. + let Some(p) = state.panes.iter_mut().find(|p| p.id == pane) else { + return; + }; + if (p.rows, p.cols) == (rows, cols) { + return; + } + backend.resize(pane, rows, cols); + p.rows = rows; + p.cols = cols; + // Every client's emulator has to wrap where the child now does, so the + // size it was actually set to goes to all of them — including the one + // that asked, which learns here if its request was clamped. + if let Ok(json) = serde_json::to_string(&ServerMessage::Resized { pane, rows, cols }) { + broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); + } + } + + /// Reorder the live panes to match `order` and tell every client the + /// result. + /// + /// `order` is a full desired sequence of pane ids. It is reconciled + /// against what is actually live so a reorder is robust to races with + /// create/close: unknown ids are dropped and any live pane the request + /// omits (e.g. one another client created in the same beat) is kept, + /// appended in its current order (see [`canonical_order`]). The hub + /// converges on that one canonical order and broadcasts it, so the + /// sender and every other device end up with the same layout. Reordering + /// only restyles the grid — pane ids, scrollback, and the live PTYs are + /// untouched. A no-op reorder sends nothing. + pub(super) fn reorder_panes(&self, order: Vec) { + let mut state = self.state.lock().expect("terminal state poisoned"); + let before: Vec = state.panes.iter().map(|p| p.id).collect(); + let target = canonical_order(&before, &order); + if target == before { + return; + } + // `target` is a permutation of `before`, so every id resolves and `old` + // ends empty. Move each `PaneState` rather than clone it — it owns the + // pane's scrollback. + let mut old = std::mem::take(&mut state.panes); + let mut reordered = Vec::with_capacity(old.len()); + for id in &target { + if let Some(pos) = old.iter().position(|p| p.id == *id) { + reordered.push(old.remove(pos)); + } + } + state.panes = reordered; + if let Ok(json) = serde_json::to_string(&ServerMessage::Reordered { order: target }) { + broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); + } + } +} diff --git a/src/web/viewer/terminal/hub_plugins.rs b/src/web/viewer/terminal/hub_plugins.rs new file mode 100644 index 00000000..bdafdda3 --- /dev/null +++ b/src/web/viewer/terminal/hub_plugins.rs @@ -0,0 +1,262 @@ +//! The plugin side of a terminal worker: which panes a plugin may see, the +//! hosts watching them, and the slots being held open for a relaunch. +//! +//! Every field here is worker-local. A plugin can drive a pane's keyboard, so +//! none of this is reachable from a connection thread — the only way in is the +//! command queue the worker already drains, and the only way out is +//! [`crate::plugin::Guard`]. +//! +//! A pane appears here **only** if its `[[startup_command]]` named a plugin by +//! hand. A pane a client opened has no association and never gains one, which is +//! what makes "an arbitrary shell is never plugin-controlled" a property of the +//! code rather than a promise. + +use super::hub_helpers::PaneState; +use crate::backend::{PaneId, PtyBackend}; +use crate::config::{PluginConfig, StartupCommand}; +use crate::plugin::{Guard, PluginHost, RateLimits}; +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; + +/// How long a pane must be quiet before its plugin is told it is idle. +/// +/// Deliberately the same value the guard requires before it will let a plugin +/// type into that pane: announcing idleness any earlier would only invite +/// commands the guard is bound to refuse. Ten seconds is well past the pause a +/// CLI takes mid-answer and well short of a wait a person would notice. +pub(super) const PANE_IDLE_THRESHOLD: Duration = Duration::from_secs(10); + +/// How long an exited pane's slot is kept so a relaunch can still reuse its +/// token. +/// +/// This is a backstop against a plugin that died or lost interest, so it has to +/// outlast every wait a plugin may legitimately be in the middle of. Providers +/// quote windows in hours *and* in days — a weekly quota is a real case — so a +/// value picked around the five-hour window would silently throw the pane's +/// identity away days before the wait paid off, and the relaunch it was being +/// kept for would fail. Nine days clears the longest window a bundled plugin +/// will wait out (`nightcrow-recovery`'s own clamp is eight days) with slack for +/// a reset that lands late. +/// +/// Holding it that long is cheap on purpose: a token, a generation and a command +/// string. The process, its fds and its threads were let go the moment it exited +/// (see [`PtyBackend::release_process`]), and closing the pane or stopping the +/// session retires the slot immediately either way. +pub(super) const PENDING_RELAUNCH_TTL: Duration = Duration::from_secs(9 * 24 * 60 * 60); + +/// Commands taken from any one plugin per loop iteration. +/// +/// One thread serves every pane in the repository, so a plugin that writes +/// without pause must not be able to hold it. Eight per 8 ms tick is a thousand +/// a second — far past anything a legitimate plugin needs, and bounded. +pub(super) const MAX_COMMANDS_PER_TICK: usize = 8; + +/// Where a pane sat and what it looked like, captured before it is removed. +pub(super) struct PaneSpot { + /// Its position in the client-visible order, so a relaunch lands back where + /// the operator left it instead of at the end of the row. + pub(super) index: usize, + pub(super) rows: u16, + pub(super) cols: u16, + pub(super) title: Option, +} + +impl PaneSpot { + pub(super) fn of(index: usize, pane: &PaneState) -> Self { + Self { + index, + rows: pane.rows, + cols: pane.cols, + title: pane.title.clone(), + } + } +} + +/// A pane whose process exited while a plugin was watching it, held so that +/// plugin still has something to relaunch. +pub(super) struct Pending { + pub(super) spot: PaneSpot, + /// When the slot is given up on. See [`PENDING_RELAUNCH_TTL`]. + deadline: Instant, +} + +pub(super) struct Plugins { + /// Live plugin children, by configured name. A plugin that failed to launch + /// is absent, and its panes are therefore never adopted below. + pub(super) hosts: HashMap, + /// Which plugin owns which pane. The authority for `opted_in`. + pub(super) owners: HashMap, + /// Each plugin's `allowed_resume_flags`, as the guard needs them. + pub(super) allowed_flags: HashMap>, + pub(super) guard: Guard, + pub(super) pending: HashMap, + /// Panes whose plugin has already been told about the current quiet period, + /// so it is told once rather than on every tick. + pub(super) idle_announced: HashSet, + /// The repository the panes run in, reported with every `PaneOpened`. + pub(super) cwd: String, +} + +impl Plugins { + /// Launch a host for every plugin that is enabled *and* that some startup + /// pane opted into. + /// + /// Both conditions, because a host with no pane to watch is a child process + /// that can never be given anything to do. A plugin that will not launch is + /// logged and left out: its panes then behave exactly like unwatched ones, + /// so a broken plugin costs the operator a warning rather than a terminal. + pub(super) fn start(cwd: &str, configs: &[PluginConfig], startup: &[StartupCommand]) -> Self { + let dir = crate::plugin::registry::default_plugins_dir() + .inspect_err(|error| { + tracing::debug!(%error, "viewer: no plugin directory; resolving plugins on PATH"); + }) + .ok(); + let mut hosts = HashMap::new(); + let mut allowed_flags = HashMap::new(); + for cfg in configs { + let opted_in = startup + .iter() + .any(|sc| sc.plugin.as_deref() == Some(cfg.name.as_str())); + if !cfg.enabled || !opted_in { + continue; + } + match PluginHost::spawn(cfg, dir.as_deref()) { + Ok(host) => { + allowed_flags.insert(cfg.name.clone(), cfg.allowed_resume_flags.clone()); + hosts.insert(cfg.name.clone(), host); + } + Err(error) => tracing::warn!( + plugin = %cfg.name, + %error, + "viewer: plugin did not launch; its panes run unwatched" + ), + } + } + Self { + hosts, + owners: HashMap::new(), + allowed_flags, + guard: Guard::new(PANE_IDLE_THRESHOLD, RateLimits::default()), + pending: HashMap::new(), + idle_announced: HashSet::new(), + cwd: cwd.to_string(), + } + } + + /// Hand `pane` to `plugin`, reporting whether it took. + /// + /// Refused when that plugin has no host: recording an association nothing + /// can act on would put the pane on the relaunch path — its slot kept alive + /// after an exit for a plugin that will never ask — for no benefit. + pub(super) fn adopt(&mut self, pane: PaneId, plugin: &str) -> bool { + if !self.hosts.contains_key(plugin) { + return false; + } + self.owners.insert(pane, plugin.to_string()); + true + } + + /// Which plugin watches `pane`, if any. + pub(super) fn owner(&self, pane: PaneId) -> Option<&str> { + self.owners.get(&pane).map(String::as_str) + } + + /// Whether there is nothing for the per-tick work to do, which is the common + /// case: no host means no pane can be watched and no command can arrive. + /// + /// Deliberately *not* "nothing is watched yet". A host's inbound queue is + /// drained only by that per-tick work, so skipping it while a live plugin was + /// writing — before any startup pane had been claimed, say — would let its + /// commands pile up unread with nothing to bound them. + pub(super) fn is_inert(&self) -> bool { + self.hosts.is_empty() + } + + /// Hold `pane`'s slot open for a relaunch. Nothing is held for a pane no + /// plugin watches — that pane's slot is gone by the time this is reached. + pub(super) fn hold_for_relaunch(&mut self, pane: PaneId, spot: PaneSpot, now: Instant) { + if !self.owners.contains_key(&pane) { + return; + } + self.idle_announced.remove(&pane); + self.pending.insert( + pane, + Pending { + spot, + deadline: now + PENDING_RELAUNCH_TTL, + }, + ); + } + + /// Take the hold on `pane`, if it is still within its window. + pub(super) fn claim_pending(&mut self, pane: PaneId) -> Option { + self.pending.remove(&pane) + } + + /// Put a hold back after a relaunch attempt failed, so the pane keeps its + /// remaining window instead of being retired by a single bad try. + pub(super) fn restore_pending(&mut self, pane: PaneId, pending: Pending) { + self.pending.insert(pane, pending); + } + + /// Move a pane's association onto the process that replaced it. + /// + /// The spent budget is deliberately left alone. It is keyed by the slot's + /// token, which a relaunch preserves, and that is the only thing bounding a + /// plugin that answers every exit with another relaunch — clearing it here + /// would hand out a fresh allowance on every attempt and the ceiling would + /// never be reached. + pub(super) fn take_over(&mut self, old: PaneId, new: PaneId) { + if let Some(plugin) = self.owners.remove(&old) { + self.owners.insert(new, plugin); + } + self.idle_announced.remove(&old); + } + + /// Forget `pane` entirely. The caller still has to retire its slot. + /// + /// Takes the backend because the budget is keyed by the slot's token, so it + /// has to be read before the slot is retired. + pub(super) fn forget(&mut self, backend: &PtyBackend, pane: PaneId) { + self.owners.remove(&pane); + self.pending.remove(&pane); + self.idle_announced.remove(&pane); + if let Some(slot) = backend.slot(pane) { + self.guard.cancel(&slot.identity.token.clone()); + } + } + + /// Retire the slots nobody relaunched in time, reporting which panes those + /// were so the caller can tell the clients still showing their deadlines. + pub(super) fn expire_pending(&mut self, backend: &mut PtyBackend, now: Instant) -> Vec { + let expired: Vec = self + .pending + .iter() + .filter(|(_, held)| now >= held.deadline) + .map(|(pane, _)| *pane) + .collect(); + for pane in &expired { + tracing::info!( + pane, + "viewer: no relaunch within the window; retiring the pane's slot" + ); + self.forget(backend, *pane); + backend.retire_slot(*pane); + } + expired + } + + /// Stop every plugin child. + /// + /// The only place that happens: a plugin is not one of `PtyBackend`'s panes, + /// so nothing else will ever reap it. + pub(super) fn shutdown(&mut self) { + for (name, host) in self.hosts.iter_mut() { + let dropped = host.dropped_events(); + if dropped > 0 { + tracing::warn!(plugin = %name, dropped, "viewer: plugin fell behind its events"); + } + host.shutdown(); + } + } +} diff --git a/src/web/viewer/terminal/hub_recovery.rs b/src/web/viewer/terminal/hub_recovery.rs new file mode 100644 index 00000000..189f5c41 --- /dev/null +++ b/src/web/viewer/terminal/hub_recovery.rs @@ -0,0 +1,117 @@ +//! Making a plugin's recovery visible to people, and cancellable by them. +//! +//! The hub keeps no recovery state of its own: a report is broadcast as it +//! arrives and forgotten. What the hub *does* own is the hold on an exited +//! pane's slot, and that is the thing a person can take away — so cancelling is +//! the one place here that touches the backend. + +use super::TerminalHub; +use super::frame::{ServerMessage, TerminalFrame}; +use super::hub_helpers::broadcast_locked; +use super::hub_plugins::Plugins; +use crate::backend::{PaneId, PtyBackend, TerminalBackend}; +use std::time::Instant; + +/// The `state` the hub itself sends when a pane's recovery is over without +/// having succeeded — cancelled by a person, or given up on when the hold ran +/// out. Every client reads it as "stop showing a deadline for this pane". +pub(crate) const RECOVERY_CANCELLED: &str = "cancelled"; + +impl TerminalHub { + /// Tell every client the latest word on a pane's recovery. + pub(super) fn broadcast_recovery( + &self, + pane: PaneId, + state: &str, + detail: Option<&str>, + deadline_epoch: Option, + attempt: u32, + ) { + let Ok(json) = serde_json::to_string(&ServerMessage::Recovery { + pane, + state: state.to_string(), + detail: detail.map(str::to_string), + deadline_epoch, + attempt, + }) else { + return; + }; + // Serialized before the lock and broadcast under it, exactly as the pane + // announcements are: a client either connects before this frame or after, + // never into the middle of it. + let mut state = self.state.lock().expect("terminal state poisoned"); + broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); + } + + /// Tell every client there is nothing pending for `pane` any more. + /// + /// Sent wherever a hold ends without leaving one behind — cancelled, expired, + /// relaunched, or closed for good. Without it a client keeps the last report + /// it saw, and a deadline that has already come and gone stays on screen. + pub(super) fn end_recovery(&self, pane: PaneId) { + self.broadcast_recovery(pane, RECOVERY_CANCELLED, None, None, 0); + } + + /// A pane's process ended. + /// + /// For a pane no plugin watches this is the long-standing path: destroy it + /// and tell everyone. For a watched one the slot has to survive, because its + /// token is the only thing a relaunch can reuse — so the process alone is let + /// go and the slot is held until the plugin acts or the window closes. + pub(super) fn pane_exited( + &self, + backend: &mut PtyBackend, + plugins: &mut Plugins, + pane: PaneId, + ) { + if plugins.owner(pane).is_none() { + backend.destroy_pane(pane); + self.remove_pane_and_announce(pane); + return; + } + // Where it sat, read before the removal below takes it out of the order. + match self.pane_spot(pane) { + Some(spot) => { + backend.release_process(pane); + plugins.hold_for_relaunch(pane, spot, Instant::now()); + plugins.pane_exited(backend, pane); + } + // Not in the client-visible order, so there is nowhere to put a + // replacement and no reason to keep the slot alive for one. + None => { + plugins.pane_closed(backend, pane); + plugins.forget(backend, pane); + backend.destroy_pane(pane); + self.end_recovery(pane); + } + } + // Clients see the truth either way: the process is gone, and a relaunch + // arrives as a new pane rather than as this one coming back. + self.remove_pane_and_announce(pane); + } + + /// A person has given up on `pane`'s recovery. + /// + /// Taking the hold is what makes this a cancellation rather than a no-op: the + /// hold *is* the pending recovery, so a pane without one has nothing to + /// invalidate and is left alone. When there is one, the plugin is told the + /// slot is going while it can still be named, and only then is it retired — + /// `forget` reads the slot's token to drop the plugin's spent budget, so it + /// has to run before `retire_slot` takes the slot away. + pub(super) fn cancel_recovery( + &self, + backend: &mut PtyBackend, + plugins: &mut Plugins, + pane: PaneId, + ) { + if plugins.claim_pending(pane).is_none() { + tracing::debug!(pane, "viewer: cancel with no recovery pending; ignored"); + return; + } + tracing::info!(pane, "viewer: a client cancelled a pane's recovery"); + plugins.pane_closed(backend, pane); + plugins.forget(backend, pane); + backend.retire_slot(pane); + self.end_recovery(pane); + } +} diff --git a/src/web/viewer/terminal/hub_relaunch.rs b/src/web/viewer/terminal/hub_relaunch.rs new file mode 100644 index 00000000..a5b20a76 --- /dev/null +++ b/src/web/viewer/terminal/hub_relaunch.rs @@ -0,0 +1,241 @@ +//! Carrying out what a plugin was allowed to do. +//! +//! Everything here runs on the worker thread and acts only on an [`Approved`] +//! value. There is no path from a [`PluginCommand`](crate::plugin::protocol::PluginCommand) +//! to a pane that does not pass through [`Plugins::judge`] first, and a +//! [`Refused`] is logged and dropped — never retried, never acted on. + +use super::TerminalHub; +use super::frame::{ServerMessage, TerminalFrame}; +use super::hub_helpers::broadcast_locked; +use super::hub_plugins::{PaneSpot, Plugins}; +use crate::backend::{PaneId, PtyBackend, TerminalBackend}; +use crate::plugin::protocol::LogLevel; +use crate::plugin::{Approved, Refused}; +use std::time::Instant; + +impl TerminalHub { + /// Take everything the plugins have asked for this tick, judge it, and do + /// whatever survived. + pub(super) fn dispatch_plugin_commands( + &self, + backend: &mut PtyBackend, + plugins: &mut Plugins, + now: Instant, + ) { + for (plugin, command) in plugins.take_commands() { + let verdict = plugins.judge(&plugin, command, backend, now); + match verdict { + // Straight to the PTY rather than back through + // `Command::Input`: that queue is the human's, and a plugin's + // own input arriving on it would raise `UserInput` and cancel + // the very recovery this input is part of. + Ok(Approved::SendInput { pane, data }) => { + if let Err(error) = backend.send_input(pane, &data) { + tracing::warn!( + plugin = %plugin, + pane, + %error, + "viewer: plugin input could not be written" + ); + } + } + Ok(Approved::Relaunch { + pane, + resume_args, + command_line, + }) => self.relaunch_for_plugin( + backend, + plugins, + &plugin, + pane, + &resume_args, + &command_line, + ), + // Observability, and now a client-visible one. Still logged + // whole: the log is the record of what a plugin claimed, and a + // client that was not connected saw none of it. + Ok(Approved::Status { + pane, + state, + detail, + deadline_epoch, + attempt, + }) => { + tracing::info!( + plugin = %plugin, + pane, + state = %state, + detail = ?detail, + deadline_epoch = ?deadline_epoch, + attempt, + "viewer: plugin reports a pane's state" + ); + self.broadcast_recovery( + pane, + &state, + detail.as_deref(), + deadline_epoch, + attempt, + ); + } + Ok(Approved::Log { level, message }) => log_plugin_line(&plugin, level, &message), + Err(refused) => log_refusal(&plugin, &refused), + } + } + } + + /// Put a process back into the slot a plugin was holding. + /// + /// Only for a pane the hub is actually holding: that hold is the proof the + /// pane exited and is still inside its window, and without it the slot has + /// either been retired or never belonged to this flow at all. + fn relaunch_for_plugin( + &self, + backend: &mut PtyBackend, + plugins: &mut Plugins, + plugin: &str, + pane: PaneId, + resume_args: &[String], + command_line: &str, + ) { + let Some(held) = plugins.claim_pending(pane) else { + tracing::debug!( + plugin = %plugin, + pane, + "viewer: relaunch for a pane with no hold; ignored" + ); + return; + }; + // The cap still binds. The pane's own slot came free when its process + // ended, but a hold can be open for hours and a client may have taken + // that slot in the meantime — and the ceiling counts real processes, not + // who is entitled to one. Left held so the plugin can try again while its + // window lasts, rather than losing the recovery to a full grid. + if !self.has_free_slot() { + tracing::warn!( + plugin = %plugin, + pane, + "viewer: no terminal slot free for a relaunch; the pane keeps its hold" + ); + plugins.restore_pending(pane, held); + return; + } + let (rows, cols, index) = (held.spot.rows, held.spot.cols, held.spot.index); + let title = held.spot.title.clone(); + let allowed = plugins + .allowed_flags + .get(plugin) + .cloned() + .unwrap_or_default(); + + match backend.relaunch_pane(pane, rows, cols, resume_args, &allowed) { + Ok(replacement) => { + // `relaunch_pane` composes the line itself from the same slot, + // args and flags the guard used, so the approved text is the + // text that runs; logging it records exactly that without + // giving the command line a second source of truth. + tracing::info!( + plugin = %plugin, + pane, + replacement, + command_line = %command_line, + "viewer: relaunched a pane for its plugin" + ); + self.register_pane(replacement, rows, cols, None, title.clone()); + self.restore_pane_index(replacement, index); + plugins.take_over(pane, replacement); + plugins.pane_opened(backend, replacement, title.as_deref()); + // The old pane id is spent, so any report a client is still + // showing for it is now about nothing. The plugin's next report + // arrives under the replacement's id. + self.end_recovery(pane); + } + // Held rather than retried. The window is what bounds this, and a + // retry loop here would spend the plugin's whole relaunch budget + // inside one 8 ms tick. + Err(error) => { + tracing::warn!( + plugin = %plugin, + pane, + %error, + "viewer: relaunch failed; the pane keeps its hold" + ); + plugins.restore_pending(pane, held); + } + } + } + + /// Put `pane` back at `index` in the client-visible order and tell every + /// client the result. + /// + /// The order *is* `Shared::panes`, so this is how a relaunched pane keeps + /// its predecessor's place without a new wire message: clients already + /// apply `Reordered`. + fn restore_pane_index(&self, pane: PaneId, index: usize) { + let mut state = self.state.lock().expect("terminal state poisoned"); + let Some(from) = state.panes.iter().position(|p| p.id == pane) else { + return; + }; + // Clamped, because panes can have closed while the hold was open and an + // index past the end would panic `insert`. + let to = index.min(state.panes.len().saturating_sub(1)); + if from == to { + return; + } + let entry = state.panes.remove(from); + state.panes.insert(to, entry); + let order: Vec = state.panes.iter().map(|p| p.id).collect(); + if let Ok(json) = serde_json::to_string(&ServerMessage::Reordered { order }) { + broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); + } + } + + /// Where `pane` sits and what it looks like, captured before it is removed + /// so its replacement can be put back in the same place under the same name. + pub(super) fn pane_spot(&self, pane: PaneId) -> Option { + let state = self.state.lock().expect("terminal state poisoned"); + let index = state.panes.iter().position(|p| p.id == pane)?; + Some(PaneSpot::of(index, &state.panes[index])) + } +} + +/// A plugin's own log line, attributed to it so it cannot be mistaken for one +/// of nightcrow's. +fn log_plugin_line(plugin: &str, level: LogLevel, message: &str) { + match level { + LogLevel::Error => tracing::error!(plugin = %plugin, "{message}"), + LogLevel::Warn => tracing::warn!(plugin = %plugin, "{message}"), + LogLevel::Info => tracing::info!(plugin = %plugin, "{message}"), + LogLevel::Debug => tracing::debug!(plugin = %plugin, "{message}"), + } +} + +/// Log a refusal at the level that says whether anyone should look into it. +/// +/// A plugin decides asynchronously, so being late is ordinary traffic rather +/// than a fault: the pane moved on, or is not quiet yet. The rest mean the +/// plugin asked for something it was never allowed — a pane that is not its, an +/// oversized or control-laden payload, a flag the config does not list, or more +/// attempts than the budget allows — and that is worth an operator's attention. +/// Matched exhaustively on purpose, so a new refusal has to be classified rather +/// than defaulting to silence. +fn log_refusal(plugin: &str, refused: &Refused) { + let ordinary = match refused { + Refused::UnknownPane { .. } + | Refused::StaleGeneration { .. } + | Refused::PaneNotRunning { .. } + | Refused::PaneBusy { .. } + | Refused::PaneStillRunning { .. } => true, + Refused::NotOptedIn { .. } + | Refused::InputTooLarge { .. } + | Refused::ControlCharacter { .. } + | Refused::ResumeArgsRejected { .. } + | Refused::RateLimited { .. } => false, + }; + if ordinary { + tracing::debug!(plugin = %plugin, "viewer: plugin command refused: {refused}"); + } else { + tracing::warn!(plugin = %plugin, "viewer: plugin command refused: {refused}"); + } +} diff --git a/src/web/viewer/terminal/hub_run.rs b/src/web/viewer/terminal/hub_run.rs index fc910133..a27cdd5a 100644 --- a/src/web/viewer/terminal/hub_run.rs +++ b/src/web/viewer/terminal/hub_run.rs @@ -1,6 +1,7 @@ use super::TerminalHub; use super::frame::{ServerMessage, TerminalFrame}; -use super::hub_helpers::{Command, PaneState, broadcast_locked, canonical_order, push_scrollback}; +use super::hub_helpers::{Command, PaneState, broadcast_locked, push_scrollback}; +use super::hub_plugins::Plugins; use crate::backend::{BackendEvent, PaneId, PtyBackend, TerminalBackend}; use crate::web::viewer::limits; use std::collections::VecDeque; @@ -8,13 +9,17 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::Receiver; use std::thread; -use std::time::Duration; +use std::time::{Duration, Instant}; const POLL_INTERVAL: Duration = Duration::from_millis(8); impl TerminalHub { pub(super) fn run(&self, cwd: &str, commands: Receiver, stop: Arc) { let mut backend = PtyBackend::new(cwd); + // Before the loop, because a pane can be created on the first iteration + // and a plugin has to exist to be told about it. Only the plugins some + // configured pane opted into are launched (see `Plugins::start`). + let mut plugins = Plugins::start(cwd, &self.plugins, &self.startup); while !stop.load(Ordering::Acquire) { while let Ok(command) = commands.try_recv() { @@ -33,7 +38,9 @@ impl TerminalHub { } match backend.open_pane(rows, cols, command.as_deref()) { // Unnamed: a pane a client asked for is that client's - // to name, and the hub has nothing to add. + // to name, and the hub has nothing to add. No plugin + // association either, ever — a shell a client opened + // is nobody's to drive but the person at it. Ok(pane) => self.register_pane(pane, rows, cols, Some(client), None), Err(err) => { tracing::warn!(%err, "viewer: could not create a terminal"); @@ -45,10 +52,18 @@ impl TerminalHub { panes, client, reserved, - } => self.open_startup_panes(&mut backend, panes, client, reserved), + } => { + self.open_startup_panes(&mut backend, &mut plugins, panes, client, reserved) + } // Unknown pane ids are ignored rather than errored: a // client racing a pane exit is normal, not an attack. Command::Input { pane, data } if self.pane_is_live(pane) => { + // A person at the keyboard has taken the pane back, so + // its plugin is told and everything it had planned for + // the pane is dropped — before the bytes land, so the + // cancellation cannot be overtaken by a decision made + // from the output this input produces. + plugins.user_input(&backend, pane); let _ = backend.send_input(pane, &data); } Command::Resize { @@ -60,17 +75,34 @@ impl TerminalHub { self.resize_pane(&mut backend, pane, rows, cols, client); } Command::Close { pane } if self.pane_is_live(pane) => { + // Closed for good, unlike an exit: the slot goes with + // the process, so there is nothing left to relaunch. + if plugins.owner(pane).is_some() { + plugins.pane_closed(&backend, pane); + plugins.forget(&backend, pane); + backend.retire_slot(pane); + self.end_recovery(pane); + } backend.destroy_pane(pane); self.remove_pane_and_announce(pane); } Command::Reorder { order } => self.reorder_panes(order), + // Deliberately not gated on the pane being live: a pane with + // a recovery pending is one whose process has already ended, + // so it is no longer in the client-visible list. + Command::CancelRecovery { pane } => { + self.cancel_recovery(&mut backend, &mut plugins, pane) + } _ => {} } } for event in backend.drain_events() { match event { - BackendEvent::Output { pane, data } => self.record_and_broadcast(pane, data), + BackendEvent::Output { pane, data } => { + plugins.pane_output(&backend, pane, &data); + self.record_and_broadcast(pane, data); + } // Destroyed as well as forgotten. `PtyBackend` leaves pane // removal to its caller (see its `drain_events`), so a pane // that ended on its own — the user typed `exit`, or the @@ -79,8 +111,7 @@ impl TerminalHub { // counts live panes, not those, so open-and-exit in a loop // accumulated descriptors with nothing to stop it. BackendEvent::Exited { pane } => { - backend.destroy_pane(pane); - self.remove_pane_and_announce(pane); + self.pane_exited(&mut backend, &mut plugins, pane) } // The hub owns its PTYs outright: it opens them through // `open_pane`, which answers directly, and it is what @@ -89,14 +120,32 @@ impl TerminalHub { BackendEvent::Created { pane, .. } | BackendEvent::Resized { pane, .. } => { tracing::debug!(pane, "hub: unexpected event from its own backend"); } - BackendEvent::SizeOwnership { .. } | BackendEvent::Reordered { .. } => { + BackendEvent::SizeOwnership { .. } + | BackendEvent::Reordered { .. } + | BackendEvent::Recovery { .. } => { tracing::debug!("hub: unexpected session event from its own backend"); } } } + + if !plugins.is_inert() { + let now = Instant::now(); + plugins.notify_idle(&backend, now); + self.dispatch_plugin_commands(&mut backend, &mut plugins, now); + // Told to the clients, or one that was shown a deadline keeps + // counting down to a moment that has already passed. + for pane in plugins.expire_pending(&mut backend, now) { + self.end_recovery(pane); + } + } thread::sleep(POLL_INTERVAL); } + // Ahead of the panes: a plugin child is not one of `PtyBackend`'s panes, + // so this is the only place it is ever reaped, and telling it to stop + // before its panes disappear beneath it is the courteous order. + plugins.shutdown(); + let ids: Vec = self .state .lock() @@ -171,49 +220,6 @@ impl TerminalHub { } } - /// Resize a live pane's PTY at the sizing owner's request, record the size it - /// is now set to, and tell every client what it is. - /// - /// All under one lock, and the liveness check with them. `connect` reports - /// each pane's size from this record and the client caches it as "already - /// applied"; a client that slipped between the two would be told the old - /// size for a PTY that has the new one, and would then skip the resize that - /// would have corrected it. The `resize` itself is an ioctl on the master — - /// far cheaper than the broadcast this lock already covers. - fn resize_pane( - &self, - backend: &mut PtyBackend, - pane: PaneId, - rows: u16, - cols: u16, - client: u64, - ) { - let mut state = self.state.lock().expect("terminal state poisoned"); - // Not this client's to set. Dropped rather than refused: a client can - // lose the sizing between laying out a frame and this arriving, which is - // ordinary rather than an error worth interrupting anyone over. - if state.size_owner != Some(client) { - return; - } - // An unknown pane is ignored rather than errored: a client racing a - // pane exit is normal, not an attack. - let Some(p) = state.panes.iter_mut().find(|p| p.id == pane) else { - return; - }; - if (p.rows, p.cols) == (rows, cols) { - return; - } - backend.resize(pane, rows, cols); - p.rows = rows; - p.cols = cols; - // Every client's emulator has to wrap where the child now does, so the - // size it was actually set to goes to all of them — including the one - // that asked, which learns here if its request was clamped. - if let Ok(json) = serde_json::to_string(&ServerMessage::Resized { pane, rows, cols }) { - broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); - } - } - /// Append output to the pane's bounded scrollback and broadcast it, both /// under one lock so a concurrently connecting client cannot slip a replay /// snapshot between the append and the broadcast. @@ -228,7 +234,7 @@ impl TerminalHub { /// Drop a pane and tell every client, but only if it was still live — a pane /// closed by command and then reported `Exited` by the backend must announce /// once, not twice. - fn remove_pane_and_announce(&self, pane: PaneId) { + pub(super) fn remove_pane_and_announce(&self, pane: PaneId) { let json = serde_json::to_string(&ServerMessage::Exited { pane }).ok(); let mut state = self.state.lock().expect("terminal state poisoned"); let existed = state.panes.iter().any(|p| p.id == pane); @@ -241,41 +247,6 @@ impl TerminalHub { } } - /// Reorder the live panes to match `order` and tell every client the - /// result. - /// - /// `order` is a full desired sequence of pane ids. It is reconciled - /// against what is actually live so a reorder is robust to races with - /// create/close: unknown ids are dropped and any live pane the request - /// omits (e.g. one another client created in the same beat) is kept, - /// appended in its current order (see [`canonical_order`]). The hub - /// converges on that one canonical order and broadcasts it, so the - /// sender and every other device end up with the same layout. Reordering - /// only restyles the grid — pane ids, scrollback, and the live PTYs are - /// untouched. A no-op reorder sends nothing. - fn reorder_panes(&self, order: Vec) { - let mut state = self.state.lock().expect("terminal state poisoned"); - let before: Vec = state.panes.iter().map(|p| p.id).collect(); - let target = canonical_order(&before, &order); - if target == before { - return; - } - // `target` is a permutation of `before`, so every id resolves and `old` - // ends empty. Move each `PaneState` rather than clone it — it owns the - // pane's scrollback. - let mut old = std::mem::take(&mut state.panes); - let mut reordered = Vec::with_capacity(old.len()); - for id in &target { - if let Some(pos) = old.iter().position(|p| p.id == *id) { - reordered.push(old.remove(pos)); - } - } - state.panes = reordered; - if let Ok(json) = serde_json::to_string(&ServerMessage::Reordered { order: target }) { - broadcast_locked(&mut state.clients, TerminalFrame::Control(json)); - } - } - pub(super) fn send_error_to(&self, client_id: u64, message: &str) { let Ok(json) = serde_json::to_string(&ServerMessage::Error { message: message.to_string(), diff --git a/src/web/viewer/terminal/mod.rs b/src/web/viewer/terminal/mod.rs index 5816374a..b5fa2699 100644 --- a/src/web/viewer/terminal/mod.rs +++ b/src/web/viewer/terminal/mod.rs @@ -16,7 +16,12 @@ //! bytes would leave a subtly wrong screen. pub mod frame; +mod hub_events; mod hub_helpers; +mod hub_layout; +mod hub_plugins; +mod hub_recovery; +mod hub_relaunch; mod hub_run; mod session; mod size_owner; @@ -53,6 +58,11 @@ pub struct TerminalHub { /// were configured under. Empty means a single bare shell (matching the /// TUI's default). startup: Vec, + /// The `[[plugin]]` table. The worker launches a host for each entry that is + /// enabled *and* that some `startup` entry opted into, and nothing else — a + /// plugin no pane named is never started, so declaring one costs nothing + /// until a pane hands itself over. + plugins: Vec, /// Set when a client claims the startup terminals by answering with their /// sizes, so they are created exactly once for the hub's life rather than /// on every (re)connection. See [`TerminalHub::claim_startup`]. @@ -61,8 +71,13 @@ pub struct TerminalHub { impl TerminalHub { /// Start a hub whose terminals run in `cwd`. `startup` is the list of - /// commands to launch when the first client connects (empty = one shell). - pub fn spawn(cwd: &str, startup: Vec) -> Arc { + /// commands to launch when the first client connects (empty = one shell), + /// and `plugins` the configured plugin table those commands may opt into. + pub fn spawn( + cwd: &str, + startup: Vec, + plugins: Vec, + ) -> Arc { let (commands, command_rx) = mpsc::sync_channel::(256); let hub = Arc::new(Self { commands, @@ -76,6 +91,7 @@ impl TerminalHub { stop: Arc::new(AtomicBool::new(false)), worker: Mutex::new(None), startup, + plugins, started: AtomicBool::new(false), }); diff --git a/src/web/viewer/terminal/session.rs b/src/web/viewer/terminal/session.rs index 6f749235..d86a997a 100644 --- a/src/web/viewer/terminal/session.rs +++ b/src/web/viewer/terminal/session.rs @@ -64,6 +64,7 @@ impl TerminalSession { } ClientMessage::Close { pane } => Command::Close { pane }, ClientMessage::Reorder { order } => Command::Reorder { order }, + ClientMessage::CancelRecovery { pane } => Command::CancelRecovery { pane }, // Off the worker queue for the same reason `start` is: it decides // who may resize, and a backed-up hub must not drop the message that // hands the sizing over — the client would then be a spectator with diff --git a/src/web/viewer/terminal/startup.rs b/src/web/viewer/terminal/startup.rs index 40f2d914..26299cc3 100644 --- a/src/web/viewer/terminal/startup.rs +++ b/src/web/viewer/terminal/startup.rs @@ -53,6 +53,9 @@ impl TerminalHub { .clone() .unwrap_or_else(|| sc.command.trim().to_string()) }), + // The opt-in, carried through so the worker can record it. Only + // a configured pane can have one; nothing else ever gains it. + plugin: configured.as_ref().and_then(|sc| sc.plugin.clone()), command: configured.map(|sc| sc.command), }) .collect(); diff --git a/src/web/viewer/terminal/startup_run.rs b/src/web/viewer/terminal/startup_run.rs index d7518c87..fd48fb2d 100644 --- a/src/web/viewer/terminal/startup_run.rs +++ b/src/web/viewer/terminal/startup_run.rs @@ -6,6 +6,7 @@ use super::TerminalHub; use super::hub_helpers::StartupPane; +use super::hub_plugins::Plugins; use crate::backend::PtyBackend; /// How a startup pane is named back to the client when it could not be opened. @@ -27,6 +28,7 @@ impl TerminalHub { pub(super) fn open_startup_panes( &self, backend: &mut PtyBackend, + plugins: &mut Plugins, panes: Vec, client: u64, reserved: usize, @@ -66,7 +68,23 @@ impl TerminalHub { // first, so they must not pull that client's // focus onto them. Ok(id) => { - self.register_pane(id, pane.size.rows, pane.size.cols, None, pane.title.clone()) + self.register_pane( + id, + pane.size.rows, + pane.size.cols, + None, + pane.title.clone(), + ); + // Only here, and only from the pane's own configuration: + // this is the single place a pane ever becomes visible to a + // plugin. `adopt` refuses when the named plugin has no live + // host, so a pane whose plugin failed to launch stays an + // ordinary terminal. + if let Some(name) = pane.plugin.as_deref() + && plugins.adopt(id, name) + { + plugins.pane_opened(backend, id, pane.title.as_deref()); + } } Err(err) => { tracing::warn!(%err, "viewer: could not start a terminal"); diff --git a/src/web/viewer/terminal/tests/behavior.rs b/src/web/viewer/terminal/tests/behavior.rs index 8e2885a2..1b317f10 100644 --- a/src/web/viewer/terminal/tests/behavior.rs +++ b/src/web/viewer/terminal/tests/behavior.rs @@ -9,7 +9,7 @@ use crate::web::viewer::terminal::frame::{ClientMessage, PaneSize, TerminalFrame #[test] fn creating_a_terminal_announces_it_and_streams_output() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); session.dispatch(ClientMessage::Create { rows: 24, cols: 80 }); @@ -40,7 +40,7 @@ fn creating_a_terminal_announces_it_and_streams_output() { #[test] fn the_per_repo_terminal_cap_is_enforced() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); for _ in 0..limits::MAX_PTYS_PER_REPO + 2 { @@ -61,7 +61,7 @@ fn the_per_repo_terminal_cap_is_enforced() { #[test] fn a_dropped_session_stops_receiving() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); assert_eq!(hub.client_count(), 1); @@ -74,7 +74,7 @@ fn a_dropped_session_stops_receiving() { #[test] fn reordering_panes_echoes_the_order_and_replays_it_to_a_later_joiner() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); // The startup shell (claimed with a size, as a client does) plus one @@ -111,7 +111,7 @@ fn reordering_panes_echoes_the_order_and_replays_it_to_a_later_joiner() { #[test] fn a_reconnecting_client_receives_existing_panes_and_scrollback() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); first.dispatch(ClientMessage::Create { rows: 24, cols: 80 }); @@ -151,7 +151,7 @@ fn a_replayed_pane_reports_the_size_it_was_last_resized_to() { // this reported the birth size instead, every reload would send a resize // the PTY does not need and cost the child a full repaint. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); first.dispatch(ClientMessage::Create { rows: 24, cols: 80 }); @@ -205,7 +205,7 @@ fn input_for_an_unknown_pane_is_ignored() { // A client racing a pane exit is normal traffic, not an error worth // tearing the connection down for. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); session.dispatch(ClientMessage::Input { @@ -232,7 +232,7 @@ fn input_for_an_unknown_pane_is_ignored() { #[test] fn stop_is_idempotent() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); hub.stop(); hub.stop(); } diff --git a/src/web/viewer/terminal/tests/mod.rs b/src/web/viewer/terminal/tests/mod.rs index e184bce8..ebc32486 100644 --- a/src/web/viewer/terminal/tests/mod.rs +++ b/src/web/viewer/terminal/tests/mod.rs @@ -1,15 +1,18 @@ +//! The hub's test harness: the shared deadline and the frame accessors every +//! hub test reads its assertions through. The tests themselves live beside it. + mod behavior; +mod plugin_rules; +mod plugin_slots; +mod plugins; +mod recovery; mod scrollback_depth; mod size_owner; mod startup; +mod wire; use crate::backend::PaneId; -use crate::web::viewer::limits; -use crate::web::viewer::terminal::frame::{ - ClientMessage, PaneSize, ServerMessage, TerminalFrame, decode_output, encode_output, -}; -use crate::web::viewer::terminal::hub_helpers::{canonical_order, push_scrollback}; -use std::collections::VecDeque; +use crate::web::viewer::terminal::frame::TerminalFrame; use std::thread; use std::time::{Duration, Instant}; @@ -57,6 +60,18 @@ pub(super) fn created_pane(frame: &TerminalFrame) -> Option { None } +/// The pane an `exited` frame announces the end of. +pub(super) fn exited_pane(frame: &TerminalFrame) -> Option { + let TerminalFrame::Control(json) = frame else { + return None; + }; + let value: serde_json::Value = serde_json::from_str(json).ok()?; + if value["type"] != "exited" { + return None; + } + value["pane"].as_u64().map(|n| n as PaneId) +} + /// The pane count a `pending` frame offers for sizing. pub(super) fn pending_count(frame: &TerminalFrame) -> Option { let TerminalFrame::Control(json) = frame else { @@ -144,157 +159,3 @@ pub(super) fn collect_created(session: &TerminalSession, n: usize) -> Vec(r#"{"type":"nope"}"#).is_err()); - assert!(serde_json::from_str::(r#"{"type":"create"}"#).is_err()); -} - -#[test] -fn server_messages_serialize_with_a_type_tag() { - let json = serde_json::to_string(&ServerMessage::Created { - pane: 2, - rows: 40, - cols: 120, - client: None, - title: None, - }) - .unwrap(); - // No requester, no field: the browser reads these already, and a pane - // nobody asked for must look to it exactly as it did before. - assert_eq!(json, r#"{"type":"created","pane":2,"rows":40,"cols":120}"#); - - let json = serde_json::to_string(&ServerMessage::Created { - pane: 2, - rows: 40, - cols: 120, - client: Some(7), - title: None, - }) - .unwrap(); - assert_eq!( - json, - r#"{"type":"created","pane":2,"rows":40,"cols":120,"client":7}"# - ); - - // And back, because the daemon reads these off a hub session to relay them. - let created: ServerMessage = - serde_json::from_str(r#"{"type":"created","pane":1,"rows":2,"cols":3}"#).unwrap(); - assert!(matches!( - created, - ServerMessage::Created { client: None, .. } - )); - - let json = serde_json::to_string(&ServerMessage::Reordered { order: vec![2, 1] }).unwrap(); - assert_eq!(json, r#"{"type":"reordered","order":[2,1]}"#); - - let json = serde_json::to_string(&ServerMessage::Pending { count: 2 }).unwrap(); - assert_eq!(json, r#"{"type":"pending","count":2}"#); -} - -#[test] -fn a_pane_size_is_clamped_into_the_bounds_a_pty_can_use() { - // These arrive from the client's own measurement, so they are input from - // outside. Zero gives the child a terminal it cannot draw in and can fail - // `openpty`; the far end asks a full-screen program for a screen buffer of - // rows * cells. - assert_eq!( - PaneSize { rows: 0, cols: 0 }.clamped(), - PaneSize { - rows: limits::MIN_PANE_DIMENSION, - cols: limits::MIN_PANE_DIMENSION - } - ); - assert_eq!( - PaneSize { - rows: u16::MAX, - cols: u16::MAX - } - .clamped(), - PaneSize { - rows: limits::MAX_PANE_ROWS, - cols: limits::MAX_PANE_COLS - } - ); - // A real display passes through untouched. - let real = PaneSize { - rows: 48, - cols: 210, - }; - assert_eq!(real.clamped(), real); -} - -#[test] -fn canonical_order_reconciles_a_request_against_the_live_panes() { - // A full permutation is honored verbatim. - assert_eq!(canonical_order(&[1, 2, 3], &[3, 1, 2]), vec![3, 1, 2]); - // A partial request moves the named panes; the rest keep their order. - assert_eq!(canonical_order(&[1, 2, 3], &[3]), vec![3, 1, 2]); - // An id that is no longer live (closed in a race) is dropped. - assert_eq!(canonical_order(&[1, 2], &[9, 2, 1]), vec![2, 1]); - // A repeated id is taken once, keeping the result a permutation. - assert_eq!(canonical_order(&[1, 2], &[2, 2, 1]), vec![2, 1]); - // An empty request leaves the order untouched. - assert_eq!(canonical_order(&[1, 2], &[]), vec![1, 2]); -} - -#[test] -fn scrollback_is_bounded_and_keeps_the_most_recent_bytes() { - let cap = limits::MAX_TERMINAL_SCROLLBACK_BYTES; - let mut buf = VecDeque::new(); - for _ in 0..(cap / 1000 + 5) { - push_scrollback(&mut buf, &vec![b'x'; 1000]); - } - assert_eq!(buf.len(), cap, "scrollback must be capped"); - - // The tail is what restores the visible screen, so the newest bytes must - // survive eviction. - push_scrollback(&mut buf, b"TAIL"); - assert_eq!(buf.len(), cap); - let contents: Vec = buf.iter().copied().collect(); - assert!(contents.ends_with(b"TAIL"), "newest bytes must be retained"); -} diff --git a/src/web/viewer/terminal/tests/plugin_rules.rs b/src/web/viewer/terminal/tests/plugin_rules.rs new file mode 100644 index 00000000..9a65b04b --- /dev/null +++ b/src/web/viewer/terminal/tests/plugin_rules.rs @@ -0,0 +1,155 @@ +//! The rules the worker applies to a plugin's panes, driven directly against a +//! real `PtyBackend` rather than through the hub. +//! +//! Deterministic on purpose: [`Plugins::judge`] and the idle check both take +//! `now` as a parameter, so the clock is an input and nothing here has to wait +//! for time to pass. The hub's *routing* — which of these calls it makes on an +//! exit — is pinned by the integration tests in `plugins.rs`. + +use super::plugins::{fixture, recorder}; +use crate::backend::{PaneId, PaneToken, PtyBackend, TerminalBackend}; +use crate::config::StartupCommand; +use crate::plugin::Refused; +use crate::plugin::protocol::{PROTOCOL_VERSION, PluginCommand}; +use crate::web::viewer::terminal::hub_plugins::{PANE_IDLE_THRESHOLD, Plugins}; +use std::time::Instant; + +pub(super) const ROWS: u16 = 24; +pub(super) const COLS: u16 = 80; +pub(super) const PLUGIN: &str = "watch"; +/// Long enough that the panes below stay alive for the whole test. +pub(super) const LONG_RUNNING: &str = "sleep 30"; + +pub(super) fn opt_in() -> StartupCommand { + StartupCommand { + name: None, + command: LONG_RUNNING.to_string(), + plugin: Some(PLUGIN.to_string()), + } +} + +/// Far enough past a pane's birth that the guard's idle rule is satisfied +/// without the test sleeping. +pub(super) fn well_idle() -> Instant { + Instant::now() + PANE_IDLE_THRESHOLD * 2 +} + +pub(super) fn token_of(backend: &PtyBackend, pane: PaneId) -> PaneToken { + backend + .slot(pane) + .expect("the pane should still have a slot") + .identity + .token + .clone() +} + +pub(super) fn send_input(token: &PaneToken, generation: u32) -> PluginCommand { + PluginCommand::SendInput { + v: PROTOCOL_VERSION, + token: token.clone(), + generation, + data: "recovered\n".to_string(), + } +} + +#[test] +fn a_plugin_no_pane_opted_into_is_never_launched() { + // A host with no pane to watch is a child process that can never be given + // anything to do, so declaring a plugin must not by itself start one. + let f = fixture(); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[]); + + assert!( + !plugins.adopt(1, PLUGIN), + "a plugin nothing opted into must have no host to adopt with" + ); +} + +#[test] +fn a_disabled_plugin_is_never_launched_even_when_a_pane_opted_in() { + let f = fixture(); + let mut off = recorder(PLUGIN, &f.log); + off.enabled = false; + let mut plugins = Plugins::start(&f.cwd(), &[off], &[opt_in()]); + + assert!(!plugins.adopt(1, PLUGIN)); +} + +#[test] +fn a_plugin_that_will_not_launch_leaves_its_panes_unmanaged() { + // Unmanaged rather than half-managed: a pane recorded against a host that + // does not exist would take the relaunch path on exit, holding its slot open + // for hours for a plugin that will never ask. + let f = fixture(); + let mut broken = recorder(PLUGIN, &f.log); + broken.command = "/nonexistent/nightcrow-test-plugin".to_string(); + let mut plugins = Plugins::start(&f.cwd(), &[broken], &[opt_in()]); + + assert!(!plugins.adopt(1, PLUGIN)); + assert_eq!(plugins.owner(1), None); +} + +#[test] +fn a_command_for_a_pane_that_did_not_opt_in_is_refused() { + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let token = token_of(&backend, pane); + + // Deliberately not adopted: the token resolves, but nothing handed the pane + // over. + let refused = plugins + .judge(PLUGIN, send_input(&token, 1), &backend, well_idle()) + .expect_err("a pane nobody handed over must not be typed into"); + + assert!(matches!(refused, Refused::NotOptedIn { .. }), "{refused}"); + backend.destroy_pane(pane); +} + +#[test] +fn a_command_naming_a_stale_generation_is_refused_and_leaves_the_replacement_alone() { + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let token = token_of(&backend, pane); + assert!(plugins.adopt(pane, PLUGIN)); + + // The exit-and-relaunch the hub performs, without the hub. + backend.release_process(pane); + let replacement = backend + .relaunch_pane(pane, ROWS, COLS, &[], &[]) + .expect("relaunch the slot"); + plugins.take_over(pane, replacement); + assert_eq!( + token_of(&backend, replacement), + token, + "the slot's token must survive the relaunch" + ); + + let refused = plugins + .judge(PLUGIN, send_input(&token, 1), &backend, well_idle()) + .expect_err("a command about the previous process must not land on this one"); + + assert!( + matches!( + refused, + Refused::StaleGeneration { + claimed: 1, + current: 2, + .. + } + ), + "{refused}" + ); + assert!( + backend.is_process_alive(replacement), + "a refused command must leave the replacement process untouched" + ); + backend.destroy_pane(replacement); +} diff --git a/src/web/viewer/terminal/tests/plugin_slots.rs b/src/web/viewer/terminal/tests/plugin_slots.rs new file mode 100644 index 00000000..be7fed76 --- /dev/null +++ b/src/web/viewer/terminal/tests/plugin_slots.rs @@ -0,0 +1,168 @@ +//! How long a watched pane's slot lives, and how a quiet one is announced. +//! +//! Beside `plugin_rules.rs` rather than in it: those tests are about what a +//! plugin is allowed to ask for, these about what the worker keeps, gives up on +//! its own, or hands back to a person. Both drive a real `PtyBackend` with the +//! clock as an input. + +use super::plugin_rules::{ + COLS, LONG_RUNNING, PLUGIN, ROWS, opt_in, send_input, token_of, well_idle, +}; +use super::plugins::{fixture, logged, logged_event, recorder}; +use crate::backend::{PtyBackend, TerminalBackend}; +use crate::plugin::{Approved, RateLimits, Refused}; +use crate::web::viewer::terminal::hub_plugins::{PENDING_RELAUNCH_TTL, PaneSpot, Plugins}; +use std::time::Instant; + +#[test] +fn an_exited_watched_pane_keeps_its_token_while_a_plain_pane_loses_its() { + // The two calls the hub chooses between on an exit. `release_process` is + // what makes a relaunch possible at all; `destroy_pane` is what makes an + // ordinary pane unaddressable the moment it ends. + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let watched = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let plain = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let (watched_token, plain_token) = (token_of(&backend, watched), token_of(&backend, plain)); + + backend.release_process(watched); + backend.destroy_pane(plain); + + assert_eq!( + backend.pane_for_token(&watched_token), + Some(watched), + "a watched pane's slot must outlive its process" + ); + assert_eq!( + backend.pane_for_token(&plain_token), + None, + "a plain pane must leave nothing addressable behind" + ); + backend.destroy_pane(watched); +} + +#[test] +fn a_hold_that_runs_out_of_time_retires_the_slot() { + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let token = token_of(&backend, pane); + assert!(plugins.adopt(pane, PLUGIN)); + let exited_at = Instant::now(); + backend.release_process(pane); + plugins.hold_for_relaunch( + pane, + PaneSpot { + index: 0, + rows: ROWS, + cols: COLS, + title: None, + }, + exited_at, + ); + + // Still inside the window: nothing is given up. + plugins.expire_pending(&mut backend, exited_at + PENDING_RELAUNCH_TTL / 2); + assert_eq!(backend.pane_for_token(&token), Some(pane)); + + plugins.expire_pending(&mut backend, exited_at + PENDING_RELAUNCH_TTL); + + assert_eq!( + backend.pane_for_token(&token), + None, + "a hold nobody used must retire the slot" + ); + assert_eq!(plugins.owner(pane), None); +} + +#[test] +fn a_human_typing_into_a_watched_pane_clears_what_its_plugin_had_spent() { + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let token = token_of(&backend, pane); + assert!(plugins.adopt(pane, PLUGIN)); + let now = well_idle(); + + // Spend the whole per-pane allowance, then confirm it really is spent. + for _ in 0..RateLimits::default().max_sends_per_window { + plugins + .judge(PLUGIN, send_input(&token, 1), &backend, now) + .expect("an idle, opted-in pane accepts input"); + } + let refused = plugins + .judge(PLUGIN, send_input(&token, 1), &backend, now) + .expect_err("the allowance should be gone"); + assert!(matches!(refused, Refused::RateLimited { .. }), "{refused}"); + + plugins.user_input(&backend, pane); + + // The pane belongs to the person now, so what the plugin had spent on the + // situation it thought it was in is void — including the budget. + assert!( + matches!( + plugins.judge(PLUGIN, send_input(&token, 1), &backend, now), + Ok(Approved::SendInput { .. }) + ), + "a human taking the pane back must clear the plugin's spent budget" + ); + assert!( + logged_event(&f.log, |e| e["event"] == "user_input").is_some(), + "the plugin must be told a human took the pane back" + ); + backend.destroy_pane(pane); +} + +#[test] +fn a_quiet_pane_is_announced_idle_once_until_it_speaks_again() { + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[recorder(PLUGIN, &f.log)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + assert!(plugins.adopt(pane, PLUGIN)); + let now = well_idle(); + + plugins.notify_idle(&backend, now); + plugins.notify_idle(&backend, now); + assert!( + logged_event(&f.log, |e| e["event"] == "pane_idle").is_some(), + "the plugin was never told the pane went quiet" + ); + assert_eq!( + logged(&f.log) + .iter() + .filter(|e| e["event"] == "pane_idle") + .count(), + 1, + "one quiet period must be announced once, not on every tick" + ); + + // Output ends the quiet period, so the next one is announced again. + plugins.pane_output(&backend, pane, b"still here\n"); + plugins.notify_idle(&backend, now); + + assert!( + super::wait_for(|| { + let idles = logged(&f.log) + .iter() + .filter(|e| e["event"] == "pane_idle") + .count(); + (idles == 2).then_some(()) + }) + .is_some(), + "a new quiet period must be announced again" + ); + backend.destroy_pane(pane); +} diff --git a/src/web/viewer/terminal/tests/plugins.rs b/src/web/viewer/terminal/tests/plugins.rs new file mode 100644 index 00000000..b1a6eeca --- /dev/null +++ b/src/web/viewer/terminal/tests/plugins.rs @@ -0,0 +1,291 @@ +//! The plugin wiring, driven through a real hub and a real plugin child. +//! +//! The fake plugin is `/bin/sh`, which is present wherever these run. It appends +//! every event it is handed to a file the test polls, so what is asserted is what +//! the hub actually sent rather than what it meant to send — and a pane the hub +//! must never mention simply never appears in that file. + +use super::{collect_created, created_pane, exited_pane, next_matching, reordered_order, wait_for}; +use crate::config::{PluginConfig, StartupCommand}; +use crate::plugin::protocol::PROTOCOL_VERSION; +use crate::web::viewer::terminal::TerminalHub; +use crate::web::viewer::terminal::frame::{ClientMessage, PaneSize, TerminalFrame}; +use std::path::{Path, PathBuf}; + +/// Env var the fake plugin appends the events it receives to. +const LOG_ENV: &str = "NC_TEST_PLUGIN_LOG"; +/// Env var holding the protocol version the fake plugin answers with, so no +/// script hard-codes a number a version bump would silently invalidate. +const VERSION_ENV: &str = "NC_TEST_PLUGIN_V"; +/// Env var naming the file the relaunching plugin uses to act exactly once. A +/// puppet that relaunched on every exit would keep relaunching a command that +/// exits immediately, which is the test's own doing rather than the hub's. +const ONCE_ENV: &str = "NC_TEST_PLUGIN_ONCE"; + +/// A plugin that only records. +fn logging_script() -> String { + format!(r#"while IFS= read -r line; do printf '%s\n' "$line" >> "${LOG_ENV}"; done"#) +} + +/// A plugin that records, then answers the first `pane_exited` with a relaunch +/// of exactly the generation it was told about. `{{`/`}}` are `format!`'s +/// escapes for the JSON braces the command line needs. +fn relaunch_script() -> String { + format!( + r#"while IFS= read -r line; do + printf '%s\n' "$line" >> "${LOG_ENV}" + case "$line" in + *pane_exited*) + if [ ! -f "${ONCE_ENV}" ]; then + : > "${ONCE_ENV}" + t=$(printf '%s' "$line" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + g=$(printf '%s' "$line" | sed -n 's/.*"generation":\([0-9]*\).*/\1/p') + printf '{{"cmd":"relaunch","v":%s,"token":"%s","generation":%s,"resume_args":[]}}\n' \ + "${VERSION_ENV}" "$t" "$g" + fi + ;; + esac +done"# + ) +} + +pub(super) fn shell_plugin(name: &str, script: String, log: &Path, once: &Path) -> PluginConfig { + PluginConfig { + name: name.to_string(), + command: "/bin/sh".to_string(), + args: vec!["-c".to_string(), script], + env: [ + (LOG_ENV.to_string(), log.to_string_lossy().into_owned()), + (ONCE_ENV.to_string(), once.to_string_lossy().into_owned()), + (VERSION_ENV.to_string(), PROTOCOL_VERSION.to_string()), + ] + .into_iter() + .collect(), + enabled: true, + ..PluginConfig::default() + } +} + +/// A plugin that records every event, for a test that only needs to see what the +/// hub sent it. +pub(super) fn recorder(name: &str, log: &Path) -> PluginConfig { + shell_plugin(name, logging_script(), log, Path::new("/dev/null")) +} + +fn pane(name: &str, command: &str, plugin: Option<&str>) -> StartupCommand { + StartupCommand { + name: Some(name.to_string()), + command: command.to_string(), + plugin: plugin.map(str::to_string), + } +} + +/// Every complete event line the fake plugin has logged so far. A half-written +/// line simply fails to parse and is skipped until the next poll. +pub(super) fn logged(log: &Path) -> Vec { + std::fs::read_to_string(log) + .unwrap_or_default() + .lines() + .filter_map(|line| serde_json::from_str(line).ok()) + .collect() +} + +/// Wait for the plugin to log an event `want` accepts. +pub(super) fn logged_event( + log: &Path, + want: impl Fn(&serde_json::Value) -> bool, +) -> Option { + wait_for(|| logged(log).into_iter().find(|event| want(event))) +} + +fn is(event: &serde_json::Value, kind: &str) -> bool { + event["event"] == kind +} + +fn token_of(event: &serde_json::Value) -> String { + event["token"].as_str().unwrap_or_default().to_string() +} + +/// A temp directory plus the paths the fake plugin writes inside it. +pub(super) struct Fixture { + dir: tempfile::TempDir, + pub(super) log: PathBuf, + pub(super) once: PathBuf, +} + +pub(super) fn fixture() -> Fixture { + let dir = tempfile::TempDir::new().unwrap(); + let log = dir.path().join("events.ndjson"); + let once = dir.path().join("relaunched"); + Fixture { dir, log, once } +} + +impl Fixture { + pub(super) fn cwd(&self) -> String { + self.dir.path().to_string_lossy().into_owned() + } +} + +#[test] +fn a_pane_that_did_not_opt_in_is_never_shown_to_a_plugin() { + // The plain pane is *first*, so if the hub ever announced it its line would + // precede the watched pane's on the plugin's single event stream. Seeing the + // watched pane's `pane_opened` therefore proves the plain one was not + // announced, rather than merely not announced yet. + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![ + pane("plain", "printf plain-is-done", None), + pane("watched", "sleep 30", Some("watch")), + ], + vec![recorder("watch", &f.log)], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { sizes: Vec::new() }); + let ids = collect_created(&session, 2); + + let first = logged_event(&f.log, |e| is(e, "pane_opened")).expect("no pane_opened logged"); + assert_eq!( + first["title"], "watched", + "the first pane announced must be the one that opted in: {first}" + ); + + // And the plain pane's exit is the path it always was: the client is told, + // and the plugin is not. + assert!( + next_matching(&session, |frame| exited_pane(frame) == Some(ids[0])).is_some(), + "the plain pane's exit must still reach the client" + ); + let seen: Vec = logged(&f.log).iter().map(|e| e.to_string()).collect(); + assert!( + !seen.iter().any(|line| line.contains("plain")), + "a pane with no opt-in must not appear in a plugin's events: {seen:?}" + ); + hub.stop(); +} + +#[test] +fn two_opted_in_panes_are_tracked_under_separate_tokens_that_do_not_cross() { + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![ + pane("alpha", "printf ALPHA-MARK; sleep 30", Some("watch")), + pane("beta", "printf BETA-MARK; sleep 30", Some("watch")), + ], + vec![recorder("watch", &f.log)], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { + sizes: vec![PaneSize { rows: 24, cols: 80 }; 2], + }); + + let alpha = logged_event(&f.log, |e| is(e, "pane_opened") && e["title"] == "alpha") + .map(|e| token_of(&e)) + .expect("alpha was not announced"); + let beta = logged_event(&f.log, |e| is(e, "pane_opened") && e["title"] == "beta") + .map(|e| token_of(&e)) + .expect("beta was not announced"); + assert_ne!(alpha, beta, "two panes must not share a token"); + + // Each pane's own output must arrive under its own token, or a plugin + // watching two panes could not tell which one spoke. + let carried = |mark: &'static str| { + logged_event(&f.log, move |e| { + is(e, "pane_output") && e["text"].as_str().is_some_and(|t| t.contains(mark)) + }) + .map(|e| token_of(&e)) + }; + assert_eq!(carried("ALPHA-MARK"), Some(alpha)); + assert_eq!(carried("BETA-MARK"), Some(beta)); + hub.stop(); +} + +#[test] +fn a_relaunch_reuses_the_token_advances_the_generation_and_lands_back_at_its_index() { + // The pane that exits is first in the order, so a replacement left at the + // end would show up as a wrong order rather than pass unnoticed. + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![ + pane("recovered", "printf gone; exit 0", Some("watch")), + pane("other", "sleep 30", None), + ], + vec![shell_plugin("watch", relaunch_script(), &f.log, &f.once)], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { sizes: Vec::new() }); + let ids = collect_created(&session, 2); + + let opened = logged_event(&f.log, |e| is(e, "pane_opened")).expect("no pane_opened"); + let token = token_of(&opened); + assert_eq!(opened["generation"], 1); + + let reopened = logged_event(&f.log, |e| is(e, "pane_opened") && e["generation"] == 2) + .expect("the pane was never relaunched"); + assert_eq!( + token_of(&reopened), + token, + "a relaunch must keep the slot's token, or an observer loses its place" + ); + + // The replacement is a new pane to every client, so it arrives as a create + // and is then moved back to where its predecessor sat. + let replacement = next_matching(&session, |frame| { + created_pane(frame).is_some_and(|id| !ids.contains(&id)) + }) + .and_then(|frame| created_pane(&frame)) + .expect("the replacement pane was never announced"); + let order = next_matching(&session, |frame| reordered_order(frame).is_some()) + .and_then(|frame| reordered_order(&frame)) + .expect("the replacement was not put back in the order"); + assert_eq!( + order, + vec![replacement, ids[1]], + "the relaunched pane must land at its predecessor's index" + ); + hub.stop(); +} + +#[test] +fn a_plugin_that_cannot_be_launched_leaves_the_terminal_session_working() { + let f = fixture(); + let mut broken = recorder("watch", &f.log); + broken.command = "/nonexistent/nightcrow-test-plugin".to_string(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![pane( + "watched", + "printf STILL-HERE; sleep 30", + Some("watch"), + )], + vec![broken], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { sizes: Vec::new() }); + + let created = next_matching(&session, |frame| created_pane(frame).is_some()) + .and_then(|frame| created_pane(&frame)) + .expect("a pane whose plugin failed to launch must still open"); + assert!( + next_matching( + &session, + |frame| matches!(frame, TerminalFrame::Output { pane, data } + if *pane == created && String::from_utf8_lossy(data).contains("STILL-HERE")) + ) + .is_some(), + "the pane must still stream its output" + ); + + // And the hub keeps serving: a client can still open another terminal. + session.dispatch(ClientMessage::Create { rows: 24, cols: 80 }); + assert!( + next_matching(&session, |frame| created_pane(frame) + .is_some_and(|id| id != created)) + .is_some(), + "the hub stopped serving after a plugin failed to launch" + ); + hub.stop(); +} diff --git a/src/web/viewer/terminal/tests/recovery.rs b/src/web/viewer/terminal/tests/recovery.rs new file mode 100644 index 00000000..0060f16b --- /dev/null +++ b/src/web/viewer/terminal/tests/recovery.rs @@ -0,0 +1,257 @@ +//! What a person sees of a plugin's recovery, and what cancelling it does. +//! +//! Driven through a real hub and a real plugin child, like `plugins.rs`: what is +//! asserted is the frame that actually left the hub, not what it meant to send. + +use super::plugin_rules::{COLS, LONG_RUNNING, PLUGIN, ROWS, opt_in, token_of}; +use super::plugins::{Fixture, fixture, logged_event, shell_plugin}; +use super::{collect_created, created_pane, next_matching}; +use crate::backend::{PaneId, PtyBackend}; +use crate::config::{PluginConfig, StartupCommand}; +use crate::web::viewer::terminal::frame::{ClientMessage, TerminalFrame}; +use crate::web::viewer::terminal::hub_plugins::{PENDING_RELAUNCH_TTL, PaneSpot, Plugins}; +use crate::web::viewer::terminal::hub_recovery::RECOVERY_CANCELLED; +use crate::web::viewer::terminal::{TerminalHub, TerminalSession}; +use std::path::Path; +use std::time::{Duration, Instant}; + +/// How long a test watches for a frame it expects *not* to arrive. Long enough +/// for the 8 ms worker loop to have drained the cancel it was given several +/// hundred times over, and short enough that proving a negative is cheap. +const QUIET_WINDOW: Duration = Duration::from_millis(500); + +/// The state and detail the fake plugin reports, so the assertions name the same +/// strings the script prints. +const REPORTED_STATE: &str = "waiting_for_reset"; +const REPORTED_DETAIL: &str = "provider window closed"; +const REPORTED_DEADLINE: i64 = 1_700_000_000; +const REPORTED_ATTEMPT: u32 = 2; + +/// A plugin that answers the pane it is given with one status report. +/// +/// Once, on `pane_opened`: a report per event would make "the client saw one" +/// unfalsifiable, and the point is that a single report reaches every client. +fn status_script() -> String { + format!( + r#"while IFS= read -r line; do + printf '%s\n' "$line" >> "${{NC_TEST_PLUGIN_LOG}}" + case "$line" in + *pane_opened*) + t=$(printf '%s' "$line" | sed -n 's/.*"token":"\([^"]*\)".*/\1/p') + g=$(printf '%s' "$line" | sed -n 's/.*"generation":\([0-9]*\).*/\1/p') + printf '{{"cmd":"status","v":%s,"token":"%s","generation":%s,"state":"{REPORTED_STATE}","detail":"{REPORTED_DETAIL}","deadline_epoch":{REPORTED_DEADLINE},"attempt":{REPORTED_ATTEMPT}}}\n' \ + "${{NC_TEST_PLUGIN_V}}" "$t" "$g" + ;; + esac +done"# + ) +} + +fn reporter(name: &str, f: &Fixture) -> PluginConfig { + shell_plugin(name, status_script(), &f.log, Path::new("/dev/null")) +} + +fn watched(name: &str, command: &str) -> StartupCommand { + StartupCommand { + name: Some(name.to_string()), + command: command.to_string(), + plugin: Some(PLUGIN.to_string()), + } +} + +/// One recovery report as it came off the wire. +#[derive(Debug, PartialEq, Eq)] +struct Report { + pane: PaneId, + state: String, + detail: Option, + deadline_epoch: Option, + attempt: u32, +} + +/// The whole recovery report a frame carries, or `None` for any other frame. +fn recovery(frame: &TerminalFrame) -> Option { + let TerminalFrame::Control(json) = frame else { + return None; + }; + let value: serde_json::Value = serde_json::from_str(json).ok()?; + if value["type"] != "recovery" { + return None; + } + Some(Report { + pane: value["pane"].as_u64()? as PaneId, + state: value["state"].as_str()?.to_string(), + detail: value["detail"].as_str().map(str::to_string), + deadline_epoch: value["deadline_epoch"].as_i64(), + attempt: value["attempt"].as_u64()? as u32, + }) +} + +#[test] +fn a_plugin_status_report_reaches_a_client_as_a_recovery_frame() { + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![watched("watched", LONG_RUNNING)], + vec![reporter(PLUGIN, &f)], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { sizes: Vec::new() }); + let ids = collect_created(&session, 1); + + let report = next_matching(&session, |frame| recovery(frame).is_some()) + .and_then(|frame| recovery(&frame)) + .expect("the plugin's report never reached the client"); + + assert_eq!( + report, + Report { + pane: ids[0], + state: REPORTED_STATE.to_string(), + detail: Some(REPORTED_DETAIL.to_string()), + deadline_epoch: Some(REPORTED_DEADLINE), + attempt: REPORTED_ATTEMPT, + }, + "the report must reach the client exactly as the plugin gave it" + ); + hub.stop(); +} + +#[test] +fn cancelling_a_recovery_releases_the_hold_and_tells_every_client() { + // Two clients, because the point of broadcasting `cancelled` is that the one + // who did not press the key stops showing a deadline too. + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![watched("watched", "printf gone; exit 0")], + vec![reporter(PLUGIN, &f)], + ); + let presser = hub.connect(); + let watcher = hub.connect(); + presser.dispatch(ClientMessage::Start { sizes: Vec::new() }); + let ids = collect_created(&presser, 1); + + // The hold exists once the plugin has been told its pane exited — that event + // is sent on the same path that creates it. + assert!( + logged_event(&f.log, |e| e["event"] == "pane_exited").is_some(), + "the pane never exited into a hold" + ); + + presser.dispatch(ClientMessage::CancelRecovery { pane: ids[0] }); + + for (who, session) in [("the presser", &presser), ("the other client", &watcher)] { + let report = next_matching(session, |frame| { + recovery(frame).is_some_and(|r| r.state == RECOVERY_CANCELLED) + }) + .and_then(|frame| recovery(&frame)) + .unwrap_or_else(|| panic!("{who} was never told the recovery was cancelled")); + assert_eq!(report.pane, ids[0]); + assert_eq!( + report.deadline_epoch, None, + "a cancelled report carries no deadline" + ); + } + + // The slot is gone, and the plugin was told before it went: `pane_closed` + // carries the slot's identity, so it cannot be sent afterwards. + assert!( + logged_event(&f.log, |e| e["event"] == "pane_closed").is_some(), + "the plugin must be told the slot it was holding is gone" + ); + hub.stop(); +} + +#[test] +fn cancelling_a_pane_with_nothing_pending_is_harmless() { + let f = fixture(); + let hub = TerminalHub::spawn( + &f.cwd(), + vec![watched("watched", "printf STILL-HERE; sleep 30")], + vec![reporter(PLUGIN, &f)], + ); + let session = hub.connect(); + session.dispatch(ClientMessage::Start { sizes: Vec::new() }); + let ids = collect_created(&session, 1); + + // A live pane has no hold, and neither has an id that never existed. + session.dispatch(ClientMessage::CancelRecovery { pane: ids[0] }); + session.dispatch(ClientMessage::CancelRecovery { pane: 4242 }); + + assert!( + !cancelled_within(&session, QUIET_WINDOW), + "a cancel with nothing pending must announce nothing" + ); + + // And the worker is still serving: an unknown or inapplicable cancel is a + // no-op, not an error that wedges the queue. + session.dispatch(ClientMessage::Create { + rows: ROWS, + cols: COLS, + }); + assert!( + next_matching(&session, |frame| created_pane(frame) + .is_some_and(|id| id != ids[0])) + .is_some(), + "the hub stopped serving after a cancel it had nothing to do with" + ); + hub.stop(); +} + +/// Whether a `cancelled` report arrives inside `window`. Frames that are not +/// recovery reports are drained and ignored — a real pane is streaming output the +/// whole time. +fn cancelled_within(session: &TerminalSession, window: Duration) -> bool { + let deadline = Instant::now() + window; + while Instant::now() < deadline { + let Some(frame) = session.next_frame(Duration::from_millis(20)) else { + continue; + }; + if recovery(&frame).is_some_and(|r| r.state == RECOVERY_CANCELLED) { + return true; + } + } + false +} + +#[test] +fn a_hold_that_runs_out_of_time_reports_the_pane_it_gave_up_on() { + // The pane id is what the worker turns into the final `cancelled` broadcast, + // so an expiry that retires the slot silently would leave every client + // counting down to a moment that has passed. + let f = fixture(); + let mut backend = PtyBackend::new(f.cwd()); + let mut plugins = Plugins::start(&f.cwd(), &[reporter(PLUGIN, &f)], &[opt_in()]); + let pane = backend + .open_pane(ROWS, COLS, Some(LONG_RUNNING)) + .expect("open a pane"); + let token = token_of(&backend, pane); + assert!(plugins.adopt(pane, PLUGIN)); + let exited_at = Instant::now(); + backend.release_process(pane); + plugins.hold_for_relaunch( + pane, + PaneSpot { + index: 0, + rows: ROWS, + cols: COLS, + title: None, + }, + exited_at, + ); + + assert!( + plugins + .expire_pending(&mut backend, exited_at + PENDING_RELAUNCH_TTL / 2) + .is_empty(), + "a hold still inside its window must not be reported" + ); + + assert_eq!( + plugins.expire_pending(&mut backend, exited_at + PENDING_RELAUNCH_TTL), + vec![pane], + "the expired pane must be named so its clients can be told" + ); + assert_eq!(backend.pane_for_token(&token), None); +} diff --git a/src/web/viewer/terminal/tests/size_owner.rs b/src/web/viewer/terminal/tests/size_owner.rs index 1a7fb167..48f51ff9 100644 --- a/src/web/viewer/terminal/tests/size_owner.rs +++ b/src/web/viewer/terminal/tests/size_owner.rs @@ -48,7 +48,7 @@ fn the_client_that_just_arrived_owns_the_sizing() { // tmux's `window-size latest`: the newest client is the one someone is // sitting at, so the panes should fit its screen. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); assert!(verdict(&first), "the only client owns it"); @@ -65,7 +65,7 @@ fn the_sizing_passes_to_the_newest_client_still_attached() { // Somebody has to hold it, or the panes stay frozen at the size of a client // that has gone. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); let second = hub.connect(); let third = hub.connect(); @@ -84,7 +84,7 @@ fn a_client_can_take_the_sizing_back_on_request() { // sizing, asking for it does. Otherwise glancing at a phone would repaint // everybody's screen. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); let second = hub.connect(); assert!(verdict(&second)); @@ -100,7 +100,7 @@ fn a_client_can_take_the_sizing_back_on_request() { #[test] fn claiming_what_this_client_already_owns_says_nothing() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let only = hub.connect(); assert!(verdict(&only)); @@ -119,7 +119,7 @@ fn only_the_owner_resizes_the_pty_and_everyone_is_told_the_size() { // Two clients fitting one PTY to two layouts would leave the child drawing // for a width neither of them has. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); first.dispatch(ClientMessage::Start { sizes: vec![PaneSize { rows: 24, cols: 80 }], diff --git a/src/web/viewer/terminal/tests/startup.rs b/src/web/viewer/terminal/tests/startup.rs index 6e46a07c..992bf4ab 100644 --- a/src/web/viewer/terminal/tests/startup.rs +++ b/src/web/viewer/terminal/tests/startup.rs @@ -19,6 +19,7 @@ fn startup(command: &str) -> StartupCommand { StartupCommand { name: None, command: command.to_string(), + plugin: None, } } @@ -27,7 +28,11 @@ fn a_startup_terminal_is_offered_for_sizing_and_born_at_that_size() { // The whole point of the handshake: the child must never draw a frame at a // size no client chose, so the PTY does not exist until one has measured. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), vec![startup("printf hello")]); + let hub = TerminalHub::spawn( + &dir.path().to_string_lossy(), + vec![startup("printf hello")], + Vec::new(), + ); let session = hub.connect(); assert_eq!( @@ -56,7 +61,7 @@ fn a_startup_terminal_is_offered_for_sizing_and_born_at_that_size() { #[test] fn an_empty_startup_offers_one_shell() { let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); assert_eq!( @@ -80,7 +85,7 @@ fn a_startup_size_of_zero_is_clamped_rather_than_reaching_openpty() { // spent by then — the hub would hold `started` with no terminal to show // for it and never offer them again. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); next_matching(&session, |f| pending_count(f).is_some()).expect("no offer"); @@ -110,7 +115,7 @@ fn a_startup_set_that_fills_the_cap_still_gets_every_terminal() { let configured: Vec = (0..limits::MAX_PTYS_PER_REPO) .map(|i| startup(&format!("printf startup{i}"))) .collect(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), configured); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), configured, Vec::new()); let session = hub.connect(); assert_eq!( next_matching(&session, |f| pending_count(f).is_some()).and_then(|f| pending_count(&f)), @@ -145,7 +150,7 @@ fn a_startup_command_the_cap_turned_away_is_named() { let configured: Vec = (0..limits::MAX_PTYS_PER_REPO) .map(|i| startup(&format!("printf startup{i}"))) .collect(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), configured); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), configured, Vec::new()); let session = hub.connect(); next_matching(&session, |f| pending_count(f).is_some()).expect("no offer"); @@ -177,7 +182,7 @@ fn an_unanswered_offer_is_made_again_to_the_next_client() { // Nothing consumes the offer but an answer, so the hub cannot end up with // no terminals and no way to ever open them. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let abandoned = hub.connect(); assert!( @@ -200,7 +205,7 @@ fn only_the_first_answer_opens_the_startup_terminals() { // Both clients were offered the panes, so both may answer. Creating them // twice would double every configured command. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let first = hub.connect(); let second = hub.connect(); @@ -239,7 +244,9 @@ fn a_configured_startup_terminal_is_announced_under_its_name() { vec![StartupCommand { name: Some("Claude".into()), command: "sleep 30".into(), + plugin: None, }], + Vec::new(), ); let session = hub.connect(); session.dispatch(ClientMessage::Start { @@ -261,7 +268,11 @@ fn a_configured_startup_terminal_is_announced_under_its_name() { fn an_unnamed_startup_terminal_falls_back_to_its_command() { // What the operator wrote is what they would recognise it by. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), vec![startup("printf hello")]); + let hub = TerminalHub::spawn( + &dir.path().to_string_lossy(), + vec![startup("printf hello")], + Vec::new(), + ); let session = hub.connect(); session.dispatch(ClientMessage::Start { sizes: vec![PaneSize { rows: 24, cols: 80 }], @@ -277,7 +288,7 @@ fn a_pane_a_client_opened_is_left_unnamed_by_the_session() { // That client named it, or nothing did — either way the hub has nothing to // add, and stamping a name here would override a title the client chose. let dir = tempfile::TempDir::new().unwrap(); - let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new()); + let hub = TerminalHub::spawn(&dir.path().to_string_lossy(), Vec::new(), Vec::new()); let session = hub.connect(); session.dispatch(ClientMessage::Create { rows: 24, cols: 80 }); diff --git a/src/web/viewer/terminal/tests/wire.rs b/src/web/viewer/terminal/tests/wire.rs new file mode 100644 index 00000000..0c80cfc8 --- /dev/null +++ b/src/web/viewer/terminal/tests/wire.rs @@ -0,0 +1,220 @@ +//! Contracts that need no hub: the wire encodings, the size clamp, and the two +//! pure helpers the worker leans on. + +use crate::web::viewer::limits; +use crate::web::viewer::terminal::frame::{ + ClientMessage, PaneSize, ServerMessage, decode_output, encode_output, +}; +use crate::web::viewer::terminal::hub_helpers::{canonical_order, push_scrollback}; +use std::collections::VecDeque; + +#[test] +fn output_frames_round_trip_through_the_binary_encoding() { + // Raw PTY bytes are not always valid UTF-8; the framing must not care. + let payload = vec![0x1b, b'[', b'0', b'm', 0xff, 0xfe, 0x00]; + + let encoded = encode_output(7, &payload); + let (pane, data) = decode_output(&encoded).unwrap(); + + assert_eq!(pane, 7); + assert_eq!(data, &payload[..]); +} + +#[test] +fn decode_output_rejects_a_frame_too_short_to_carry_a_pane_id() { + assert!(decode_output(&[]).is_none()); + assert!(decode_output(&[1, 2, 3]).is_none()); + assert_eq!(decode_output(&[1, 0, 0, 0]), Some((1, &[][..]))); +} + +#[test] +fn client_messages_parse_from_the_wire_shape() { + let create: ClientMessage = + serde_json::from_str(r#"{"type":"create","rows":24,"cols":80}"#).unwrap(); + assert!(matches!( + create, + ClientMessage::Create { rows: 24, cols: 80 } + )); + + let input: ClientMessage = + serde_json::from_str(r#"{"type":"input","pane":3,"data":"ls\n"}"#).unwrap(); + assert!(matches!(input, ClientMessage::Input { pane: 3, .. })); + + let reorder: ClientMessage = + serde_json::from_str(r#"{"type":"reorder","order":[3,1,2]}"#).unwrap(); + assert!(matches!(reorder, ClientMessage::Reorder { order } if order == vec![3, 1, 2])); + + let start: ClientMessage = + serde_json::from_str(r#"{"type":"start","sizes":[{"rows":40,"cols":120}]}"#).unwrap(); + assert!( + matches!(start, ClientMessage::Start { sizes } if sizes == vec![PaneSize { rows: 40, cols: 120 }]) + ); + // A client that measured nothing still answers, so the panes open. + let empty: ClientMessage = serde_json::from_str(r#"{"type":"start","sizes":[]}"#).unwrap(); + assert!(matches!(empty, ClientMessage::Start { sizes } if sizes.is_empty())); + + let cancel: ClientMessage = + serde_json::from_str(r#"{"type":"cancel_recovery","pane":4}"#).unwrap(); + assert_eq!(cancel, ClientMessage::CancelRecovery { pane: 4 }); + // And out again: the daemon relays what a client sent, so the tag it writes + // has to be the tag it reads. + assert_eq!( + serde_json::to_string(&ClientMessage::CancelRecovery { pane: 4 }).unwrap(), + r#"{"type":"cancel_recovery","pane":4}"# + ); + + assert!(serde_json::from_str::(r#"{"type":"nope"}"#).is_err()); + assert!(serde_json::from_str::(r#"{"type":"create"}"#).is_err()); +} + +#[test] +fn server_messages_serialize_with_a_type_tag() { + let json = serde_json::to_string(&ServerMessage::Created { + pane: 2, + rows: 40, + cols: 120, + client: None, + title: None, + }) + .unwrap(); + // No requester, no field: the browser reads these already, and a pane + // nobody asked for must look to it exactly as it did before. + assert_eq!(json, r#"{"type":"created","pane":2,"rows":40,"cols":120}"#); + + let json = serde_json::to_string(&ServerMessage::Created { + pane: 2, + rows: 40, + cols: 120, + client: Some(7), + title: None, + }) + .unwrap(); + assert_eq!( + json, + r#"{"type":"created","pane":2,"rows":40,"cols":120,"client":7}"# + ); + + // And back, because the daemon reads these off a hub session to relay them. + let created: ServerMessage = + serde_json::from_str(r#"{"type":"created","pane":1,"rows":2,"cols":3}"#).unwrap(); + assert!(matches!( + created, + ServerMessage::Created { client: None, .. } + )); + + let json = serde_json::to_string(&ServerMessage::Reordered { order: vec![2, 1] }).unwrap(); + assert_eq!(json, r#"{"type":"reordered","order":[2,1]}"#); + + let json = serde_json::to_string(&ServerMessage::Pending { count: 2 }).unwrap(); + assert_eq!(json, r#"{"type":"pending","count":2}"#); +} + +#[test] +fn a_recovery_report_has_a_fixed_wire_shape() { + let json = serde_json::to_string(&ServerMessage::Recovery { + pane: 6, + state: "waiting_for_reset".to_string(), + detail: Some("provider window closed".to_string()), + deadline_epoch: Some(1_700_000_000), + attempt: 2, + }) + .unwrap(); + assert_eq!( + json, + r#"{"type":"recovery","pane":6,"state":"waiting_for_reset","detail":"provider window closed","deadline_epoch":1700000000,"attempt":2}"# + ); + + // Absent, not null and not zero: a client must be able to tell "no deadline" + // from "the epoch", and rendering a wrong wall-clock time reads as fact. + let json = serde_json::to_string(&ServerMessage::Recovery { + pane: 6, + state: "cancelled".to_string(), + detail: None, + deadline_epoch: None, + attempt: 0, + }) + .unwrap(); + assert_eq!( + json, + r#"{"type":"recovery","pane":6,"state":"cancelled","attempt":0}"# + ); + + // And back, because the daemon reads these off a hub session to relay them. + let parsed: ServerMessage = serde_json::from_str( + r#"{"type":"recovery","pane":6,"state":"backoff","deadline_epoch":-1,"attempt":9}"#, + ) + .unwrap(); + assert_eq!( + parsed, + ServerMessage::Recovery { + pane: 6, + state: "backoff".to_string(), + detail: None, + deadline_epoch: Some(-1), + attempt: 9, + } + ); +} + +#[test] +fn a_pane_size_is_clamped_into_the_bounds_a_pty_can_use() { + // These arrive from the client's own measurement, so they are input from + // outside. Zero gives the child a terminal it cannot draw in and can fail + // `openpty`; the far end asks a full-screen program for a screen buffer of + // rows * cells. + assert_eq!( + PaneSize { rows: 0, cols: 0 }.clamped(), + PaneSize { + rows: limits::MIN_PANE_DIMENSION, + cols: limits::MIN_PANE_DIMENSION + } + ); + assert_eq!( + PaneSize { + rows: u16::MAX, + cols: u16::MAX + } + .clamped(), + PaneSize { + rows: limits::MAX_PANE_ROWS, + cols: limits::MAX_PANE_COLS + } + ); + // A real display passes through untouched. + let real = PaneSize { + rows: 48, + cols: 210, + }; + assert_eq!(real.clamped(), real); +} + +#[test] +fn canonical_order_reconciles_a_request_against_the_live_panes() { + // A full permutation is honored verbatim. + assert_eq!(canonical_order(&[1, 2, 3], &[3, 1, 2]), vec![3, 1, 2]); + // A partial request moves the named panes; the rest keep their order. + assert_eq!(canonical_order(&[1, 2, 3], &[3]), vec![3, 1, 2]); + // An id that is no longer live (closed in a race) is dropped. + assert_eq!(canonical_order(&[1, 2], &[9, 2, 1]), vec![2, 1]); + // A repeated id is taken once, keeping the result a permutation. + assert_eq!(canonical_order(&[1, 2], &[2, 2, 1]), vec![2, 1]); + // An empty request leaves the order untouched. + assert_eq!(canonical_order(&[1, 2], &[]), vec![1, 2]); +} + +#[test] +fn scrollback_is_bounded_and_keeps_the_most_recent_bytes() { + let cap = limits::MAX_TERMINAL_SCROLLBACK_BYTES; + let mut buf = VecDeque::new(); + for _ in 0..(cap / 1000 + 5) { + push_scrollback(&mut buf, &vec![b'x'; 1000]); + } + assert_eq!(buf.len(), cap, "scrollback must be capped"); + + // The tail is what restores the visible screen, so the newest bytes must + // survive eviction. + push_scrollback(&mut buf, b"TAIL"); + assert_eq!(buf.len(), cap); + let contents: Vec = buf.iter().copied().collect(); + assert!(contents.ends_with(b"TAIL"), "newest bytes must be retained"); +} diff --git a/viewer-ui/dist/assets/Html-CCQq4xAj.js b/viewer-ui/dist/assets/Html-Bzz2sKDk.js similarity index 69% rename from viewer-ui/dist/assets/Html-CCQq4xAj.js rename to viewer-ui/dist/assets/Html-Bzz2sKDk.js index b2f30fc2..6175625d 100644 --- a/viewer-ui/dist/assets/Html-CCQq4xAj.js +++ b/viewer-ui/dist/assets/Html-Bzz2sKDk.js @@ -1 +1 @@ -import{a as e}from"./index-CQToaW-W.js";var t=e();function n({source:e}){return(0,t.jsx)(`iframe`,{title:`HTML preview`,sandbox:``,srcDoc:e,className:`h-full w-full border-0 bg-white`})}export{n as HtmlView}; \ No newline at end of file +import{a as e}from"./index-BN9ccC5R.js";var t=e();function n({source:e}){return(0,t.jsx)(`iframe`,{title:`HTML preview`,sandbox:``,srcDoc:e,className:`h-full w-full border-0 bg-white`})}export{n as HtmlView}; \ No newline at end of file diff --git a/viewer-ui/dist/assets/Markdown-C0-sv82C.js b/viewer-ui/dist/assets/Markdown-B9Na3tqX.js similarity index 99% rename from viewer-ui/dist/assets/Markdown-C0-sv82C.js rename to viewer-ui/dist/assets/Markdown-B9Na3tqX.js index 574251e0..31880793 100644 --- a/viewer-ui/dist/assets/Markdown-C0-sv82C.js +++ b/viewer-ui/dist/assets/Markdown-B9Na3tqX.js @@ -1,4 +1,4 @@ -import{a as e,d as t,f as n,l as r,u as i}from"./index-CQToaW-W.js";function a(e,t){let n=t||{};return(e[e.length-1]===``?[...e,``]:e).join((n.padRight?` `:``)+`,`+(n.padLeft===!1?``:` `)).trim()}var o=/^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,s=/^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,c={};function l(e,t){return((t||c).jsx?s:o).test(e)}var u=/[ \t\n\f\r]/g;function d(e){return typeof e==`object`?e.type===`text`&&f(e.value):f(e)}function f(e){return e.replace(u,``)===``}var p=class{constructor(e,t,n){this.normal=t,this.property=e,n&&(this.space=n)}};p.prototype.normal={},p.prototype.property={},p.prototype.space=void 0;function m(e,t){let n={},r={};for(let t of e)Object.assign(n,t.property),Object.assign(r,t.normal);return new p(n,r,t)}function h(e){return e.toLowerCase()}var g=class{constructor(e,t){this.attribute=t,this.property=e}};g.prototype.attribute=``,g.prototype.booleanish=!1,g.prototype.boolean=!1,g.prototype.commaOrSpaceSeparated=!1,g.prototype.commaSeparated=!1,g.prototype.defined=!1,g.prototype.mustUseProperty=!1,g.prototype.number=!1,g.prototype.overloadedBoolean=!1,g.prototype.property=``,g.prototype.spaceSeparated=!1,g.prototype.space=void 0;var _=t({boolean:()=>y,booleanish:()=>b,commaOrSpaceSeparated:()=>T,commaSeparated:()=>w,number:()=>S,overloadedBoolean:()=>x,spaceSeparated:()=>C}),v=0,y=E(),b=E(),x=E(),S=E(),C=E(),w=E(),T=E();function E(){return 2**++v}var D=Object.keys(_),O=class extends g{constructor(e,t,n,r){let i=-1;if(super(e,t),k(this,`space`,r),typeof n==`number`)for(;++i4&&n.slice(0,4)===`data`&&re.test(t)){if(t.charAt(4)===`-`){let e=t.slice(5).replace(ne,oe);r=`data`+e.charAt(0).toUpperCase()+e.slice(1)}else{let e=t.slice(4);if(!ne.test(e)){let n=e.replace(te,ae);n.charAt(0)!==`-`&&(n=`-`+n),t=`data`+n}}i=O}return new i(r,t)}function ae(e){return`-`+e.toLowerCase()}function oe(e){return e.charAt(1).toUpperCase()}var se=m([j,P,I,L,R],`html`),ce=m([j,F,I,L,R],`svg`);function le(e){return e.join(` `).trim()}var ue=i(((e,t)=>{var n=/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g,r=/\n/g,i=/^\s*/,a=/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/,o=/^:\s*/,s=/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/,c=/^[;\s]*/,l=/^\s+|\s+$/g;function u(e,t){if(typeof e!=`string`)throw TypeError(`First argument must be a string`);if(!e)return[];t||={};var l=1,u=1;function f(e){var t=e.match(r);t&&(l+=t.length);var n=e.lastIndexOf(` +import{a as e,d as t,f as n,l as r,u as i}from"./index-BN9ccC5R.js";function a(e,t){let n=t||{};return(e[e.length-1]===``?[...e,``]:e).join((n.padRight?` `:``)+`,`+(n.padLeft===!1?``:` `)).trim()}var o=/^[$_\p{ID_Start}][$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,s=/^[$_\p{ID_Start}][-$_\u{200C}\u{200D}\p{ID_Continue}]*$/u,c={};function l(e,t){return((t||c).jsx?s:o).test(e)}var u=/[ \t\n\f\r]/g;function d(e){return typeof e==`object`?e.type===`text`&&f(e.value):f(e)}function f(e){return e.replace(u,``)===``}var p=class{constructor(e,t,n){this.normal=t,this.property=e,n&&(this.space=n)}};p.prototype.normal={},p.prototype.property={},p.prototype.space=void 0;function m(e,t){let n={},r={};for(let t of e)Object.assign(n,t.property),Object.assign(r,t.normal);return new p(n,r,t)}function h(e){return e.toLowerCase()}var g=class{constructor(e,t){this.attribute=t,this.property=e}};g.prototype.attribute=``,g.prototype.booleanish=!1,g.prototype.boolean=!1,g.prototype.commaOrSpaceSeparated=!1,g.prototype.commaSeparated=!1,g.prototype.defined=!1,g.prototype.mustUseProperty=!1,g.prototype.number=!1,g.prototype.overloadedBoolean=!1,g.prototype.property=``,g.prototype.spaceSeparated=!1,g.prototype.space=void 0;var _=t({boolean:()=>y,booleanish:()=>b,commaOrSpaceSeparated:()=>T,commaSeparated:()=>w,number:()=>S,overloadedBoolean:()=>x,spaceSeparated:()=>C}),v=0,y=E(),b=E(),x=E(),S=E(),C=E(),w=E(),T=E();function E(){return 2**++v}var D=Object.keys(_),O=class extends g{constructor(e,t,n,r){let i=-1;if(super(e,t),k(this,`space`,r),typeof n==`number`)for(;++i4&&n.slice(0,4)===`data`&&re.test(t)){if(t.charAt(4)===`-`){let e=t.slice(5).replace(ne,oe);r=`data`+e.charAt(0).toUpperCase()+e.slice(1)}else{let e=t.slice(4);if(!ne.test(e)){let n=e.replace(te,ae);n.charAt(0)!==`-`&&(n=`-`+n),t=`data`+n}}i=O}return new i(r,t)}function ae(e){return`-`+e.toLowerCase()}function oe(e){return e.charAt(1).toUpperCase()}var se=m([j,P,I,L,R],`html`),ce=m([j,F,I,L,R],`svg`);function le(e){return e.join(` `).trim()}var ue=i(((e,t)=>{var n=/\/\*[^*]*\*+([^/*][^*]*\*+)*\//g,r=/\n/g,i=/^\s*/,a=/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/,o=/^:\s*/,s=/^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/,c=/^[;\s]*/,l=/^\s+|\s+$/g;function u(e,t){if(typeof e!=`string`)throw TypeError(`First argument must be a string`);if(!e)return[];t||={};var l=1,u=1;function f(e){var t=e.match(r);t&&(l+=t.length);var n=e.lastIndexOf(` `);u=~n?e.length-n:u+e.length}function p(){var e={line:l,column:u};return function(t){return t.position=new m(e),_(),t}}function m(e){this.start=e,this.end={line:l,column:u},this.source=t.source}m.prototype.content=e;function h(n){var r=Error(t.source+`:`+l+`:`+u+`: `+n);if(r.reason=n,r.filename=t.source,r.line=l,r.column=u,r.source=e,!t.silent)throw r}function g(t){var n=t.exec(e);if(n){var r=n[0];return f(r),e=e.slice(r.length),n}}function _(){g(i)}function v(e){var t;for(e||=[];t=y();)t!==!1&&e.push(t);return e}function y(){var t=p();if(!(e.charAt(0)!=`/`||e.charAt(1)!=`*`)){for(var n=2;e.charAt(n)!=``&&(e.charAt(n)!=`*`||e.charAt(n+1)!=`/`);)++n;if(n+=2,e.charAt(n-1)===``)return h(`End of comment missing`);var r=e.slice(2,n-2);return u+=2,f(r),e=e.slice(n),u+=2,t({type:`comment`,comment:r})}}function b(){var e=p(),t=g(a);if(t){if(y(),!g(o))return h(`property missing ':'`);var r=g(s),i=e({type:`declaration`,property:d(t[0].replace(n,``)),value:r?d(r[0].replace(n,``)):``});return g(c),i}}function x(){var e=[];v(e);for(var t;t=b();)t!==!1&&(e.push(t),v(e));return e}return _(),x()}function d(e){return e?e.replace(l,``):``}t.exports=u})),de=i((e=>{var t=e&&e.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(e,"__esModule",{value:!0}),e.default=r;var n=t(ue());function r(e,t){let r=null;if(!e||typeof e!=`string`)return r;let i=(0,n.default)(e),a=typeof t==`function`;return i.forEach(e=>{if(e.type!==`declaration`)return;let{property:n,value:i}=e;a?t(n,i,e):i&&(r||={},r[n]=i)}),r}})),fe=i((e=>{Object.defineProperty(e,"__esModule",{value:!0}),e.camelCase=void 0;var t=/^--[a-zA-Z0-9_-]+$/,n=/-([a-z])/g,r=/^[^-]+$/,i=/^-(webkit|moz|ms|o|khtml)-/,a=/^-(ms)-/,o=function(e){return!e||r.test(e)||t.test(e)},s=function(e,t){return t.toUpperCase()},c=function(e,t){return`${t}-`};e.camelCase=function(e,t){return t===void 0&&(t={}),o(e)?e:(e=e.toLowerCase(),e=t.reactCompat?e.replace(a,c):e.replace(i,c),e.replace(n,s))}})),pe=i(((e,t)=>{var n=(e&&e.__importDefault||function(e){return e&&e.__esModule?e:{default:e}})(de()),r=fe();function i(e,t){var i={};return!e||typeof e!=`string`||(0,n.default)(e,function(e,n){e&&n&&(i[(0,r.camelCase)(e,t)]=n)}),i}i.default=i,t.exports=i})),me=he(`end`),z=he(`start`);function he(e){return t;function t(t){let n=t&&t.position&&t.position[e]||{};if(typeof n.line==`number`&&n.line>0&&typeof n.column==`number`&&n.column>0)return{line:n.line,column:n.column,offset:typeof n.offset==`number`&&n.offset>-1?n.offset:void 0}}}function ge(e){let t=z(e),n=me(e);if(t&&n)return{start:t,end:n}}function _e(e){return!e||typeof e!=`object`?``:`position`in e||`type`in e?ye(e.position):`start`in e||`end`in e?ye(e):`line`in e||`column`in e?ve(e):``}function ve(e){return be(e&&e.line)+`:`+be(e&&e.column)}function ye(e){return ve(e&&e.start)+`-`+ve(e&&e.end)}function be(e){return e&&typeof e==`number`?e:1}var B=class extends Error{constructor(e,t,n){super(),typeof t==`string`&&(n=t,t=void 0);let r=``,i={},a=!1;if(t&&(i=`line`in t&&`column`in t||`start`in t&&`end`in t?{place:t}:`type`in t?{ancestors:[t],place:t.position}:{...t}),typeof e==`string`?r=e:!i.cause&&e&&(a=!0,r=e.message,i.cause=e),!i.ruleId&&!i.source&&typeof n==`string`){let e=n.indexOf(`:`);e===-1?i.ruleId=n:(i.source=n.slice(0,e),i.ruleId=n.slice(e+1))}if(!i.place&&i.ancestors&&i.ancestors){let e=i.ancestors[i.ancestors.length-1];e&&(i.place=e.position)}let o=i.place&&`start`in i.place?i.place.start:i.place;this.ancestors=i.ancestors||void 0,this.cause=i.cause||void 0,this.column=o?o.column:void 0,this.fatal=void 0,this.file=``,this.message=r,this.line=o?o.line:void 0,this.name=_e(i.place)||`1:1`,this.place=i.place||void 0,this.reason=this.message,this.ruleId=i.ruleId||void 0,this.source=i.source||void 0,this.stack=a&&i.cause&&typeof i.cause.stack==`string`?i.cause.stack:``,this.actual=void 0,this.expected=void 0,this.note=void 0,this.url=void 0}};B.prototype.file=``,B.prototype.name=``,B.prototype.reason=``,B.prototype.message=``,B.prototype.stack=``,B.prototype.column=void 0,B.prototype.line=void 0,B.prototype.ancestors=void 0,B.prototype.cause=void 0,B.prototype.fatal=void 0,B.prototype.place=void 0,B.prototype.ruleId=void 0,B.prototype.source=void 0;var xe=n(pe(),1),Se={}.hasOwnProperty,Ce=new Map,we=/[A-Z]/g,Te=new Set([`table`,`tbody`,`thead`,`tfoot`,`tr`]),Ee=new Set([`td`,`th`]);function De(e,t){if(!t||t.Fragment===void 0)throw TypeError("Expected `Fragment` in options");let n=t.filePath||void 0,r;if(t.development){if(typeof t.jsxDEV!=`function`)throw TypeError("Expected `jsxDEV` in options when `development: true`");r=Re(n,t.jsxDEV)}else{if(typeof t.jsx!=`function`)throw TypeError("Expected `jsx` in production options");if(typeof t.jsxs!=`function`)throw TypeError("Expected `jsxs` in production options");r=Le(n,t.jsx,t.jsxs)}let i={Fragment:t.Fragment,ancestors:[],components:t.components||{},create:r,elementAttributeNameCase:t.elementAttributeNameCase||`react`,evaluater:t.createEvaluater?t.createEvaluater():void 0,filePath:n,ignoreInvalidStyle:t.ignoreInvalidStyle||!1,passKeys:t.passKeys!==!1,passNode:t.passNode||!1,schema:t.space===`svg`?ce:se,stylePropertyNameCase:t.stylePropertyNameCase||`dom`,tableCellAlignToStyle:t.tableCellAlignToStyle!==!1},a=Oe(i,e,void 0);return a&&typeof a!=`string`?a:i.create(e,i.Fragment,{children:a||void 0},void 0)}function Oe(e,t,n){if(t.type===`element`)return ke(e,t,n);if(t.type===`mdxFlowExpression`||t.type===`mdxTextExpression`)return Ae(e,t);if(t.type===`mdxJsxFlowElement`||t.type===`mdxJsxTextElement`)return Me(e,t,n);if(t.type===`mdxjsEsm`)return je(e,t);if(t.type===`root`)return Ne(e,t,n);if(t.type===`text`)return Pe(e,t)}function ke(e,t,n){let r=e.schema,i=r;t.tagName.toLowerCase()===`svg`&&r.space===`html`&&(i=ce,e.schema=i),e.ancestors.push(t);let a=We(e,t.tagName,!1),o=ze(e,t),s=Ve(e,t);return Te.has(t.tagName)&&(s=s.filter(function(e){return typeof e!=`string`||!d(e)})),Fe(e,o,a,t),Ie(o,s),e.ancestors.pop(),e.schema=r,e.create(t,a,o,n)}function Ae(e,t){if(t.data&&t.data.estree&&e.evaluater){let n=t.data.estree.body[0];return n.type,e.evaluater.evaluateExpression(n.expression)}Ge(e,t.position)}function je(e,t){if(t.data&&t.data.estree&&e.evaluater)return e.evaluater.evaluateProgram(t.data.estree);Ge(e,t.position)}function Me(e,t,n){let r=e.schema,i=r;t.name===`svg`&&r.space===`html`&&(i=ce,e.schema=i),e.ancestors.push(t);let a=t.name===null?e.Fragment:We(e,t.name,!0),o=Be(e,t),s=Ve(e,t);return Fe(e,o,a,t),Ie(o,s),e.ancestors.pop(),e.schema=r,e.create(t,a,o,n)}function Ne(e,t,n){let r={};return Ie(r,Ve(e,t)),e.create(t,e.Fragment,r,n)}function Pe(e,t){return t.value}function Fe(e,t,n,r){typeof n!=`string`&&n!==e.Fragment&&e.passNode&&(t.node=r)}function Ie(e,t){if(t.length>0){let n=t.length>1?t:t[0];n&&(e.children=n)}}function Le(e,t,n){return r;function r(e,r,i,a){let o=Array.isArray(i.children)?n:t;return a?o(r,i,a):o(r,i)}}function Re(e,t){return n;function n(n,r,i,a){let o=Array.isArray(i.children),s=z(n);return t(r,i,a,o,{columnNumber:s?s.column-1:void 0,fileName:e,lineNumber:s?s.line:void 0},void 0)}}function ze(e,t){let n={},r,i;for(i in t.properties)if(i!==`children`&&Se.call(t.properties,i)){let a=He(e,i,t.properties[i]);if(a){let[i,o]=a;e.tableCellAlignToStyle&&i===`align`&&typeof o==`string`&&Ee.has(t.tagName)?r=o:n[i]=o}}if(r){let t=n.style||={};t[e.stylePropertyNameCase===`css`?`text-align`:`textAlign`]=r}return n}function Be(e,t){let n={};for(let r of t.attributes)if(r.type===`mdxJsxExpressionAttribute`)if(r.data&&r.data.estree&&e.evaluater){let t=r.data.estree.body[0];t.type;let i=t.expression;i.type;let a=i.properties[0];a.type,Object.assign(n,e.evaluater.evaluateExpression(a.argument))}else Ge(e,t.position);else{let i=r.name,a;if(r.value&&typeof r.value==`object`)if(r.value.data&&r.value.data.estree&&e.evaluater){let t=r.value.data.estree.body[0];t.type,a=e.evaluater.evaluateExpression(t.expression)}else Ge(e,t.position);else a=r.value===null||r.value;n[i]=a}return n}function Ve(e,t){let n=[],r=-1,i=e.passKeys?new Map:Ce;for(;++ri?0:i+t:t>i?i:t,n=n>0?n:0,r.length<1e4)o=Array.from(r),o.unshift(t,n),e.splice(...o);else for(n&&e.splice(t,n);a0?(V(e,e.length,0,t),e):t}var rt={}.hasOwnProperty;function it(e){let t={},n=-1;for(;++n13&&n<32||n>126&&n<160||n>55295&&n<57344||n>64975&&n<65008||(n&65535)==65535||(n&65535)==65534||n>1114111?`�`:String.fromCodePoint(n)}function U(e){return e.replace(/[\t\n\r ]+/g,` `).replace(/^ | $/g,``).toLowerCase().toUpperCase()}var W=ht(/[A-Za-z]/),G=ht(/[\dA-Za-z]/),ct=ht(/[#-'*+\--9=?A-Z^-~]/);function lt(e){return e!==null&&(e<32||e===127)}var ut=ht(/\d/),dt=ht(/[\dA-Fa-f]/),ft=ht(/[!-/:-@[-`{-~]/);function K(e){return e!==null&&e<-2}function q(e){return e!==null&&(e<0||e===32)}function J(e){return e===-2||e===-1||e===32}var pt=ht(/\p{P}|\p{S}/u),mt=ht(/\s/);function ht(e){return t;function t(t){return t!==null&&t>-1&&e.test(String.fromCharCode(t))}}function gt(e){let t=[],n=-1,r=0,i=0;for(;++n55295&&a<57344){let t=e.charCodeAt(n+1);a<56320&&t>56319&&t<57344?(o=String.fromCharCode(a,t),i=1):o=`�`}else o=String.fromCharCode(a);o&&=(t.push(e.slice(r,n),encodeURIComponent(o)),r=n+i+1,``),i&&=(n+=i,0)}return t.join(``)+e.slice(r)}function Y(e,t,n,r){let i=r?r-1:1/0,a=0;return o;function o(r){return J(r)?(e.enter(n),s(r)):t(r)}function s(r){return J(r)&&a++o))return;let n=t.events.length,a=n,s,c;for(;a--;)if(t.events[a][0]===`exit`&&t.events[a][1].type===`chunkFlow`){if(s){c=t.events[a][1].end;break}s=!0}for(_(r),e=n;er;){let r=n[i];t.containerState=r[1],r[0].exit.call(t,e)}n.length=r}function v(){i.write([null]),a=void 0,i=void 0,t.containerState._closeFlow=void 0}}function St(e,t,n){return Y(e,e.attempt(this.parser.constructs.document,t,n),`linePrefix`,this.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4)}function Ct(e){if(e===null||q(e)||mt(e))return 1;if(pt(e))return 2}function wt(e,t,n){let r=[],i=-1;for(;++i1&&e[n][1].end.offset-e[n][1].start.offset>1?2:1;let d={...e[r][1].end},f={...e[n][1].start};Ot(d,-c),Ot(f,c),o={type:c>1?`strongSequence`:`emphasisSequence`,start:d,end:{...e[r][1].end}},s={type:c>1?`strongSequence`:`emphasisSequence`,start:{...e[n][1].start},end:f},a={type:c>1?`strongText`:`emphasisText`,start:{...e[r][1].end},end:{...e[n][1].start}},i={type:c>1?`strong`:`emphasis`,start:{...o.start},end:{...s.end}},e[r][1].end={...o.start},e[n][1].start={...s.end},l=[],e[r][1].end.offset-e[r][1].start.offset&&(l=H(l,[[`enter`,e[r][1],t],[`exit`,e[r][1],t]])),l=H(l,[[`enter`,i,t],[`enter`,o,t],[`exit`,o,t],[`enter`,a,t]]),l=H(l,wt(t.parser.constructs.insideSpan.null,e.slice(r+1,n),t)),l=H(l,[[`exit`,a,t],[`enter`,s,t],[`exit`,s,t],[`exit`,i,t]]),e[n][1].end.offset-e[n][1].start.offset?(u=2,l=H(l,[[`enter`,e[n][1],t],[`exit`,e[n][1],t]])):u=0,V(e,r-1,n-r+3,l),n=r+l.length-u-2;break}}for(n=-1;++n0&&J(t)?Y(e,v,`linePrefix`,a+1)(t):v(t)}function v(t){return t===null||K(t)?e.check(Vt,h,b)(t):(e.enter(`codeFlowValue`),y(t))}function y(t){return t===null||K(t)?(e.exit(`codeFlowValue`),v(t)):(e.consume(t),y)}function b(n){return e.exit(`codeFenced`),t(n)}function x(e,t,n){let i=0;return a;function a(t){return e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),c}function c(t){return e.enter(`codeFencedFence`),J(t)?Y(e,l,`linePrefix`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4)(t):l(t)}function l(t){return t===s?(e.enter(`codeFencedFenceSequence`),u(t)):n(t)}function u(t){return t===s?(i++,e.consume(t),u):i>=o?(e.exit(`codeFencedFenceSequence`),J(t)?Y(e,d,`whitespace`)(t):d(t)):n(t)}function d(r){return r===null||K(r)?(e.exit(`codeFencedFence`),t(r)):n(r)}}}function Wt(e,t,n){let r=this;return i;function i(t){return t===null?n(t):(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),a)}function a(e){return r.parser.lazy[r.now().line]?n(e):t(e)}}var Gt={name:`codeIndented`,tokenize:qt},Kt={partial:!0,tokenize:Jt};function qt(e,t,n){let r=this;return i;function i(t){return e.enter(`codeIndented`),Y(e,a,`linePrefix`,5)(t)}function a(e){let t=r.events[r.events.length-1];return t&&t[1].type===`linePrefix`&&t[2].sliceSerialize(t[1],!0).length>=4?o(e):n(e)}function o(t){return t===null?c(t):K(t)?e.attempt(Kt,o,c)(t):(e.enter(`codeFlowValue`),s(t))}function s(t){return t===null||K(t)?(e.exit(`codeFlowValue`),o(t)):(e.consume(t),s)}function c(n){return e.exit(`codeIndented`),t(n)}}function Jt(e,t,n){let r=this;return i;function i(t){return r.parser.lazy[r.now().line]?n(t):K(t)?(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),i):Y(e,a,`linePrefix`,5)(t)}function a(e){let a=r.events[r.events.length-1];return a&&a[1].type===`linePrefix`&&a[2].sliceSerialize(a[1],!0).length>=4?t(e):K(e)?i(e):n(e)}}var Yt={name:`codeText`,previous:Zt,resolve:Xt,tokenize:Qt};function Xt(e){let t=e.length-4,n=3,r,i;if((e[n][1].type===`lineEnding`||e[n][1].type===`space`)&&(e[t][1].type===`lineEnding`||e[t][1].type===`space`)){for(r=n;++r=this.left.length+this.right.length)throw RangeError("Cannot access index `"+e+"` in a splice buffer of size `"+(this.left.length+this.right.length)+"`");return ethis.left.length?this.right.slice(this.right.length-n+this.left.length,this.right.length-e+this.left.length).reverse():this.left.slice(e).concat(this.right.slice(this.right.length-n+this.left.length).reverse())}splice(e,t,n){let r=t||0;this.setCursor(Math.trunc(e));let i=this.right.splice(this.right.length-r,1/0);return n&&en(this.left,n),i.reverse()}pop(){return this.setCursor(1/0),this.left.pop()}push(e){this.setCursor(1/0),this.left.push(e)}pushMany(e){this.setCursor(1/0),en(this.left,e)}unshift(e){this.setCursor(0),this.right.push(e)}unshiftMany(e){this.setCursor(0),en(this.right,e.reverse())}setCursor(e){if(!(e===this.left.length||e>this.left.length&&this.right.length===0||e<0&&this.left.length===0))if(e=4?t(i):e.interrupt(r.parser.constructs.flow,n,t)(i)}}function ln(e,t,n,r,i,a,o,s,c){let l=c||1/0,u=0;return d;function d(t){return t===60?(e.enter(r),e.enter(i),e.enter(a),e.consume(t),e.exit(a),f):t===null||t===32||t===41||lt(t)?n(t):(e.enter(r),e.enter(o),e.enter(s),e.enter(`chunkString`,{contentType:`string`}),h(t))}function f(n){return n===62?(e.enter(a),e.consume(n),e.exit(a),e.exit(i),e.exit(r),t):(e.enter(s),e.enter(`chunkString`,{contentType:`string`}),p(n))}function p(t){return t===62?(e.exit(`chunkString`),e.exit(s),f(t)):t===null||t===60||K(t)?n(t):(e.consume(t),t===92?m:p)}function m(t){return t===60||t===62||t===92?(e.consume(t),p):p(t)}function h(i){return!u&&(i===null||i===41||q(i))?(e.exit(`chunkString`),e.exit(s),e.exit(o),e.exit(r),t(i)):u999||l===null||l===91||l===93&&!c||l===94&&!s&&`_hiddenFootnoteSupport`in o.parser.constructs?n(l):l===93?(e.exit(a),e.enter(i),e.consume(l),e.exit(i),e.exit(r),t):K(l)?(e.enter(`lineEnding`),e.consume(l),e.exit(`lineEnding`),u):(e.enter(`chunkString`,{contentType:`string`}),d(l))}function d(t){return t===null||t===91||t===93||K(t)||s++>999?(e.exit(`chunkString`),u(t)):(e.consume(t),c||=!J(t),t===92?f:d)}function f(t){return t===91||t===92||t===93?(e.consume(t),s++,d):d(t)}}function dn(e,t,n,r,i,a){let o;return s;function s(t){return t===34||t===39||t===40?(e.enter(r),e.enter(i),e.consume(t),e.exit(i),o=t===40?41:t,c):n(t)}function c(n){return n===o?(e.enter(i),e.consume(n),e.exit(i),e.exit(r),t):(e.enter(a),l(n))}function l(t){return t===o?(e.exit(a),c(o)):t===null?n(t):K(t)?(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),Y(e,l,`linePrefix`)):(e.enter(`chunkString`,{contentType:`string`}),u(t))}function u(t){return t===o||t===null||K(t)?(e.exit(`chunkString`),l(t)):(e.consume(t),t===92?d:u)}function d(t){return t===o||t===92?(e.consume(t),u):u(t)}}function fn(e,t){let n;return r;function r(i){return K(i)?(e.enter(`lineEnding`),e.consume(i),e.exit(`lineEnding`),n=!0,r):J(i)?Y(e,r,n?`linePrefix`:`lineSuffix`)(i):t(i)}}var pn={name:`definition`,tokenize:hn},mn={partial:!0,tokenize:gn};function hn(e,t,n){let r=this,i;return a;function a(t){return e.enter(`definition`),o(t)}function o(t){return un.call(r,e,s,n,`definitionLabel`,`definitionLabelMarker`,`definitionLabelString`)(t)}function s(t){return i=U(r.sliceSerialize(r.events[r.events.length-1][1]).slice(1,-1)),t===58?(e.enter(`definitionMarker`),e.consume(t),e.exit(`definitionMarker`),c):n(t)}function c(t){return q(t)?fn(e,l)(t):l(t)}function l(t){return ln(e,u,n,`definitionDestination`,`definitionDestinationLiteral`,`definitionDestinationLiteralMarker`,`definitionDestinationRaw`,`definitionDestinationString`)(t)}function u(t){return e.attempt(mn,d,d)(t)}function d(t){return J(t)?Y(e,f,`whitespace`)(t):f(t)}function f(a){return a===null||K(a)?(e.exit(`definition`),r.parser.defined.push(i),t(a)):n(a)}}function gn(e,t,n){return r;function r(t){return q(t)?fn(e,i)(t):n(t)}function i(t){return dn(e,a,n,`definitionTitle`,`definitionTitleMarker`,`definitionTitleString`)(t)}function a(t){return J(t)?Y(e,o,`whitespace`)(t):o(t)}function o(e){return e===null||K(e)?t(e):n(e)}}var _n={name:`hardBreakEscape`,tokenize:vn};function vn(e,t,n){return r;function r(t){return e.enter(`hardBreakEscape`),e.consume(t),i}function i(r){return K(r)?(e.exit(`hardBreakEscape`),t(r)):n(r)}}var yn={name:`headingAtx`,resolve:bn,tokenize:xn};function bn(e,t){let n=e.length-2,r=3,i,a;return e[r][1].type===`whitespace`&&(r+=2),n-2>r&&e[n][1].type===`whitespace`&&(n-=2),e[n][1].type===`atxHeadingSequence`&&(r===n-1||n-4>r&&e[n-2][1].type===`whitespace`)&&(n-=r+1===n?2:4),n>r&&(i={type:`atxHeadingText`,start:e[r][1].start,end:e[n][1].end},a={type:`chunkText`,start:e[r][1].start,end:e[n][1].end,contentType:`text`},V(e,r,n-r+1,[[`enter`,i,t],[`enter`,a,t],[`exit`,a,t],[`exit`,i,t]])),e}function xn(e,t,n){let r=0;return i;function i(t){return e.enter(`atxHeading`),a(t)}function a(t){return e.enter(`atxHeadingSequence`),o(t)}function o(t){return t===35&&r++<6?(e.consume(t),o):t===null||q(t)?(e.exit(`atxHeadingSequence`),s(t)):n(t)}function s(n){return n===35?(e.enter(`atxHeadingSequence`),c(n)):n===null||K(n)?(e.exit(`atxHeading`),t(n)):J(n)?Y(e,s,`whitespace`)(n):(e.enter(`atxHeadingText`),l(n))}function c(t){return t===35?(e.consume(t),c):(e.exit(`atxHeadingSequence`),s(t))}function l(t){return t===null||t===35||q(t)?(e.exit(`atxHeadingText`),s(t)):(e.consume(t),l)}}var Sn=`address.article.aside.base.basefont.blockquote.body.caption.center.col.colgroup.dd.details.dialog.dir.div.dl.dt.fieldset.figcaption.figure.footer.form.frame.frameset.h1.h2.h3.h4.h5.h6.head.header.hr.html.iframe.legend.li.link.main.menu.menuitem.nav.noframes.ol.optgroup.option.p.param.search.section.summary.table.tbody.td.tfoot.th.thead.title.tr.track.ul`.split(`.`),Cn=[`pre`,`script`,`style`,`textarea`],wn={concrete:!0,name:`htmlFlow`,resolveTo:Dn,tokenize:On},Tn={partial:!0,tokenize:An},En={partial:!0,tokenize:kn};function Dn(e){let t=e.length;for(;t--&&!(e[t][0]===`enter`&&e[t][1].type===`htmlFlow`););return t>1&&e[t-2][1].type===`linePrefix`&&(e[t][1].start=e[t-2][1].start,e[t+1][1].start=e[t-2][1].start,e.splice(t-2,2)),e}function On(e,t,n){let r=this,i,a,o,s,c;return l;function l(e){return u(e)}function u(t){return e.enter(`htmlFlow`),e.enter(`htmlFlowData`),e.consume(t),d}function d(s){return s===33?(e.consume(s),f):s===47?(e.consume(s),a=!0,h):s===63?(e.consume(s),i=3,r.interrupt?t:I):W(s)?(e.consume(s),o=String.fromCharCode(s),g):n(s)}function f(a){return a===45?(e.consume(a),i=2,p):a===91?(e.consume(a),i=5,s=0,m):W(a)?(e.consume(a),i=4,r.interrupt?t:I):n(a)}function p(i){return i===45?(e.consume(i),r.interrupt?t:I):n(i)}function m(i){return i===`CDATA[`.charCodeAt(s++)?(e.consume(i),s===6?r.interrupt?t:O:m):n(i)}function h(t){return W(t)?(e.consume(t),o=String.fromCharCode(t),g):n(t)}function g(s){if(s===null||s===47||s===62||q(s)){let c=s===47,l=o.toLowerCase();return!c&&!a&&Cn.includes(l)?(i=1,r.interrupt?t(s):O(s)):Sn.includes(o.toLowerCase())?(i=6,c?(e.consume(s),_):r.interrupt?t(s):O(s)):(i=7,r.interrupt&&!r.parser.lazy[r.now().line]?n(s):a?v(s):y(s))}return s===45||G(s)?(e.consume(s),o+=String.fromCharCode(s),g):n(s)}function _(i){return i===62?(e.consume(i),r.interrupt?t:O):n(i)}function v(t){return J(t)?(e.consume(t),v):E(t)}function y(t){return t===47?(e.consume(t),E):t===58||t===95||W(t)?(e.consume(t),b):J(t)?(e.consume(t),y):E(t)}function b(t){return t===45||t===46||t===58||t===95||G(t)?(e.consume(t),b):x(t)}function x(t){return t===61?(e.consume(t),S):J(t)?(e.consume(t),x):y(t)}function S(t){return t===null||t===60||t===61||t===62||t===96?n(t):t===34||t===39?(e.consume(t),c=t,C):J(t)?(e.consume(t),S):w(t)}function C(t){return t===c?(e.consume(t),c=null,T):t===null||K(t)?n(t):(e.consume(t),C)}function w(t){return t===null||t===34||t===39||t===47||t===60||t===61||t===62||t===96||q(t)?x(t):(e.consume(t),w)}function T(e){return e===47||e===62||J(e)?y(e):n(e)}function E(t){return t===62?(e.consume(t),D):n(t)}function D(t){return t===null||K(t)?O(t):J(t)?(e.consume(t),D):n(t)}function O(t){return t===45&&i===2?(e.consume(t),M):t===60&&i===1?(e.consume(t),N):t===62&&i===4?(e.consume(t),L):t===63&&i===3?(e.consume(t),I):t===93&&i===5?(e.consume(t),F):K(t)&&(i===6||i===7)?(e.exit(`htmlFlowData`),e.check(Tn,R,k)(t)):t===null||K(t)?(e.exit(`htmlFlowData`),k(t)):(e.consume(t),O)}function k(t){return e.check(En,A,R)(t)}function A(t){return e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),j}function j(t){return t===null||K(t)?k(t):(e.enter(`htmlFlowData`),O(t))}function M(t){return t===45?(e.consume(t),I):O(t)}function N(t){return t===47?(e.consume(t),o=``,P):O(t)}function P(t){if(t===62){let n=o.toLowerCase();return Cn.includes(n)?(e.consume(t),L):O(t)}return W(t)&&o.length<8?(e.consume(t),o+=String.fromCharCode(t),P):O(t)}function F(t){return t===93?(e.consume(t),I):O(t)}function I(t){return t===62?(e.consume(t),L):t===45&&i===2?(e.consume(t),I):O(t)}function L(t){return t===null||K(t)?(e.exit(`htmlFlowData`),R(t)):(e.consume(t),L)}function R(n){return e.exit(`htmlFlow`),t(n)}}function kn(e,t,n){let r=this;return i;function i(t){return K(t)?(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),a):n(t)}function a(e){return r.parser.lazy[r.now().line]?n(e):t(e)}}function An(e,t,n){return r;function r(r){return e.enter(`lineEnding`),e.consume(r),e.exit(`lineEnding`),e.attempt(jt,t,n)}}var jn={name:`htmlText`,tokenize:Mn};function Mn(e,t,n){let r=this,i,a,o;return s;function s(t){return e.enter(`htmlText`),e.enter(`htmlTextData`),e.consume(t),c}function c(t){return t===33?(e.consume(t),l):t===47?(e.consume(t),x):t===63?(e.consume(t),y):W(t)?(e.consume(t),w):n(t)}function l(t){return t===45?(e.consume(t),u):t===91?(e.consume(t),a=0,m):W(t)?(e.consume(t),v):n(t)}function u(t){return t===45?(e.consume(t),p):n(t)}function d(t){return t===null?n(t):t===45?(e.consume(t),f):K(t)?(o=d,N(t)):(e.consume(t),d)}function f(t){return t===45?(e.consume(t),p):d(t)}function p(e){return e===62?M(e):e===45?f(e):d(e)}function m(t){return t===`CDATA[`.charCodeAt(a++)?(e.consume(t),a===6?h:m):n(t)}function h(t){return t===null?n(t):t===93?(e.consume(t),g):K(t)?(o=h,N(t)):(e.consume(t),h)}function g(t){return t===93?(e.consume(t),_):h(t)}function _(t){return t===62?M(t):t===93?(e.consume(t),_):h(t)}function v(t){return t===null||t===62?M(t):K(t)?(o=v,N(t)):(e.consume(t),v)}function y(t){return t===null?n(t):t===63?(e.consume(t),b):K(t)?(o=y,N(t)):(e.consume(t),y)}function b(e){return e===62?M(e):y(e)}function x(t){return W(t)?(e.consume(t),S):n(t)}function S(t){return t===45||G(t)?(e.consume(t),S):C(t)}function C(t){return K(t)?(o=C,N(t)):J(t)?(e.consume(t),C):M(t)}function w(t){return t===45||G(t)?(e.consume(t),w):t===47||t===62||q(t)?T(t):n(t)}function T(t){return t===47?(e.consume(t),M):t===58||t===95||W(t)?(e.consume(t),E):K(t)?(o=T,N(t)):J(t)?(e.consume(t),T):M(t)}function E(t){return t===45||t===46||t===58||t===95||G(t)?(e.consume(t),E):D(t)}function D(t){return t===61?(e.consume(t),O):K(t)?(o=D,N(t)):J(t)?(e.consume(t),D):T(t)}function O(t){return t===null||t===60||t===61||t===62||t===96?n(t):t===34||t===39?(e.consume(t),i=t,k):K(t)?(o=O,N(t)):J(t)?(e.consume(t),O):(e.consume(t),A)}function k(t){return t===i?(e.consume(t),i=void 0,j):t===null?n(t):K(t)?(o=k,N(t)):(e.consume(t),k)}function A(t){return t===null||t===34||t===39||t===60||t===61||t===96?n(t):t===47||t===62||q(t)?T(t):(e.consume(t),A)}function j(e){return e===47||e===62||q(e)?T(e):n(e)}function M(r){return r===62?(e.consume(r),e.exit(`htmlTextData`),e.exit(`htmlText`),t):n(r)}function N(t){return e.exit(`htmlTextData`),e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),P}function P(t){return J(t)?Y(e,F,`linePrefix`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4)(t):F(t)}function F(t){return e.enter(`htmlTextData`),o(t)}}var Nn={name:`labelEnd`,resolveAll:Ln,resolveTo:Rn,tokenize:zn},Pn={tokenize:Bn},Fn={tokenize:Vn},In={tokenize:Hn};function Ln(e){let t=-1,n=[];for(;++t=3&&(a===null||K(a))?(e.exit(`thematicBreak`),t(a)):n(a)}function c(t){return t===i?(e.consume(t),r++,c):(e.exit(`thematicBreakSequence`),J(t)?Y(e,s,`whitespace`)(t):s(t))}}var X={continuation:{tokenize:er},exit:nr,name:`list`,tokenize:$n},Zn={partial:!0,tokenize:rr},Qn={partial:!0,tokenize:tr};function $n(e,t,n){let r=this,i=r.events[r.events.length-1],a=i&&i[1].type===`linePrefix`?i[2].sliceSerialize(i[1],!0).length:0,o=0;return s;function s(t){let i=r.containerState.type||(t===42||t===43||t===45?`listUnordered`:`listOrdered`);if(i===`listUnordered`?!r.containerState.marker||t===r.containerState.marker:ut(t)){if(r.containerState.type||(r.containerState.type=i,e.enter(i,{_container:!0})),i===`listUnordered`)return e.enter(`listItemPrefix`),t===42||t===45?e.check(Yn,n,l)(t):l(t);if(!r.interrupt||t===49)return e.enter(`listItemPrefix`),e.enter(`listItemValue`),c(t)}return n(t)}function c(t){return ut(t)&&++o<10?(e.consume(t),c):(!r.interrupt||o<2)&&(r.containerState.marker?t===r.containerState.marker:t===41||t===46)?(e.exit(`listItemValue`),l(t)):n(t)}function l(t){return e.enter(`listItemMarker`),e.consume(t),e.exit(`listItemMarker`),r.containerState.marker=r.containerState.marker||t,e.check(jt,r.interrupt?n:u,e.attempt(Zn,f,d))}function u(e){return r.containerState.initialBlankLine=!0,a++,f(e)}function d(t){return J(t)?(e.enter(`listItemPrefixWhitespace`),e.consume(t),e.exit(`listItemPrefixWhitespace`),f):n(t)}function f(n){return r.containerState.size=a+r.sliceSerialize(e.exit(`listItemPrefix`),!0).length,t(n)}}function er(e,t,n){let r=this;return r.containerState._closeFlow=void 0,e.check(jt,i,a);function i(n){return r.containerState.furtherBlankLines=r.containerState.furtherBlankLines||r.containerState.initialBlankLine,Y(e,t,`listItemIndent`,r.containerState.size+1)(n)}function a(n){return r.containerState.furtherBlankLines||!J(n)?(r.containerState.furtherBlankLines=void 0,r.containerState.initialBlankLine=void 0,o(n)):(r.containerState.furtherBlankLines=void 0,r.containerState.initialBlankLine=void 0,e.attempt(Qn,t,o)(n))}function o(i){return r.containerState._closeFlow=!0,r.interrupt=void 0,Y(e,e.attempt(X,t,n),`linePrefix`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4)(i)}}function tr(e,t,n){let r=this;return Y(e,i,`listItemIndent`,r.containerState.size+1);function i(e){let i=r.events[r.events.length-1];return i&&i[1].type===`listItemIndent`&&i[2].sliceSerialize(i[1],!0).length===r.containerState.size?t(e):n(e)}}function nr(e){e.exit(this.containerState.type)}function rr(e,t,n){let r=this;return Y(e,i,`listItemPrefixWhitespace`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:5);function i(e){let i=r.events[r.events.length-1];return!J(e)&&i&&i[1].type===`listItemPrefixWhitespace`?t(e):n(e)}}var ir={name:`setextUnderline`,resolveTo:ar,tokenize:or};function ar(e,t){let n=e.length,r,i,a;for(;n--;)if(e[n][0]===`enter`){if(e[n][1].type===`content`){r=n;break}e[n][1].type===`paragraph`&&(i=n)}else e[n][1].type===`content`&&e.splice(n,1),!a&&e[n][1].type===`definition`&&(a=n);let o={type:`setextHeading`,start:{...e[r][1].start},end:{...e[e.length-1][1].end}};return e[i][1].type=`setextHeadingText`,a?(e.splice(i,0,[`enter`,o,t]),e.splice(a+1,0,[`exit`,e[r][1],t]),e[r][1].end={...e[a][1].end}):e[r][1]=o,e.push([`exit`,o,t]),e}function or(e,t,n){let r=this,i;return a;function a(t){let a=r.events.length,s;for(;a--;)if(r.events[a][1].type!==`lineEnding`&&r.events[a][1].type!==`linePrefix`&&r.events[a][1].type!==`content`){s=r.events[a][1].type===`paragraph`;break}return!r.parser.lazy[r.now().line]&&(r.interrupt||s)?(e.enter(`setextHeadingLine`),i=t,o(t)):n(t)}function o(t){return e.enter(`setextHeadingLineSequence`),s(t)}function s(t){return t===i?(e.consume(t),s):(e.exit(`setextHeadingLineSequence`),J(t)?Y(e,c,`lineSuffix`)(t):c(t))}function c(r){return r===null||K(r)?(e.exit(`setextHeadingLine`),t(r)):n(r)}}var sr={tokenize:cr};function cr(e){let t=this,n=e.attempt(jt,r,e.attempt(this.parser.constructs.flowInitial,i,Y(e,e.attempt(this.parser.constructs.flow,i,e.attempt(rn,i)),`linePrefix`)));return n;function r(r){if(r===null){e.consume(r);return}return e.enter(`lineEndingBlank`),e.consume(r),e.exit(`lineEndingBlank`),t.currentConstruct=void 0,n}function i(r){if(r===null){e.consume(r);return}return e.enter(`lineEnding`),e.consume(r),e.exit(`lineEnding`),t.currentConstruct=void 0,n}}var lr={resolveAll:pr()},ur=fr(`string`),dr=fr(`text`);function fr(e){return{resolveAll:pr(e===`text`?mr:void 0),tokenize:t};function t(t){let n=this,r=this.parser.constructs[e],i=t.attempt(r,a,o);return a;function a(e){return c(e)?i(e):o(e)}function o(e){if(e===null){t.consume(e);return}return t.enter(`data`),t.consume(e),s}function s(e){return c(e)?(t.exit(`data`),i(e)):(t.consume(e),s)}function c(e){if(e===null)return!0;let t=r[e],i=-1;if(t)for(;++iCr,contentInitial:()=>_r,disable:()=>wr,document:()=>gr,flow:()=>yr,flowInitial:()=>vr,insideSpan:()=>Sr,string:()=>br,text:()=>xr}),gr={42:X,43:X,45:X,48:X,49:X,50:X,51:X,52:X,53:X,54:X,55:X,56:X,57:X,62:Nt},_r={91:pn},vr={[-2]:Gt,[-1]:Gt,32:Gt},yr={35:yn,42:Yn,45:[ir,Yn],60:wn,61:ir,95:Yn,96:Ht,126:Ht},br={38:zt,92:Lt},xr={[-5]:qn,[-4]:qn,[-3]:qn,33:Un,38:zt,42:Tt,60:[kt,jn],91:Gn,92:[_n,Lt],93:Nn,95:Tt,96:Yt},Sr={null:[Tt,lr]},Cr={null:[42,95]},wr={null:[]};function Tr(e,t,n){let r={_bufferIndex:-1,_index:0,line:n&&n.line||1,column:n&&n.column||1,offset:n&&n.offset||0},i={},a=[],o=[],s=[],c={attempt:C(x),check:C(S),consume:v,enter:y,exit:b,interrupt:C(S,{interrupt:!0})},l={code:null,containerState:{},defineSkip:h,events:[],now:m,parser:e,previous:null,sliceSerialize:f,sliceStream:p,write:d},u=t.tokenize.call(l,c);return t.resolveAll&&a.push(t),l;function d(e){return o=H(o,e),g(),o[o.length-1]===null?(w(t,0),l.events=wt(a,l.events,l),l.events):[]}function f(e,t){return Dr(p(e),t)}function p(e){return Er(o,e)}function m(){let{_bufferIndex:e,_index:t,line:n,column:i,offset:a}=r;return{_bufferIndex:e,_index:t,line:n,column:i,offset:a}}function h(e){i[e.line]=e.column,E()}function g(){let e;for(;r._index-1){let e=o[0];typeof e==`string`?o[0]=e.slice(r):o.shift()}a>0&&o.push(e[i].slice(0,a))}return o}function Dr(e,t){let n=-1,r=[],i;for(;++n0){let e=a.tokenStack[a.tokenStack.length-1];(e[1]||Vr).call(a,void 0,e[0])}for(r.position={start:Rr(e.length>0?e[0][1].start:{line:1,column:1,offset:0}),end:Rr(e.length>0?e[e.length-2][1].end:{line:1,column:1,offset:0})},d=-1;++de*t/u(e,t),1),i=[];return n.forEach((e,t)=>{let n=r/e;for(let r=0;r=4352&&e<=4447||e>=11904&&e<=12350||e>=12353&&e<=13311||e>=13312&&e<=19903||e>=19968&&e<=40959||e>=40960&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65072&&e<=65103||e>=65280&&e<=65376||e>=65504&&e<=65510||e>=127744&&e<=129791||e>=131072&&e<=262141}function m(e,t){let n=0;for(let t of e)n+=p(t.codePointAt(0)??0)?2:1;if(n<=t)return e;let r=0,i=``;for(let n of e){let e=p(n.codePointAt(0)??0)?2:1;if(r+e>t-1)break;i+=n,r+=e}return`${i}…`}function h({panes:e,zoomed:t,onFocus:n,onReorder:r}){let i=(0,l.useRef)(null),a=(0,l.useRef)(null),o=(0,l.useRef)(null),c=(0,l.useRef)(!1),[u,d]=(0,l.useState)(null),[f,p]=(0,l.useState)(null),m=t===null&&e.length>1,h=()=>{i.current=null,a.current=null,o.current=null,c.current=!1,d(null),p(null)};return{draggingPane:u,dragOverPane:f,reorderable:m,endPaneDrag:h,onPaneDragStart:(e,t)=>{e.target.closest(`button`)||(n(t),!(e.button!==0||!m)&&(i.current=t,a.current={x:e.clientX,y:e.clientY},c.current=!1,e.currentTarget.setPointerCapture(e.pointerId)))},onPaneDragMove:e=>{let t=i.current,n=a.current;if(t===null||n===null||!c.current&&Math.hypot(e.clientX-n.x,e.clientY-n.y)<4)return;c.current=!0,d(t);let r=document.elementFromPoint(e.clientX,e.clientY)?.closest(`[data-pane-id]`),s=r?Number(r.getAttribute(`data-pane-id`)):null,l=s!==null&&s!==t?s:null;o.current=l,p(l)},onPaneDragEnd:()=>{let t=i.current,n=o.current;t!==null&&c.current&&n!==null&&r(s(e,t,n)),h()}}}function g({repo:e,socketRef:n,viewsRef:r,pendingRef:i,sentSizesRef:o,lastActiveByRepoRef:s,expectCreateRef:c,setPending:u,setPanes:d,setActive:f,setZoomed:p,setTitles:m,setOwnsSize:h}){(0,l.useLayoutEffect)(()=>{c.current=0;let l=!1,g,_=()=>{r.current.forEach(e=>e.term.dispose()),r.current.clear(),i.current.clear(),o.current.clear()},v=()=>{u(null),d([]),f(null),p(null),m({}),h(!0),_();let ee=location.protocol===`https:`?`wss:`:`ws:`,y=new WebSocket(`${ee}//${location.host}/ws/term?repo=${encodeURIComponent(e)}`);y.binaryType=`arraybuffer`,n.current=y,y.onmessage=l=>{if(n.current!==y)return;if(typeof l.data==`string`){let n=JSON.parse(l.data);if(n.type===`pending`)u(n.count);else if(n.type===`created`){let t=n.pane;o.current.set(t,{rows:n.rows,cols:n.cols}),typeof n.title==`string`&&n.title&&m(e=>({...e,[t]:n.title})),d(e=>[...e,t]),c.current>0?(--c.current,f(t),s.current.set(e,t)):s.current.get(e)===t&&f(t)}else n.type===`exited`?(d(e=>e.filter(e=>e!==n.pane)),f(e=>e===n.pane?null:e),p(e=>e===n.pane?null:e),i.current.delete(n.pane),o.current.delete(n.pane),m(e=>{if(!(n.pane in e))return e;let t={...e};return delete t[n.pane],t})):n.type===`resized`?(o.current.set(n.pane,{rows:n.rows,cols:n.cols}),r.current.get(n.pane)?.term.resize(n.cols,n.rows)):n.type===`size_owner`?h(n.owned):n.type===`reordered`?d(e=>a(e,n.order)):n.type===`error`&&(c.current=0,t.error(n.message));return}let g=new Uint8Array(l.data);if(g.length<4)return;let _=new DataView(g.buffer).getUint32(0,!0),v=g.subarray(4),ee=r.current.get(_);if(ee)ee.term.write(v);else{let e=i.current.get(_)??[];e.push(v),i.current.set(_,e)}},y.onclose=()=>{l||(g=setTimeout(v,1e3))}};return v(),()=>{l=!0,g&&clearTimeout(g),n.current?.close(),_()}},[e])}var _=Object.defineProperty,v=Object.getOwnPropertyDescriptor,ee=(e,t)=>{for(var n in t)_(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,r)=>{for(var i=r>1?void 0:r?v(t,n):t,a=e.length-1,o;a>=0;a--)(o=e[a])&&(i=(r?o(t,n,i):o(i))||i);return r&&i&&_(t,n,i),i},b=(e,t)=>(n,r)=>t(n,r,e),te=`Terminal input`,x={get:()=>te,set:e=>te=e},ne=`Too much output to announce, navigate to rows manually to read`,S={get:()=>ne,set:e=>ne=e};function re(e){return e.replace(/\r?\n/g,`\r`)}function C(e,t){return t?`\x1B[200~`+e+`\x1B[201~`:e}function ie(e,t){e.clipboardData&&e.clipboardData.setData(`text/plain`,t.selectionText),e.preventDefault()}function w(e,t,n,r){e.stopPropagation(),e.clipboardData&&ae(e.clipboardData.getData(`text/plain`),t,n,r)}function ae(e,t,n,r){e=re(e),e=C(e,n.decPrivateModes.bracketedPasteMode&&r.rawOptions.ignoreBracketedPasteMode!==!0),n.triggerDataEvent(e,!0),t.value=``}function oe(e,t,n){let r=n.getBoundingClientRect(),i=e.clientX-r.left-10,a=e.clientY-r.top-10;t.style.width=`20px`,t.style.height=`20px`,t.style.left=`${i}px`,t.style.top=`${a}px`,t.style.zIndex=`1000`,t.focus()}function se(e,t,n,r,i){oe(e,t,n),i&&r.rightClickSelect(e),t.value=r.selectionText,t.select()}function T(e){return e>65535?(e-=65536,String.fromCharCode((e>>10)+55296)+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)}function ce(e,t=0,n=e.length){let r=``;for(let i=t;i65535?(t-=65536,r+=String.fromCharCode((t>>10)+55296)+String.fromCharCode(t%1024+56320)):r+=String.fromCharCode(t)}return r}var le=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){let n=e.length;if(!n)return 0;let r=0,i=0;if(this._interim){let n=e.charCodeAt(i++);56320<=n&&n<=57343?t[r++]=(this._interim-55296)*1024+n-56320+65536:(t[r++]=this._interim,t[r++]=n),this._interim=0}for(let a=i;a=n)return this._interim=i,r;let o=e.charCodeAt(a);56320<=o&&o<=57343?t[r++]=(i-55296)*1024+o-56320+65536:(t[r++]=i,t[r++]=o);continue}i!==65279&&(t[r++]=i)}return r}},ue=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){let n=e.length;if(!n)return 0;let r=0,i,a,o,s,c=0,l=0;if(this.interim[0]){let i=!1,a=this.interim[0];a&=(a&224)==192?31:(a&240)==224?15:7;let o=0,s;for(;(s=this.interim[++o]&63)&&o<4;)a<<=6,a|=s;let c=(this.interim[0]&224)==192?2:(this.interim[0]&240)==224?3:4,u=c-o;for(;l=n)return 0;if(s=e[l++],(s&192)!=128){l--,i=!0;break}else this.interim[o++]=s,a<<=6,a|=s&63}i||(c===2?a<128?l--:t[r++]=a:c===3?a<2048||a>=55296&&a<=57343||a===65279||(t[r++]=a):a<65536||a>1114111||(t[r++]=a)),this.interim.fill(0)}let u=n-4,d=l;for(;d=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(c=(i&31)<<6|a&63,c<128){d--;continue}t[r++]=c}else if((i&240)==224){if(d>=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,r;if(o=e[d++],(o&192)!=128){d--;continue}if(c=(i&15)<<12|(a&63)<<6|o&63,c<2048||c>=55296&&c<=57343||c===65279)continue;t[r++]=c}else if((i&248)==240){if(d>=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,r;if(o=e[d++],(o&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,this.interim[2]=o,r;if(s=e[d++],(s&192)!=128){d--;continue}if(c=(i&7)<<18|(a&63)<<12|(o&63)<<6|s&63,c<65536||c>1114111)continue;t[r++]=c}}return r}},de=``,fe=` `,pe=class e{constructor(){this.fg=0,this.bg=0,this.extended=new me}static toColorRGB(e){return[e>>>16&255,e>>>8&255,e&255]}static fromColorRGB(e){return(e[0]&255)<<16|(e[1]&255)<<8|e[2]&255}clone(){let t=new e;return t.fg=this.fg,t.bg=this.bg,t.extended=this.extended.clone(),t}isInverse(){return this.fg&67108864}isBold(){return this.fg&134217728}isUnderline(){return this.hasExtendedAttrs()&&this.extended.underlineStyle!==0?1:this.fg&268435456}isBlink(){return this.fg&536870912}isInvisible(){return this.fg&1073741824}isItalic(){return this.bg&67108864}isDim(){return this.bg&134217728}isStrikethrough(){return this.fg&2147483648}isProtected(){return this.bg&536870912}isOverline(){return this.bg&1073741824}getFgColorMode(){return this.fg&50331648}getBgColorMode(){return this.bg&50331648}isFgRGB(){return(this.fg&50331648)==50331648}isBgRGB(){return(this.bg&50331648)==50331648}isFgPalette(){return(this.fg&50331648)==16777216||(this.fg&50331648)==33554432}isBgPalette(){return(this.bg&50331648)==16777216||(this.bg&50331648)==33554432}isFgDefault(){return(this.fg&50331648)==0}isBgDefault(){return(this.bg&50331648)==0}isAttributeDefault(){return this.fg===0&&this.bg===0}getFgColor(){switch(this.fg&50331648){case 16777216:case 33554432:return this.fg&255;case 50331648:return this.fg&16777215;default:return-1}}getBgColor(){switch(this.bg&50331648){case 16777216:case 33554432:return this.bg&255;case 50331648:return this.bg&16777215;default:return-1}}hasExtendedAttrs(){return this.bg&268435456}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(this.bg&268435456&&~this.extended.underlineColor)switch(this.extended.underlineColor&50331648){case 16777216:case 33554432:return this.extended.underlineColor&255;case 50331648:return this.extended.underlineColor&16777215;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return this.bg&268435456&&~this.extended.underlineColor?this.extended.underlineColor&50331648:this.getFgColorMode()}isUnderlineColorRGB(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==50331648:this.isFgRGB()}isUnderlineColorPalette(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==16777216||(this.extended.underlineColor&50331648)==33554432:this.isFgPalette()}isUnderlineColorDefault(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==0:this.isFgDefault()}getUnderlineStyle(){return this.fg&268435456?this.bg&268435456?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}},me=class e{constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}get ext(){return this._urlId?this._ext&-469762049|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(this._ext&469762048)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return this._ext&67108863}set underlineColor(e){this._ext&=-67108864,this._ext|=e&67108863}get urlId(){return this._urlId}set urlId(e){this._urlId=e}get underlineVariantOffset(){let e=(this._ext&3758096384)>>29;return e<0?e^4294967288:e}set underlineVariantOffset(e){this._ext&=536870911,this._ext|=e<<29&3758096384}clone(){return new e(this._ext,this._urlId)}isEmpty(){return this.underlineStyle===0&&this._urlId===0}},E=class e extends pe{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new me,this.combinedData=``}static fromCharData(t){let n=new e;return n.setFromCharData(t),n}isCombined(){return this.content&2097152}getWidth(){return this.content>>22}getChars(){return this.content&2097152?this.combinedData:this.content&2097151?T(this.content&2097151):``}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):this.content&2097151}setFromCharData(e){this.fg=e[0],this.bg=0;let t=!1;if(e[1].length>2)t=!0;else if(e[1].length===2){let n=e[1].charCodeAt(0);if(55296<=n&&n<=56319){let r=e[1].charCodeAt(1);56320<=r&&r<=57343?this.content=(n-55296)*1024+r-56320+65536|e[2]<<22:t=!0}else t=!0}else this.content=e[1].charCodeAt(0)|e[2]<<22;t&&(this.combinedData=e[1],this.content=2097152|e[2]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}},he=`di$target`,ge=`di$dependencies`,_e=new Map;function ve(e){return e[ge]||[]}function D(e){if(_e.has(e))return _e.get(e);let t=function(e,n,r){if(arguments.length!==3)throw Error(`@IServiceName-decorator can only be used to decorate a parameter`);ye(t,e,r)};return t._id=e,_e.set(e,t),t}function ye(e,t,n){t[he]===t?t[ge].push({id:e,index:n}):(t[ge]=[{id:e,index:n}],t[he]=t)}var O=D(`BufferService`),be=D(`CoreMouseService`),xe=D(`CoreService`),Se=D(`CharsetService`),Ce=D(`InstantiationService`),we=D(`LogService`),k=D(`OptionsService`),Te=D(`OscLinkService`),Ee=D(`UnicodeService`),De=D(`DecorationService`),Oe=class{constructor(e,t,n){this._bufferService=e,this._optionsService=t,this._oscLinkService=n}provideLinks(e,t){let n=this._bufferService.buffer.lines.get(e-1);if(!n){t(void 0);return}let r=[],i=this._optionsService.rawOptions.linkHandler,a=new E,o=n.getTrimmedLength(),s=-1,c=-1,l=!1;for(let t=0;ti?i.activate(e,t,a):ke(e,t),hover:(e,t)=>i?.hover?.(e,t,a),leave:(e,t)=>i?.leave?.(e,t,a)})}l=!1,a.hasExtendedAttrs()&&a.extended.urlId?(c=t,s=a.extended.urlId):(c=-1,s=-1)}}t(r)}};Oe=y([b(0,O),b(1,k),b(2,Te)],Oe);function ke(e,t){if(confirm(`Do you want to navigate to ${t}? - -WARNING: This link could potentially be dangerous`)){let e=window.open();if(e){try{e.opener=null}catch{}e.location.href=t}else console.warn(`Opening link blocked as opener could not be cleared`)}}var Ae=D(`CharSizeService`),je=D(`CoreBrowserService`),Me=D(`MouseService`),Ne=D(`RenderService`),Pe=D(`SelectionService`),Fe=D(`CharacterJoinerService`),Ie=D(`ThemeService`),Le=D(`LinkProviderService`),Re=new class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(e){setTimeout(()=>{throw e.stack?We.isErrorNoTelemetry(e)?new We(e.message+` - -`+e.stack):Error(e.message+` - -`+e.stack):e},0)}}addListener(e){return this.listeners.push(e),()=>{this._removeListener(e)}}emit(e){this.listeners.forEach(t=>{t(e)})}_removeListener(e){this.listeners.splice(this.listeners.indexOf(e),1)}setUnexpectedErrorHandler(e){this.unexpectedErrorHandler=e}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(e){this.unexpectedErrorHandler(e),this.emit(e)}onUnexpectedExternalError(e){this.unexpectedErrorHandler(e)}};function ze(e){Ve(e)||Re.onUnexpectedError(e)}var Be=`Canceled`;function Ve(e){return e instanceof He||e instanceof Error&&e.name===Be&&e.message===Be}var He=class extends Error{constructor(){super(Be),this.name=this.message}};function Ue(e){return Error(e?`Illegal argument: ${e}`:`Illegal argument`)}var We=class e extends Error{constructor(e){super(e),this.name=`CodeExpectedError`}static fromError(t){if(t instanceof e)return t;let n=new e;return n.message=t.message,n.stack=t.stack,n}static isErrorNoTelemetry(e){return e.name===`CodeExpectedError`}},Ge=class e extends Error{constructor(t){super(t||`An unexpected bug occurred.`),Object.setPrototypeOf(this,e.prototype)}};function Ke(e,t,n=0,r=e.length){let i=n,a=r;for(;i{function t(e){return e<0}e.isLessThan=t;function n(e){return e<=0}e.isLessThanOrEqual=n;function r(e){return e>0}e.isGreaterThan=r;function i(e){return e===0}e.isNeitherLessOrGreaterThan=i,e.greaterThan=1,e.lessThan=-1,e.neitherLessOrGreaterThan=0})(Ye||={});function Xe(e,t){return(n,r)=>t(e(n),e(r))}var Ze=(e,t)=>e-t,Qe=class e{constructor(e){this.iterate=e}forEach(e){this.iterate(t=>(e(t),!0))}toArray(){let e=[];return this.iterate(t=>(e.push(t),!0)),e}filter(t){return new e(e=>this.iterate(n=>!t(n)||e(n)))}map(t){return new e(e=>this.iterate(n=>e(t(n))))}some(e){let t=!1;return this.iterate(n=>(t=e(n),!t)),t}findFirst(e){let t;return this.iterate(n=>e(n)?(t=n,!1):!0),t}findLast(e){let t;return this.iterate(n=>(e(n)&&(t=n),!0)),t}findLastMaxBy(e){let t,n=!0;return this.iterate(r=>((n||Ye.isGreaterThan(e(r,t)))&&(n=!1,t=r),!0)),t}};Qe.empty=new Qe(e=>{});function $e(e,t){let n=Object.create(null);for(let r of e){let e=t(r),i=n[e];i||=n[e]=[],i.push(r)}return n}var et=class{constructor(){this.map=new Map}add(e,t){let n=this.map.get(e);n||(n=new Set,this.map.set(e,n)),n.add(t)}delete(e,t){let n=this.map.get(e);n&&(n.delete(t),n.size===0&&this.map.delete(e))}forEach(e,t){let n=this.map.get(e);n&&n.forEach(t)}get(e){return this.map.get(e)||new Set}};function tt(e,t){let n=this,r=!1,i;return function(){if(r)return i;if(r=!0,t)try{i=e.apply(n,arguments)}finally{t()}else i=e.apply(n,arguments);return i}}var nt;(e=>{function t(e){return e&&typeof e==`object`&&typeof e[Symbol.iterator]==`function`}e.is=t;let n=Object.freeze([]);function r(){return n}e.empty=r;function*i(e){yield e}e.single=i;function a(e){return t(e)?e:i(e)}e.wrap=a;function o(e){return e||n}e.from=o;function*s(e){for(let t=e.length-1;t>=0;t--)yield e[t]}e.reverse=s;function c(e){return!e||e[Symbol.iterator]().next().done===!0}e.isEmpty=c;function l(e){return e[Symbol.iterator]().next().value}e.first=l;function u(e,t){let n=0;for(let r of e)if(t(r,n++))return!0;return!1}e.some=u;function d(e,t){for(let n of e)if(t(n))return n}e.find=d;function*f(e,t){for(let n of e)t(n)&&(yield n)}e.filter=f;function*p(e,t){let n=0;for(let r of e)yield t(r,n++)}e.map=p;function*m(e,t){let n=0;for(let r of e)yield*t(r,n++)}e.flatMap=m;function*h(...e){for(let t of e)yield*t}e.concat=h;function g(e,t,n){let r=n;for(let n of e)r=t(r,n);return r}e.reduce=g;function*_(e,t,n=e.length){for(t<0&&(t+=e.length),n<0?n+=e.length:n>e.length&&(n=e.length);tt.source!==null&&!this.getRootParent(t,e).isSingleton).flatMap(([e])=>e)}computeLeakingDisposables(e=10,t){let n;if(t)n=t;else{let e=new Map,t=[...this.livingDisposables.values()].filter(t=>t.source!==null&&!this.getRootParent(t,e).isSingleton);if(t.length===0)return;let r=new Set(t.map(e=>e.value));if(n=t.filter(e=>!(e.parent&&r.has(e.parent))),n.length===0)throw Error(`There are cyclic diposable chains!`)}if(!n)return;function r(e){function t(e,t){for(;e.length>0&&t.some(t=>typeof t==`string`?t===e[0]:e[0].match(t));)e.shift()}let n=e.source.split(` -`).map(e=>e.trim().replace(`at `,``)).filter(e=>e!==``);return t(n,[`Error`,/^trackDisposable \(.*\)$/,/^DisposableTracker.trackDisposable \(.*\)$/]),n.reverse()}let i=new et;for(let e of n){let t=r(e);for(let n=0;n<=t.length;n++)i.add(t.slice(0,n).join(` -`),e)}n.sort(Xe(e=>e.idx,Ze));let a=``,o=0;for(let t of n.slice(0,e)){o++;let e=r(t),s=[];for(let t=0;tr(e)[t]),e=>e);delete o[e[t]];for(let[e,t]of Object.entries(o))s.unshift(` - stacktraces of ${t.length} other leaks continue with ${e}`);s.unshift(a)}a+=` - - -==================== Leaking disposable ${o}/${n.length}: ${t.value.constructor.name} ==================== -${s.join(` -`)} -============================================================ - -`}return n.length>e&&(a+=` - - -... and ${n.length-e} more leaking disposables - -`),{leaks:n,details:a}}};it.idx=0;function at(e){return rt?.trackDisposable(e),e}function ot(e){rt?.markAsDisposed(e)}function st(e,t){rt?.setParent(e,t)}function ct(e){return rt?.markAsSingleton(e),e}function lt(e){if(nt.is(e)){let t=[];for(let n of e)if(n)try{n.dispose()}catch(e){t.push(e)}if(t.length===1)throw t[0];if(t.length>1)throw AggregateError(t,`Encountered errors while disposing of store`);return Array.isArray(e)?[]:e}else if(e)return e.dispose(),e}function ut(...e){return A(()=>lt(e))}function A(e){let t=at({dispose:tt(()=>{ot(t),e()})});return t}var dt=class e{constructor(){this._toDispose=new Set,this._isDisposed=!1,at(this)}dispose(){this._isDisposed||(ot(this),this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{lt(this._toDispose)}finally{this._toDispose.clear()}}add(t){if(!t)return t;if(t===this)throw Error(`Cannot register a disposable on itself!`);return st(t,this),this._isDisposed?e.DISABLE_DISPOSED_WARNING||console.warn(Error(`Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!`).stack):this._toDispose.add(t),t}delete(e){if(e){if(e===this)throw Error(`Cannot dispose a disposable on itself!`);this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),st(e,null))}};dt.DISABLE_DISPOSED_WARNING=!1;var ft=dt,j=class{constructor(){this._store=new ft,at(this),st(this._store,this)}dispose(){ot(this),this._store.dispose()}_register(e){if(e===this)throw Error(`Cannot register a disposable on itself!`);return this._store.add(e)}};j.None=Object.freeze({dispose(){}});var pt=class{constructor(){this._isDisposed=!1,at(this)}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),e&&st(e,this),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,ot(this),this._value?.dispose(),this._value=void 0}clearAndLeak(){let e=this._value;return this._value=void 0,e&&st(e,null),e}},mt=typeof window==`object`?window:globalThis,ht=class e{constructor(t){this.element=t,this.next=e.Undefined,this.prev=e.Undefined}};ht.Undefined=new ht(void 0);var M=ht,gt=class{constructor(){this._first=M.Undefined,this._last=M.Undefined,this._size=0}get size(){return this._size}isEmpty(){return this._first===M.Undefined}clear(){let e=this._first;for(;e!==M.Undefined;){let t=e.next;e.prev=M.Undefined,e.next=M.Undefined,e=t}this._first=M.Undefined,this._last=M.Undefined,this._size=0}unshift(e){return this._insert(e,!1)}push(e){return this._insert(e,!0)}_insert(e,t){let n=new M(e);if(this._first===M.Undefined)this._first=n,this._last=n;else if(t){let e=this._last;this._last=n,n.prev=e,e.next=n}else{let e=this._first;this._first=n,n.next=e,e.prev=n}this._size+=1;let r=!1;return()=>{r||(r=!0,this._remove(n))}}shift(){if(this._first!==M.Undefined){let e=this._first.element;return this._remove(this._first),e}}pop(){if(this._last!==M.Undefined){let e=this._last.element;return this._remove(this._last),e}}_remove(e){if(e.prev!==M.Undefined&&e.next!==M.Undefined){let t=e.prev;t.next=e.next,e.next.prev=t}else e.prev===M.Undefined&&e.next===M.Undefined?(this._first=M.Undefined,this._last=M.Undefined):e.next===M.Undefined?(this._last=this._last.prev,this._last.next=M.Undefined):e.prev===M.Undefined&&(this._first=this._first.next,this._first.prev=M.Undefined);--this._size}*[Symbol.iterator](){let e=this._first;for(;e!==M.Undefined;)yield e.element,e=e.next}},_t=globalThis.performance&&typeof globalThis.performance.now==`function`,vt=class e{static create(t){return new e(t)}constructor(e){this._now=_t&&e===!1?Date.now:globalThis.performance.now.bind(globalThis.performance),this._startTime=this._now(),this._stopTime=-1}stop(){this._stopTime=this._now()}reset(){this._startTime=this._now(),this._stopTime=-1}elapsed(){return this._stopTime===-1?this._now()-this._startTime:this._stopTime-this._startTime}},N;(e=>{e.None=()=>j.None;function t(e,t){return d(e,()=>{},0,void 0,!0,void 0,t)}e.defer=t;function n(e){return(t,n=null,r)=>{let i=!1,a;return a=e(e=>{if(!i)return a?a.dispose():i=!0,t.call(n,e)},null,r),i&&a.dispose(),a}}e.once=n;function r(e,t,n){return l((n,r=null,i)=>e(e=>n.call(r,t(e)),null,i),n)}e.map=r;function i(e,t,n){return l((n,r=null,i)=>e(e=>{t(e),n.call(r,e)},null,i),n)}e.forEach=i;function a(e,t,n){return l((n,r=null,i)=>e(e=>t(e)&&n.call(r,e),null,i),n)}e.filter=a;function o(e){return e}e.signal=o;function s(...e){return(t,n=null,r)=>u(ut(...e.map(e=>e(e=>t.call(n,e)))),r)}e.any=s;function c(e,t,n,i){let a=n;return r(e,e=>(a=t(a,e),a),i)}e.reduce=c;function l(e,t){let n,r=new P({onWillAddFirstListener(){n=e(r.fire,r)},onDidRemoveLastListener(){n?.dispose()}});return t?.add(r),r.event}function u(e,t){return t instanceof Array?t.push(e):t&&t.add(e),e}function d(e,t,n=100,r=!1,i=!1,a,o){let s,c,l,u=0,d,f=new P({leakWarningThreshold:a,onWillAddFirstListener(){s=e(e=>{u++,c=t(c,e),r&&!l&&(f.fire(c),c=void 0),d=()=>{let e=c;c=void 0,l=void 0,(!r||u>1)&&f.fire(e),u=0},typeof n==`number`?(clearTimeout(l),l=setTimeout(d,n)):l===void 0&&(l=0,queueMicrotask(d))})},onWillRemoveListener(){i&&u>0&&d?.()},onDidRemoveLastListener(){d=void 0,s.dispose()}});return o?.add(f),f.event}e.debounce=d;function f(t,n=0,r){return e.debounce(t,(e,t)=>e?(e.push(t),e):[t],n,void 0,!0,void 0,r)}e.accumulate=f;function p(e,t=(e,t)=>e===t,n){let r=!0,i;return a(e,e=>{let n=r||!t(e,i);return r=!1,i=e,n},n)}e.latch=p;function m(t,n,r){return[e.filter(t,n,r),e.filter(t,e=>!n(e),r)]}e.split=m;function h(e,t=!1,n=[],r){let i=n.slice(),a=e(e=>{i?i.push(e):s.fire(e)});r&&r.add(a);let o=()=>{i?.forEach(e=>s.fire(e)),i=null},s=new P({onWillAddFirstListener(){a||(a=e(e=>s.fire(e)),r&&r.add(a))},onDidAddFirstListener(){i&&(t?setTimeout(o):o())},onDidRemoveLastListener(){a&&a.dispose(),a=null}});return r&&r.add(s),s.event}e.buffer=h;function g(e,t){return(n,r,i)=>{let a=t(new v);return e(function(e){let t=a.evaluate(e);t!==_&&n.call(r,t)},void 0,i)}}e.chain=g;let _=Symbol(`HaltChainable`);class v{constructor(){this.steps=[]}map(e){return this.steps.push(e),this}forEach(e){return this.steps.push(t=>(e(t),t)),this}filter(e){return this.steps.push(t=>e(t)?t:_),this}reduce(e,t){let n=t;return this.steps.push(t=>(n=e(n,t),n)),this}latch(e=(e,t)=>e===t){let t=!0,n;return this.steps.push(r=>{let i=t||!e(r,n);return t=!1,n=r,i?r:_}),this}evaluate(e){for(let t of this.steps)if(e=t(e),e===_)break;return e}}function ee(e,t,n=e=>e){let r=(...e)=>i.fire(n(...e)),i=new P({onWillAddFirstListener:()=>e.on(t,r),onDidRemoveLastListener:()=>e.removeListener(t,r)});return i.event}e.fromNodeEventEmitter=ee;function y(e,t,n=e=>e){let r=(...e)=>i.fire(n(...e)),i=new P({onWillAddFirstListener:()=>e.addEventListener(t,r),onDidRemoveLastListener:()=>e.removeEventListener(t,r)});return i.event}e.fromDOMEventEmitter=y;function b(e){return new Promise(t=>n(e)(t))}e.toPromise=b;function te(e){let t=new P;return e.then(e=>{t.fire(e)},()=>{t.fire(void 0)}).finally(()=>{t.dispose()}),t.event}e.fromPromise=te;function x(e,t){return e(e=>t.fire(e))}e.forward=x;function ne(e,t,n){return t(n),e(e=>t(e))}e.runAndSubscribe=ne;class S{constructor(e,t){this._observable=e,this._counter=0,this._hasChanged=!1;let n={onWillAddFirstListener:()=>{e.addObserver(this)},onDidRemoveLastListener:()=>{e.removeObserver(this)}};this.emitter=new P(n),t&&t.add(this.emitter)}beginUpdate(e){this._counter++}handlePossibleChange(e){}handleChange(e,t){this._hasChanged=!0}endUpdate(e){this._counter--,this._counter===0&&(this._observable.reportChanges(),this._hasChanged&&(this._hasChanged=!1,this.emitter.fire(this._observable.get())))}}function re(e,t){return new S(e,t).emitter.event}e.fromObservable=re;function C(e){return(t,n,r)=>{let i=0,a=!1,o={beginUpdate(){i++},endUpdate(){i--,i===0&&(e.reportChanges(),a&&(a=!1,t.call(n)))},handlePossibleChange(){},handleChange(){a=!0}};e.addObserver(o),e.reportChanges();let s={dispose(){e.removeObserver(o)}};return r instanceof ft?r.add(s):Array.isArray(r)&&r.push(s),s}}e.fromObservableLight=C})(N||={});var yt=class e{constructor(t){this.listenerCount=0,this.invocationCount=0,this.elapsedOverall=0,this.durations=[],this.name=`${t}_${e._idPool++}`,e.all.add(this)}start(e){this._stopWatch=new vt,this.listenerCount=e}stop(){if(this._stopWatch){let e=this._stopWatch.elapsed();this.durations.push(e),this.elapsedOverall+=e,this.invocationCount+=1,this._stopWatch=void 0}}};yt.all=new Set,yt._idPool=0;var bt=yt,xt=-1,St=class e{constructor(t,n,r=(e._idPool++).toString(16).padStart(3,`0`)){this._errorHandler=t,this.threshold=n,this.name=r,this._warnCountdown=0}dispose(){this._stacks?.clear()}check(e,t){let n=this.threshold;if(n<=0||t{let t=this._stacks.get(e.value)||0;this._stacks.set(e.value,t-1)}}getMostFrequentStack(){if(!this._stacks)return;let e,t=0;for(let[n,r]of this._stacks)(!e||t0||this._options?.leakWarningThreshold?new Ct(e?.onListenerError??ze,this._options?.leakWarningThreshold??xt):void 0,this._perfMon=this._options?._profName?new bt(this._options._profName):void 0,this._deliveryQueue=this._options?.deliveryQueue}dispose(){this._disposed||(this._disposed=!0,this._deliveryQueue?.current===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),this._options?.onDidRemoveLastListener?.(),this._leakageMon?.dispose())}get event(){return this._event??=(e,t,n)=>{if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let e=`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`;console.warn(e);let t=this._leakageMon.getMostFrequentStack()??[`UNKNOWN stack`,-1],n=new Et(`${e}. HINT: Stack shows most frequent listener (${t[1]}-times)`,t[0]);return(this._options?.onListenerError||ze)(n),j.None}if(this._disposed)return j.None;t&&(e=e.bind(t));let r=new Ot(e),i;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(r.stack=wt.create(),i=this._leakageMon.check(r.stack,this._size+1)),this._listeners?this._listeners instanceof Ot?(this._deliveryQueue??=new jt,this._listeners=[this._listeners,r]):this._listeners.push(r):(this._options?.onWillAddFirstListener?.(this),this._listeners=r,this._options?.onDidAddFirstListener?.(this)),this._size++;let a=A(()=>{At?.unregister(a),i?.(),this._removeListener(r)});if(n instanceof ft?n.add(a):Array.isArray(n)&&n.push(a),At){let e=Error().stack.split(` -`).slice(2,3).join(` -`).trim(),t=/(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(e);At.register(a,t?.[2]??e,a)}return a},this._event}_removeListener(e){if(this._options?.onWillRemoveListener?.(this),!this._listeners)return;if(this._size===1){this._listeners=void 0,this._options?.onDidRemoveLastListener?.(this),this._size=0;return}let t=this._listeners,n=t.indexOf(e);if(n===-1)throw console.log(`disposed?`,this._disposed),console.log(`size?`,this._size),console.log(`arr?`,JSON.stringify(this._listeners)),Error(`Attempted to dispose unknown listener`);this._size--,t[n]=void 0;let r=this._deliveryQueue.current===this;if(this._size*kt<=t.length){let e=0;for(let n=0;n0}},jt=class{constructor(){this.i=-1,this.end=0}enqueue(e,t,n){this.i=0,this.end=n,this.current=e,this.value=t}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},Mt=class{constructor(){this.mapWindowIdToZoomLevel=new Map,this._onDidChangeZoomLevel=new P,this.onDidChangeZoomLevel=this._onDidChangeZoomLevel.event,this.mapWindowIdToZoomFactor=new Map,this._onDidChangeFullscreen=new P,this.onDidChangeFullscreen=this._onDidChangeFullscreen.event,this.mapWindowIdToFullScreen=new Map}getZoomLevel(e){return this.mapWindowIdToZoomLevel.get(this.getWindowId(e))??0}setZoomLevel(e,t){if(this.getZoomLevel(t)===e)return;let n=this.getWindowId(t);this.mapWindowIdToZoomLevel.set(n,e),this._onDidChangeZoomLevel.fire(n)}getZoomFactor(e){return this.mapWindowIdToZoomFactor.get(this.getWindowId(e))??1}setZoomFactor(e,t){this.mapWindowIdToZoomFactor.set(this.getWindowId(t),e)}setFullscreen(e,t){if(this.isFullscreen(t)===e)return;let n=this.getWindowId(t);this.mapWindowIdToFullScreen.set(n,e),this._onDidChangeFullscreen.fire(n)}isFullscreen(e){return!!this.mapWindowIdToFullScreen.get(this.getWindowId(e))}getWindowId(e){return e.vscodeWindowId}};Mt.INSTANCE=new Mt;var Nt=Mt;function Pt(e,t,n){typeof t==`string`&&(t=e.matchMedia(t)),t.addEventListener(`change`,n)}Nt.INSTANCE.onDidChangeZoomLevel;function Ft(e){return Nt.INSTANCE.getZoomFactor(e)}Nt.INSTANCE.onDidChangeFullscreen;var It=typeof navigator==`object`?navigator.userAgent:``,Lt=It.indexOf(`Firefox`)>=0,Rt=It.indexOf(`AppleWebKit`)>=0,zt=It.indexOf(`Chrome`)>=0,Bt=!zt&&It.indexOf(`Safari`)>=0;It.indexOf(`Electron/`),It.indexOf(`Android`);var Vt=!1;if(typeof mt.matchMedia==`function`){let e=mt.matchMedia(`(display-mode: standalone) or (display-mode: window-controls-overlay)`),t=mt.matchMedia(`(display-mode: fullscreen)`);Vt=e.matches,Pt(mt,e,({matches:e})=>{Vt&&t.matches||(Vt=e)})}function Ht(){return Vt}var Ut=`en`,Wt=!1,Gt=!1,Kt=!1,qt=!1,Jt=!1,Yt=Ut,Xt,Zt=globalThis,Qt;typeof Zt.vscode<`u`&&typeof Zt.vscode.process<`u`?Qt=Zt.vscode.process:typeof process<`u`&&typeof process?.versions?.node==`string`&&(Qt=process);var $t=typeof Qt?.versions?.electron==`string`&&Qt?.type===`renderer`;if(typeof Qt==`object`){Wt=Qt.platform===`win32`,Gt=Qt.platform===`darwin`,Kt=Qt.platform===`linux`,Kt&&Qt.env.SNAP&&Qt.env.SNAP_REVISION,Qt.env.CI||Qt.env.BUILD_ARTIFACTSTAGINGDIRECTORY,Yt=Ut;let e=Qt.env.VSCODE_NLS_CONFIG;if(e)try{let t=JSON.parse(e);t.userLocale,t.osLocale,Yt=t.resolvedLanguage||Ut,t.languagePack?.translationsConfigFile}catch{}qt=!0}else typeof navigator==`object`&&!$t?(Xt=navigator.userAgent,Wt=Xt.indexOf(`Windows`)>=0,Gt=Xt.indexOf(`Macintosh`)>=0,(Xt.indexOf(`Macintosh`)>=0||Xt.indexOf(`iPad`)>=0||Xt.indexOf(`iPhone`)>=0)&&navigator.maxTouchPoints&&navigator.maxTouchPoints,Kt=Xt.indexOf(`Linux`)>=0,Xt?.indexOf(`Mobi`),Jt=!0,Yt=globalThis._VSCODE_NLS_LANGUAGE||Ut,navigator.language.toLowerCase()):console.error(`Unable to resolve platform.`);var en=Wt,tn=Gt,nn=Kt,rn=qt;Jt&&typeof Zt.importScripts==`function`&&Zt.origin;var an=Xt,on=Yt,sn;(e=>{function t(){return on}e.value=t;function n(){return on.length===2?on===`en`:on.length>=3&&on[0]===`e`&&on[1]===`n`&&on[2]===`-`}e.isDefaultVariant=n;function r(){return on===`en`}e.isDefault=r})(sn||={});var cn=typeof Zt.postMessage==`function`&&!Zt.importScripts;(()=>{if(cn){let e=[];Zt.addEventListener(`message`,t=>{if(t.data&&t.data.vscodeScheduleAsyncWork)for(let n=0,r=e.length;n{let r=++t;e.push({id:r,callback:n}),Zt.postMessage({vscodeScheduleAsyncWork:r},`*`)}}return e=>setTimeout(e)})();var ln=!!(an&&an.indexOf(`Chrome`)>=0);an&&an.indexOf(`Firefox`),!ln&&an&&an.indexOf(`Safari`),an&&an.indexOf(`Edg/`),an&&an.indexOf(`Android`);var un=typeof navigator==`object`?navigator:{};rn||document.queryCommandSupported&&document.queryCommandSupported(`copy`)||un&&un.clipboard&&un.clipboard.writeText,rn||un&&un.clipboard&&un.clipboard.readText,rn||Ht()||un.keyboard,`ontouchstart`in mt||un.maxTouchPoints,mt.PointerEvent&&(`ontouchstart`in mt||navigator.maxTouchPoints);var dn=class{constructor(){this._keyCodeToStr=[],this._strToKeyCode=Object.create(null)}define(e,t){this._keyCodeToStr[e]=t,this._strToKeyCode[t.toLowerCase()]=e}keyCodeToStr(e){return this._keyCodeToStr[e]}strToKeyCode(e){return this._strToKeyCode[e.toLowerCase()]||0}},fn=new dn,pn=new dn,mn=new dn,hn=Array(230),gn;(e=>{function t(e){return fn.keyCodeToStr(e)}e.toString=t;function n(e){return fn.strToKeyCode(e)}e.fromString=n;function r(e){return pn.keyCodeToStr(e)}e.toUserSettingsUS=r;function i(e){return mn.keyCodeToStr(e)}e.toUserSettingsGeneral=i;function a(e){return pn.strToKeyCode(e)||mn.strToKeyCode(e)}e.fromUserSettings=a;function o(e){if(e>=98&&e<=113)return null;switch(e){case 16:return`Up`;case 18:return`Down`;case 15:return`Left`;case 17:return`Right`}return fn.keyCodeToStr(e)}e.toElectronAccelerator=o})(gn||={});var _n=class e{constructor(e,t,n,r,i){this.ctrlKey=e,this.shiftKey=t,this.altKey=n,this.metaKey=r,this.keyCode=i}equals(t){return t instanceof e&&this.ctrlKey===t.ctrlKey&&this.shiftKey===t.shiftKey&&this.altKey===t.altKey&&this.metaKey===t.metaKey&&this.keyCode===t.keyCode}getHashCode(){return`K${this.ctrlKey?`1`:`0`}${this.shiftKey?`1`:`0`}${this.altKey?`1`:`0`}${this.metaKey?`1`:`0`}${this.keyCode}`}isModifierKey(){return this.keyCode===0||this.keyCode===5||this.keyCode===57||this.keyCode===6||this.keyCode===4}toKeybinding(){return new vn([this])}isDuplicateModifierCase(){return this.ctrlKey&&this.keyCode===5||this.shiftKey&&this.keyCode===4||this.altKey&&this.keyCode===6||this.metaKey&&this.keyCode===57}},vn=class{constructor(e){if(e.length===0)throw Ue(`chords`);this.chords=e}getHashCode(){let e=``;for(let t=0,n=this.chords.length;t{function t(t){return t===e.None||t===e.Cancelled||t instanceof Mn?!0:!t||typeof t!=`object`?!1:typeof t.isCancellationRequested==`boolean`&&typeof t.onCancellationRequested==`function`}e.isCancellationToken=t,e.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:N.None}),e.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:An})})(jn||={});var Mn=class{constructor(){this._isCancelled=!1,this._emitter=null}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()))}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?An:(this._emitter||=new P,this._emitter.event)}dispose(){this._emitter&&=(this._emitter.dispose(),null)}},Nn=class{constructor(e,t){this._isDisposed=!1,this._token=-1,typeof e==`function`&&typeof t==`number`&&this.setIfNotSet(e,t)}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._token!==-1&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Ge(`Calling 'cancelAndSet' on a disposed TimeoutTimer`);this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Ge(`Calling 'setIfNotSet' on a disposed TimeoutTimer`);this._token===-1&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},Pn=class{constructor(){this.disposable=void 0,this.isDisposed=!1}cancel(){this.disposable?.dispose(),this.disposable=void 0}cancelAndSet(e,t,n=globalThis){if(this.isDisposed)throw new Ge(`Calling 'cancelAndSet' on a disposed IntervalTimer`);this.cancel();let r=n.setInterval(()=>{e()},t);this.disposable=A(()=>{n.clearInterval(r),this.disposable=void 0})}dispose(){this.cancel(),this.isDisposed=!0}};(function(){typeof globalThis.requestIdleCallback!=`function`||globalThis.cancelIdleCallback})();var Fn;(e=>{async function t(e){let t,n=await Promise.all(e.map(e=>e.then(e=>e,e=>{t||=e})));if(typeof t<`u`)throw t;return n}e.settled=t;function n(e){return new Promise(async(t,n)=>{try{await e(t,n)}catch(e){n(e)}})}e.withAsyncBody=n})(Fn||={});var In=class e{static fromArray(t){return new e(e=>{e.emitMany(t)})}static fromPromise(t){return new e(async e=>{e.emitMany(await t)})}static fromPromises(t){return new e(async e=>{await Promise.all(t.map(async t=>e.emitOne(await t)))})}static merge(t){return new e(async e=>{await Promise.all(t.map(async t=>{for await(let n of t)e.emitOne(n)}))})}constructor(e,t){this._state=0,this._results=[],this._error=null,this._onReturn=t,this._onStateChanged=new P,queueMicrotask(async()=>{let t={emitOne:e=>this.emitOne(e),emitMany:e=>this.emitMany(e),reject:e=>this.reject(e)};try{await Promise.resolve(e(t)),this.resolve()}catch(e){this.reject(e)}finally{t.emitOne=void 0,t.emitMany=void 0,t.reject=void 0}})}[Symbol.asyncIterator](){let e=0;return{next:async()=>{do{if(this._state===2)throw this._error;if(e(this._onReturn?.(),{done:!0,value:void 0})}}static map(t,n){return new e(async e=>{for await(let r of t)e.emitOne(n(r))})}map(t){return e.map(this,t)}static filter(t,n){return new e(async e=>{for await(let r of t)n(r)&&e.emitOne(r)})}filter(t){return e.filter(this,t)}static coalesce(t){return e.filter(t,e=>!!e)}coalesce(){return e.coalesce(this)}static async toPromise(e){let t=[];for await(let n of e)t.push(n);return t}toPromise(){return e.toPromise(this)}emitOne(e){this._state===0&&(this._results.push(e),this._onStateChanged.fire())}emitMany(e){this._state===0&&(this._results=this._results.concat(e),this._onStateChanged.fire())}resolve(){this._state===0&&(this._state=1,this._onStateChanged.fire())}reject(e){this._state===0&&(this._state=2,this._error=e,this._onStateChanged.fire())}};In.EMPTY=In.fromArray([]);function Ln(e){return 55296<=e&&e<=56319}function Rn(e){return 56320<=e&&e<=57343}function zn(e,t){return(e-55296<<10)+(t-56320)+65536}function Bn(e){return Vn(e,0)}function Vn(e,t){switch(typeof e){case`object`:return e===null?Hn(349,t):Array.isArray(e)?Gn(e,t):Kn(e,t);case`string`:return Wn(e,t);case`boolean`:return Un(e,t);case`number`:return Hn(e,t);case`undefined`:return Hn(937,t);default:return Hn(617,t)}}function Hn(e,t){return(t<<5)-t+e|0}function Un(e,t){return Hn(e?433:863,t)}function Wn(e,t){t=Hn(149417,t);for(let n=0,r=e.length;nVn(t,e),t)}function Kn(e,t){return t=Hn(181387,t),Object.keys(e).sort().reduce((t,n)=>(t=Wn(n,t),Vn(e[n],t)),t)}function qn(e,t,n=32){let r=n-t,i=~((1<>>r)>>>0}function Jn(e,t=0,n=e.byteLength,r=0){for(let i=0;ie.toString(16).padStart(2,`0`)).join(``):Yn((e>>>0).toString(16),t/4)}var Zn=class e{constructor(){this._h0=1732584193,this._h1=4023233417,this._h2=2562383102,this._h3=271733878,this._h4=3285377520,this._buff=new Uint8Array(67),this._buffDV=new DataView(this._buff.buffer),this._buffLen=0,this._totalLen=0,this._leftoverHighSurrogate=0,this._finished=!1}update(e){let t=e.length;if(t===0)return;let n=this._buff,r=this._buffLen,i=this._leftoverHighSurrogate,a,o;for(i===0?(a=e.charCodeAt(0),o=0):(a=i,o=-1,i=0);;){let s=a;if(Ln(a))if(o+1>>6,e[t++]=128|(n&63)>>>0):n<65536?(e[t++]=224|(n&61440)>>>12,e[t++]=128|(n&4032)>>>6,e[t++]=128|(n&63)>>>0):(e[t++]=240|(n&1835008)>>>18,e[t++]=128|(n&258048)>>>12,e[t++]=128|(n&4032)>>>6,e[t++]=128|(n&63)>>>0),t>=64&&(this._step(),t-=64,this._totalLen+=64,e[0]=e[64],e[1]=e[65],e[2]=e[66]),t}digest(){return this._finished||(this._finished=!0,this._leftoverHighSurrogate&&(this._leftoverHighSurrogate=0,this._buffLen=this._push(this._buff,this._buffLen,65533)),this._totalLen+=this._buffLen,this._wrapUp()),Xn(this._h0)+Xn(this._h1)+Xn(this._h2)+Xn(this._h3)+Xn(this._h4)}_wrapUp(){this._buff[this._buffLen++]=128,Jn(this._buff,this._buffLen),this._buffLen>56&&(this._step(),Jn(this._buff));let e=8*this._totalLen;this._buffDV.setUint32(56,Math.floor(e/4294967296),!1),this._buffDV.setUint32(60,e%4294967296,!1),this._step()}_step(){let t=e._bigBlock32,n=this._buffDV;for(let e=0;e<64;e+=4)t.setUint32(e,n.getUint32(e,!1),!1);for(let e=64;e<320;e+=4)t.setUint32(e,qn(t.getUint32(e-12,!1)^t.getUint32(e-32,!1)^t.getUint32(e-56,!1)^t.getUint32(e-64,!1),1),!1);let r=this._h0,i=this._h1,a=this._h2,o=this._h3,s=this._h4,c,l,u;for(let e=0;e<80;e++)e<20?(c=i&a|~i&o,l=1518500249):e<40?(c=i^a^o,l=1859775393):e<60?(c=i&a|i&o|a&o,l=2400959708):(c=i^a^o,l=3395469782),u=qn(r,5)+c+s+l+t.getUint32(e*4,!1)&4294967295,s=o,o=a,a=qn(i,30),i=r,r=u;this._h0=this._h0+r&4294967295,this._h1=this._h1+i&4294967295,this._h2=this._h2+a&4294967295,this._h3=this._h3+o&4294967295,this._h4=this._h4+s&4294967295}};Zn._bigBlock32=new DataView(new ArrayBuffer(320));var{registerWindow:Qn,getWindow:$n,getDocument:er,getWindows:tr,getWindowsCount:nr,getWindowId:rr,getWindowById:ir,hasWindow:ar,onDidRegisterWindow:or,onWillUnregisterWindow:sr,onDidUnregisterWindow:cr}=function(){let e=new Map,t={window:mt,disposables:new ft};e.set(mt.vscodeWindowId,t);let n=new P,r=new P,i=new P;function a(n,r){return(typeof n==`number`?e.get(n):void 0)??(r?t:void 0)}return{onDidRegisterWindow:n.event,onWillUnregisterWindow:i.event,onDidUnregisterWindow:r.event,registerWindow(t){if(e.has(t.vscodeWindowId))return j.None;let a=new ft,o={window:t,disposables:a.add(new ft)};return e.set(t.vscodeWindowId,o),a.add(A(()=>{e.delete(t.vscodeWindowId),r.fire(t)})),a.add(F(t,I.BEFORE_UNLOAD,()=>{i.fire(t)})),n.fire(o),a},getWindows(){return e.values()},getWindowsCount(){return e.size},getWindowId(e){return e.vscodeWindowId},hasWindow(t){return e.has(t)},getWindowById:a,getWindow(e){let t=e;if(t?.ownerDocument?.defaultView)return t.ownerDocument.defaultView.window;let n=e;return n?.view?n.view.window:mt},getDocument(e){return $n(e).document}}}(),lr=class{constructor(e,t,n,r){this._node=e,this._type=t,this._handler=n,this._options=r||!1,this._node.addEventListener(this._type,this._handler,this._options)}dispose(){this._handler&&=(this._node.removeEventListener(this._type,this._handler,this._options),this._node=null,null)}};function F(e,t,n,r){return new lr(e,t,n,r)}function ur(e,t){return function(n){return t(new On(e,n))}}function dr(e){return function(t){return e(new wn(t))}}var fr=function(e,t,n,r){let i=n;return t===`click`||t===`mousedown`||t===`contextmenu`?i=ur($n(e),n):(t===`keydown`||t===`keypress`||t===`keyup`)&&(i=dr(n)),F(e,t,i,r)},pr,mr=class extends Pn{constructor(e){super(),this.defaultTarget=e&&$n(e)}cancelAndSet(e,t,n){return super.cancelAndSet(e,t,n??this.defaultTarget)}},hr=class{constructor(e,t=0){this._runner=e,this.priority=t,this._canceled=!1}dispose(){this._canceled=!0}execute(){if(!this._canceled)try{this._runner()}catch(e){ze(e)}}static sort(e,t){return t.priority-e.priority}};(function(){let e=new Map,t=new Map,n=new Map,r=new Map,i=i=>{n.set(i,!1);let a=e.get(i)??[];for(t.set(i,a),e.set(i,[]),r.set(i,!0);a.length>0;)a.sort(hr.sort),a.shift().execute();r.set(i,!1)};pr=(t,r,a=0)=>{let o=rr(t),s=new hr(r,a),c=e.get(o);return c||(c=[],e.set(o,c)),c.push(s),n.get(o)||(n.set(o,!0),t.requestAnimationFrame(()=>i(o))),s}})();var gr=class e{constructor(e,t){this.width=e,this.height=t}with(t=this.width,n=this.height){return t!==this.width||n!==this.height?new e(t,n):this}static is(e){return typeof e==`object`&&typeof e.height==`number`&&typeof e.width==`number`}static lift(t){return t instanceof e?t:new e(t.width,t.height)}static equals(e,t){return e===t?!0:!e||!t?!1:e.width===t.width&&e.height===t.height}};gr.None=new gr(0,0);function _r(e){let t=e.getBoundingClientRect(),n=$n(e);return{left:t.left+n.scrollX,top:t.top+n.scrollY,width:t.width,height:t.height}}new class{constructor(){this.mutationObservers=new Map}observe(e,t,n){let r=this.mutationObservers.get(e);r||(r=new Map,this.mutationObservers.set(e,r));let i=Bn(n),a=r.get(i);if(a)a.users+=1;else{let o=new P,s=new MutationObserver(e=>o.fire(e));s.observe(e,n);let c=a={users:1,observer:s,onDidMutate:o.event};t.add(A(()=>{--c.users,c.users===0&&(o.dispose(),s.disconnect(),r?.delete(i),r?.size===0&&this.mutationObservers.delete(e))})),r.set(i,a)}return a.onDidMutate}};var I={CLICK:`click`,AUXCLICK:`auxclick`,DBLCLICK:`dblclick`,MOUSE_UP:`mouseup`,MOUSE_DOWN:`mousedown`,MOUSE_OVER:`mouseover`,MOUSE_MOVE:`mousemove`,MOUSE_OUT:`mouseout`,MOUSE_ENTER:`mouseenter`,MOUSE_LEAVE:`mouseleave`,MOUSE_WHEEL:`wheel`,POINTER_UP:`pointerup`,POINTER_DOWN:`pointerdown`,POINTER_MOVE:`pointermove`,POINTER_LEAVE:`pointerleave`,CONTEXT_MENU:`contextmenu`,WHEEL:`wheel`,KEY_DOWN:`keydown`,KEY_PRESS:`keypress`,KEY_UP:`keyup`,LOAD:`load`,BEFORE_UNLOAD:`beforeunload`,UNLOAD:`unload`,PAGE_SHOW:`pageshow`,PAGE_HIDE:`pagehide`,PASTE:`paste`,ABORT:`abort`,ERROR:`error`,RESIZE:`resize`,SCROLL:`scroll`,FULLSCREEN_CHANGE:`fullscreenchange`,WK_FULLSCREEN_CHANGE:`webkitfullscreenchange`,SELECT:`select`,CHANGE:`change`,SUBMIT:`submit`,RESET:`reset`,FOCUS:`focus`,FOCUS_IN:`focusin`,FOCUS_OUT:`focusout`,BLUR:`blur`,INPUT:`input`,STORAGE:`storage`,DRAG_START:`dragstart`,DRAG:`drag`,DRAG_ENTER:`dragenter`,DRAG_LEAVE:`dragleave`,DRAG_OVER:`dragover`,DROP:`drop`,DRAG_END:`dragend`,ANIMATION_START:Rt?`webkitAnimationStart`:`animationstart`,ANIMATION_END:Rt?`webkitAnimationEnd`:`animationend`,ANIMATION_ITERATION:Rt?`webkitAnimationIteration`:`animationiteration`},vr=/([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;function yr(e,t,n,...r){let i=vr.exec(t);if(!i)throw Error(`Bad use of emmet`);let a=i[1]||`div`,o;return o=e===`http://www.w3.org/1999/xhtml`?document.createElement(a):document.createElementNS(e,a),i[3]&&(o.id=i[3]),i[4]&&(o.className=i[4].replace(/\./g,` `).trim()),n&&Object.entries(n).forEach(([e,t])=>{typeof t>`u`||(/^on\w+$/.test(e)?o[e]=t:e===`selected`?t&&o.setAttribute(e,`true`):o.setAttribute(e,t))}),o.append(...r),o}function br(e,t,...n){return yr(`http://www.w3.org/1999/xhtml`,e,t,...n)}br.SVG=function(e,t,...n){return yr(`http://www.w3.org/2000/svg`,e,t,...n)};var xr=class{constructor(e){this.domNode=e,this._maxWidth=``,this._width=``,this._height=``,this._top=``,this._left=``,this._bottom=``,this._right=``,this._paddingTop=``,this._paddingLeft=``,this._paddingBottom=``,this._paddingRight=``,this._fontFamily=``,this._fontWeight=``,this._fontSize=``,this._fontStyle=``,this._fontFeatureSettings=``,this._fontVariationSettings=``,this._textDecoration=``,this._lineHeight=``,this._letterSpacing=``,this._className=``,this._display=``,this._position=``,this._visibility=``,this._color=``,this._backgroundColor=``,this._layerHint=!1,this._contain=`none`,this._boxShadow=``}setMaxWidth(e){let t=Sr(e);this._maxWidth!==t&&(this._maxWidth=t,this.domNode.style.maxWidth=this._maxWidth)}setWidth(e){let t=Sr(e);this._width!==t&&(this._width=t,this.domNode.style.width=this._width)}setHeight(e){let t=Sr(e);this._height!==t&&(this._height=t,this.domNode.style.height=this._height)}setTop(e){let t=Sr(e);this._top!==t&&(this._top=t,this.domNode.style.top=this._top)}setLeft(e){let t=Sr(e);this._left!==t&&(this._left=t,this.domNode.style.left=this._left)}setBottom(e){let t=Sr(e);this._bottom!==t&&(this._bottom=t,this.domNode.style.bottom=this._bottom)}setRight(e){let t=Sr(e);this._right!==t&&(this._right=t,this.domNode.style.right=this._right)}setPaddingTop(e){let t=Sr(e);this._paddingTop!==t&&(this._paddingTop=t,this.domNode.style.paddingTop=this._paddingTop)}setPaddingLeft(e){let t=Sr(e);this._paddingLeft!==t&&(this._paddingLeft=t,this.domNode.style.paddingLeft=this._paddingLeft)}setPaddingBottom(e){let t=Sr(e);this._paddingBottom!==t&&(this._paddingBottom=t,this.domNode.style.paddingBottom=this._paddingBottom)}setPaddingRight(e){let t=Sr(e);this._paddingRight!==t&&(this._paddingRight=t,this.domNode.style.paddingRight=this._paddingRight)}setFontFamily(e){this._fontFamily!==e&&(this._fontFamily=e,this.domNode.style.fontFamily=this._fontFamily)}setFontWeight(e){this._fontWeight!==e&&(this._fontWeight=e,this.domNode.style.fontWeight=this._fontWeight)}setFontSize(e){let t=Sr(e);this._fontSize!==t&&(this._fontSize=t,this.domNode.style.fontSize=this._fontSize)}setFontStyle(e){this._fontStyle!==e&&(this._fontStyle=e,this.domNode.style.fontStyle=this._fontStyle)}setFontFeatureSettings(e){this._fontFeatureSettings!==e&&(this._fontFeatureSettings=e,this.domNode.style.fontFeatureSettings=this._fontFeatureSettings)}setFontVariationSettings(e){this._fontVariationSettings!==e&&(this._fontVariationSettings=e,this.domNode.style.fontVariationSettings=this._fontVariationSettings)}setTextDecoration(e){this._textDecoration!==e&&(this._textDecoration=e,this.domNode.style.textDecoration=this._textDecoration)}setLineHeight(e){let t=Sr(e);this._lineHeight!==t&&(this._lineHeight=t,this.domNode.style.lineHeight=this._lineHeight)}setLetterSpacing(e){let t=Sr(e);this._letterSpacing!==t&&(this._letterSpacing=t,this.domNode.style.letterSpacing=this._letterSpacing)}setClassName(e){this._className!==e&&(this._className=e,this.domNode.className=this._className)}toggleClassName(e,t){this.domNode.classList.toggle(e,t),this._className=this.domNode.className}setDisplay(e){this._display!==e&&(this._display=e,this.domNode.style.display=this._display)}setPosition(e){this._position!==e&&(this._position=e,this.domNode.style.position=this._position)}setVisibility(e){this._visibility!==e&&(this._visibility=e,this.domNode.style.visibility=this._visibility)}setColor(e){this._color!==e&&(this._color=e,this.domNode.style.color=this._color)}setBackgroundColor(e){this._backgroundColor!==e&&(this._backgroundColor=e,this.domNode.style.backgroundColor=this._backgroundColor)}setLayerHinting(e){this._layerHint!==e&&(this._layerHint=e,this.domNode.style.transform=this._layerHint?`translate3d(0px, 0px, 0px)`:``)}setBoxShadow(e){this._boxShadow!==e&&(this._boxShadow=e,this.domNode.style.boxShadow=e)}setContain(e){this._contain!==e&&(this._contain=e,this.domNode.style.contain=this._contain)}setAttribute(e,t){this.domNode.setAttribute(e,t)}removeAttribute(e){this.domNode.removeAttribute(e)}appendChild(e){this.domNode.appendChild(e.domNode)}removeChild(e){this.domNode.removeChild(e.domNode)}};function Sr(e){return typeof e==`number`?`${e}px`:e}function Cr(e){return new xr(e)}var wr=class{constructor(){this._hooks=new ft,this._pointerMoveCallback=null,this._onStopCallback=null}dispose(){this.stopMonitoring(!1),this._hooks.dispose()}stopMonitoring(e,t){if(!this.isMonitoring())return;this._hooks.clear(),this._pointerMoveCallback=null;let n=this._onStopCallback;this._onStopCallback=null,e&&n&&n(t)}isMonitoring(){return!!this._pointerMoveCallback}startMonitoring(e,t,n,r,i){this.isMonitoring()&&this.stopMonitoring(!1),this._pointerMoveCallback=r,this._onStopCallback=i;let a=e;try{e.setPointerCapture(t),this._hooks.add(A(()=>{try{e.releasePointerCapture(t)}catch{}}))}catch{a=$n(e)}this._hooks.add(F(a,I.POINTER_MOVE,e=>{if(e.buttons!==n){this.stopMonitoring(!0);return}e.preventDefault(),this._pointerMoveCallback(e)})),this._hooks.add(F(a,I.POINTER_UP,e=>this.stopMonitoring(!0)))}};function Tr(e,t,n){let r=null,i=null;if(typeof n.value==`function`?(r=`value`,i=n.value,i.length!==0&&console.warn(`Memoize should only be used in functions with zero parameters`)):typeof n.get==`function`&&(r=`get`,i=n.get),!i)throw Error(`not supported`);let a=`$memoize$${t}`;n[r]=function(...e){return this.hasOwnProperty(a)||Object.defineProperty(this,a,{configurable:!1,enumerable:!1,writable:!1,value:i.apply(this,e)}),this[a]}}var Er;(e=>(e.Tap=`-xterm-gesturetap`,e.Change=`-xterm-gesturechange`,e.Start=`-xterm-gesturestart`,e.End=`-xterm-gesturesend`,e.Contextmenu=`-xterm-gesturecontextmenu`))(Er||={});var Dr=class e extends j{constructor(){super(),this.dispatched=!1,this.targets=new gt,this.ignoreTargets=new gt,this.activeTouches={},this.handle=null,this._lastSetTapCountTime=0,this._register(N.runAndSubscribe(or,({window:e,disposables:t})=>{t.add(F(e.document,`touchstart`,e=>this.onTouchStart(e),{passive:!1})),t.add(F(e.document,`touchend`,t=>this.onTouchEnd(e,t))),t.add(F(e.document,`touchmove`,e=>this.onTouchMove(e),{passive:!1}))},{window:mt,disposables:this._store}))}static addTarget(t){return e.isTouchDevice()?(e.INSTANCE||=ct(new e),A(e.INSTANCE.targets.push(t))):j.None}static ignoreTarget(t){return e.isTouchDevice()?(e.INSTANCE||=ct(new e),A(e.INSTANCE.ignoreTargets.push(t))):j.None}static isTouchDevice(){return`ontouchstart`in mt||navigator.maxTouchPoints>0}dispose(){this.handle&&=(this.handle.dispose(),null),super.dispose()}onTouchStart(e){let t=Date.now();this.handle&&=(this.handle.dispose(),null);for(let n=0,r=e.targetTouches.length;n=e.HOLD_DELAY&&Math.abs(s.initialPageX-Je(s.rollingPageX))<30&&Math.abs(s.initialPageY-Je(s.rollingPageY))<30){let e=this.newGestureEvent(Er.Contextmenu,s.initialTarget);e.pageX=Je(s.rollingPageX),e.pageY=Je(s.rollingPageY),this.dispatchEvent(e)}else if(i===1){let e=Je(s.rollingPageX),n=Je(s.rollingPageY),i=Je(s.rollingTimestamps)-s.rollingTimestamps[0],a=e-s.rollingPageX[0],o=n-s.rollingPageY[0],c=[...this.targets].filter(e=>s.initialTarget instanceof Node&&e.contains(s.initialTarget));this.inertia(t,c,r,Math.abs(a)/i,a>0?1:-1,e,Math.abs(o)/i,o>0?1:-1,n)}this.dispatchEvent(this.newGestureEvent(Er.End,s.initialTarget)),delete this.activeTouches[o.identifier]}this.dispatched&&=(n.preventDefault(),n.stopPropagation(),!1)}newGestureEvent(e,t){let n=document.createEvent(`CustomEvent`);return n.initEvent(e,!1,!0),n.initialTarget=t,n.tapCount=0,n}dispatchEvent(t){if(t.type===Er.Tap){let n=new Date().getTime(),r=0;r=n-this._lastSetTapCountTime>e.CLEAR_TAP_COUNT_TIME?1:2,this._lastSetTapCountTime=n,t.tapCount=r}else(t.type===Er.Change||t.type===Er.Contextmenu)&&(this._lastSetTapCountTime=0);if(t.initialTarget instanceof Node){for(let e of this.ignoreTargets)if(e.contains(t.initialTarget))return;let e=[];for(let n of this.targets)if(n.contains(t.initialTarget)){let r=0,i=t.initialTarget;for(;i&&i!==n;)r++,i=i.parentElement;e.push([r,n])}e.sort((e,t)=>e[0]-t[0]);for(let[n,r]of e)r.dispatchEvent(t),this.dispatched=!0}}inertia(t,n,r,i,a,o,s,c,l){this.handle=pr(t,()=>{let u=Date.now(),d=u-r,f=0,p=0,m=!0;i+=e.SCROLL_FRICTION*d,s+=e.SCROLL_FRICTION*d,i>0&&(m=!1,f=a*i*d),s>0&&(m=!1,p=c*s*d);let h=this.newGestureEvent(Er.Change);h.translationX=f,h.translationY=p,n.forEach(e=>e.dispatchEvent(h)),m||this.inertia(t,n,u,i,a,o+f,s,c,l+p)})}onTouchMove(e){let t=Date.now();for(let n=0,r=e.changedTouches.length;n3&&(i.rollingPageX.shift(),i.rollingPageY.shift(),i.rollingTimestamps.shift()),i.rollingPageX.push(r.pageX),i.rollingPageY.push(r.pageY),i.rollingTimestamps.push(t)}this.dispatched&&=(e.preventDefault(),e.stopPropagation(),!1)}};Dr.SCROLL_FRICTION=-.005,Dr.HOLD_DELAY=700,Dr.CLEAR_TAP_COUNT_TIME=400,y([Tr],Dr,`isTouchDevice`,1);var Or=Dr,kr=class extends j{onclick(e,t){this._register(F(e,I.CLICK,n=>t(new On($n(e),n))))}onmousedown(e,t){this._register(F(e,I.MOUSE_DOWN,n=>t(new On($n(e),n))))}onmouseover(e,t){this._register(F(e,I.MOUSE_OVER,n=>t(new On($n(e),n))))}onmouseleave(e,t){this._register(F(e,I.MOUSE_LEAVE,n=>t(new On($n(e),n))))}onkeydown(e,t){this._register(F(e,I.KEY_DOWN,e=>t(new wn(e))))}onkeyup(e,t){this._register(F(e,I.KEY_UP,e=>t(new wn(e))))}oninput(e,t){this._register(F(e,I.INPUT,t))}onblur(e,t){this._register(F(e,I.BLUR,t))}onfocus(e,t){this._register(F(e,I.FOCUS,t))}onchange(e,t){this._register(F(e,I.CHANGE,t))}ignoreGesture(e){return Or.ignoreTarget(e)}},Ar=11,jr=class extends kr{constructor(e){super(),this._onActivate=e.onActivate,this.bgDomNode=document.createElement(`div`),this.bgDomNode.className=`arrow-background`,this.bgDomNode.style.position=`absolute`,this.bgDomNode.style.width=e.bgWidth+`px`,this.bgDomNode.style.height=e.bgHeight+`px`,typeof e.top<`u`&&(this.bgDomNode.style.top=`0px`),typeof e.left<`u`&&(this.bgDomNode.style.left=`0px`),typeof e.bottom<`u`&&(this.bgDomNode.style.bottom=`0px`),typeof e.right<`u`&&(this.bgDomNode.style.right=`0px`),this.domNode=document.createElement(`div`),this.domNode.className=e.className,this.domNode.style.position=`absolute`,this.domNode.style.width=Ar+`px`,this.domNode.style.height=Ar+`px`,typeof e.top<`u`&&(this.domNode.style.top=e.top+`px`),typeof e.left<`u`&&(this.domNode.style.left=e.left+`px`),typeof e.bottom<`u`&&(this.domNode.style.bottom=e.bottom+`px`),typeof e.right<`u`&&(this.domNode.style.right=e.right+`px`),this._pointerMoveMonitor=this._register(new wr),this._register(fr(this.bgDomNode,I.POINTER_DOWN,e=>this._arrowPointerDown(e))),this._register(fr(this.domNode,I.POINTER_DOWN,e=>this._arrowPointerDown(e))),this._pointerdownRepeatTimer=this._register(new mr),this._pointerdownScheduleRepeatTimer=this._register(new Nn)}_arrowPointerDown(e){!e.target||!(e.target instanceof Element)||(this._onActivate(),this._pointerdownRepeatTimer.cancel(),this._pointerdownScheduleRepeatTimer.cancelAndSet(()=>{this._pointerdownRepeatTimer.cancelAndSet(()=>this._onActivate(),1e3/24,$n(e))},200),this._pointerMoveMonitor.startMonitoring(e.target,e.pointerId,e.buttons,e=>{},()=>{this._pointerdownRepeatTimer.cancel(),this._pointerdownScheduleRepeatTimer.cancel()}),e.preventDefault())}},Mr=class e{constructor(e,t,n,r,i,a,o){this._forceIntegerValues=e,this._scrollStateBrand=void 0,this._forceIntegerValues&&(t|=0,n|=0,r|=0,i|=0,a|=0,o|=0),this.rawScrollLeft=r,this.rawScrollTop=o,t<0&&(t=0),r+t>n&&(r=n-t),r<0&&(r=0),i<0&&(i=0),o+i>a&&(o=a-i),o<0&&(o=0),this.width=t,this.scrollWidth=n,this.scrollLeft=r,this.height=i,this.scrollHeight=a,this.scrollTop=o}equals(e){return this.rawScrollLeft===e.rawScrollLeft&&this.rawScrollTop===e.rawScrollTop&&this.width===e.width&&this.scrollWidth===e.scrollWidth&&this.scrollLeft===e.scrollLeft&&this.height===e.height&&this.scrollHeight===e.scrollHeight&&this.scrollTop===e.scrollTop}withScrollDimensions(t,n){return new e(this._forceIntegerValues,typeof t.width<`u`?t.width:this.width,typeof t.scrollWidth<`u`?t.scrollWidth:this.scrollWidth,n?this.rawScrollLeft:this.scrollLeft,typeof t.height<`u`?t.height:this.height,typeof t.scrollHeight<`u`?t.scrollHeight:this.scrollHeight,n?this.rawScrollTop:this.scrollTop)}withScrollPosition(t){return new e(this._forceIntegerValues,this.width,this.scrollWidth,typeof t.scrollLeft<`u`?t.scrollLeft:this.rawScrollLeft,this.height,this.scrollHeight,typeof t.scrollTop<`u`?t.scrollTop:this.rawScrollTop)}createScrollEvent(e,t){let n=this.width!==e.width,r=this.scrollWidth!==e.scrollWidth,i=this.scrollLeft!==e.scrollLeft,a=this.height!==e.height,o=this.scrollHeight!==e.scrollHeight,s=this.scrollTop!==e.scrollTop;return{inSmoothScrolling:t,oldWidth:e.width,oldScrollWidth:e.scrollWidth,oldScrollLeft:e.scrollLeft,width:this.width,scrollWidth:this.scrollWidth,scrollLeft:this.scrollLeft,oldHeight:e.height,oldScrollHeight:e.scrollHeight,oldScrollTop:e.scrollTop,height:this.height,scrollHeight:this.scrollHeight,scrollTop:this.scrollTop,widthChanged:n,scrollWidthChanged:r,scrollLeftChanged:i,heightChanged:a,scrollHeightChanged:o,scrollTopChanged:s}}},Nr=class extends j{constructor(e){super(),this._scrollableBrand=void 0,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._smoothScrollDuration=e.smoothScrollDuration,this._scheduleAtNextAnimationFrame=e.scheduleAtNextAnimationFrame,this._state=new Mr(e.forceIntegerValues,0,0,0,0,0,0),this._smoothScrolling=null}dispose(){this._smoothScrolling&&=(this._smoothScrolling.dispose(),null),super.dispose()}setSmoothScrollDuration(e){this._smoothScrollDuration=e}validateScrollPosition(e){return this._state.withScrollPosition(e)}getScrollDimensions(){return this._state}setScrollDimensions(e,t){let n=this._state.withScrollDimensions(e,t);this._setState(n,!!this._smoothScrolling),this._smoothScrolling?.acceptScrollDimensions(this._state)}getFutureScrollPosition(){return this._smoothScrolling?this._smoothScrolling.to:this._state}getCurrentScrollPosition(){return this._state}setScrollPositionNow(e){let t=this._state.withScrollPosition(e);this._smoothScrolling&&=(this._smoothScrolling.dispose(),null),this._setState(t,!1)}setScrollPositionSmooth(e,t){if(this._smoothScrollDuration===0)return this.setScrollPositionNow(e);if(this._smoothScrolling){e={scrollLeft:typeof e.scrollLeft>`u`?this._smoothScrolling.to.scrollLeft:e.scrollLeft,scrollTop:typeof e.scrollTop>`u`?this._smoothScrolling.to.scrollTop:e.scrollTop};let n=this._state.withScrollPosition(e);if(this._smoothScrolling.to.scrollLeft===n.scrollLeft&&this._smoothScrolling.to.scrollTop===n.scrollTop)return;let r;r=t?new Lr(this._smoothScrolling.from,n,this._smoothScrolling.startTime,this._smoothScrolling.duration):this._smoothScrolling.combine(this._state,n,this._smoothScrollDuration),this._smoothScrolling.dispose(),this._smoothScrolling=r}else{let t=this._state.withScrollPosition(e);this._smoothScrolling=Lr.start(this._state,t,this._smoothScrollDuration)}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{this._smoothScrolling&&(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}hasPendingScrollAnimation(){return!!this._smoothScrolling}_performSmoothScrolling(){if(!this._smoothScrolling)return;let e=this._smoothScrolling.tick(),t=this._state.withScrollPosition(e);if(this._setState(t,!0),this._smoothScrolling){if(e.isDone){this._smoothScrolling.dispose(),this._smoothScrolling=null;return}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{this._smoothScrolling&&(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}}_setState(e,t){let n=this._state;n.equals(e)||(this._state=e,this._onScroll.fire(this._state.createScrollEvent(n,t)))}},Pr=class{constructor(e,t,n){this.scrollLeft=e,this.scrollTop=t,this.isDone=n}};function Fr(e,t){let n=t-e;return function(t){return e+n*zr(t)}}function Ir(e,t,n){return function(r){return r2.5*n){let r,i;return e{this._domNode?.setClassName(this._visibleClassName)},0))}_hide(e){this._revealTimer.cancel(),this._isVisible&&(this._isVisible=!1,this._domNode?.setClassName(this._invisibleClassName+(e?` fade`:``)))}},Vr=140,Hr=class extends kr{constructor(e){super(),this._lazyRender=e.lazyRender,this._host=e.host,this._scrollable=e.scrollable,this._scrollByPage=e.scrollByPage,this._scrollbarState=e.scrollbarState,this._visibilityController=this._register(new Br(e.visibility,`visible scrollbar `+e.extraScrollbarClassName,`invisible scrollbar `+e.extraScrollbarClassName)),this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._pointerMoveMonitor=this._register(new wr),this._shouldRender=!0,this.domNode=Cr(document.createElement(`div`)),this.domNode.setAttribute(`role`,`presentation`),this.domNode.setAttribute(`aria-hidden`,`true`),this._visibilityController.setDomNode(this.domNode),this.domNode.setPosition(`absolute`),this._register(F(this.domNode.domNode,I.POINTER_DOWN,e=>this._domNodePointerDown(e)))}_createArrow(e){let t=this._register(new jr(e));this.domNode.domNode.appendChild(t.bgDomNode),this.domNode.domNode.appendChild(t.domNode)}_createSlider(e,t,n,r){this.slider=Cr(document.createElement(`div`)),this.slider.setClassName(`slider`),this.slider.setPosition(`absolute`),this.slider.setTop(e),this.slider.setLeft(t),typeof n==`number`&&this.slider.setWidth(n),typeof r==`number`&&this.slider.setHeight(r),this.slider.setLayerHinting(!0),this.slider.setContain(`strict`),this.domNode.domNode.appendChild(this.slider.domNode),this._register(F(this.slider.domNode,I.POINTER_DOWN,e=>{e.button===0&&(e.preventDefault(),this._sliderPointerDown(e))})),this.onclick(this.slider.domNode,e=>{e.leftButton&&e.stopPropagation()})}_onElementSize(e){return this._scrollbarState.setVisibleSize(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollSize(e){return this._scrollbarState.setScrollSize(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollPosition(e){return this._scrollbarState.setScrollPosition(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}beginReveal(){this._visibilityController.setShouldBeVisible(!0)}beginHide(){this._visibilityController.setShouldBeVisible(!1)}render(){this._shouldRender&&(this._shouldRender=!1,this._renderDomNode(this._scrollbarState.getRectangleLargeSize(),this._scrollbarState.getRectangleSmallSize()),this._updateSlider(this._scrollbarState.getSliderSize(),this._scrollbarState.getArrowSize()+this._scrollbarState.getSliderPosition()))}_domNodePointerDown(e){e.target===this.domNode.domNode&&this._onPointerDown(e)}delegatePointerDown(e){let t=this.domNode.domNode.getClientRects()[0].top,n=t+this._scrollbarState.getSliderPosition(),r=t+this._scrollbarState.getSliderPosition()+this._scrollbarState.getSliderSize(),i=this._sliderPointerPosition(e);n<=i&&i<=r?e.button===0&&(e.preventDefault(),this._sliderPointerDown(e)):this._onPointerDown(e)}_onPointerDown(e){let t,n;if(e.target===this.domNode.domNode&&typeof e.offsetX==`number`&&typeof e.offsetY==`number`)t=e.offsetX,n=e.offsetY;else{let r=_r(this.domNode.domNode);t=e.pageX-r.left,n=e.pageY-r.top}let r=this._pointerDownRelativePosition(t,n);this._setDesiredScrollPositionNow(this._scrollByPage?this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(r):this._scrollbarState.getDesiredScrollPositionFromOffset(r)),e.button===0&&(e.preventDefault(),this._sliderPointerDown(e))}_sliderPointerDown(e){if(!e.target||!(e.target instanceof Element))return;let t=this._sliderPointerPosition(e),n=this._sliderOrthogonalPointerPosition(e),r=this._scrollbarState.clone();this.slider.toggleClassName(`active`,!0),this._pointerMoveMonitor.startMonitoring(e.target,e.pointerId,e.buttons,e=>{let i=this._sliderOrthogonalPointerPosition(e),a=Math.abs(i-n);if(en&&a>Vr){this._setDesiredScrollPositionNow(r.getScrollPosition());return}let o=this._sliderPointerPosition(e)-t;this._setDesiredScrollPositionNow(r.getDesiredScrollPositionFromDelta(o))},()=>{this.slider.toggleClassName(`active`,!1),this._host.onDragEnd()}),this._host.onDragStart()}_setDesiredScrollPositionNow(e){let t={};this.writeScrollPosition(t,e),this._scrollable.setScrollPositionNow(t)}updateScrollbarSize(e){this._updateScrollbarSize(e),this._scrollbarState.setScrollbarSize(e),this._shouldRender=!0,this._lazyRender||this.render()}isNeeded(){return this._scrollbarState.isNeeded()}},Ur=class e{constructor(e,t,n,r,i,a){this._scrollbarSize=Math.round(t),this._oppositeScrollbarSize=Math.round(n),this._arrowSize=Math.round(e),this._visibleSize=r,this._scrollSize=i,this._scrollPosition=a,this._computedAvailableSize=0,this._computedIsNeeded=!1,this._computedSliderSize=0,this._computedSliderRatio=0,this._computedSliderPosition=0,this._refreshComputedValues()}clone(){return new e(this._arrowSize,this._scrollbarSize,this._oppositeScrollbarSize,this._visibleSize,this._scrollSize,this._scrollPosition)}setVisibleSize(e){let t=Math.round(e);return this._visibleSize===t?!1:(this._visibleSize=t,this._refreshComputedValues(),!0)}setScrollSize(e){let t=Math.round(e);return this._scrollSize===t?!1:(this._scrollSize=t,this._refreshComputedValues(),!0)}setScrollPosition(e){let t=Math.round(e);return this._scrollPosition===t?!1:(this._scrollPosition=t,this._refreshComputedValues(),!0)}setScrollbarSize(e){this._scrollbarSize=Math.round(e)}setOppositeScrollbarSize(e){this._oppositeScrollbarSize=Math.round(e)}static _computeValues(e,t,n,r,i){let a=Math.max(0,n-e),o=Math.max(0,a-2*t),s=r>0&&r>n;if(!s)return{computedAvailableSize:Math.round(a),computedIsNeeded:s,computedSliderSize:Math.round(o),computedSliderRatio:0,computedSliderPosition:0};let c=Math.round(Math.max(20,Math.floor(n*o/r))),l=(o-c)/(r-n),u=i*l;return{computedAvailableSize:Math.round(a),computedIsNeeded:s,computedSliderSize:Math.round(c),computedSliderRatio:l,computedSliderPosition:Math.round(u)}}_refreshComputedValues(){let t=e._computeValues(this._oppositeScrollbarSize,this._arrowSize,this._visibleSize,this._scrollSize,this._scrollPosition);this._computedAvailableSize=t.computedAvailableSize,this._computedIsNeeded=t.computedIsNeeded,this._computedSliderSize=t.computedSliderSize,this._computedSliderRatio=t.computedSliderRatio,this._computedSliderPosition=t.computedSliderPosition}getArrowSize(){return this._arrowSize}getScrollPosition(){return this._scrollPosition}getRectangleLargeSize(){return this._computedAvailableSize}getRectangleSmallSize(){return this._scrollbarSize}isNeeded(){return this._computedIsNeeded}getSliderSize(){return this._computedSliderSize}getSliderPosition(){return this._computedSliderPosition}getDesiredScrollPositionFromOffset(e){if(!this._computedIsNeeded)return 0;let t=e-this._arrowSize-this._computedSliderSize/2;return Math.round(t/this._computedSliderRatio)}getDesiredScrollPositionFromOffsetPaged(e){if(!this._computedIsNeeded)return 0;let t=e-this._arrowSize,n=this._scrollPosition;return t0&&Math.abs(e.deltaY)>0)return 1;let n=.5;if((!this._isAlmostInt(e.deltaX)||!this._isAlmostInt(e.deltaY))&&(n+=.25),t){let r=Math.abs(e.deltaX),i=Math.abs(e.deltaY),a=Math.abs(t.deltaX),o=Math.abs(t.deltaY),s=Math.max(Math.min(r,a),1),c=Math.max(Math.min(i,o),1),l=Math.max(r,a),u=Math.max(i,o);l%s===0&&u%c===0&&(n-=.5)}return Math.min(Math.max(n,0),1)}_isAlmostInt(e){return Math.abs(Math.round(e)-e)<.01}};Xr.INSTANCE=new Xr;var Zr=Xr,Qr=class extends kr{constructor(e,t,n){super(),this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._onWillScroll=this._register(new P),this.onWillScroll=this._onWillScroll.event,this._options=ei(t),this._scrollable=n,this._register(this._scrollable.onScroll(e=>{this._onWillScroll.fire(e),this._onDidScroll(e),this._onScroll.fire(e)}));let r={onMouseWheel:e=>this._onMouseWheel(e),onDragStart:()=>this._onDragStart(),onDragEnd:()=>this._onDragEnd()};this._verticalScrollbar=this._register(new Gr(this._scrollable,this._options,r)),this._horizontalScrollbar=this._register(new Wr(this._scrollable,this._options,r)),this._domNode=document.createElement(`div`),this._domNode.className=`xterm-scrollable-element `+this._options.className,this._domNode.setAttribute(`role`,`presentation`),this._domNode.style.position=`relative`,this._domNode.appendChild(e),this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode),this._domNode.appendChild(this._verticalScrollbar.domNode.domNode),this._options.useShadows?(this._leftShadowDomNode=Cr(document.createElement(`div`)),this._leftShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._leftShadowDomNode.domNode),this._topShadowDomNode=Cr(document.createElement(`div`)),this._topShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._topShadowDomNode.domNode),this._topLeftShadowDomNode=Cr(document.createElement(`div`)),this._topLeftShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._topLeftShadowDomNode.domNode)):(this._leftShadowDomNode=null,this._topShadowDomNode=null,this._topLeftShadowDomNode=null),this._listenOnDomNode=this._options.listenOnDomNode||this._domNode,this._mouseWheelToDispose=[],this._setListeningToMouseWheel(this._options.handleMouseWheel),this.onmouseover(this._listenOnDomNode,e=>this._onMouseOver(e)),this.onmouseleave(this._listenOnDomNode,e=>this._onMouseLeave(e)),this._hideTimeout=this._register(new Nn),this._isDragging=!1,this._mouseIsOver=!1,this._shouldRender=!0,this._revealOnScroll=!0}get options(){return this._options}dispose(){this._mouseWheelToDispose=lt(this._mouseWheelToDispose),super.dispose()}getDomNode(){return this._domNode}getOverviewRulerLayoutInfo(){return{parent:this._domNode,insertBefore:this._verticalScrollbar.domNode.domNode}}delegateVerticalScrollbarPointerDown(e){this._verticalScrollbar.delegatePointerDown(e)}getScrollDimensions(){return this._scrollable.getScrollDimensions()}setScrollDimensions(e){this._scrollable.setScrollDimensions(e,!1)}updateClassName(e){this._options.className=e,tn&&(this._options.className+=` mac`),this._domNode.className=`xterm-scrollable-element `+this._options.className}updateOptions(e){typeof e.handleMouseWheel<`u`&&(this._options.handleMouseWheel=e.handleMouseWheel,this._setListeningToMouseWheel(this._options.handleMouseWheel)),typeof e.mouseWheelScrollSensitivity<`u`&&(this._options.mouseWheelScrollSensitivity=e.mouseWheelScrollSensitivity),typeof e.fastScrollSensitivity<`u`&&(this._options.fastScrollSensitivity=e.fastScrollSensitivity),typeof e.scrollPredominantAxis<`u`&&(this._options.scrollPredominantAxis=e.scrollPredominantAxis),typeof e.horizontal<`u`&&(this._options.horizontal=e.horizontal),typeof e.vertical<`u`&&(this._options.vertical=e.vertical),typeof e.horizontalScrollbarSize<`u`&&(this._options.horizontalScrollbarSize=e.horizontalScrollbarSize),typeof e.verticalScrollbarSize<`u`&&(this._options.verticalScrollbarSize=e.verticalScrollbarSize),typeof e.scrollByPage<`u`&&(this._options.scrollByPage=e.scrollByPage),this._horizontalScrollbar.updateOptions(this._options),this._verticalScrollbar.updateOptions(this._options),this._options.lazyRender||this._render()}setRevealOnScroll(e){this._revealOnScroll=e}delegateScrollFromMouseWheelEvent(e){this._onMouseWheel(new kn(e))}_setListeningToMouseWheel(e){this._mouseWheelToDispose.length>0!==e&&(this._mouseWheelToDispose=lt(this._mouseWheelToDispose),e)&&this._mouseWheelToDispose.push(F(this._listenOnDomNode,I.MOUSE_WHEEL,e=>{this._onMouseWheel(new kn(e))},{passive:!1}))}_onMouseWheel(e){if(e.browserEvent?.defaultPrevented)return;let t=Zr.INSTANCE;Jr&&t.acceptStandardWheelEvent(e);let n=!1;if(e.deltaY||e.deltaX){let r=e.deltaY*this._options.mouseWheelScrollSensitivity,i=e.deltaX*this._options.mouseWheelScrollSensitivity;this._options.scrollPredominantAxis&&(this._options.scrollYToX&&i+r===0?i=r=0:Math.abs(r)>=Math.abs(i)?i=0:r=0),this._options.flipAxes&&([r,i]=[i,r]);let a=!tn&&e.browserEvent&&e.browserEvent.shiftKey;(this._options.scrollYToX||a)&&!i&&(i=r,r=0),e.browserEvent&&e.browserEvent.altKey&&(i*=this._options.fastScrollSensitivity,r*=this._options.fastScrollSensitivity);let o=this._scrollable.getFutureScrollPosition(),s={};if(r){let e=qr*r,t=o.scrollTop-(e<0?Math.floor(e):Math.ceil(e));this._verticalScrollbar.writeScrollPosition(s,t)}if(i){let e=qr*i,t=o.scrollLeft-(e<0?Math.floor(e):Math.ceil(e));this._horizontalScrollbar.writeScrollPosition(s,t)}s=this._scrollable.validateScrollPosition(s),(o.scrollLeft!==s.scrollLeft||o.scrollTop!==s.scrollTop)&&(Jr&&this._options.mouseWheelSmoothScroll&&t.isPhysicalMouseWheel()?this._scrollable.setScrollPositionSmooth(s):this._scrollable.setScrollPositionNow(s),n=!0)}let r=n;!r&&this._options.alwaysConsumeMouseWheel&&(r=!0),!r&&this._options.consumeMouseWheelIfScrollbarIsNeeded&&(this._verticalScrollbar.isNeeded()||this._horizontalScrollbar.isNeeded())&&(r=!0),r&&(e.preventDefault(),e.stopPropagation())}_onDidScroll(e){this._shouldRender=this._horizontalScrollbar.onDidScroll(e)||this._shouldRender,this._shouldRender=this._verticalScrollbar.onDidScroll(e)||this._shouldRender,this._options.useShadows&&(this._shouldRender=!0),this._revealOnScroll&&this._reveal(),this._options.lazyRender||this._render()}renderNow(){if(!this._options.lazyRender)throw Error("Please use `lazyRender` together with `renderNow`!");this._render()}_render(){if(this._shouldRender&&(this._shouldRender=!1,this._horizontalScrollbar.render(),this._verticalScrollbar.render(),this._options.useShadows)){let e=this._scrollable.getCurrentScrollPosition(),t=e.scrollTop>0,n=e.scrollLeft>0,r=n?` left`:``,i=t?` top`:``,a=n||t?` top-left-corner`:``;this._leftShadowDomNode.setClassName(`shadow${r}`),this._topShadowDomNode.setClassName(`shadow${i}`),this._topLeftShadowDomNode.setClassName(`shadow${a}${i}${r}`)}}_onDragStart(){this._isDragging=!0,this._reveal()}_onDragEnd(){this._isDragging=!1,this._hide()}_onMouseLeave(e){this._mouseIsOver=!1,this._hide()}_onMouseOver(e){this._mouseIsOver=!0,this._reveal()}_reveal(){this._verticalScrollbar.beginReveal(),this._horizontalScrollbar.beginReveal(),this._scheduleHide()}_hide(){!this._mouseIsOver&&!this._isDragging&&(this._verticalScrollbar.beginHide(),this._horizontalScrollbar.beginHide())}_scheduleHide(){!this._mouseIsOver&&!this._isDragging&&this._hideTimeout.cancelAndSet(()=>this._hide(),Kr)}},$r=class extends Qr{constructor(e,t,n){super(e,t,n)}setScrollPosition(e){e.reuseAnimation?this._scrollable.setScrollPositionSmooth(e,e.reuseAnimation):this._scrollable.setScrollPositionNow(e)}getScrollPosition(){return this._scrollable.getCurrentScrollPosition()}};function ei(e){let t={lazyRender:typeof e.lazyRender<`u`&&e.lazyRender,className:typeof e.className<`u`?e.className:``,useShadows:typeof e.useShadows<`u`?e.useShadows:!0,handleMouseWheel:typeof e.handleMouseWheel<`u`?e.handleMouseWheel:!0,flipAxes:typeof e.flipAxes<`u`&&e.flipAxes,consumeMouseWheelIfScrollbarIsNeeded:typeof e.consumeMouseWheelIfScrollbarIsNeeded<`u`&&e.consumeMouseWheelIfScrollbarIsNeeded,alwaysConsumeMouseWheel:typeof e.alwaysConsumeMouseWheel<`u`&&e.alwaysConsumeMouseWheel,scrollYToX:typeof e.scrollYToX<`u`&&e.scrollYToX,mouseWheelScrollSensitivity:typeof e.mouseWheelScrollSensitivity<`u`?e.mouseWheelScrollSensitivity:1,fastScrollSensitivity:typeof e.fastScrollSensitivity<`u`?e.fastScrollSensitivity:5,scrollPredominantAxis:typeof e.scrollPredominantAxis<`u`?e.scrollPredominantAxis:!0,mouseWheelSmoothScroll:typeof e.mouseWheelSmoothScroll<`u`?e.mouseWheelSmoothScroll:!0,arrowSize:typeof e.arrowSize<`u`?e.arrowSize:11,listenOnDomNode:typeof e.listenOnDomNode<`u`?e.listenOnDomNode:null,horizontal:typeof e.horizontal<`u`?e.horizontal:1,horizontalScrollbarSize:typeof e.horizontalScrollbarSize<`u`?e.horizontalScrollbarSize:10,horizontalSliderSize:typeof e.horizontalSliderSize<`u`?e.horizontalSliderSize:0,horizontalHasArrows:typeof e.horizontalHasArrows<`u`&&e.horizontalHasArrows,vertical:typeof e.vertical<`u`?e.vertical:1,verticalScrollbarSize:typeof e.verticalScrollbarSize<`u`?e.verticalScrollbarSize:10,verticalHasArrows:typeof e.verticalHasArrows<`u`&&e.verticalHasArrows,verticalSliderSize:typeof e.verticalSliderSize<`u`?e.verticalSliderSize:0,scrollByPage:typeof e.scrollByPage<`u`&&e.scrollByPage};return t.horizontalSliderSize=typeof e.horizontalSliderSize<`u`?e.horizontalSliderSize:t.horizontalScrollbarSize,t.verticalSliderSize=typeof e.verticalSliderSize<`u`?e.verticalSliderSize:t.verticalScrollbarSize,tn&&(t.className+=` mac`),t}var ti=class extends j{constructor(e,t,n,r,i,a,o,s){super(),this._bufferService=n,this._optionsService=o,this._renderService=s,this._onRequestScrollLines=this._register(new P),this.onRequestScrollLines=this._onRequestScrollLines.event,this._isSyncing=!1,this._isHandlingScroll=!1,this._suppressOnScrollHandler=!1;let c=this._register(new Nr({forceIntegerValues:!1,smoothScrollDuration:this._optionsService.rawOptions.smoothScrollDuration,scheduleAtNextAnimationFrame:e=>pr(r.window,e)}));this._register(this._optionsService.onSpecificOptionChange(`smoothScrollDuration`,()=>{c.setSmoothScrollDuration(this._optionsService.rawOptions.smoothScrollDuration)})),this._scrollableElement=this._register(new $r(t,{vertical:1,horizontal:2,useShadows:!1,mouseWheelSmoothScroll:!0,...this._getChangeOptions()},c)),this._register(this._optionsService.onMultipleOptionChange([`scrollSensitivity`,`fastScrollSensitivity`,`overviewRuler`],()=>this._scrollableElement.updateOptions(this._getChangeOptions()))),this._register(i.onProtocolChange(e=>{this._scrollableElement.updateOptions({handleMouseWheel:!(e&16)})})),this._scrollableElement.setScrollDimensions({height:0,scrollHeight:0}),this._register(N.runAndSubscribe(a.onChangeColors,()=>{this._scrollableElement.getDomNode().style.backgroundColor=a.colors.background.css})),e.appendChild(this._scrollableElement.getDomNode()),this._register(A(()=>this._scrollableElement.getDomNode().remove())),this._styleElement=r.mainDocument.createElement(`style`),t.appendChild(this._styleElement),this._register(A(()=>this._styleElement.remove())),this._register(N.runAndSubscribe(a.onChangeColors,()=>{this._styleElement.textContent=[`.xterm .xterm-scrollable-element > .scrollbar > .slider {`,` background: ${a.colors.scrollbarSliderBackground.css};`,`}`,`.xterm .xterm-scrollable-element > .scrollbar > .slider:hover {`,` background: ${a.colors.scrollbarSliderHoverBackground.css};`,`}`,`.xterm .xterm-scrollable-element > .scrollbar > .slider.active {`,` background: ${a.colors.scrollbarSliderActiveBackground.css};`,`}`].join(` -`)})),this._register(this._bufferService.onResize(()=>this.queueSync())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._latestYDisp=void 0,this.queueSync()})),this._register(this._bufferService.onScroll(()=>this._sync())),this._register(this._scrollableElement.onScroll(e=>this._handleScroll(e)))}scrollLines(e){let t=this._scrollableElement.getScrollPosition();this._scrollableElement.setScrollPosition({reuseAnimation:!0,scrollTop:t.scrollTop+e*this._renderService.dimensions.css.cell.height})}scrollToLine(e,t){t&&(this._latestYDisp=e),this._scrollableElement.setScrollPosition({reuseAnimation:!t,scrollTop:e*this._renderService.dimensions.css.cell.height})}_getChangeOptions(){return{mouseWheelScrollSensitivity:this._optionsService.rawOptions.scrollSensitivity,fastScrollSensitivity:this._optionsService.rawOptions.fastScrollSensitivity,verticalScrollbarSize:this._optionsService.rawOptions.overviewRuler?.width||14}}queueSync(e){e!==void 0&&(this._latestYDisp=e),this._queuedAnimationFrame===void 0&&(this._queuedAnimationFrame=this._renderService.addRefreshCallback(()=>{this._queuedAnimationFrame=void 0,this._sync(this._latestYDisp)}))}_sync(e=this._bufferService.buffer.ydisp){!this._renderService||this._isSyncing||(this._isSyncing=!0,this._suppressOnScrollHandler=!0,this._scrollableElement.setScrollDimensions({height:this._renderService.dimensions.css.canvas.height,scrollHeight:this._renderService.dimensions.css.cell.height*this._bufferService.buffer.lines.length}),this._suppressOnScrollHandler=!1,e!==this._latestYDisp&&this._scrollableElement.setScrollPosition({scrollTop:e*this._renderService.dimensions.css.cell.height}),this._isSyncing=!1)}_handleScroll(e){if(!this._renderService||this._isHandlingScroll||this._suppressOnScrollHandler)return;this._isHandlingScroll=!0;let t=Math.round(e.scrollTop/this._renderService.dimensions.css.cell.height),n=t-this._bufferService.buffer.ydisp;n!==0&&(this._latestYDisp=t,this._onRequestScrollLines.fire(n)),this._isHandlingScroll=!1}};ti=y([b(2,O),b(3,je),b(4,be),b(5,Ie),b(6,k),b(7,Ne)],ti);var ni=class extends j{constructor(e,t,n,r,i){super(),this._screenElement=e,this._bufferService=t,this._coreBrowserService=n,this._decorationService=r,this._renderService=i,this._decorationElements=new Map,this._altBufferIsActive=!1,this._dimensionsChanged=!1,this._container=document.createElement(`div`),this._container.classList.add(`xterm-decoration-container`),this._screenElement.appendChild(this._container),this._register(this._renderService.onRenderedViewportChange(()=>this._doRefreshDecorations())),this._register(this._renderService.onDimensionsChange(()=>{this._dimensionsChanged=!0,this._queueRefresh()})),this._register(this._coreBrowserService.onDprChange(()=>this._queueRefresh())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._altBufferIsActive=this._bufferService.buffer===this._bufferService.buffers.alt})),this._register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh())),this._register(this._decorationService.onDecorationRemoved(e=>this._removeDecoration(e))),this._register(A(()=>{this._container.remove(),this._decorationElements.clear()}))}_queueRefresh(){this._animationFrame===void 0&&(this._animationFrame=this._renderService.addRefreshCallback(()=>{this._doRefreshDecorations(),this._animationFrame=void 0}))}_doRefreshDecorations(){for(let e of this._decorationService.decorations)this._renderDecoration(e);this._dimensionsChanged=!1}_renderDecoration(e){this._refreshStyle(e),this._dimensionsChanged&&this._refreshXPosition(e)}_createElement(e){let t=this._coreBrowserService.mainDocument.createElement(`div`);t.classList.add(`xterm-decoration`),t.classList.toggle(`xterm-decoration-top-layer`,e?.options?.layer===`top`),t.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,t.style.height=`${(e.options.height||1)*this._renderService.dimensions.css.cell.height}px`,t.style.top=`${(e.marker.line-this._bufferService.buffers.active.ydisp)*this._renderService.dimensions.css.cell.height}px`,t.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`;let n=e.options.x??0;return n&&n>this._bufferService.cols&&(t.style.display=`none`),this._refreshXPosition(e,t),t}_refreshStyle(e){let t=e.marker.line-this._bufferService.buffers.active.ydisp;if(t<0||t>=this._bufferService.rows)e.element&&(e.element.style.display=`none`,e.onRenderEmitter.fire(e.element));else{let n=this._decorationElements.get(e);n||(n=this._createElement(e),e.element=n,this._decorationElements.set(e,n),this._container.appendChild(n),e.onDispose(()=>{this._decorationElements.delete(e),n.remove()})),n.style.display=this._altBufferIsActive?`none`:`block`,this._altBufferIsActive||(n.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,n.style.height=`${(e.options.height||1)*this._renderService.dimensions.css.cell.height}px`,n.style.top=`${t*this._renderService.dimensions.css.cell.height}px`,n.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`),e.onRenderEmitter.fire(n)}}_refreshXPosition(e,t=e.element){if(!t)return;let n=e.options.x??0;(e.options.anchor||`left`)===`right`?t.style.right=n?`${n*this._renderService.dimensions.css.cell.width}px`:``:t.style.left=n?`${n*this._renderService.dimensions.css.cell.width}px`:``}_removeDecoration(e){this._decorationElements.get(e)?.remove(),this._decorationElements.delete(e),e.dispose()}};ni=y([b(1,O),b(2,je),b(3,De),b(4,Ne)],ni);var ri=class{constructor(){this._zones=[],this._zonePool=[],this._zonePoolIndex=0,this._linePadding={full:0,left:0,center:0,right:0}}get zones(){return this._zonePool.length=Math.min(this._zonePool.length,this._zones.length),this._zones}clear(){this._zones.length=0,this._zonePoolIndex=0}addDecoration(e){if(e.options.overviewRulerOptions){for(let t of this._zones)if(t.color===e.options.overviewRulerOptions.color&&t.position===e.options.overviewRulerOptions.position){if(this._lineIntersectsZone(t,e.marker.line))return;if(this._lineAdjacentToZone(t,e.marker.line,e.options.overviewRulerOptions.position)){this._addLineToZone(t,e.marker.line);return}}if(this._zonePoolIndex=e.startBufferLine&&t<=e.endBufferLine}_lineAdjacentToZone(e,t,n){return t>=e.startBufferLine-this._linePadding[n||`full`]&&t<=e.endBufferLine+this._linePadding[n||`full`]}_addLineToZone(e,t){e.startBufferLine=Math.min(e.startBufferLine,t),e.endBufferLine=Math.max(e.endBufferLine,t)}},ii={full:0,left:0,center:0,right:0},ai={full:0,left:0,center:0,right:0},oi={full:0,left:0,center:0,right:0},si=class extends j{constructor(e,t,n,r,i,a,o,s){super(),this._viewportElement=e,this._screenElement=t,this._bufferService=n,this._decorationService=r,this._renderService=i,this._optionsService=a,this._themeService=o,this._coreBrowserService=s,this._colorZoneStore=new ri,this._shouldUpdateDimensions=!0,this._shouldUpdateAnchor=!0,this._lastKnownBufferLength=0,this._canvas=this._coreBrowserService.mainDocument.createElement(`canvas`),this._canvas.classList.add(`xterm-decoration-overview-ruler`),this._refreshCanvasDimensions(),this._viewportElement.parentElement?.insertBefore(this._canvas,this._viewportElement),this._register(A(()=>this._canvas?.remove()));let c=this._canvas.getContext(`2d`);if(c)this._ctx=c;else throw Error(`Ctx cannot be null`);this._register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh(void 0,!0))),this._register(this._decorationService.onDecorationRemoved(()=>this._queueRefresh(void 0,!0))),this._register(this._renderService.onRenderedViewportChange(()=>this._queueRefresh())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._canvas.style.display=this._bufferService.buffer===this._bufferService.buffers.alt?`none`:`block`})),this._register(this._bufferService.onScroll(()=>{this._lastKnownBufferLength!==this._bufferService.buffers.normal.lines.length&&(this._refreshDrawHeightConstants(),this._refreshColorZonePadding())})),this._register(this._renderService.onRender(()=>{(!this._containerHeight||this._containerHeight!==this._screenElement.clientHeight)&&(this._queueRefresh(!0),this._containerHeight=this._screenElement.clientHeight)})),this._register(this._coreBrowserService.onDprChange(()=>this._queueRefresh(!0))),this._register(this._optionsService.onSpecificOptionChange(`overviewRuler`,()=>this._queueRefresh(!0))),this._register(this._themeService.onChangeColors(()=>this._queueRefresh())),this._queueRefresh(!0)}get _width(){return this._optionsService.options.overviewRuler?.width||0}_refreshDrawConstants(){let e=Math.floor((this._canvas.width-1)/3),t=Math.ceil((this._canvas.width-1)/3);ai.full=this._canvas.width,ai.left=e,ai.center=t,ai.right=e,this._refreshDrawHeightConstants(),oi.full=1,oi.left=1,oi.center=1+ai.left,oi.right=1+ai.left+ai.center}_refreshDrawHeightConstants(){ii.full=Math.round(2*this._coreBrowserService.dpr);let e=this._canvas.height/this._bufferService.buffer.lines.length,t=Math.round(Math.max(Math.min(e,12),6)*this._coreBrowserService.dpr);ii.left=t,ii.center=t,ii.right=t}_refreshColorZonePadding(){this._colorZoneStore.setPadding({full:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*ii.full),left:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*ii.left),center:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*ii.center),right:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*ii.right)}),this._lastKnownBufferLength=this._bufferService.buffers.normal.lines.length}_refreshCanvasDimensions(){this._canvas.style.width=`${this._width}px`,this._canvas.width=Math.round(this._width*this._coreBrowserService.dpr),this._canvas.style.height=`${this._screenElement.clientHeight}px`,this._canvas.height=Math.round(this._screenElement.clientHeight*this._coreBrowserService.dpr),this._refreshDrawConstants(),this._refreshColorZonePadding()}_refreshDecorations(){this._shouldUpdateDimensions&&this._refreshCanvasDimensions(),this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this._colorZoneStore.clear();for(let e of this._decorationService.decorations)this._colorZoneStore.addDecoration(e);this._ctx.lineWidth=1,this._renderRulerOutline();let e=this._colorZoneStore.zones;for(let t of e)t.position!==`full`&&this._renderColorZone(t);for(let t of e)t.position===`full`&&this._renderColorZone(t);this._shouldUpdateDimensions=!1,this._shouldUpdateAnchor=!1}_renderRulerOutline(){this._ctx.fillStyle=this._themeService.colors.overviewRulerBorder.css,this._ctx.fillRect(0,0,1,this._canvas.height),this._optionsService.rawOptions.overviewRuler.showTopBorder&&this._ctx.fillRect(1,0,this._canvas.width-1,1),this._optionsService.rawOptions.overviewRuler.showBottomBorder&&this._ctx.fillRect(1,this._canvas.height-1,this._canvas.width-1,this._canvas.height)}_renderColorZone(e){this._ctx.fillStyle=e.color,this._ctx.fillRect(oi[e.position||`full`],Math.round((this._canvas.height-1)*(e.startBufferLine/this._bufferService.buffers.active.lines.length)-ii[e.position||`full`]/2),ai[e.position||`full`],Math.round((this._canvas.height-1)*((e.endBufferLine-e.startBufferLine)/this._bufferService.buffers.active.lines.length)+ii[e.position||`full`]))}_queueRefresh(e,t){this._shouldUpdateDimensions=e||this._shouldUpdateDimensions,this._shouldUpdateAnchor=t||this._shouldUpdateAnchor,this._animationFrame===void 0&&(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>{this._refreshDecorations(),this._animationFrame=void 0}))}};si=y([b(2,O),b(3,De),b(4,Ne),b(5,k),b(6,Ie),b(7,je)],si);var L;(e=>(e.NUL=`\0`,e.SOH=``,e.STX=``,e.ETX=``,e.EOT=``,e.ENQ=``,e.ACK=``,e.BEL=`\x07`,e.BS=`\b`,e.HT=` `,e.LF=` -`,e.VT=`\v`,e.FF=`\f`,e.CR=`\r`,e.SO=``,e.SI=``,e.DLE=``,e.DC1=``,e.DC2=``,e.DC3=``,e.DC4=``,e.NAK=``,e.SYN=``,e.ETB=``,e.CAN=``,e.EM=``,e.SUB=``,e.ESC=`\x1B`,e.FS=``,e.GS=``,e.RS=``,e.US=``,e.SP=` `,e.DEL=``))(L||={});var ci;(e=>(e.PAD=`€`,e.HOP=``,e.BPH=`‚`,e.NBH=`ƒ`,e.IND=`„`,e.NEL=`…`,e.SSA=`†`,e.ESA=`‡`,e.HTS=`ˆ`,e.HTJ=`‰`,e.VTS=`Š`,e.PLD=`‹`,e.PLU=`Œ`,e.RI=``,e.SS2=`Ž`,e.SS3=``,e.DCS=``,e.PU1=`‘`,e.PU2=`’`,e.STS=`“`,e.CCH=`”`,e.MW=`•`,e.SPA=`–`,e.EPA=`—`,e.SOS=`˜`,e.SGCI=`™`,e.SCI=`š`,e.CSI=`›`,e.ST=`œ`,e.OSC=``,e.PM=`ž`,e.APC=`Ÿ`))(ci||={});var li;(e=>e.ST=`${L.ESC}\\`)(li||={});var ui=class{constructor(e,t,n,r,i,a){this._textarea=e,this._compositionView=t,this._bufferService=n,this._optionsService=r,this._coreService=i,this._renderService=a,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=``}get isComposing(){return this._isComposing}compositionstart(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent=``,this._dataAlreadySent=``,this._compositionView.classList.add(`active`)}compositionupdate(e){this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout(()=>{this._compositionPosition.end=this._textarea.value.length},0)}compositionend(){this._finalizeComposition(!0)}keydown(e){if(this._isComposing||this._isSendingComposition){if(e.keyCode===20||e.keyCode===229||e.keyCode===16||e.keyCode===17||e.keyCode===18)return!1;this._finalizeComposition(!1)}return e.keyCode===229?(this._handleAnyTextareaChanges(),!1):!0}_finalizeComposition(e){if(this._compositionView.classList.remove(`active`),this._isComposing=!1,e){let e={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout(()=>{if(this._isSendingComposition){this._isSendingComposition=!1;let t;e.start+=this._dataAlreadySent.length,t=this._isComposing?this._textarea.value.substring(e.start,this._compositionPosition.start):this._textarea.value.substring(e.start),t.length>0&&this._coreService.triggerDataEvent(t,!0)}},0)}else{this._isSendingComposition=!1;let e=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(e,!0)}}_handleAnyTextareaChanges(){let e=this._textarea.value;setTimeout(()=>{if(!this._isComposing){let t=this._textarea.value,n=t.replace(e,``);this._dataAlreadySent=n,t.length>e.length?this._coreService.triggerDataEvent(n,!0):t.lengththis.updateCompositionElements(!0),0)}}};ui=y([b(2,O),b(3,k),b(4,xe),b(5,Ne)],ui);var R=0,z=0,B=0,V=0,di={css:`#00000000`,rgba:0},H;(e=>{function t(e,t,n,r){return r===void 0?`#${pi(e)}${pi(t)}${pi(n)}`:`#${pi(e)}${pi(t)}${pi(n)}${pi(r)}`}e.toCss=t;function n(e,t,n,r=255){return(e<<24|t<<16|n<<8|r)>>>0}e.toRgba=n;function r(t,n,r,i){return{css:e.toCss(t,n,r,i),rgba:e.toRgba(t,n,r,i)}}e.toColor=r})(H||={});var U;(e=>{function t(e,t){if(V=(t.rgba&255)/255,V===1)return{css:t.css,rgba:t.rgba};let n=t.rgba>>24&255,r=t.rgba>>16&255,i=t.rgba>>8&255,a=e.rgba>>24&255,o=e.rgba>>16&255,s=e.rgba>>8&255;return R=a+Math.round((n-a)*V),z=o+Math.round((r-o)*V),B=s+Math.round((i-s)*V),{css:H.toCss(R,z,B),rgba:H.toRgba(R,z,B)}}e.blend=t;function n(e){return(e.rgba&255)==255}e.isOpaque=n;function r(e,t,n){let r=fi.ensureContrastRatio(e.rgba,t.rgba,n);if(r)return H.toColor(r>>24&255,r>>16&255,r>>8&255)}e.ensureContrastRatio=r;function i(e){let t=(e.rgba|255)>>>0;return[R,z,B]=fi.toChannels(t),{css:H.toCss(R,z,B),rgba:t}}e.opaque=i;function a(e,t){return V=Math.round(t*255),[R,z,B]=fi.toChannels(e.rgba),{css:H.toCss(R,z,B,V),rgba:H.toRgba(R,z,B,V)}}e.opacity=a;function o(e,t){return V=e.rgba&255,a(e,V*t/255)}e.multiplyOpacity=o;function s(e){return[e.rgba>>24&255,e.rgba>>16&255,e.rgba>>8&255]}e.toColorRGB=s})(U||={});var W;(e=>{let t,n;try{let e=document.createElement(`canvas`);e.width=1,e.height=1;let r=e.getContext(`2d`,{willReadFrequently:!0});r&&(t=r,t.globalCompositeOperation=`copy`,n=t.createLinearGradient(0,0,1,1))}catch{}function r(e){if(e.match(/#[\da-f]{3,8}/i))switch(e.length){case 4:return R=parseInt(e.slice(1,2).repeat(2),16),z=parseInt(e.slice(2,3).repeat(2),16),B=parseInt(e.slice(3,4).repeat(2),16),H.toColor(R,z,B);case 5:return R=parseInt(e.slice(1,2).repeat(2),16),z=parseInt(e.slice(2,3).repeat(2),16),B=parseInt(e.slice(3,4).repeat(2),16),V=parseInt(e.slice(4,5).repeat(2),16),H.toColor(R,z,B,V);case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}let r=e.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);if(r)return R=parseInt(r[1]),z=parseInt(r[2]),B=parseInt(r[3]),V=Math.round((r[5]===void 0?1:parseFloat(r[5]))*255),H.toColor(R,z,B,V);if(!t||!n||(t.fillStyle=n,t.fillStyle=e,typeof t.fillStyle!=`string`)||(t.fillRect(0,0,1,1),[R,z,B,V]=t.getImageData(0,0,1,1).data,V!==255))throw Error(`css.toColor: Unsupported css format`);return{rgba:H.toRgba(R,z,B,V),css:e}}e.toColor=r})(W||={});var G;(e=>{function t(e){return n(e>>16&255,e>>8&255,e&255)}e.relativeLuminance=t;function n(e,t,n){let r=e/255,i=t/255,a=n/255,o=r<=.03928?r/12.92:((r+.055)/1.055)**2.4,s=i<=.03928?i/12.92:((i+.055)/1.055)**2.4,c=a<=.03928?a/12.92:((a+.055)/1.055)**2.4;return o*.2126+s*.7152+c*.0722}e.relativeLuminance2=n})(G||={});var fi;(e=>{function t(e,t){if(V=(t&255)/255,V===1)return t;let n=t>>24&255,r=t>>16&255,i=t>>8&255,a=e>>24&255,o=e>>16&255,s=e>>8&255;return R=a+Math.round((n-a)*V),z=o+Math.round((r-o)*V),B=s+Math.round((i-s)*V),H.toRgba(R,z,B)}e.blend=t;function n(e,t,n){let a=G.relativeLuminance(e>>8),o=G.relativeLuminance(t>>8);if(mi(a,o)>8));if(smi(a,G.relativeLuminance(r>>8))?o:r}return o}let s=i(e,t,n),c=mi(a,G.relativeLuminance(s>>8));if(cmi(a,G.relativeLuminance(i>>8))?s:i}return s}}e.ensureContrastRatio=n;function r(e,t,n){let r=e>>24&255,i=e>>16&255,a=e>>8&255,o=t>>24&255,s=t>>16&255,c=t>>8&255,l=mi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));for(;l0||s>0||c>0);)o-=Math.max(0,Math.ceil(o*.1)),s-=Math.max(0,Math.ceil(s*.1)),c-=Math.max(0,Math.ceil(c*.1)),l=mi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));return(o<<24|s<<16|c<<8|255)>>>0}e.reduceLuminance=r;function i(e,t,n){let r=e>>24&255,i=e>>16&255,a=e>>8&255,o=t>>24&255,s=t>>16&255,c=t>>8&255,l=mi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));for(;l>>0}e.increaseLuminance=i;function a(e){return[e>>24&255,e>>16&255,e>>8&255,e&255]}e.toChannels=a})(fi||={});function pi(e){let t=e.toString(16);return t.length<2?`0`+t:t}function mi(e,t){return e1){let e=this._getJoinedRanges(r,o,a,t,i);for(let t=0;t1){let e=this._getJoinedRanges(r,o,a,t,i);for(let t=0;t=re,se=w,T=this._workCell;if(f.length>0&&w===f[0][0]&&oe){let r=f.shift(),i=this._isCellInSelection(r[0],t);for(v=r[0]+1;v=r[1],oe?(ae=!0,T=new hi(this._workCell,e.translateToString(!0,r[0],r[1]),r[1]-r[0]),se=r[1]-1,m=T.getWidth()):re=r[1]}let ce=this._isCellInSelection(w,t),le=n&&w===a,ue=ie&&w>=l&&w<=u,de=!1;this._decorationService.forEachDecorationAtCell(w,t,void 0,e=>{de=!0});let me=T.getChars()||fe;if(me===` `&&(T.isUnderline()||T.isOverline())&&(me=`\xA0`),S=m*s-c.get(me,T.isBold(),T.isItalic()),!h)h=this._document.createElement(`span`);else if(g&&(ce&&ne||!ce&&!ne&&T.bg===ee)&&(ce&&ne&&p.selectionForeground||T.fg===y)&&T.extended.ext===b&&ue===te&&S===x&&!le&&!ae&&!de&&oe){T.isInvisible()?_+=fe:_+=me,g++;continue}else g&&(h.textContent=_),h=this._document.createElement(`span`),g=0,_=``;if(ee=T.bg,y=T.fg,b=T.extended.ext,te=ue,x=S,ne=ce,ae&&a>=w&&a<=se&&(a=w),!this._coreService.isCursorHidden&&le&&this._coreService.isCursorInitialized){if(C.push(`xterm-cursor`),this._coreBrowserService.isFocused)o&&C.push(`xterm-cursor-blink`),C.push(r===`bar`?`xterm-cursor-bar`:r===`underline`?`xterm-cursor-underline`:`xterm-cursor-block`);else if(i)switch(i){case`outline`:C.push(`xterm-cursor-outline`);break;case`block`:C.push(`xterm-cursor-block`);break;case`bar`:C.push(`xterm-cursor-bar`);break;case`underline`:C.push(`xterm-cursor-underline`);break;default:break}}if(T.isBold()&&C.push(`xterm-bold`),T.isItalic()&&C.push(`xterm-italic`),T.isDim()&&C.push(`xterm-dim`),_=T.isInvisible()?fe:T.getChars()||fe,T.isUnderline()&&(C.push(`xterm-underline-${T.extended.underlineStyle}`),_===` `&&(_=`\xA0`),!T.isUnderlineColorDefault()))if(T.isUnderlineColorRGB())h.style.textDecorationColor=`rgb(${pe.toColorRGB(T.getUnderlineColor()).join(`,`)})`;else{let e=T.getUnderlineColor();this._optionsService.rawOptions.drawBoldTextInBrightColors&&T.isBold()&&e<8&&(e+=8),h.style.textDecorationColor=p.ansi[e].css}T.isOverline()&&(C.push(`xterm-overline`),_===` `&&(_=`\xA0`)),T.isStrikethrough()&&C.push(`xterm-strikethrough`),ue&&(h.style.textDecoration=`underline`);let E=T.getFgColor(),he=T.getFgColorMode(),ge=T.getBgColor(),_e=T.getBgColorMode(),ve=!!T.isInverse();if(ve){let e=E;E=ge,ge=e;let t=he;he=_e,_e=t}let D,ye,O=!1;this._decorationService.forEachDecorationAtCell(w,t,void 0,e=>{e.options.layer!==`top`&&O||(e.backgroundColorRGB&&(_e=50331648,ge=e.backgroundColorRGB.rgba>>8&16777215,D=e.backgroundColorRGB),e.foregroundColorRGB&&(he=50331648,E=e.foregroundColorRGB.rgba>>8&16777215,ye=e.foregroundColorRGB),O=e.options.layer===`top`)}),!O&&ce&&(D=this._coreBrowserService.isFocused?p.selectionBackgroundOpaque:p.selectionInactiveBackgroundOpaque,ge=D.rgba>>8&16777215,_e=50331648,O=!0,p.selectionForeground&&(he=50331648,E=p.selectionForeground.rgba>>8&16777215,ye=p.selectionForeground)),O&&C.push(`xterm-decoration-top`);let be;switch(_e){case 16777216:case 33554432:be=p.ansi[ge],C.push(`xterm-bg-${ge}`);break;case 50331648:be=H.toColor(ge>>16,ge>>8&255,ge&255),this._addStyle(h,`background-color:#${Ci((ge>>>0).toString(16),`0`,6)}`);break;default:ve?(be=p.foreground,C.push(`xterm-bg-257`)):be=p.background}switch(D||T.isDim()&&(D=U.multiplyOpacity(be,.5)),he){case 16777216:case 33554432:T.isBold()&&E<8&&this._optionsService.rawOptions.drawBoldTextInBrightColors&&(E+=8),this._applyMinimumContrast(h,be,p.ansi[E],T,D,void 0)||C.push(`xterm-fg-${E}`);break;case 50331648:let e=H.toColor(E>>16&255,E>>8&255,E&255);this._applyMinimumContrast(h,be,e,T,D,ye)||this._addStyle(h,`color:#${Ci(E.toString(16),`0`,6)}`);break;default:this._applyMinimumContrast(h,be,p.foreground,T,D,ye)||ve&&C.push(`xterm-fg-257`)}C.length&&=(h.className=C.join(` `),0),!le&&!ae&&!de&&oe?g++:h.textContent=_,S!==this.defaultSpacing&&(h.style.letterSpacing=`${S}px`),d.push(h),w=se}return h&&g&&(h.textContent=_),d}_applyMinimumContrast(e,t,n,r,i,a){if(this._optionsService.rawOptions.minimumContrastRatio===1||yi(r.getCode()))return!1;let o=this._getContrastCache(r),s;if(!i&&!a&&(s=o.getColor(t.rgba,n.rgba)),s===void 0){let e=this._optionsService.rawOptions.minimumContrastRatio/(r.isDim()?2:1);s=U.ensureContrastRatio(i||t,a||n,e),o.setColor((i||t).rgba,(a||n).rgba,s??null)}return s?(this._addStyle(e,`color:${s.css}`),!0):!1}_getContrastCache(e){return e.isDim()?this._themeService.colors.halfContrastCache:this._themeService.colors.contrastCache}_addStyle(e,t){e.setAttribute(`style`,`${e.getAttribute(`style`)||``}${t};`)}_isCellInSelection(e,t){let n=this._selectionStart,r=this._selectionEnd;return!n||!r?!1:this._columnSelectMode?n[0]<=r[0]?e>=n[0]&&t>=n[1]&&e=n[1]&&e>=r[0]&&t<=r[1]:t>n[1]&&t=n[0]&&e=n[0]}};Si=y([b(1,Fe),b(2,k),b(3,je),b(4,xe),b(5,De),b(6,Ie)],Si);function Ci(e,t,n){for(;e.length0&&(this._flat[r]=t),t}let i=e;t&&(i+=`B`),n&&(i+=`I`);let a=this._holey.get(i);if(a===void 0){let r=0;t&&(r|=1),n&&(r|=2),a=this._measure(e,r),a>0&&this._holey.set(i,a)}return a}_measure(e,t){let n=this._measureElements[t];return n.textContent=e.repeat(32),n.offsetWidth/32}},Ti=class{constructor(){this.clear()}clear(){this.hasSelection=!1,this.columnSelectMode=!1,this.viewportStartRow=0,this.viewportEndRow=0,this.viewportCappedStartRow=0,this.viewportCappedEndRow=0,this.startCol=0,this.endCol=0,this.selectionStart=void 0,this.selectionEnd=void 0}update(e,t,n,r=!1){if(this.selectionStart=t,this.selectionEnd=n,!t||!n||t[0]===n[0]&&t[1]===n[1]){this.clear();return}let i=e.buffers.active.ydisp,a=t[1]-i,o=n[1]-i,s=Math.max(a,0),c=Math.min(o,e.rows-1);if(s>=e.rows||c<0){this.clear();return}this.hasSelection=!0,this.columnSelectMode=r,this.viewportStartRow=a,this.viewportEndRow=o,this.viewportCappedStartRow=s,this.viewportCappedEndRow=c,this.startCol=t[0],this.endCol=n[0]}isCellSelected(e,t,n){return this.hasSelection?(n-=e.buffer.active.viewportY,this.columnSelectMode?this.startCol<=this.endCol?t>=this.startCol&&n>=this.viewportCappedStartRow&&t=this.viewportCappedStartRow&&t>=this.endCol&&n<=this.viewportCappedEndRow:n>this.viewportStartRow&&n=this.startCol&&t=this.startCol):!1}};function Ei(){return new Ti}var Di=`xterm-dom-renderer-owner-`,Oi=`xterm-rows`,ki=`xterm-fg-`,Ai=`xterm-bg-`,ji=`xterm-focus`,Mi=`xterm-selection`,Ni=1,Pi=class extends j{constructor(e,t,n,r,i,a,o,s,c,l,u,d,f,p){super(),this._terminal=e,this._document=t,this._element=n,this._screenElement=r,this._viewportElement=i,this._helperContainer=a,this._linkifier2=o,this._charSizeService=c,this._optionsService=l,this._bufferService=u,this._coreService=d,this._coreBrowserService=f,this._themeService=p,this._terminalClass=Ni++,this._rowElements=[],this._selectionRenderModel=Ei(),this.onRequestRedraw=this._register(new P).event,this._rowContainer=this._document.createElement(`div`),this._rowContainer.classList.add(Oi),this._rowContainer.style.lineHeight=`normal`,this._rowContainer.setAttribute(`aria-hidden`,`true`),this._refreshRowElements(this._bufferService.cols,this._bufferService.rows),this._selectionContainer=this._document.createElement(`div`),this._selectionContainer.classList.add(Mi),this._selectionContainer.setAttribute(`aria-hidden`,`true`),this.dimensions=bi(),this._updateDimensions(),this._register(this._optionsService.onOptionChange(()=>this._handleOptionsChanged())),this._register(this._themeService.onChangeColors(e=>this._injectCss(e))),this._injectCss(this._themeService.colors),this._rowFactory=s.createInstance(Si,document),this._element.classList.add(Di+this._terminalClass),this._screenElement.appendChild(this._rowContainer),this._screenElement.appendChild(this._selectionContainer),this._register(this._linkifier2.onShowLinkUnderline(e=>this._handleLinkHover(e))),this._register(this._linkifier2.onHideLinkUnderline(e=>this._handleLinkLeave(e))),this._register(A(()=>{this._element.classList.remove(Di+this._terminalClass),this._rowContainer.remove(),this._selectionContainer.remove(),this._widthCache.dispose(),this._themeStyleElement.remove(),this._dimensionsStyleElement.remove()})),this._widthCache=new wi(this._document,this._helperContainer),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}_updateDimensions(){let e=this._coreBrowserService.dpr;this.dimensions.device.char.width=this._charSizeService.width*e,this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*e),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.left=0,this.dimensions.device.char.top=0,this.dimensions.device.canvas.width=this.dimensions.device.cell.width*this._bufferService.cols,this.dimensions.device.canvas.height=this.dimensions.device.cell.height*this._bufferService.rows,this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/e),this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/e),this.dimensions.css.cell.width=this.dimensions.css.canvas.width/this._bufferService.cols,this.dimensions.css.cell.height=this.dimensions.css.canvas.height/this._bufferService.rows;for(let e of this._rowElements)e.style.width=`${this.dimensions.css.canvas.width}px`,e.style.height=`${this.dimensions.css.cell.height}px`,e.style.lineHeight=`${this.dimensions.css.cell.height}px`,e.style.overflow=`hidden`;this._dimensionsStyleElement||(this._dimensionsStyleElement=this._document.createElement(`style`),this._screenElement.appendChild(this._dimensionsStyleElement));let t=`${this._terminalSelector} .${Oi} span { display: inline-block; height: 100%; vertical-align: top;}`;this._dimensionsStyleElement.textContent=t,this._selectionContainer.style.height=this._viewportElement.style.height,this._screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._screenElement.style.height=`${this.dimensions.css.canvas.height}px`}_injectCss(e){this._themeStyleElement||(this._themeStyleElement=this._document.createElement(`style`),this._screenElement.appendChild(this._themeStyleElement));let t=`${this._terminalSelector} .${Oi} { pointer-events: none; color: ${e.foreground.css}; font-family: ${this._optionsService.rawOptions.fontFamily}; font-size: ${this._optionsService.rawOptions.fontSize}px; font-kerning: none; white-space: pre}`;t+=`${this._terminalSelector} .${Oi} .xterm-dim { color: ${U.multiplyOpacity(e.foreground,.5).css};}`,t+=`${this._terminalSelector} span:not(.xterm-bold) { font-weight: ${this._optionsService.rawOptions.fontWeight};}${this._terminalSelector} span.xterm-bold { font-weight: ${this._optionsService.rawOptions.fontWeightBold};}${this._terminalSelector} span.xterm-italic { font-style: italic;}`;let n=`blink_underline_${this._terminalClass}`,r=`blink_bar_${this._terminalClass}`,i=`blink_block_${this._terminalClass}`;t+=`@keyframes ${n} { 50% { border-bottom-style: hidden; }}`,t+=`@keyframes ${r} { 50% { box-shadow: none; }}`,t+=`@keyframes ${i} { 0% { background-color: ${e.cursor.css}; color: ${e.cursorAccent.css}; } 50% { background-color: inherit; color: ${e.cursor.css}; }}`,t+=`${this._terminalSelector} .${Oi}.${ji} .xterm-cursor.xterm-cursor-blink.xterm-cursor-underline { animation: ${n} 1s step-end infinite;}${this._terminalSelector} .${Oi}.${ji} .xterm-cursor.xterm-cursor-blink.xterm-cursor-bar { animation: ${r} 1s step-end infinite;}${this._terminalSelector} .${Oi}.${ji} .xterm-cursor.xterm-cursor-blink.xterm-cursor-block { animation: ${i} 1s step-end infinite;}${this._terminalSelector} .${Oi} .xterm-cursor.xterm-cursor-block { background-color: ${e.cursor.css}; color: ${e.cursorAccent.css};}${this._terminalSelector} .${Oi} .xterm-cursor.xterm-cursor-block:not(.xterm-cursor-blink) { background-color: ${e.cursor.css} !important; color: ${e.cursorAccent.css} !important;}${this._terminalSelector} .${Oi} .xterm-cursor.xterm-cursor-outline { outline: 1px solid ${e.cursor.css}; outline-offset: -1px;}${this._terminalSelector} .${Oi} .xterm-cursor.xterm-cursor-bar { box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${e.cursor.css} inset;}${this._terminalSelector} .${Oi} .xterm-cursor.xterm-cursor-underline { border-bottom: 1px ${e.cursor.css}; border-bottom-style: solid; height: calc(100% - 1px);}`,t+=`${this._terminalSelector} .${Mi} { position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;}${this._terminalSelector}.focus .${Mi} div { position: absolute; background-color: ${e.selectionBackgroundOpaque.css};}${this._terminalSelector} .${Mi} div { position: absolute; background-color: ${e.selectionInactiveBackgroundOpaque.css};}`;for(let[n,r]of e.ansi.entries())t+=`${this._terminalSelector} .${ki}${n} { color: ${r.css}; }${this._terminalSelector} .${ki}${n}.xterm-dim { color: ${U.multiplyOpacity(r,.5).css}; }${this._terminalSelector} .${Ai}${n} { background-color: ${r.css}; }`;t+=`${this._terminalSelector} .${ki}257 { color: ${U.opaque(e.background).css}; }${this._terminalSelector} .${ki}257.xterm-dim { color: ${U.multiplyOpacity(U.opaque(e.background),.5).css}; }${this._terminalSelector} .${Ai}257 { background-color: ${e.foreground.css}; }`,this._themeStyleElement.textContent=t}_setDefaultSpacing(){let e=this.dimensions.css.cell.width-this._widthCache.get(`W`,!1,!1);this._rowContainer.style.letterSpacing=`${e}px`,this._rowFactory.defaultSpacing=e}handleDevicePixelRatioChange(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}_refreshRowElements(e,t){for(let e=this._rowElements.length;e<=t;e++){let e=this._document.createElement(`div`);this._rowContainer.appendChild(e),this._rowElements.push(e)}for(;this._rowElements.length>t;)this._rowContainer.removeChild(this._rowElements.pop())}handleResize(e,t){this._refreshRowElements(e,t),this._updateDimensions(),this.handleSelectionChanged(this._selectionRenderModel.selectionStart,this._selectionRenderModel.selectionEnd,this._selectionRenderModel.columnSelectMode)}handleCharSizeChanged(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}handleBlur(){this._rowContainer.classList.remove(ji),this.renderRows(0,this._bufferService.rows-1)}handleFocus(){this._rowContainer.classList.add(ji),this.renderRows(this._bufferService.buffer.y,this._bufferService.buffer.y)}handleSelectionChanged(e,t,n){if(this._selectionContainer.replaceChildren(),this._rowFactory.handleSelectionChanged(e,t,n),this.renderRows(0,this._bufferService.rows-1),!e||!t||(this._selectionRenderModel.update(this._terminal,e,t,n),!this._selectionRenderModel.hasSelection))return;let r=this._selectionRenderModel.viewportStartRow,i=this._selectionRenderModel.viewportEndRow,a=this._selectionRenderModel.viewportCappedStartRow,o=this._selectionRenderModel.viewportCappedEndRow,s=this._document.createDocumentFragment();if(n){let n=e[0]>t[0];s.appendChild(this._createSelectionElement(a,n?t[0]:e[0],n?e[0]:t[0],o-a+1))}else{let n=r===a?e[0]:0,c=a===i?t[0]:this._bufferService.cols;s.appendChild(this._createSelectionElement(a,n,c));let l=o-a-1;if(s.appendChild(this._createSelectionElement(a+1,0,this._bufferService.cols,l)),a!==o){let e=i===o?t[0]:this._bufferService.cols;s.appendChild(this._createSelectionElement(o,0,e))}}this._selectionContainer.appendChild(s)}_createSelectionElement(e,t,n,r=1){let i=this._document.createElement(`div`),a=t*this.dimensions.css.cell.width,o=this.dimensions.css.cell.width*(n-t);return a+o>this.dimensions.css.canvas.width&&(o=this.dimensions.css.canvas.width-a),i.style.height=`${r*this.dimensions.css.cell.height}px`,i.style.top=`${e*this.dimensions.css.cell.height}px`,i.style.left=`${a}px`,i.style.width=`${o}px`,i}handleCursorMove(){}_handleOptionsChanged(){this._updateDimensions(),this._injectCss(this._themeService.colors),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}clear(){for(let e of this._rowElements)e.replaceChildren()}renderRows(e,t){let n=this._bufferService.buffer,r=n.ybase+n.y,i=Math.min(n.x,this._bufferService.cols-1),a=this._coreService.decPrivateModes.cursorBlink??this._optionsService.rawOptions.cursorBlink,o=this._coreService.decPrivateModes.cursorStyle??this._optionsService.rawOptions.cursorStyle,s=this._optionsService.rawOptions.cursorInactiveStyle;for(let c=e;c<=t;c++){let e=c+n.ydisp,t=this._rowElements[c],l=n.lines.get(e);if(!t||!l)break;t.replaceChildren(...this._rowFactory.createRow(l,e,e===r,o,s,i,a,this.dimensions.css.cell.width,this._widthCache,-1,-1))}}get _terminalSelector(){return`.${Di}${this._terminalClass}`}_handleLinkHover(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!0)}_handleLinkLeave(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!1)}_setCellUnderline(e,t,n,r,i,a){n<0&&(e=0),r<0&&(t=0);let o=this._bufferService.rows-1;n=Math.max(Math.min(n,o),0),r=Math.max(Math.min(r,o),0),i=Math.min(i,this._bufferService.cols);let s=this._bufferService.buffer,c=s.ybase+s.y,l=Math.min(s.x,i-1),u=this._optionsService.rawOptions.cursorBlink,d=this._optionsService.rawOptions.cursorStyle,f=this._optionsService.rawOptions.cursorInactiveStyle;for(let o=n;o<=r;++o){let p=o+s.ydisp,m=this._rowElements[o],h=s.lines.get(p);if(!m||!h)break;m.replaceChildren(...this._rowFactory.createRow(h,p,p===c,d,f,l,u,this.dimensions.css.cell.width,this._widthCache,a?o===n?e:0:-1,a?(o===r?t:i)-1:-1))}}};Pi=y([b(7,Ce),b(8,Ae),b(9,k),b(10,O),b(11,xe),b(12,je),b(13,Ie)],Pi);var Fi=class extends j{constructor(e,t,n){super(),this._optionsService=n,this.width=0,this.height=0,this._onCharSizeChange=this._register(new P),this.onCharSizeChange=this._onCharSizeChange.event;try{this._measureStrategy=this._register(new Ri(this._optionsService))}catch{this._measureStrategy=this._register(new Li(e,t,this._optionsService))}this._register(this._optionsService.onMultipleOptionChange([`fontFamily`,`fontSize`],()=>this.measure()))}get hasValidSize(){return this.width>0&&this.height>0}measure(){let e=this._measureStrategy.measure();(e.width!==this.width||e.height!==this.height)&&(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())}};Fi=y([b(2,k)],Fi);var Ii=class extends j{constructor(){super(...arguments),this._result={width:0,height:0}}_validateAndSet(e,t){e!==void 0&&e>0&&t!==void 0&&t>0&&(this._result.width=e,this._result.height=t)}},Li=class extends Ii{constructor(e,t,n){super(),this._document=e,this._parentElement=t,this._optionsService=n,this._measureElement=this._document.createElement(`span`),this._measureElement.classList.add(`xterm-char-measure-element`),this._measureElement.textContent=`W`.repeat(32),this._measureElement.setAttribute(`aria-hidden`,`true`),this._measureElement.style.whiteSpace=`pre`,this._measureElement.style.fontKerning=`none`,this._parentElement.appendChild(this._measureElement)}measure(){return this._measureElement.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._measureElement.style.fontSize=`${this._optionsService.rawOptions.fontSize}px`,this._validateAndSet(Number(this._measureElement.offsetWidth)/32,Number(this._measureElement.offsetHeight)),this._result}},Ri=class extends Ii{constructor(e){super(),this._optionsService=e,this._canvas=new OffscreenCanvas(100,100),this._ctx=this._canvas.getContext(`2d`);let t=this._ctx.measureText(`W`);if(!(`width`in t&&`fontBoundingBoxAscent`in t&&`fontBoundingBoxDescent`in t))throw Error(`Required font metrics not supported`)}measure(){this._ctx.font=`${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;let e=this._ctx.measureText(`W`);return this._validateAndSet(e.width,e.fontBoundingBoxAscent+e.fontBoundingBoxDescent),this._result}},zi=class extends j{constructor(e,t,n){super(),this._textarea=e,this._window=t,this.mainDocument=n,this._isFocused=!1,this._cachedIsFocused=void 0,this._screenDprMonitor=this._register(new Bi(this._window)),this._onDprChange=this._register(new P),this.onDprChange=this._onDprChange.event,this._onWindowChange=this._register(new P),this.onWindowChange=this._onWindowChange.event,this._register(this.onWindowChange(e=>this._screenDprMonitor.setWindow(e))),this._register(N.forward(this._screenDprMonitor.onDprChange,this._onDprChange)),this._register(F(this._textarea,`focus`,()=>this._isFocused=!0)),this._register(F(this._textarea,`blur`,()=>this._isFocused=!1))}get window(){return this._window}set window(e){this._window!==e&&(this._window=e,this._onWindowChange.fire(this._window))}get dpr(){return this.window.devicePixelRatio}get isFocused(){return this._cachedIsFocused===void 0&&(this._cachedIsFocused=this._isFocused&&this._textarea.ownerDocument.hasFocus(),queueMicrotask(()=>this._cachedIsFocused=void 0)),this._cachedIsFocused}},Bi=class extends j{constructor(e){super(),this._parentWindow=e,this._windowResizeListener=this._register(new pt),this._onDprChange=this._register(new P),this.onDprChange=this._onDprChange.event,this._outerListener=()=>this._setDprAndFireIfDiffers(),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._updateDpr(),this._setWindowResizeListener(),this._register(A(()=>this.clearListener()))}setWindow(e){this._parentWindow=e,this._setWindowResizeListener(),this._setDprAndFireIfDiffers()}_setWindowResizeListener(){this._windowResizeListener.value=F(this._parentWindow,`resize`,()=>this._setDprAndFireIfDiffers())}_setDprAndFireIfDiffers(){this._parentWindow.devicePixelRatio!==this._currentDevicePixelRatio&&this._onDprChange.fire(this._parentWindow.devicePixelRatio),this._updateDpr()}_updateDpr(){this._outerListener&&(this._resolutionMediaMatchList?.removeListener(this._outerListener),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._resolutionMediaMatchList=this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`),this._resolutionMediaMatchList.addListener(this._outerListener))}clearListener(){!this._resolutionMediaMatchList||!this._outerListener||(this._resolutionMediaMatchList.removeListener(this._outerListener),this._resolutionMediaMatchList=void 0,this._outerListener=void 0)}},Vi=class extends j{constructor(){super(),this.linkProviders=[],this._register(A(()=>this.linkProviders.length=0))}registerLinkProvider(e){return this.linkProviders.push(e),{dispose:()=>{let t=this.linkProviders.indexOf(e);t!==-1&&this.linkProviders.splice(t,1)}}}};function Hi(e,t,n){let r=n.getBoundingClientRect(),i=e.getComputedStyle(n),a=parseInt(i.getPropertyValue(`padding-left`)),o=parseInt(i.getPropertyValue(`padding-top`));return[t.clientX-r.left-a,t.clientY-r.top-o]}function Ui(e,t,n,r,i,a,o,s,c){if(!a)return;let l=Hi(e,t,n);if(l)return l[0]=Math.ceil((l[0]+(c?o/2:0))/o),l[1]=Math.ceil(l[1]/s),l[0]=Math.min(Math.max(l[0],1),r+ +!!c),l[1]=Math.min(Math.max(l[1],1),i),l}var Wi=class{constructor(e,t){this._renderService=e,this._charSizeService=t}getCoords(e,t,n,r,i){return Ui(window,e,t,n,r,this._charSizeService.hasValidSize,this._renderService.dimensions.css.cell.width,this._renderService.dimensions.css.cell.height,i)}getMouseReportCoords(e,t){let n=Hi(window,e,t);if(this._charSizeService.hasValidSize)return n[0]=Math.min(Math.max(n[0],0),this._renderService.dimensions.css.canvas.width-1),n[1]=Math.min(Math.max(n[1],0),this._renderService.dimensions.css.canvas.height-1),{col:Math.floor(n[0]/this._renderService.dimensions.css.cell.width),row:Math.floor(n[1]/this._renderService.dimensions.css.cell.height),x:Math.floor(n[0]),y:Math.floor(n[1])}}};Wi=y([b(0,Ne),b(1,Ae)],Wi);var Gi=class{constructor(e,t){this._renderCallback=e,this._coreBrowserService=t,this._refreshCallbacks=[]}dispose(){this._animationFrame&&=(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),void 0)}addRefreshCallback(e){return this._refreshCallbacks.push(e),this._animationFrame||=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()),this._animationFrame}refresh(e,t,n){this._rowCount=n,e=e===void 0?0:e,t=t===void 0?this._rowCount-1:t,this._rowStart=this._rowStart===void 0?e:Math.min(this._rowStart,e),this._rowEnd=this._rowEnd===void 0?t:Math.max(this._rowEnd,t),!this._animationFrame&&(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0){this._runRefreshCallbacks();return}let e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(let e of this._refreshCallbacks)e(0);this._refreshCallbacks=[]}},Ki={};ee(Ki,{getSafariVersion:()=>$i,isChromeOS:()=>aa,isFirefox:()=>Xi,isIpad:()=>ta,isIphone:()=>na,isLegacyEdge:()=>Zi,isLinux:()=>ia,isMac:()=>ea,isNode:()=>qi,isSafari:()=>Qi,isWindows:()=>ra});var qi=typeof process<`u`&&`title`in process,Ji=qi?`node`:navigator.userAgent,Yi=qi?`node`:navigator.platform,Xi=Ji.includes(`Firefox`),Zi=Ji.includes(`Edge`),Qi=/^((?!chrome|android).)*safari/i.test(Ji);function $i(){if(!Qi)return 0;let e=Ji.match(/Version\/(\d+)/);return e===null||e.length<2?0:parseInt(e[1])}var ea=[`Macintosh`,`MacIntel`,`MacPPC`,`Mac68K`].includes(Yi),ta=Yi===`iPad`,na=Yi===`iPhone`,ra=[`Windows`,`Win16`,`Win32`,`WinCE`].includes(Yi),ia=Yi.indexOf(`Linux`)>=0,aa=/\bCrOS\b/.test(Ji),oa=class{constructor(){this._tasks=[],this._i=0}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._ii){r-t<-20&&console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(r-t))}ms`),this._start();return}r=i}this.clear()}},sa=class extends oa{_requestCallback(e){return setTimeout(()=>e(this._createDeadline(16)))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){let t=performance.now()+e;return{timeRemaining:()=>Math.max(0,t-performance.now())}}},ca=class extends oa{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}},la=!qi&&`requestIdleCallback`in window?ca:sa,ua=class{constructor(){this._queue=new la}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}},da=class extends j{constructor(e,t,n,r,i,a,o,s,c){super(),this._rowCount=e,this._optionsService=n,this._charSizeService=r,this._coreService=i,this._coreBrowserService=s,this._renderer=this._register(new pt),this._pausedResizeTask=new ua,this._observerDisposable=this._register(new pt),this._isPaused=!1,this._needsFullRefresh=!1,this._isNextRenderRedrawOnly=!0,this._needsSelectionRefresh=!1,this._canvasWidth=0,this._canvasHeight=0,this._selectionState={start:void 0,end:void 0,columnSelectMode:!1},this._onDimensionsChange=this._register(new P),this.onDimensionsChange=this._onDimensionsChange.event,this._onRenderedViewportChange=this._register(new P),this.onRenderedViewportChange=this._onRenderedViewportChange.event,this._onRender=this._register(new P),this.onRender=this._onRender.event,this._onRefreshRequest=this._register(new P),this.onRefreshRequest=this._onRefreshRequest.event,this._renderDebouncer=new Gi((e,t)=>this._renderRows(e,t),this._coreBrowserService),this._register(this._renderDebouncer),this._syncOutputHandler=new fa(this._coreBrowserService,this._coreService,()=>this._fullRefresh()),this._register(A(()=>this._syncOutputHandler.dispose())),this._register(this._coreBrowserService.onDprChange(()=>this.handleDevicePixelRatioChange())),this._register(o.onResize(()=>this._fullRefresh())),this._register(o.buffers.onBufferActivate(()=>this._renderer.value?.clear())),this._register(this._optionsService.onOptionChange(()=>this._handleOptionsChanged())),this._register(this._charSizeService.onCharSizeChange(()=>this.handleCharSizeChanged())),this._register(a.onDecorationRegistered(()=>this._fullRefresh())),this._register(a.onDecorationRemoved(()=>this._fullRefresh())),this._register(this._optionsService.onMultipleOptionChange([`customGlyphs`,`drawBoldTextInBrightColors`,`letterSpacing`,`lineHeight`,`fontFamily`,`fontSize`,`fontWeight`,`fontWeightBold`,`minimumContrastRatio`,`rescaleOverlappingGlyphs`],()=>{this.clear(),this.handleResize(o.cols,o.rows),this._fullRefresh()})),this._register(this._optionsService.onMultipleOptionChange([`cursorBlink`,`cursorStyle`],()=>this.refreshRows(o.buffer.y,o.buffer.y,!0))),this._register(c.onChangeColors(()=>this._fullRefresh())),this._registerIntersectionObserver(this._coreBrowserService.window,t),this._register(this._coreBrowserService.onWindowChange(e=>this._registerIntersectionObserver(e,t)))}get dimensions(){return this._renderer.value.dimensions}_registerIntersectionObserver(e,t){if(`IntersectionObserver`in e){let n=new e.IntersectionObserver(e=>this._handleIntersectionChange(e[e.length-1]),{threshold:0});n.observe(t),this._observerDisposable.value=A(()=>n.disconnect())}}_handleIntersectionChange(e){this._isPaused=e.isIntersecting===void 0?e.intersectionRatio===0:!e.isIntersecting,!this._isPaused&&!this._charSizeService.hasValidSize&&this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this._pausedResizeTask.flush(),this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)}refreshRows(e,t,n=!1){if(this._isPaused){this._needsFullRefresh=!0;return}if(this._coreService.decPrivateModes.synchronizedOutput){this._syncOutputHandler.bufferRows(e,t);return}let r=this._syncOutputHandler.flush();r&&(e=Math.min(e,r.start),t=Math.max(t,r.end)),n||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount)}_renderRows(e,t){if(this._renderer.value){if(this._coreService.decPrivateModes.synchronizedOutput){this._syncOutputHandler.bufferRows(e,t);return}e=Math.min(e,this._rowCount-1),t=Math.min(t,this._rowCount-1),this._renderer.value.renderRows(e,t),this._needsSelectionRefresh&&=(this._renderer.value.handleSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),!1),this._isNextRenderRedrawOnly||this._onRenderedViewportChange.fire({start:e,end:t}),this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0}}resize(e,t){this._rowCount=t,this._fireOnCanvasResize()}_handleOptionsChanged(){this._renderer.value&&(this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize())}_fireOnCanvasResize(){this._renderer.value&&(this._renderer.value.dimensions.css.canvas.width===this._canvasWidth&&this._renderer.value.dimensions.css.canvas.height===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.value.dimensions))}hasRenderer(){return!!this._renderer.value}setRenderer(e){this._renderer.value=e,this._renderer.value&&(this._renderer.value.onRequestRedraw(e=>this.refreshRows(e.start,e.end,!0)),this._needsSelectionRefresh=!0,this._fullRefresh())}addRefreshCallback(e){return this._renderDebouncer.addRefreshCallback(e)}_fullRefresh(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)}clearTextureAtlas(){this._renderer.value&&(this._renderer.value.clearTextureAtlas?.(),this._fullRefresh())}handleDevicePixelRatioChange(){this._charSizeService.measure(),this._renderer.value&&(this._renderer.value.handleDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1))}handleResize(e,t){this._renderer.value&&(this._isPaused?this._pausedResizeTask.set(()=>this._renderer.value?.handleResize(e,t)):this._renderer.value.handleResize(e,t),this._fullRefresh())}handleCharSizeChanged(){this._renderer.value?.handleCharSizeChanged()}handleBlur(){this._renderer.value?.handleBlur()}handleFocus(){this._renderer.value?.handleFocus()}handleSelectionChanged(e,t,n){this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=n,this._renderer.value?.handleSelectionChanged(e,t,n)}handleCursorMove(){this._renderer.value?.handleCursorMove()}clear(){this._renderer.value?.clear()}};da=y([b(2,k),b(3,Ae),b(4,xe),b(5,De),b(6,O),b(7,je),b(8,Ie)],da);var fa=class{constructor(e,t,n){this._coreBrowserService=e,this._coreService=t,this._onTimeout=n,this._start=0,this._end=0,this._isBuffering=!1}bufferRows(e,t){this._isBuffering?(this._start=Math.min(this._start,e),this._end=Math.max(this._end,t)):(this._start=e,this._end=t,this._isBuffering=!0),this._timeout===void 0&&(this._timeout=this._coreBrowserService.window.setTimeout(()=>{this._timeout=void 0,this._coreService.decPrivateModes.synchronizedOutput=!1,this._onTimeout()},1e3))}flush(){if(this._timeout!==void 0&&(this._coreBrowserService.window.clearTimeout(this._timeout),this._timeout=void 0),!this._isBuffering)return;let e={start:this._start,end:this._end};return this._isBuffering=!1,e}dispose(){this._timeout!==void 0&&(this._coreBrowserService.window.clearTimeout(this._timeout),this._timeout=void 0)}};function pa(e,t,n,r){let i=n.buffer.x,a=n.buffer.y;if(!n.buffer.hasScrollback)return ga(i,a,e,t,n,r)+_a(a,t,n,r)+va(i,a,e,t,n,r);let o;if(a===t)return o=i>e?`D`:`C`,Ta(Math.abs(i-e),wa(o,r));o=a>t?`D`:`C`;let s=Math.abs(a-t);return Ta(ha(a>t?e:i,n)+(s-1)*n.cols+1+ma(a>t?i:e,n),wa(o,r))}function ma(e,t){return e-1}function ha(e,t){return t.cols-e}function ga(e,t,n,r,i,a){return _a(t,r,i,a).length===0?``:Ta(Ca(e,t,e,t-ba(t,i),!1,i).length,wa(`D`,a))}function _a(e,t,n,r){let i=e-ba(e,n),a=t-ba(t,n);return Ta(Math.abs(i-a)-ya(e,t,n),wa(Sa(e,t),r))}function va(e,t,n,r,i,a){let o;o=_a(t,r,i,a).length>0?r-ba(r,i):t;let s=r,c=xa(e,t,n,r,i,a);return Ta(Ca(e,o,n,s,c===`C`,i).length,wa(c,a))}function ya(e,t,n){let r=0,i=e-ba(e,n),a=t-ba(t,n);for(let o=0;o=0&&e0?r-ba(r,i):t,e=n&&ot?`A`:`B`}function Ca(e,t,n,r,i,a){let o=e,s=t,c=``;for(;(o!==n||s!==r)&&s>=0&&sa.cols-1?(c+=a.buffer.translateBufferLineToString(s,!1,e,o),o=0,e=0,s++):!i&&o<0&&(c+=a.buffer.translateBufferLineToString(s,!1,0,e+1),o=a.cols-1,e=o,s--);return c+a.buffer.translateBufferLineToString(s,!1,e,o)}function wa(e,t){let n=t?`O`:`[`;return L.ESC+n+e}function Ta(e,t){e=Math.floor(e);let n=``;for(let r=0;rthis._bufferService.cols?e%this._bufferService.cols===0?[this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)-1]:[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}if(this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]){let e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[Math.max(e,this.selectionEnd[0]),this.selectionEnd[1]]}return this.selectionEnd}}areSelectionValuesReversed(){let e=this.selectionStart,t=this.selectionEnd;return!e||!t?!1:e[1]>t[1]||e[1]===t[1]&&e[0]>t[0]}handleTrim(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)}};function Da(e,t){if(e.start.y>e.end.y)throw Error(`Buffer range end (${e.end.x}, ${e.end.y}) cannot be before start (${e.start.x}, ${e.start.y})`);return t*(e.end.y-e.start.y)+(e.end.x-e.start.x+1)}var Oa=50,ka=15,Aa=50,ja=500,Ma=RegExp(`\xA0`,`g`),Na=class extends j{constructor(e,t,n,r,i,a,o,s,c){super(),this._element=e,this._screenElement=t,this._linkifier=n,this._bufferService=r,this._coreService=i,this._mouseService=a,this._optionsService=o,this._renderService=s,this._coreBrowserService=c,this._dragScrollAmount=0,this._enabled=!0,this._workCell=new E,this._mouseDownTimeStamp=0,this._oldHasSelection=!1,this._oldSelectionStart=void 0,this._oldSelectionEnd=void 0,this._onLinuxMouseSelection=this._register(new P),this.onLinuxMouseSelection=this._onLinuxMouseSelection.event,this._onRedrawRequest=this._register(new P),this.onRequestRedraw=this._onRedrawRequest.event,this._onSelectionChange=this._register(new P),this.onSelectionChange=this._onSelectionChange.event,this._onRequestScrollLines=this._register(new P),this.onRequestScrollLines=this._onRequestScrollLines.event,this._mouseMoveListener=e=>this._handleMouseMove(e),this._mouseUpListener=e=>this._handleMouseUp(e),this._coreService.onUserInput(()=>{this.hasSelection&&this.clearSelection()}),this._trimListener=this._bufferService.buffer.lines.onTrim(e=>this._handleTrim(e)),this._register(this._bufferService.buffers.onBufferActivate(e=>this._handleBufferActivate(e))),this.enable(),this._model=new Ea(this._bufferService),this._activeSelectionMode=0,this._register(A(()=>{this._removeMouseDownListeners()})),this._register(this._bufferService.onResize(e=>{e.rowsChanged&&this.clearSelection()}))}reset(){this.clearSelection()}disable(){this.clearSelection(),this._enabled=!1}enable(){this._enabled=!0}get selectionStart(){return this._model.finalSelectionStart}get selectionEnd(){return this._model.finalSelectionEnd}get hasSelection(){let e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!e||!t?!1:e[0]!==t[0]||e[1]!==t[1]}get selectionText(){let e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return``;let n=this._bufferService.buffer,r=[];if(this._activeSelectionMode===3){if(e[0]===t[0])return``;let i=e[0]e.replace(Ma,` `)).join(ra?`\r -`:` -`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(e){this._refreshAnimationFrame||=this._coreBrowserService.window.requestAnimationFrame(()=>this._refresh()),ia&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(e){let t=this._getMouseBufferCoords(e),n=this._model.finalSelectionStart,r=this._model.finalSelectionEnd;return!n||!r||!t?!1:this._areCoordsInSelection(t,n,r)}isCellInSelection(e,t){let n=this._model.finalSelectionStart,r=this._model.finalSelectionEnd;return!n||!r?!1:this._areCoordsInSelection([e,t],n,r)}_areCoordsInSelection(e,t,n){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]}_selectWordAtCursor(e,t){let n=this._linkifier.currentLink?.link?.range;if(n)return this._model.selectionStart=[n.start.x-1,n.start.y-1],this._model.selectionStartLength=Da(n,this._bufferService.cols),this._model.selectionEnd=void 0,!0;let r=this._getMouseBufferCoords(e);return r?(this._selectWordAt(r,t),this._model.selectionEnd=void 0,!0):!1}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()}_handleTrim(e){this._model.handleTrim(e)&&this.refresh()}_getMouseBufferCoords(e){let t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t}_getMouseEventScrollAmount(e){let t=Hi(this._coreBrowserService.window,e,this._screenElement)[1],n=this._renderService.dimensions.css.canvas.height;return t>=0&&t<=n?0:(t>n&&(t-=n),t=Math.min(Math.max(t,-Oa),Oa),t/=Oa,t/Math.abs(t)+Math.round(t*(ka-1)))}shouldForceSelection(e){return ea?e.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:e.shiftKey}handleMouseDown(e){if(this._mouseDownTimeStamp=e.timeStamp,!(e.button===2&&this.hasSelection)&&e.button===0){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._handleIncrementalClick(e):e.detail===1?this._handleSingleClick(e):e.detail===2?this._handleDoubleClick(e):e.detail===3&&this._handleTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener(`mousemove`,this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener(`mouseup`,this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval(()=>this._dragScroll(),Aa)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener(`mousemove`,this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener(`mouseup`,this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))}_handleSingleClick(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),!this._model.selectionStart)return;this._model.selectionEnd=void 0;let t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&t.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(e){this._selectWordAtCursor(e,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(e){let t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))}shouldColumnSelect(e){return e.altKey&&!(ea&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(e){if(e.stopImmediatePropagation(),!this._model.selectionStart)return;let t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),!this._model.selectionEnd){this.refresh(!0);return}this._activeSelectionMode===2?this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));let n=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}}_handleMouseUp(e){let t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&tthis._handleTrim(e))}_convertViewportColToCharacterIndex(e,t){let n=t;for(let r=0;t>=r;r++){let i=e.loadCell(r,this._workCell).getChars().length;this._workCell.getWidth()===0?n--:i>1&&t!==r&&(n+=i-1)}return n}setSelection(e,t,n){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=n,this.refresh(),this._fireEventIfSelectionChanged()}rightClickSelect(e){this._isClickInSelection(e)||(this._selectWordAtCursor(e,!1)&&this.refresh(!0),this._fireEventIfSelectionChanged())}_getWordAt(e,t,n=!0,r=!0){if(e[0]>=this._bufferService.cols)return;let i=this._bufferService.buffer,a=i.lines.get(e[1]);if(!a)return;let o=i.translateBufferLineToString(e[1],!1),s=this._convertViewportColToCharacterIndex(a,e[0]),c=s,l=e[0]-s,u=0,d=0,f=0,p=0;if(o.charAt(s)===` `){for(;s>0&&o.charAt(s-1)===` `;)s--;for(;c1&&(p+=r-1,c+=r-1);t>0&&s>0&&!this._isCharWordSeparator(a.loadCell(t-1,this._workCell));){a.loadCell(t-1,this._workCell);let e=this._workCell.getChars().length;this._workCell.getWidth()===0?(u++,t--):e>1&&(f+=e-1,s-=e-1),s--,t--}for(;n1&&(p+=e-1,c+=e-1),c++,n++}}c++;let m=s+l-u+f,h=Math.min(this._bufferService.cols,c-s+u+d-f-p);if(!(!t&&o.slice(s,c).trim()===``)){if(n&&m===0&&a.getCodePoint(0)!==32){let t=i.lines.get(e[1]-1);if(t&&a.isWrapped&&t.getCodePoint(this._bufferService.cols-1)!==32){let t=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(t){let e=this._bufferService.cols-t.start;m-=e,h+=e}}}if(r&&m+h===this._bufferService.cols&&a.getCodePoint(this._bufferService.cols-1)!==32){let t=i.lines.get(e[1]+1);if(t?.isWrapped&&t.getCodePoint(0)!==32){let t=this._getWordAt([0,e[1]+1],!1,!1,!0);t&&(h+=t.length)}}return{start:m,length:h}}}_selectWordAt(e,t){let n=this._getWordAt(e,t);if(n){for(;n.start<0;)n.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[n.start,e[1]],this._model.selectionStartLength=n.length}}_selectToWordAt(e){let t=this._getWordAt(e,!0);if(t){let n=e[1];for(;t.start<0;)t.start+=this._bufferService.cols,n--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,n++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,n]}}_isCharWordSeparator(e){return e.getWidth()!==0&&this._optionsService.rawOptions.wordSeparator.indexOf(e.getChars())>=0}_selectLineAt(e){let t=this._bufferService.buffer.getWrappedRangeForLine(e),n={start:{x:0,y:t.first},end:{x:this._bufferService.cols-1,y:t.last}};this._model.selectionStart=[0,t.first],this._model.selectionEnd=void 0,this._model.selectionStartLength=Da(n,this._bufferService.cols)}};Na=y([b(3,O),b(4,xe),b(5,Me),b(6,k),b(7,Ne),b(8,je)],Na);var Pa=class{constructor(){this._data={}}set(e,t,n){this._data[e]||(this._data[e]={}),this._data[e][t]=n}get(e,t){return this._data[e]?this._data[e][t]:void 0}clear(){this._data={}}},Fa=class{constructor(){this._color=new Pa,this._css=new Pa}setCss(e,t,n){this._css.set(e,t,n)}getCss(e,t){return this._css.get(e,t)}setColor(e,t,n){this._color.set(e,t,n)}getColor(e,t){return this._color.get(e,t)}clear(){this._color.clear(),this._css.clear()}},K=Object.freeze((()=>{let e=[W.toColor(`#2e3436`),W.toColor(`#cc0000`),W.toColor(`#4e9a06`),W.toColor(`#c4a000`),W.toColor(`#3465a4`),W.toColor(`#75507b`),W.toColor(`#06989a`),W.toColor(`#d3d7cf`),W.toColor(`#555753`),W.toColor(`#ef2929`),W.toColor(`#8ae234`),W.toColor(`#fce94f`),W.toColor(`#729fcf`),W.toColor(`#ad7fa8`),W.toColor(`#34e2e2`),W.toColor(`#eeeeec`)],t=[0,95,135,175,215,255];for(let n=0;n<216;n++){let r=t[n/36%6|0],i=t[n/6%6|0],a=t[n%6];e.push({css:H.toCss(r,i,a),rgba:H.toRgba(r,i,a)})}for(let t=0;t<24;t++){let n=8+t*10;e.push({css:H.toCss(n,n,n),rgba:H.toRgba(n,n,n)})}return e})()),Ia=W.toColor(`#ffffff`),La=W.toColor(`#000000`),Ra=W.toColor(`#ffffff`),za=La,Ba={css:`rgba(255, 255, 255, 0.3)`,rgba:4294967117},Va=Ia,Ha=class extends j{constructor(e){super(),this._optionsService=e,this._contrastCache=new Fa,this._halfContrastCache=new Fa,this._onChangeColors=this._register(new P),this.onChangeColors=this._onChangeColors.event,this._colors={foreground:Ia,background:La,cursor:Ra,cursorAccent:za,selectionForeground:void 0,selectionBackgroundTransparent:Ba,selectionBackgroundOpaque:U.blend(La,Ba),selectionInactiveBackgroundTransparent:Ba,selectionInactiveBackgroundOpaque:U.blend(La,Ba),scrollbarSliderBackground:U.opacity(Ia,.2),scrollbarSliderHoverBackground:U.opacity(Ia,.4),scrollbarSliderActiveBackground:U.opacity(Ia,.5),overviewRulerBorder:Ia,ansi:K.slice(),contrastCache:this._contrastCache,halfContrastCache:this._halfContrastCache},this._updateRestoreColors(),this._setTheme(this._optionsService.rawOptions.theme),this._register(this._optionsService.onSpecificOptionChange(`minimumContrastRatio`,()=>this._contrastCache.clear())),this._register(this._optionsService.onSpecificOptionChange(`theme`,()=>this._setTheme(this._optionsService.rawOptions.theme)))}get colors(){return this._colors}_setTheme(e={}){let t=this._colors;if(t.foreground=q(e.foreground,Ia),t.background=q(e.background,La),t.cursor=U.blend(t.background,q(e.cursor,Ra)),t.cursorAccent=U.blend(t.background,q(e.cursorAccent,za)),t.selectionBackgroundTransparent=q(e.selectionBackground,Ba),t.selectionBackgroundOpaque=U.blend(t.background,t.selectionBackgroundTransparent),t.selectionInactiveBackgroundTransparent=q(e.selectionInactiveBackground,t.selectionBackgroundTransparent),t.selectionInactiveBackgroundOpaque=U.blend(t.background,t.selectionInactiveBackgroundTransparent),t.selectionForeground=e.selectionForeground?q(e.selectionForeground,di):void 0,t.selectionForeground===di&&(t.selectionForeground=void 0),U.isOpaque(t.selectionBackgroundTransparent)&&(t.selectionBackgroundTransparent=U.opacity(t.selectionBackgroundTransparent,.3)),U.isOpaque(t.selectionInactiveBackgroundTransparent)&&(t.selectionInactiveBackgroundTransparent=U.opacity(t.selectionInactiveBackgroundTransparent,.3)),t.scrollbarSliderBackground=q(e.scrollbarSliderBackground,U.opacity(t.foreground,.2)),t.scrollbarSliderHoverBackground=q(e.scrollbarSliderHoverBackground,U.opacity(t.foreground,.4)),t.scrollbarSliderActiveBackground=q(e.scrollbarSliderActiveBackground,U.opacity(t.foreground,.5)),t.overviewRulerBorder=q(e.overviewRulerBorder,Va),t.ansi=K.slice(),t.ansi[0]=q(e.black,K[0]),t.ansi[1]=q(e.red,K[1]),t.ansi[2]=q(e.green,K[2]),t.ansi[3]=q(e.yellow,K[3]),t.ansi[4]=q(e.blue,K[4]),t.ansi[5]=q(e.magenta,K[5]),t.ansi[6]=q(e.cyan,K[6]),t.ansi[7]=q(e.white,K[7]),t.ansi[8]=q(e.brightBlack,K[8]),t.ansi[9]=q(e.brightRed,K[9]),t.ansi[10]=q(e.brightGreen,K[10]),t.ansi[11]=q(e.brightYellow,K[11]),t.ansi[12]=q(e.brightBlue,K[12]),t.ansi[13]=q(e.brightMagenta,K[13]),t.ansi[14]=q(e.brightCyan,K[14]),t.ansi[15]=q(e.brightWhite,K[15]),e.extendedAnsi){let n=Math.min(t.ansi.length-16,e.extendedAnsi.length);for(let r=0;re.index-t.index),r=[];for(let t of n){let n=this._services.get(t.id);if(!n)throw Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id._id}.`);r.push(n)}let i=n.length>0?n[0].index:t.length;if(t.length!==i)throw Error(`[createInstance] First service dependency of ${e.name} at position ${i+1} conflicts with ${t.length} static arguments`);return new e(...t,...r)}},Ga={trace:0,debug:1,info:2,warn:3,error:4,off:5},Ka=`xterm.js: `,qa=class extends j{constructor(e){super(),this._optionsService=e,this._logLevel=5,this._updateLogLevel(),this._register(this._optionsService.onSpecificOptionChange(`logLevel`,()=>this._updateLogLevel())),Ja=this}get logLevel(){return this._logLevel}_updateLogLevel(){this._logLevel=Ga[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;tthis._length)for(let t=this._length;t=e;t--)this._array[this._getCyclicIndex(t+n.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;tthis._maxLength){let e=this._length+n.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=n.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,n){if(!(t<=0)){if(e<0||e>=this._length)throw Error(`start argument out of range`);if(e+n<0)throw Error(`Cannot shift elements in list beyond index 0`);if(n>0){for(let r=t-1;r>=0;r--)this.set(e+r+n,this.get(e+r));let r=e+t+n-this._length;if(r>0)for(this._length+=r;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let r=0;r>22,t&2097152?this._combined[e].charCodeAt(this._combined[e].length-1):n]}set(e,t){this._data[e*J+1]=t[0],t[1].length>1?(this._combined[e]=t[1],this._data[e*J+0]=e|2097152|t[2]<<22):this._data[e*J+0]=t[1].charCodeAt(0)|t[2]<<22}getWidth(e){return this._data[e*J+0]>>22}hasWidth(e){return this._data[e*J+0]&12582912}getFg(e){return this._data[e*J+1]}getBg(e){return this._data[e*J+2]}hasContent(e){return this._data[e*J+0]&4194303}getCodePoint(e){let t=this._data[e*J+0];return t&2097152?this._combined[e].charCodeAt(this._combined[e].length-1):t&2097151}isCombined(e){return this._data[e*J+0]&2097152}getString(e){let t=this._data[e*J+0];return t&2097152?this._combined[e]:t&2097151?T(t&2097151):``}isProtected(e){return this._data[e*J+2]&536870912}loadCell(e,t){return Xa=e*J,t.content=this._data[Xa+0],t.fg=this._data[Xa+1],t.bg=this._data[Xa+2],t.content&2097152&&(t.combinedData=this._combined[e]),t.bg&268435456&&(t.extended=this._extendedAttrs[e]),t}setCell(e,t){t.content&2097152&&(this._combined[e]=t.combinedData),t.bg&268435456&&(this._extendedAttrs[e]=t.extended),this._data[e*J+0]=t.content,this._data[e*J+1]=t.fg,this._data[e*J+2]=t.bg}setCellFromCodepoint(e,t,n,r){r.bg&268435456&&(this._extendedAttrs[e]=r.extended),this._data[e*J+0]=t|n<<22,this._data[e*J+1]=r.fg,this._data[e*J+2]=r.bg}addCodepointToCell(e,t,n){let r=this._data[e*J+0];r&2097152?this._combined[e]+=T(t):r&2097151?(this._combined[e]=T(r&2097151)+T(t),r&=-2097152,r|=2097152):r=t|1<<22,n&&(r&=-12582913,r|=n<<22),this._data[e*J+0]=r}insertCells(e,t,n){if(e%=this.length,e&&this.getWidth(e-1)===2&&this.setCellFromCodepoint(e-1,0,1,n),t=0;--n)this.setCell(e+t+n,this.loadCell(e+n,r));for(let r=0;rthis.length){if(this._data.buffer.byteLength>=n*4)this._data=new Uint32Array(this._data.buffer,0,n);else{let e=new Uint32Array(n);e.set(this._data),this._data=e}for(let n=this.length;n=e&&delete this._combined[r]}let r=Object.keys(this._extendedAttrs);for(let t=0;t=e&&delete this._extendedAttrs[n]}}return this.length=e,n*4*Za=0;--e)if(this._data[e*J+0]&4194303)return e+(this._data[e*J+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(this._data[e*J+0]&4194303||this._data[e*J+2]&50331648)return e+(this._data[e*J+0]>>22);return 0}copyCellsFrom(e,t,n,r,i){let a=e._data;if(i)for(let i=r-1;i>=0;i--){for(let e=0;e=t&&(this._combined[i-t+n]=e._combined[i])}}translateToString(e,t,n,r){t??=0,n??=this.length,e&&(n=Math.min(n,this.getTrimmedLength())),r&&(r.length=0);let i=``;for(;t>22||1}return r&&r.push(t),i}};function $a(e,t,n,r,i,a){let o=[];for(let s=0;s=s&&r0&&(e>d||u[e].getTrimmedLength()===0);e--)h++;h>0&&(o.push(s+u.length-h),o.push(h)),s+=u.length-1}return o}function eo(e,t){let n=[],r=0,i=t[r],a=0;for(let o=0;oro(e,r,t)).reduce((e,t)=>e+t),a=0,o=0,s=0;for(;sc&&(a-=c,o++);let l=e[o].getWidth(a-1)===2;l&&a--;let u=l?n-1:n;r.push(u),s+=u}return r}function ro(e,t,n){if(t===e.length-1)return e[t].getTrimmedLength();let r=!e[t].hasContent(n-1)&&e[t].getWidth(n-1)===1,i=e[t+1].getWidth(0)===2;return r&&i?n-1:n}var io=class e{constructor(t){this.line=t,this.isDisposed=!1,this._disposables=[],this._id=e._nextId++,this._onDispose=this.register(new P),this.onDispose=this._onDispose.event}get id(){return this._id}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),lt(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}};io._nextId=1;var ao=io,X={},oo=X.B;X[0]={"`":`◆`,a:`▒`,b:`␉`,c:`␌`,d:`␍`,e:`␊`,f:`°`,g:`±`,h:`␤`,i:`␋`,j:`┘`,k:`┐`,l:`┌`,m:`└`,n:`┼`,o:`⎺`,p:`⎻`,q:`─`,r:`⎼`,s:`⎽`,t:`├`,u:`┤`,v:`┴`,w:`┬`,x:`│`,y:`≤`,z:`≥`,"{":`π`,"|":`≠`,"}":`£`,"~":`·`},X.A={"#":`£`},X.B=void 0,X[4]={"#":`£`,"@":`¾`,"[":`ij`,"\\":`½`,"]":`|`,"{":`¨`,"|":`f`,"}":`¼`,"~":`´`},X.C=X[5]={"[":`Ä`,"\\":`Ö`,"]":`Å`,"^":`Ü`,"`":`é`,"{":`ä`,"|":`ö`,"}":`å`,"~":`ü`},X.R={"#":`£`,"@":`à`,"[":`°`,"\\":`ç`,"]":`§`,"{":`é`,"|":`ù`,"}":`è`,"~":`¨`},X.Q={"@":`à`,"[":`â`,"\\":`ç`,"]":`ê`,"^":`î`,"`":`ô`,"{":`é`,"|":`ù`,"}":`è`,"~":`û`},X.K={"@":`§`,"[":`Ä`,"\\":`Ö`,"]":`Ü`,"{":`ä`,"|":`ö`,"}":`ü`,"~":`ß`},X.Y={"#":`£`,"@":`§`,"[":`°`,"\\":`ç`,"]":`é`,"`":`ù`,"{":`à`,"|":`ò`,"}":`è`,"~":`ì`},X.E=X[6]={"@":`Ä`,"[":`Æ`,"\\":`Ø`,"]":`Å`,"^":`Ü`,"`":`ä`,"{":`æ`,"|":`ø`,"}":`å`,"~":`ü`},X.Z={"#":`£`,"@":`§`,"[":`¡`,"\\":`Ñ`,"]":`¿`,"{":`°`,"|":`ñ`,"}":`ç`},X.H=X[7]={"@":`É`,"[":`Ä`,"\\":`Ö`,"]":`Å`,"^":`Ü`,"`":`é`,"{":`ä`,"|":`ö`,"}":`å`,"~":`ü`},X[`=`]={"#":`ù`,"@":`à`,"[":`é`,"\\":`ç`,"]":`ê`,"^":`î`,_:`è`,"`":`ô`,"{":`ä`,"|":`ö`,"}":`ü`,"~":`û`};var so=4294967295,co=class{constructor(e,t,n){this._hasScrollback=e,this._optionsService=t,this._bufferService=n,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=Y.clone(),this.savedCharset=oo,this.markers=[],this._nullCell=E.fromCharData([0,de,1,0]),this._whitespaceCell=E.fromCharData([0,fe,1,32]),this._isClearing=!1,this._memoryCleanupQueue=new la,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new Ya(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new me),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new me),this._whitespaceCell}getBlankLine(e,t){return new Qa(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){let e=this.ybase+this.y-this.ydisp;return e>=0&&eso?so:t}fillViewportRows(e){if(this.lines.length===0){e===void 0&&(e=Y);let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new Ya(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){let n=this.getNullCell(Y),r=0,i=this._getCorrectBufferLength(t);if(i>this.lines.maxLength&&(this.lines.maxLength=i),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+a+1?(this.ybase--,a++,this.ydisp>0&&this.ydisp--):this.lines.push(new Qa(e,n)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(i0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=i}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),a&&(this.y+=a),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){let e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&e.backend===`conpty`&&e.buildNumber>=21376:this._hasScrollback&&!this._optionsService.rawOptions.windowsMode}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){let n=this._optionsService.rawOptions.reflowCursorLine,r=$a(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(Y),n);if(r.length>0){let n=eo(this.lines,r);to(this.lines,n.layout),this._reflowLargerAdjustViewport(e,t,n.countRemoved)}}_reflowLargerAdjustViewport(e,t,n){let r=this.getNullCell(Y),i=n;for(;i-->0;)this.ybase===0?(this.y>0&&this.y--,this.lines.length=0;o--){let s=this.lines.get(o);if(!s||!s.isWrapped&&s.getTrimmedLength()<=e)continue;let c=[s];for(;s.isWrapped&&o>0;)s=this.lines.get(--o),c.unshift(s);if(!n){let e=this.ybase+this.y;if(e>=o&&e0&&(i.push({start:o+c.length+a,newLines:p}),a+=p.length),c.push(...p);let m=u.length-1,h=u[m];h===0&&(m--,h=u[m]);let g=c.length-d-1,_=l;for(;g>=0;){let e=Math.min(_,h);if(c[m]===void 0)break;c[m].copyCellsFrom(c[g],_-e,h-e,e,!0),h-=e,h===0&&(m--,h=u[m]),_-=e,_===0&&(g--,_=ro(c,Math.max(g,0),this._cols))}for(let t=0;t0;)this.ybase===0?this.y0){let e=[],t=[];for(let e=0;e=0;l--)if(s&&s.start>r+c){for(let e=s.newLines.length-1;e>=0;e--)this.lines.set(l--,s.newLines[e]);l++,e.push({index:r+1,amount:s.newLines.length}),c+=s.newLines.length,s=i[++o]}else this.lines.set(l,t[r--]);let l=0;for(let t=e.length-1;t>=0;t--)e[t].index+=l,this.lines.onInsertEmitter.fire(e[t]),l+=e[t].amount;let u=Math.max(0,n+a-this.lines.maxLength);u>0&&this.lines.onTrimEmitter.fire(u)}}translateBufferLineToString(e,t,n=0,r){let i=this.lines.get(e);return i?i.translateToString(t,n,r):``}getWrappedRangeForLine(e){let t=e,n=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;n+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(e??=this.x;!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=e,t.line<0&&t.dispose()})),t.register(this.lines.onInsert(e=>{t.line>=e.index&&(t.line+=e.amount)})),t.register(this.lines.onDelete(e=>{t.line>=e.index&&t.linee.index&&(t.line-=e.amount)})),t.register(t.onDispose(()=>this._removeMarker(t))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}},lo=class extends j{constructor(e,t){super(),this._optionsService=e,this._bufferService=t,this._onBufferActivate=this._register(new P),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this._register(this._optionsService.onSpecificOptionChange(`scrollback`,()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this._register(this._optionsService.onSpecificOptionChange(`tabStopWidth`,()=>this.setupTabStops()))}reset(){this._normal=new co(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new co(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}},uo=2,fo=1,po=class extends j{constructor(e){super(),this.isUserScrolling=!1,this._onResize=this._register(new P),this.onResize=this._onResize.event,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,uo),this.rows=Math.max(e.rawOptions.rows||0,fo),this.buffers=this._register(new lo(e,this)),this._register(this.buffers.onBufferActivate(e=>{this._onScroll.fire(e.activeBuffer.ydisp)}))}get buffer(){return this.buffers.active}resize(e,t){let n=this.cols!==e,r=this.rows!==t;this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t,colsChanged:n,rowsChanged:r})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){let n=this.buffer,r;r=this._cachedBlankLine,(!r||r.length!==this.cols||r.getFg(0)!==e.fg||r.getBg(0)!==e.bg)&&(r=n.getBlankLine(e,t),this._cachedBlankLine=r),r.isWrapped=t;let i=n.ybase+n.scrollTop,a=n.ybase+n.scrollBottom;if(n.scrollTop===0){let e=n.lines.isFull;a===n.lines.length-1?e?n.lines.recycle().copyFrom(r):n.lines.push(r.clone()):n.lines.splice(a+1,0,r.clone()),e?this.isUserScrolling&&(n.ydisp=Math.max(n.ydisp-1,0)):(n.ybase++,this.isUserScrolling||n.ydisp++)}else{let e=a-i+1;n.lines.shiftElements(i+1,e-1,-1),n.lines.set(a,r.clone())}this.isUserScrolling||(n.ydisp=n.ybase),this._onScroll.fire(n.ydisp)}scrollLines(e,t){let n=this.buffer;if(e<0){if(n.ydisp===0)return;this.isUserScrolling=!0}else e+n.ydisp>=n.ybase&&(this.isUserScrolling=!1);let r=n.ydisp;n.ydisp=Math.max(Math.min(n.ydisp+e,n.ybase),0),r!==n.ydisp&&(t||this._onScroll.fire(n.ydisp))}};po=y([b(0,k)],po);var mo={cols:80,rows:24,cursorBlink:!1,cursorStyle:`block`,cursorWidth:1,cursorInactiveStyle:`outline`,customGlyphs:!0,drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollModifier:`alt`,fastScrollSensitivity:5,fontFamily:`monospace`,fontSize:15,fontWeight:`normal`,fontWeightBold:`bold`,ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:`info`,logger:null,scrollback:1e3,scrollOnEraseInDisplay:!1,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},reflowCursorLine:!1,rescaleOverlappingGlyphs:!1,rightClickSelectsWord:ea,windowOptions:{},windowsMode:!1,windowsPty:{},wordSeparator:` ()[]{}',"\``,altClickMovesCursor:!0,convertEol:!1,termName:`xterm`,cancelEvents:!1,overviewRuler:{}},ho=[`normal`,`bold`,`100`,`200`,`300`,`400`,`500`,`600`,`700`,`800`,`900`],go=class extends j{constructor(e){super(),this._onOptionChange=this._register(new P),this.onOptionChange=this._onOptionChange.event;let t={...mo};for(let n in e)if(n in t)try{let r=e[n];t[n]=this._sanitizeAndValidateOption(n,r)}catch(e){console.error(e)}this.rawOptions=t,this.options={...t},this._setupOptions(),this._register(A(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(e,t){return this.onOptionChange(n=>{n===e&&t(this.rawOptions[e])})}onMultipleOptionChange(e,t){return this.onOptionChange(n=>{e.indexOf(n)!==-1&&t()})}_setupOptions(){let e=e=>{if(!(e in mo))throw Error(`No option with key "${e}"`);return this.rawOptions[e]},t=(e,t)=>{if(!(e in mo))throw Error(`No option with key "${e}"`);t=this._sanitizeAndValidateOption(e,t),this.rawOptions[e]!==t&&(this.rawOptions[e]=t,this._onOptionChange.fire(e))};for(let n in this.rawOptions){let r={get:e.bind(this,n),set:t.bind(this,n)};Object.defineProperty(this.options,n,r)}}_sanitizeAndValidateOption(e,t){switch(e){case`cursorStyle`:if(t||=mo[e],!_o(t))throw Error(`"${t}" is not a valid value for ${e}`);break;case`wordSeparator`:t||=mo[e];break;case`fontWeight`:case`fontWeightBold`:if(typeof t==`number`&&1<=t&&t<=1e3)break;t=ho.includes(t)?t:mo[e];break;case`cursorWidth`:t=Math.floor(t);case`lineHeight`:case`tabStopWidth`:if(t<1)throw Error(`${e} cannot be less than 1, value: ${t}`);break;case`minimumContrastRatio`:t=Math.max(1,Math.min(21,Math.round(t*10)/10));break;case`scrollback`:if(t=Math.min(t,4294967295),t<0)throw Error(`${e} cannot be less than 0, value: ${t}`);break;case`fastScrollSensitivity`:case`scrollSensitivity`:if(t<=0)throw Error(`${e} cannot be less than or equal to 0, value: ${t}`);break;case`rows`:case`cols`:if(!t&&t!==0)throw Error(`${e} must be numeric, value: ${t}`);break;case`windowsPty`:t??={};break}return t}};function _o(e){return e===`block`||e===`underline`||e===`bar`}function vo(e,t=5){if(typeof e!=`object`)return e;let n=Array.isArray(e)?[]:{};for(let r in e)n[r]=t<=1?e[r]:e[r]&&vo(e[r],t-1);return n}var yo=Object.freeze({insertMode:!1}),bo=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,cursorBlink:void 0,cursorStyle:void 0,origin:!1,reverseWraparound:!1,sendFocus:!1,synchronizedOutput:!1,wraparound:!0}),xo=class extends j{constructor(e,t,n){super(),this._bufferService=e,this._logService=t,this._optionsService=n,this.isCursorInitialized=!1,this.isCursorHidden=!1,this._onData=this._register(new P),this.onData=this._onData.event,this._onUserInput=this._register(new P),this.onUserInput=this._onUserInput.event,this._onBinary=this._register(new P),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this._register(new P),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.modes=vo(yo),this.decPrivateModes=vo(bo)}reset(){this.modes=vo(yo),this.decPrivateModes=vo(bo)}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;let n=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&n.ybase!==n.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`),this._logService.trace(`sending data (codes)`,()=>e.split(``).map(e=>e.charCodeAt(0))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`),this._logService.trace(`sending binary (codes)`,()=>e.split(``).map(e=>e.charCodeAt(0))),this._onBinary.fire(e))}};xo=y([b(0,O),b(1,we),b(2,k)],xo);var So={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:e=>e.button===4||e.action!==1?!1:(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>e.action!==32},DRAG:{events:23,restrict:e=>!(e.action===32&&e.button===3)},ANY:{events:31,restrict:e=>!0}};function Co(e,t){let n=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return e.button===4?(n|=64,n|=e.action):(n|=e.button&3,e.button&4&&(n|=64),e.button&8&&(n|=128),e.action===32?n|=32:e.action===0&&!t&&(n|=3)),n}var wo=String.fromCharCode,To={DEFAULT:e=>{let t=[Co(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?``:`\x1B[M${wo(t[0])}${wo(t[1])}${wo(t[2])}`},SGR:e=>{let t=e.action===0&&e.button!==4?`m`:`M`;return`\x1B[<${Co(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{let t=e.action===0&&e.button!==4?`m`:`M`;return`\x1B[<${Co(e,!0)};${e.x};${e.y}${t}`}},Eo=class extends j{constructor(e,t,n){super(),this._bufferService=e,this._coreService=t,this._optionsService=n,this._protocols={},this._encodings={},this._activeProtocol=``,this._activeEncoding=``,this._lastEvent=null,this._wheelPartialScroll=0,this._onProtocolChange=this._register(new P),this.onProtocolChange=this._onProtocolChange.event;for(let e of Object.keys(So))this.addProtocol(e,So[e]);for(let e of Object.keys(To))this.addEncoding(e,To[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return this._protocols[this._activeProtocol].events!==0}set activeProtocol(e){if(!this._protocols[e])throw Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol=`NONE`,this.activeEncoding=`DEFAULT`,this._lastEvent=null,this._wheelPartialScroll=0}consumeWheelEvent(e,t,n){if(e.deltaY===0||e.shiftKey||t===void 0||n===void 0)return 0;let r=t/n,i=this._applyScrollModifier(e.deltaY,e);return e.deltaMode===WheelEvent.DOM_DELTA_PIXEL?(i/=r+0,Math.abs(e.deltaY)<50&&(i*=.3),this._wheelPartialScroll+=i,i=Math.floor(Math.abs(this._wheelPartialScroll))*(this._wheelPartialScroll>0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(i*=this._bufferService.rows),i}_applyScrollModifier(e,t){return t.altKey||t.ctrlKey||t.shiftKey?e*this._optionsService.rawOptions.fastScrollSensitivity*this._optionsService.rawOptions.scrollSensitivity:e*this._optionsService.rawOptions.scrollSensitivity}triggerMouseEvent(e){if(e.col<0||e.col>=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows||e.button===4&&e.action===32||e.button===3&&e.action!==32||e.button!==4&&(e.action===2||e.action===3)||(e.col++,e.row++,e.action===32&&this._lastEvent&&this._equalEvents(this._lastEvent,e,this._activeEncoding===`SGR_PIXELS`))||!this._protocols[this._activeProtocol].restrict(e))return!1;let t=this._encodings[this._activeEncoding](e);return t&&(this._activeEncoding===`DEFAULT`?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0}explainEvents(e){return{down:!!(e&1),up:!!(e&2),drag:!!(e&4),move:!!(e&8),wheel:!!(e&16)}}_equalEvents(e,t,n){if(n){if(e.x!==t.x||e.y!==t.y)return!1}else if(e.col!==t.col||e.row!==t.row)return!1;return!(e.button!==t.button||e.action!==t.action||e.ctrl!==t.ctrl||e.alt!==t.alt||e.shift!==t.shift)}};Eo=y([b(0,O),b(1,xe),b(2,k)],Eo);var Do=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],Oo=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],Z;function ko(e,t){let n=0,r=t.length-1,i;if(et[r][1])return!1;for(;r>=n;)if(i=n+r>>1,e>t[i][1])n=i+1;else if(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let n=this.wcwidth(e),r=n===0&&t!==0;if(r){let e=jo.extractWidth(t);e===0?r=!1:e>n&&(n=e)}return jo.createPropertyValue(0,n,r)}},jo=class e{constructor(){this._providers=Object.create(null),this._active=``,this._onChange=new P,this.onChange=this._onChange.event;let e=new Ao;this.register(e),this._active=e.version,this._activeProvider=e}static extractShouldJoin(e){return(e&1)!=0}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,n=!1){return(e&16777215)<<3|(t&3)<<1|!!n}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(t){let n=0,r=0,i=t.length;for(let a=0;a=i)return n+this.wcwidth(o);let e=t.charCodeAt(a);56320<=e&&e<=57343?o=(o-55296)*1024+e-56320+65536:n+=this.wcwidth(e)}let s=this.charProperties(o,r),c=e.extractWidth(s);e.extractShouldJoin(s)&&(c-=e.extractWidth(r)),n+=c,r=s}return n}charProperties(e,t){return this._activeProvider.charProperties(e,t)}},Mo=class{constructor(){this.glevel=0,this._charsets=[]}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}};function No(e){let t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1)?.get(e.cols-1),n=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);n&&t&&(n.isWrapped=t[3]!==0&&t[3]!==32)}var Po=2147483647,Fo=256,Io=class e{constructor(e=32,t=32){if(this.maxLength=e,this.maxSubParamsLength=t,t>Fo)throw Error(`maxSubParamsLength must not be greater than 256`);this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}static fromArray(t){let n=new e;if(!t.length)return n;for(let e=+!!Array.isArray(t[0]);e>8,r=this._subParamsIdx[t]&255;r-n>0&&e.push(Array.prototype.slice.call(this._subParams,n,r))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength){this._rejectDigits=!0;return}if(e<-1)throw Error(`values lesser than -1 are not allowed`);this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>Po?Po:e}addSubParam(e){if(this._digitIsSub=!0,this.length){if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength){this._rejectSubDigits=!0;return}if(e<-1)throw Error(`values lesser than -1 are not allowed`);this._subParams[this._subParamsLength++]=e>Po?Po:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(this._subParamsIdx[e]&255)-(this._subParamsIdx[e]>>8)>0}getSubParams(e){let t=this._subParamsIdx[e]>>8,n=this._subParamsIdx[e]&255;return n-t>0?this._subParams.subarray(t,n):null}getSubParamsAll(){let e={};for(let t=0;t>8,r=this._subParamsIdx[t]&255;r-n>0&&(e[t]=this._subParams.slice(n,r))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;let n=this._digitIsSub?this._subParams:this.params,r=n[t-1];n[t-1]=~r?Math.min(r*10+e,Po):e}},Lo=[],Ro=class{constructor(){this._state=0,this._active=Lo,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]===void 0&&(this._handlers[e]=[]);let n=this._handlers[e];return n.push(t),{dispose:()=>{let e=n.indexOf(t);e!==-1&&n.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=Lo}reset(){if(this._state===2)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=Lo,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||Lo,!this._active.length)this._handlerFb(this._id,`START`);else for(let e=this._active.length-1;e>=0;e--)this._active[e].start()}_put(e,t,n){if(!this._active.length)this._handlerFb(this._id,`PUT`,ce(e,t,n));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,n)}start(){this.reset(),this._state=1}put(e,t,n){if(this._state!==3){if(this._state===1)for(;t0&&this._put(e,t,n)}}end(e,t=!0){if(this._state!==0){if(this._state!==3)if(this._state===1&&this._start(),!this._active.length)this._handlerFb(this._id,`END`,e);else{let n=!1,r=this._active.length-1,i=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,n=t,i=this._stack.fallThrough,this._stack.paused=!1),!i&&n===!1){for(;r>=0&&(n=this._active[r].end(e),n!==!0);r--)if(n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,n;r--}for(;r>=0;r--)if(n=this._active[r].end(!1),n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,n}this._active=Lo,this._id=-1,this._state=0}}},zo=class{constructor(e){this._handler=e,this._data=``,this._hitLimit=!1}start(){this._data=``,this._hitLimit=!1}put(e,t,n){this._hitLimit||(this._data+=ce(e,t,n),this._data.length>1e7&&(this._data=``,this._hitLimit=!0))}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data),t instanceof Promise))return t.then(e=>(this._data=``,this._hitLimit=!1,e));return this._data=``,this._hitLimit=!1,t}},Bo=[],Vo=class{constructor(){this._handlers=Object.create(null),this._active=Bo,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=Bo}registerHandler(e,t){this._handlers[e]===void 0&&(this._handlers[e]=[]);let n=this._handlers[e];return n.push(t),{dispose:()=>{let e=n.indexOf(t);e!==-1&&n.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=Bo,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||Bo,!this._active.length)this._handlerFb(this._ident,`HOOK`,t);else for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t)}put(e,t,n){if(!this._active.length)this._handlerFb(this._ident,`PUT`,ce(e,t,n));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,n)}unhook(e,t=!0){if(!this._active.length)this._handlerFb(this._ident,`UNHOOK`,e);else{let n=!1,r=this._active.length-1,i=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,n=t,i=this._stack.fallThrough,this._stack.paused=!1),!i&&n===!1){for(;r>=0&&(n=this._active[r].unhook(e),n!==!0);r--)if(n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,n;r--}for(;r>=0;r--)if(n=this._active[r].unhook(!1),n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,n}this._active=Bo,this._ident=0}},Ho=new Io;Ho.addParam(0);var Uo=class{constructor(e){this._handler=e,this._data=``,this._params=Ho,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():Ho,this._data=``,this._hitLimit=!1}put(e,t,n){this._hitLimit||(this._data+=ce(e,t,n),this._data.length>1e7&&(this._data=``,this._hitLimit=!0))}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data,this._params),t instanceof Promise))return t.then(e=>(this._params=Ho,this._data=``,this._hitLimit=!1,e));return this._params=Ho,this._data=``,this._hitLimit=!1,t}},Wo=class{constructor(e){this.table=new Uint8Array(e)}setDefault(e,t){this.table.fill(e<<4|t)}add(e,t,n,r){this.table[t<<8|e]=n<<4|r}addMany(e,t,n,r){for(let i=0;it),n=(e,n)=>t.slice(e,n),r=n(32,127),i=n(0,24);i.push(25),i.push.apply(i,n(28,32));let a=n(0,14),o;for(o in e.setDefault(1,0),e.addMany(r,0,2,0),a)e.addMany([24,26,153,154],o,3,0),e.addMany(n(128,144),o,3,0),e.addMany(n(144,152),o,3,0),e.add(156,o,0,0),e.add(27,o,11,1),e.add(157,o,4,8),e.addMany([152,158,159],o,0,7),e.add(155,o,11,3),e.add(144,o,11,9);return e.addMany(i,0,3,0),e.addMany(i,1,3,1),e.add(127,1,0,1),e.addMany(i,8,0,8),e.addMany(i,3,3,3),e.add(127,3,0,3),e.addMany(i,4,3,4),e.add(127,4,0,4),e.addMany(i,6,3,6),e.addMany(i,5,3,5),e.add(127,5,0,5),e.addMany(i,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(r,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(n(28,32),8,0,8),e.addMany([88,94,95],1,0,7),e.addMany(r,7,0,7),e.addMany(i,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(91,1,11,3),e.addMany(n(64,127),3,7,0),e.addMany(n(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(n(48,60),4,8,4),e.addMany(n(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(n(32,64),6,0,6),e.add(127,6,0,6),e.addMany(n(64,127),6,0,0),e.addMany(n(32,48),3,9,5),e.addMany(n(32,48),5,9,5),e.addMany(n(48,64),5,0,6),e.addMany(n(64,127),5,7,0),e.addMany(n(32,48),4,9,5),e.addMany(n(32,48),1,9,2),e.addMany(n(32,48),2,9,2),e.addMany(n(48,127),2,10,0),e.addMany(n(48,80),1,10,0),e.addMany(n(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(n(96,127),1,10,0),e.add(80,1,11,9),e.addMany(i,9,0,9),e.add(127,9,0,9),e.addMany(n(28,32),9,0,9),e.addMany(n(32,48),9,9,12),e.addMany(n(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(i,11,0,11),e.addMany(n(32,128),11,0,11),e.addMany(n(28,32),11,0,11),e.addMany(i,10,0,10),e.add(127,10,0,10),e.addMany(n(28,32),10,0,10),e.addMany(n(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(n(32,48),10,9,12),e.addMany(i,12,0,12),e.add(127,12,0,12),e.addMany(n(28,32),12,0,12),e.addMany(n(32,48),12,9,12),e.addMany(n(48,64),12,0,11),e.addMany(n(64,127),12,12,13),e.addMany(n(64,127),10,12,13),e.addMany(n(64,127),9,12,13),e.addMany(i,13,13,13),e.addMany(r,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(Go,0,2,0),e.add(Go,8,5,8),e.add(Go,6,0,6),e.add(Go,11,0,11),e.add(Go,13,13,13),e}(),qo=class extends j{constructor(e=Ko){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new Io,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(e,t,n)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this._register(A(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null)})),this._oscParser=this._register(new Ro),this._dcsParser=this._register(new Vo),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:`\\`},()=>!0)}_identifier(e,t=[64,126]){let n=0;if(e.prefix){if(e.prefix.length>1)throw Error(`only one byte as prefix supported`);if(n=e.prefix.charCodeAt(0),n&&60>n||n>63)throw Error(`prefix must be in range 0x3c .. 0x3f`)}if(e.intermediates){if(e.intermediates.length>2)throw Error(`only two bytes as intermediates are supported`);for(let t=0;tr||r>47)throw Error(`intermediate must be in range 0x20 .. 0x2f`);n<<=8,n|=r}}if(e.final.length!==1)throw Error(`final must be a single byte`);let r=e.final.charCodeAt(0);if(t[0]>r||r>t[1])throw Error(`final must be in range ${t[0]} .. ${t[1]}`);return n<<=8,n|=r,n}identToString(e){let t=[];for(;e;)t.push(String.fromCharCode(e&255)),e>>=8;return t.reverse().join(``)}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){let n=this._identifier(e,[48,126]);this._escHandlers[n]===void 0&&(this._escHandlers[n]=[]);let r=this._escHandlers[n];return r.push(t),{dispose:()=>{let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){this._executeHandlers[e.charCodeAt(0)]=t}clearExecuteHandler(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){let n=this._identifier(e);this._csiHandlers[n]===void 0&&(this._csiHandlers[n]=[]);let r=this._csiHandlers[n];return r.push(t),{dispose:()=>{let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._parseStack.state!==0&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,n,r,i){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=n,this._parseStack.transition=r,this._parseStack.chunkPos=i}parse(e,t,n){let r=0,i=0,a=0,o;if(this._parseStack.state)if(this._parseStack.state===2)this._parseStack.state=0,a=this._parseStack.chunkPos+1;else{if(n===void 0||this._parseStack.state===1)throw this._parseStack.state=1,Error(`improper continuation due to previous async handler, giving up parsing`);let t=this._parseStack.handlers,i=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(n===!1&&i>-1){for(;i>=0&&(o=t[i](this._params),o!==!0);i--)if(o instanceof Promise)return this._parseStack.handlerPos=i,o}this._parseStack.handlers=[];break;case 4:if(n===!1&&i>-1){for(;i>=0&&(o=t[i](),o!==!0);i--)if(o instanceof Promise)return this._parseStack.handlerPos=i,o}this._parseStack.handlers=[];break;case 6:if(r=e[this._parseStack.chunkPos],o=this._dcsParser.unhook(r!==24&&r!==26,n),o)return o;r===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break;case 5:if(r=e[this._parseStack.chunkPos],o=this._oscParser.end(r!==24&&r!==26,n),o)return o;r===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break}this._parseStack.state=0,a=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=this._parseStack.transition&15}for(let n=a;n>4){case 2:for(let i=n+1;;++i){if(i>=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=0&&(o=a[s](this._params),o!==!0);s--)if(o instanceof Promise)return this._preserveStack(3,a,s,i,n),o;s<0&&this._csiHandlerFb(this._collect<<8|r,this._params),this.precedingJoinState=0;break;case 8:do switch(r){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(r-48)}while(++n47&&r<60);n--;break;case 9:this._collect<<=8,this._collect|=r;break;case 10:let c=this._escHandlers[this._collect<<8|r],l=c?c.length-1:-1;for(;l>=0&&(o=c[l](),o!==!0);l--)if(o instanceof Promise)return this._preserveStack(4,c,l,i,n),o;l<0&&this._escHandlerFb(this._collect<<8|r),this.precedingJoinState=0;break;case 11:this._params.reset(),this._params.addParam(0),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|r,this._params);break;case 13:for(let i=n+1;;++i)if(i>=t||(r=e[i])===24||r===26||r===27||r>127&&r=t||(r=e[i])<32||r>127&&r>4:i>>8}return n}}function Zo(e,t){let n=e.toString(16),r=n.length<2?`0`+n:n;switch(t){case 4:return n[0];case 8:return r;case 12:return(r+r).slice(0,3);default:return r+r}}function Qo(e,t=16){let[n,r,i]=e;return`rgb:${Zo(n,t)}/${Zo(r,t)}/${Zo(i,t)}`}var $o={"(":0,")":1,"*":2,"+":3,"-":1,".":2},es=131072,ts=10;function ns(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var rs=5e3,is=0,as=class extends j{constructor(e,t,n,r,i,a,o,s,c=new qo){super(),this._bufferService=e,this._charsetService=t,this._coreService=n,this._logService=r,this._optionsService=i,this._oscLinkService=a,this._coreMouseService=o,this._unicodeService=s,this._parser=c,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new le,this._utf8Decoder=new ue,this._windowTitle=``,this._iconName=``,this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=Y.clone(),this._eraseAttrDataInternal=Y.clone(),this._onRequestBell=this._register(new P),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this._register(new P),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this._register(new P),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this._register(new P),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this._register(new P),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this._register(new P),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this._register(new P),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this._register(new P),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this._register(new P),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this._register(new P),this.onLineFeed=this._onLineFeed.event,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._onTitleChange=this._register(new P),this.onTitleChange=this._onTitleChange.event,this._onColor=this._register(new P),this.onColor=this._onColor.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this._register(this._parser),this._dirtyRowTracker=new os(this._bufferService),this._activeBuffer=this._bufferService.buffer,this._register(this._bufferService.buffers.onBufferActivate(e=>this._activeBuffer=e.activeBuffer)),this._parser.setCsiHandlerFallback((e,t)=>{this._logService.debug(`Unknown CSI code: `,{identifier:this._parser.identToString(e),params:t.toArray()})}),this._parser.setEscHandlerFallback(e=>{this._logService.debug(`Unknown ESC code: `,{identifier:this._parser.identToString(e)})}),this._parser.setExecuteHandlerFallback(e=>{this._logService.debug(`Unknown EXECUTE code: `,{code:e})}),this._parser.setOscHandlerFallback((e,t,n)=>{this._logService.debug(`Unknown OSC code: `,{identifier:e,action:t,data:n})}),this._parser.setDcsHandlerFallback((e,t,n)=>{t===`HOOK`&&(n=n.toArray()),this._logService.debug(`Unknown DCS code: `,{identifier:this._parser.identToString(e),action:t,payload:n})}),this._parser.setPrintHandler((e,t,n)=>this.print(e,t,n)),this._parser.registerCsiHandler({final:`@`},e=>this.insertChars(e)),this._parser.registerCsiHandler({intermediates:` `,final:`@`},e=>this.scrollLeft(e)),this._parser.registerCsiHandler({final:`A`},e=>this.cursorUp(e)),this._parser.registerCsiHandler({intermediates:` `,final:`A`},e=>this.scrollRight(e)),this._parser.registerCsiHandler({final:`B`},e=>this.cursorDown(e)),this._parser.registerCsiHandler({final:`C`},e=>this.cursorForward(e)),this._parser.registerCsiHandler({final:`D`},e=>this.cursorBackward(e)),this._parser.registerCsiHandler({final:`E`},e=>this.cursorNextLine(e)),this._parser.registerCsiHandler({final:`F`},e=>this.cursorPrecedingLine(e)),this._parser.registerCsiHandler({final:`G`},e=>this.cursorCharAbsolute(e)),this._parser.registerCsiHandler({final:`H`},e=>this.cursorPosition(e)),this._parser.registerCsiHandler({final:`I`},e=>this.cursorForwardTab(e)),this._parser.registerCsiHandler({final:`J`},e=>this.eraseInDisplay(e,!1)),this._parser.registerCsiHandler({prefix:`?`,final:`J`},e=>this.eraseInDisplay(e,!0)),this._parser.registerCsiHandler({final:`K`},e=>this.eraseInLine(e,!1)),this._parser.registerCsiHandler({prefix:`?`,final:`K`},e=>this.eraseInLine(e,!0)),this._parser.registerCsiHandler({final:`L`},e=>this.insertLines(e)),this._parser.registerCsiHandler({final:`M`},e=>this.deleteLines(e)),this._parser.registerCsiHandler({final:`P`},e=>this.deleteChars(e)),this._parser.registerCsiHandler({final:`S`},e=>this.scrollUp(e)),this._parser.registerCsiHandler({final:`T`},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:`X`},e=>this.eraseChars(e)),this._parser.registerCsiHandler({final:`Z`},e=>this.cursorBackwardTab(e)),this._parser.registerCsiHandler({final:"`"},e=>this.charPosAbsolute(e)),this._parser.registerCsiHandler({final:`a`},e=>this.hPositionRelative(e)),this._parser.registerCsiHandler({final:`b`},e=>this.repeatPrecedingCharacter(e)),this._parser.registerCsiHandler({final:`c`},e=>this.sendDeviceAttributesPrimary(e)),this._parser.registerCsiHandler({prefix:`>`,final:`c`},e=>this.sendDeviceAttributesSecondary(e)),this._parser.registerCsiHandler({final:`d`},e=>this.linePosAbsolute(e)),this._parser.registerCsiHandler({final:`e`},e=>this.vPositionRelative(e)),this._parser.registerCsiHandler({final:`f`},e=>this.hVPosition(e)),this._parser.registerCsiHandler({final:`g`},e=>this.tabClear(e)),this._parser.registerCsiHandler({final:`h`},e=>this.setMode(e)),this._parser.registerCsiHandler({prefix:`?`,final:`h`},e=>this.setModePrivate(e)),this._parser.registerCsiHandler({final:`l`},e=>this.resetMode(e)),this._parser.registerCsiHandler({prefix:`?`,final:`l`},e=>this.resetModePrivate(e)),this._parser.registerCsiHandler({final:`m`},e=>this.charAttributes(e)),this._parser.registerCsiHandler({final:`n`},e=>this.deviceStatus(e)),this._parser.registerCsiHandler({prefix:`?`,final:`n`},e=>this.deviceStatusPrivate(e)),this._parser.registerCsiHandler({intermediates:`!`,final:`p`},e=>this.softReset(e)),this._parser.registerCsiHandler({intermediates:` `,final:`q`},e=>this.setCursorStyle(e)),this._parser.registerCsiHandler({final:`r`},e=>this.setScrollRegion(e)),this._parser.registerCsiHandler({final:`s`},e=>this.saveCursor(e)),this._parser.registerCsiHandler({final:`t`},e=>this.windowOptions(e)),this._parser.registerCsiHandler({final:`u`},e=>this.restoreCursor(e)),this._parser.registerCsiHandler({intermediates:`'`,final:`}`},e=>this.insertColumns(e)),this._parser.registerCsiHandler({intermediates:`'`,final:`~`},e=>this.deleteColumns(e)),this._parser.registerCsiHandler({intermediates:`"`,final:`q`},e=>this.selectProtected(e)),this._parser.registerCsiHandler({intermediates:`$`,final:`p`},e=>this.requestMode(e,!0)),this._parser.registerCsiHandler({prefix:`?`,intermediates:`$`,final:`p`},e=>this.requestMode(e,!1)),this._parser.setExecuteHandler(L.BEL,()=>this.bell()),this._parser.setExecuteHandler(L.LF,()=>this.lineFeed()),this._parser.setExecuteHandler(L.VT,()=>this.lineFeed()),this._parser.setExecuteHandler(L.FF,()=>this.lineFeed()),this._parser.setExecuteHandler(L.CR,()=>this.carriageReturn()),this._parser.setExecuteHandler(L.BS,()=>this.backspace()),this._parser.setExecuteHandler(L.HT,()=>this.tab()),this._parser.setExecuteHandler(L.SO,()=>this.shiftOut()),this._parser.setExecuteHandler(L.SI,()=>this.shiftIn()),this._parser.setExecuteHandler(ci.IND,()=>this.index()),this._parser.setExecuteHandler(ci.NEL,()=>this.nextLine()),this._parser.setExecuteHandler(ci.HTS,()=>this.tabSet()),this._parser.registerOscHandler(0,new zo(e=>(this.setTitle(e),this.setIconName(e),!0))),this._parser.registerOscHandler(1,new zo(e=>this.setIconName(e))),this._parser.registerOscHandler(2,new zo(e=>this.setTitle(e))),this._parser.registerOscHandler(4,new zo(e=>this.setOrReportIndexedColor(e))),this._parser.registerOscHandler(8,new zo(e=>this.setHyperlink(e))),this._parser.registerOscHandler(10,new zo(e=>this.setOrReportFgColor(e))),this._parser.registerOscHandler(11,new zo(e=>this.setOrReportBgColor(e))),this._parser.registerOscHandler(12,new zo(e=>this.setOrReportCursorColor(e))),this._parser.registerOscHandler(104,new zo(e=>this.restoreIndexedColor(e))),this._parser.registerOscHandler(110,new zo(e=>this.restoreFgColor(e))),this._parser.registerOscHandler(111,new zo(e=>this.restoreBgColor(e))),this._parser.registerOscHandler(112,new zo(e=>this.restoreCursorColor(e))),this._parser.registerEscHandler({final:`7`},()=>this.saveCursor()),this._parser.registerEscHandler({final:`8`},()=>this.restoreCursor()),this._parser.registerEscHandler({final:`D`},()=>this.index()),this._parser.registerEscHandler({final:`E`},()=>this.nextLine()),this._parser.registerEscHandler({final:`H`},()=>this.tabSet()),this._parser.registerEscHandler({final:`M`},()=>this.reverseIndex()),this._parser.registerEscHandler({final:`=`},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:`>`},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:`c`},()=>this.fullReset()),this._parser.registerEscHandler({final:`n`},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:`o`},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:`|`},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:`}`},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:`~`},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:`%`,final:`@`},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:`%`,final:`G`},()=>this.selectDefaultCharset());for(let e in X)this._parser.registerEscHandler({intermediates:`(`,final:e},()=>this.selectCharset(`(`+e)),this._parser.registerEscHandler({intermediates:`)`,final:e},()=>this.selectCharset(`)`+e)),this._parser.registerEscHandler({intermediates:`*`,final:e},()=>this.selectCharset(`*`+e)),this._parser.registerEscHandler({intermediates:`+`,final:e},()=>this.selectCharset(`+`+e)),this._parser.registerEscHandler({intermediates:`-`,final:e},()=>this.selectCharset(`-`+e)),this._parser.registerEscHandler({intermediates:`.`,final:e},()=>this.selectCharset(`.`+e)),this._parser.registerEscHandler({intermediates:`/`,final:e},()=>this.selectCharset(`/`+e));this._parser.registerEscHandler({intermediates:`#`,final:`8`},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(e=>(this._logService.error(`Parsing error: `,e),e)),this._parser.registerDcsHandler({intermediates:`$`,final:`q`},new Uo((e,t)=>this.requestStatusString(e,t)))}getAttrData(){return this._curAttrData}_preserveStack(e,t,n,r){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=n,this._parseStack.position=r}_logSlowResolvingAsync(e){this._logService.logLevel<=3&&Promise.race([e,new Promise((e,t)=>setTimeout(()=>t(`#SLOW_TIMEOUT`),rs))]).catch(e=>{if(e!==`#SLOW_TIMEOUT`)throw e;console.warn(`async parser handler taking longer than ${rs} ms`)})}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let n,r=this._activeBuffer.x,i=this._activeBuffer.y,a=0,o=this._parseStack.paused;if(o){if(n=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(n),n;r=this._parseStack.cursorStartX,i=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>es&&(a=this._parseStack.position+es)}if(this._logService.logLevel<=1&&this._logService.debug(`parsing data ${typeof e==`string`?` "${e}"`:` "${Array.prototype.map.call(e,e=>String.fromCharCode(e)).join(``)}"`}`),this._logService.logLevel===0&&this._logService.trace(`parsing data (codes)`,typeof e==`string`?e.split(``).map(e=>e.charCodeAt(0)):e),this._parseBuffer.lengthes)for(let t=a;t0&&d.getWidth(this._activeBuffer.x-1)===2&&d.setCellFromCodepoint(this._activeBuffer.x-1,0,1,u);let f=this._parser.precedingJoinState;for(let p=t;ps){if(c){let e=d,t=this._activeBuffer.x-m;for(this._activeBuffer.x=m,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),d=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),m>0&&d instanceof Qa&&d.copyCellsFrom(e,t,0,m,!1);t=0;)d.setCellFromCodepoint(this._activeBuffer.x++,0,0,u);continue}if(l&&(d.insertCells(this._activeBuffer.x,i-m,this._activeBuffer.getNullCell(u)),d.getWidth(s-1)===2&&d.setCellFromCodepoint(s-1,0,1,u)),d.setCellFromCodepoint(this._activeBuffer.x++,r,i,u),i>0)for(;--i;)d.setCellFromCodepoint(this._activeBuffer.x++,0,0,u)}this._parser.precedingJoinState=f,this._activeBuffer.x0&&d.getWidth(this._activeBuffer.x)===0&&!d.hasContent(this._activeBuffer.x)&&d.setCellFromCodepoint(this._activeBuffer.x,0,1,u),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return e.final===`t`&&!e.prefix&&!e.intermediates?this._parser.registerCsiHandler(e,e=>!ns(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e)):this._parser.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new Uo(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new zo(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(this._activeBuffer.x===0&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;let e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){let t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){let t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){let t=e.params[0];return t===0?delete this._activeBuffer.tabs[this._activeBuffer.x]:t===3&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){let t=e.params[0];return t===1&&(this._curAttrData.bg|=536870912),(t===2||t===0)&&(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,n,r=!1,i=!1){let a=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);a.replaceCells(t,n,this._activeBuffer.getNullCell(this._eraseAttrData()),i),r&&(a.isWrapped=!1)}_resetBufferLine(e,t=!1){let n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n&&(n.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),n.isWrapped=!1)}eraseInDisplay(e,t=!1){this._restrictCursor(this._bufferService.cols);let n;switch(e.params[0]){case 0:for(n=this._activeBuffer.y,this._dirtyRowTracker.markDirty(n),this._eraseInBufferLine(n++,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);n=this._bufferService.cols&&(this._activeBuffer.lines.get(n+1).isWrapped=!1);n--;)this._resetBufferLine(n,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(n=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,n-1);n--&&!this._activeBuffer.lines.get(this._activeBuffer.ybase+n)?.getTrimmedLength(););for(;n>=0;n--)this._bufferService.scroll(this._eraseAttrData())}else{for(n=this._bufferService.rows,this._dirtyRowTracker.markDirty(n-1);n--;)this._resetBufferLine(n,t);this._dirtyRowTracker.markDirty(0)}break;case 3:let e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._onScroll.fire(0));break}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t);break}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let c=s;for(let e=1;e0||(this._is(`xterm`)||this._is(`rxvt-unicode`)||this._is(`screen`)?this._coreService.triggerDataEvent(L.ESC+`[?1;2c`):this._is(`linux`)&&this._coreService.triggerDataEvent(L.ESC+`[?6c`)),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is(`xterm`)?this._coreService.triggerDataEvent(L.ESC+`[>0;276;0c`):this._is(`rxvt-unicode`)?this._coreService.triggerDataEvent(L.ESC+`[>85;95;0c`):this._is(`linux`)?this._coreService.triggerDataEvent(e.params[0]+`c`):this._is(`screen`)&&this._coreService.triggerDataEvent(L.ESC+`[>83;40003;0c`)),!0}_is(e){return(this._optionsService.rawOptions.termName+``).indexOf(e)===0}setMode(e){for(let t=0;t(e[e.NOT_RECOGNIZED=0]=`NOT_RECOGNIZED`,e[e.SET=1]=`SET`,e[e.RESET=2]=`RESET`,e[e.PERMANENTLY_SET=3]=`PERMANENTLY_SET`,e[e.PERMANENTLY_RESET=4]=`PERMANENTLY_RESET`))(n||={});let r=this._coreService.decPrivateModes,{activeProtocol:i,activeEncoding:a}=this._coreMouseService,o=this._coreService,{buffers:s,cols:c}=this._bufferService,{active:l,alt:u}=s,d=this._optionsService.rawOptions,f=(e,n)=>(o.triggerDataEvent(`${L.ESC}[${t?``:`?`}${e};${n}$y`),!0),p=e=>e?1:2,m=e.params[0];return t?m===2?f(m,4):m===4?f(m,p(o.modes.insertMode)):m===12?f(m,3):m===20?f(m,p(d.convertEol)):f(m,0):m===1?f(m,p(r.applicationCursorKeys)):m===3?f(m,d.windowOptions.setWinLines?c===80?2:+(c===132):0):m===6?f(m,p(r.origin)):m===7?f(m,p(r.wraparound)):m===8?f(m,3):m===9?f(m,p(i===`X10`)):m===12?f(m,p(d.cursorBlink)):m===25?f(m,p(!o.isCursorHidden)):m===45?f(m,p(r.reverseWraparound)):m===66?f(m,p(r.applicationKeypad)):m===67?f(m,4):m===1e3?f(m,p(i===`VT200`)):m===1002?f(m,p(i===`DRAG`)):m===1003?f(m,p(i===`ANY`)):m===1004?f(m,p(r.sendFocus)):m===1005?f(m,4):m===1006?f(m,p(a===`SGR`)):m===1015?f(m,4):m===1016?f(m,p(a===`SGR_PIXELS`)):m===1048?f(m,1):m===47||m===1047||m===1049?f(m,p(l===u)):m===2004?f(m,p(r.bracketedPasteMode)):m===2026?f(m,p(r.synchronizedOutput)):f(m,0)}_updateAttrColor(e,t,n,r,i){return t===2?(e|=50331648,e&=-16777216,e|=pe.fromColorRGB([n,r,i])):t===5&&(e&=-50331904,e|=33554432|n&255),e}_extractColor(e,t,n){let r=[0,0,-1,0,0,0],i=0,a=0;do{if(r[a+i]=e.params[t+a],e.hasSubParams(t+a)){let n=e.getSubParams(t+a),o=0;do r[1]===5&&(i=1),r[a+o+1+i]=n[o];while(++o=2||r[1]===2&&a+i>=5)break;r[1]&&(i=1)}while(++a+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,e===0&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=Y.fg,e.bg=Y.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(e.length===1&&e.params[0]===0)return this._processSGR0(this._curAttrData),!0;let t=e.length,n,r=this._curAttrData;for(let i=0;i=30&&n<=37?(r.fg&=-50331904,r.fg|=16777216|n-30):n>=40&&n<=47?(r.bg&=-50331904,r.bg|=16777216|n-40):n>=90&&n<=97?(r.fg&=-50331904,r.fg|=n-90|16777224):n>=100&&n<=107?(r.bg&=-50331904,r.bg|=n-100|16777224):n===0?this._processSGR0(r):n===1?r.fg|=134217728:n===3?r.bg|=67108864:n===4?(r.fg|=268435456,this._processUnderline(e.hasSubParams(i)?e.getSubParams(i)[0]:1,r)):n===5?r.fg|=536870912:n===7?r.fg|=67108864:n===8?r.fg|=1073741824:n===9?r.fg|=2147483648:n===2?r.bg|=134217728:n===21?this._processUnderline(2,r):n===22?(r.fg&=-134217729,r.bg&=-134217729):n===23?r.bg&=-67108865:n===24?(r.fg&=-268435457,this._processUnderline(0,r)):n===25?r.fg&=-536870913:n===27?r.fg&=-67108865:n===28?r.fg&=-1073741825:n===29?r.fg&=2147483647:n===39?(r.fg&=-67108864,r.fg|=Y.fg&16777215):n===49?(r.bg&=-67108864,r.bg|=Y.bg&16777215):n===38||n===48||n===58?i+=this._extractColor(e,i,r):n===53?r.bg|=1073741824:n===55?r.bg&=-1073741825:n===59?(r.extended=r.extended.clone(),r.extended.underlineColor=-1,r.updateExtended()):n===100?(r.fg&=-67108864,r.fg|=Y.fg&16777215,r.bg&=-67108864,r.bg|=Y.bg&16777215):this._logService.debug(`Unknown SGR attribute: %d.`,n);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(`${L.ESC}[0n`);break;case 6:let e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${L.ESC}[${e};${t}R`);break}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:let e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${L.ESC}[?${e};${t}R`);break;case 15:break;case 25:break;case 26:break;case 53:break}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=Y.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){let t=e.length===0?1:e.params[0];if(t===0)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle=`block`;break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle=`underline`;break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle=`bar`;break}let e=t%2==1;this._coreService.decPrivateModes.cursorBlink=e}return!0}setScrollRegion(e){let t=e.params[0]||1,n;return(e.length<2||(n=e.params[1])>this._bufferService.rows||n===0)&&(n=this._bufferService.rows),n>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=n-1,this._setCursor(0,0)),!0}windowOptions(e){if(!ns(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;let t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:t!==2&&this._onRequestWindowsOptionsReport.fire(0);break;case 16:this._onRequestWindowsOptionsReport.fire(1);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`${L.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:(t===0||t===2)&&(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>ts&&this._windowTitleStack.shift()),(t===0||t===1)&&(this._iconNameStack.push(this._iconName),this._iconNameStack.length>ts&&this._iconNameStack.shift());break;case 23:(t===0||t===2)&&this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),(t===0||t===1)&&this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop());break}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,!0}restoreCursor(e){return this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._activeBuffer.savedCharset&&(this._charsetService.charset=this._activeBuffer.savedCharset),this._restrictCursor(),!0}setTitle(e){return this._windowTitle=e,this._onTitleChange.fire(e),!0}setIconName(e){return this._iconName=e,!0}setOrReportIndexedColor(e){let t=[],n=e.split(`;`);for(;n.length>1;){let e=n.shift(),r=n.shift();if(/^\d+$/.exec(e)){let n=parseInt(e);if(ss(n))if(r===`?`)t.push({type:0,index:n});else{let e=Xo(r);e&&t.push({type:1,index:n,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){let t=e.indexOf(`;`);if(t===-1)return!0;let n=e.slice(0,t).trim(),r=e.slice(t+1);return r?this._createHyperlink(n,r):!n.trim()&&this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();let n=e.split(`:`),r,i=n.findIndex(e=>e.startsWith(`id=`));return i!==-1&&(r=n[i].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:r,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){let n=e.split(`;`);for(let e=0;e=this._specialColors.length);++e,++t)if(n[e]===`?`)this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{let r=Xo(n[e]);r&&this._onColor.fire([{type:1,index:this._specialColors[t],color:r}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;let t=[],n=e.split(`;`);for(let e=0;e=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){let e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=Y.clone(),this._eraseAttrDataInternal=Y.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=this._curAttrData.bg&67108863,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){let e=new E;e.content=4194373,e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`${L.ESC}${e}${L.ESC}\\`),!0),r=this._bufferService.buffer,i=this._optionsService.rawOptions;return n(e===`"q`?`P1$r${+!!this._curAttrData.isProtected()}"q`:e===`"p`?`P1$r61;1"p`:e===`r`?`P1$r${r.scrollTop+1};${r.scrollBottom+1}r`:e===`m`?`P1$r0m`:e===` q`?`P1$r${{block:2,underline:4,bar:6}[i.cursorStyle]-+!!i.cursorBlink} q`:`P0$r`)}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}},os=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){ethis.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(is=e,e=t,t=is),ethis.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};os=y([b(0,O)],os);function ss(e){return 0<=e&&e<256}var cs=5e7,ls=12,us=50,ds=class extends j{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._onWriteParsed=this._register(new P),this.onWriteParsed=this._onWriteParsed.event}handleUserInput(){this._didUserInput=!0}writeSync(e,t){if(t!==void 0&&this._syncCalls>t){this._syncCalls=0;return}if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;this._isSyncWriting=!0;let n;for(;n=this._writeBuffer.shift();){this._action(n);let e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(this._pendingData>cs)throw Error(`write data discarded, use flow control to avoid losing data`);if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput){this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),this._innerWrite();return}setTimeout(()=>this._innerWrite())}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}_innerWrite(e=0,t=!0){let n=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){let e=this._writeBuffer[this._bufferOffset],r=this._action(e,t);if(r){r.catch(e=>(queueMicrotask(()=>{throw e}),Promise.resolve(!1))).then(e=>performance.now()-n>=ls?setTimeout(()=>this._innerWrite(0,e)):this._innerWrite(n,e));return}let i=this._callbacks[this._bufferOffset];if(i&&i(),this._bufferOffset++,this._pendingData-=e.length,performance.now()-n>=ls)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>us&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout(()=>this._innerWrite())):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}},fs=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){let t=this._bufferService.buffer;if(e.id===void 0){let n=t.addMarker(t.ybase+t.y),r={data:e,id:this._nextId++,lines:[n]};return n.onDispose(()=>this._removeMarkerFromLink(r,n)),this._dataByLinkId.set(r.id,r),r.id}let n=e,r=this._getEntryIdKey(n),i=this._entriesWithId.get(r);if(i)return this.addLineToLink(i.id,t.ybase+t.y),i.id;let a=t.addMarker(t.ybase+t.y),o={id:this._nextId++,key:this._getEntryIdKey(n),data:n,lines:[a]};return a.onDispose(()=>this._removeMarkerFromLink(o,a)),this._entriesWithId.set(o.key,o),this._dataByLinkId.set(o.id,o),o.id}addLineToLink(e,t){let n=this._dataByLinkId.get(e);if(n&&n.lines.every(e=>e.line!==t)){let e=this._bufferService.buffer.addMarker(t);n.lines.push(e),e.onDispose(()=>this._removeMarkerFromLink(n,e))}}getLinkData(e){return this._dataByLinkId.get(e)?.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){let n=e.lines.indexOf(t);n!==-1&&(e.lines.splice(n,1),e.lines.length===0&&(e.data.id!==void 0&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};fs=y([b(0,O)],fs);var ps=!1,ms=class extends j{constructor(e){super(),this._windowsWrappingHeuristics=this._register(new pt),this._onBinary=this._register(new P),this.onBinary=this._onBinary.event,this._onData=this._register(new P),this.onData=this._onData.event,this._onLineFeed=this._register(new P),this.onLineFeed=this._onLineFeed.event,this._onResize=this._register(new P),this.onResize=this._onResize.event,this._onWriteParsed=this._register(new P),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this._register(new P),this._instantiationService=new Wa,this.optionsService=this._register(new go(e)),this._instantiationService.setService(k,this.optionsService),this._bufferService=this._register(this._instantiationService.createInstance(po)),this._instantiationService.setService(O,this._bufferService),this._logService=this._register(this._instantiationService.createInstance(qa)),this._instantiationService.setService(we,this._logService),this.coreService=this._register(this._instantiationService.createInstance(xo)),this._instantiationService.setService(xe,this.coreService),this.coreMouseService=this._register(this._instantiationService.createInstance(Eo)),this._instantiationService.setService(be,this.coreMouseService),this.unicodeService=this._register(this._instantiationService.createInstance(jo)),this._instantiationService.setService(Ee,this.unicodeService),this._charsetService=this._instantiationService.createInstance(Mo),this._instantiationService.setService(Se,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(fs),this._instantiationService.setService(Te,this._oscLinkService),this._inputHandler=this._register(new as(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.coreMouseService,this.unicodeService)),this._register(N.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(this._inputHandler),this._register(N.forward(this._bufferService.onResize,this._onResize)),this._register(N.forward(this.coreService.onData,this._onData)),this._register(N.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange([`windowsMode`,`windowsPty`],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new ds((e,t)=>this._inputHandler.parse(e,t))),this._register(N.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}get onScroll(){return this._onScrollApi||(this._onScrollApi=this._register(new P),this._onScroll.event(e=>{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(let t in e)this.optionsService.options[t]=e[t]}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=3&&!ps&&(this._logService.warn(`writeSync is unreliable and will be removed soon.`),ps=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,uo),t=Math.max(t,fo),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.coreMouseService.reset()}_handleWindowsPtyOptionChange(){let e=!1,t=this.optionsService.rawOptions.windowsPty;t&&t.buildNumber!==void 0&&t.buildNumber!==void 0?e=t.backend===`conpty`&&t.buildNumber<21376:this.optionsService.rawOptions.windowsMode&&(e=!0),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){let e=[];e.push(this.onLineFeed(No.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:`H`},()=>(No(this._bufferService),!1))),this._windowsWrappingHeuristics.value=A(()=>{for(let t of e)t.dispose()})}}},hs={48:[`0`,`)`],49:[`1`,`!`],50:[`2`,`@`],51:[`3`,`#`],52:[`4`,`$`],53:[`5`,`%`],54:[`6`,`^`],55:[`7`,`&`],56:[`8`,`*`],57:[`9`,`(`],186:[`;`,`:`],187:[`=`,`+`],188:[`,`,`<`],189:[`-`,`_`],190:[`.`,`>`],191:[`/`,`?`],192:["`",`~`],219:[`[`,`{`],220:[`\\`,`|`],221:[`]`,`}`],222:[`'`,`"`]};function gs(e,t,n,r){let i={type:0,cancel:!1,key:void 0},a=!!e.shiftKey|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:e.key===`UIKeyInputUpArrow`?t?i.key=L.ESC+`OA`:i.key=L.ESC+`[A`:e.key===`UIKeyInputLeftArrow`?t?i.key=L.ESC+`OD`:i.key=L.ESC+`[D`:e.key===`UIKeyInputRightArrow`?t?i.key=L.ESC+`OC`:i.key=L.ESC+`[C`:e.key===`UIKeyInputDownArrow`&&(t?i.key=L.ESC+`OB`:i.key=L.ESC+`[B`);break;case 8:i.key=e.ctrlKey?`\b`:L.DEL,e.altKey&&(i.key=L.ESC+i.key);break;case 9:if(e.shiftKey){i.key=L.ESC+`[Z`;break}i.key=L.HT,i.cancel=!0;break;case 13:i.key=e.altKey?L.ESC+L.CR:L.CR,i.cancel=!0;break;case 27:i.key=L.ESC,e.altKey&&(i.key=L.ESC+L.ESC),i.cancel=!0;break;case 37:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`D`:t?i.key=L.ESC+`OD`:i.key=L.ESC+`[D`;break;case 39:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`C`:t?i.key=L.ESC+`OC`:i.key=L.ESC+`[C`;break;case 38:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`A`:t?i.key=L.ESC+`OA`:i.key=L.ESC+`[A`;break;case 40:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`B`:t?i.key=L.ESC+`OB`:i.key=L.ESC+`[B`;break;case 45:!e.shiftKey&&!e.ctrlKey&&(i.key=L.ESC+`[2~`);break;case 46:a?i.key=L.ESC+`[3;`+(a+1)+`~`:i.key=L.ESC+`[3~`;break;case 36:a?i.key=L.ESC+`[1;`+(a+1)+`H`:t?i.key=L.ESC+`OH`:i.key=L.ESC+`[H`;break;case 35:a?i.key=L.ESC+`[1;`+(a+1)+`F`:t?i.key=L.ESC+`OF`:i.key=L.ESC+`[F`;break;case 33:e.shiftKey?i.type=2:e.ctrlKey?i.key=L.ESC+`[5;`+(a+1)+`~`:i.key=L.ESC+`[5~`;break;case 34:e.shiftKey?i.type=3:e.ctrlKey?i.key=L.ESC+`[6;`+(a+1)+`~`:i.key=L.ESC+`[6~`;break;case 112:a?i.key=L.ESC+`[1;`+(a+1)+`P`:i.key=L.ESC+`OP`;break;case 113:a?i.key=L.ESC+`[1;`+(a+1)+`Q`:i.key=L.ESC+`OQ`;break;case 114:a?i.key=L.ESC+`[1;`+(a+1)+`R`:i.key=L.ESC+`OR`;break;case 115:a?i.key=L.ESC+`[1;`+(a+1)+`S`:i.key=L.ESC+`OS`;break;case 116:a?i.key=L.ESC+`[15;`+(a+1)+`~`:i.key=L.ESC+`[15~`;break;case 117:a?i.key=L.ESC+`[17;`+(a+1)+`~`:i.key=L.ESC+`[17~`;break;case 118:a?i.key=L.ESC+`[18;`+(a+1)+`~`:i.key=L.ESC+`[18~`;break;case 119:a?i.key=L.ESC+`[19;`+(a+1)+`~`:i.key=L.ESC+`[19~`;break;case 120:a?i.key=L.ESC+`[20;`+(a+1)+`~`:i.key=L.ESC+`[20~`;break;case 121:a?i.key=L.ESC+`[21;`+(a+1)+`~`:i.key=L.ESC+`[21~`;break;case 122:a?i.key=L.ESC+`[23;`+(a+1)+`~`:i.key=L.ESC+`[23~`;break;case 123:a?i.key=L.ESC+`[24;`+(a+1)+`~`:i.key=L.ESC+`[24~`;break;default:if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&!e.metaKey)e.keyCode>=65&&e.keyCode<=90?i.key=String.fromCharCode(e.keyCode-64):e.keyCode===32?i.key=L.NUL:e.keyCode>=51&&e.keyCode<=55?i.key=String.fromCharCode(e.keyCode-51+27):e.keyCode===56?i.key=L.DEL:e.keyCode===219?i.key=L.ESC:e.keyCode===220?i.key=L.FS:e.keyCode===221&&(i.key=L.GS);else if((!n||r)&&e.altKey&&!e.metaKey){let t=hs[e.keyCode]?.[+!!e.shiftKey];if(t)i.key=L.ESC+t;else if(e.keyCode>=65&&e.keyCode<=90){let t=e.ctrlKey?e.keyCode-64:e.keyCode+32,n=String.fromCharCode(t);e.shiftKey&&(n=n.toUpperCase()),i.key=L.ESC+n}else if(e.keyCode===32)i.key=L.ESC+(e.ctrlKey?L.NUL:` `);else if(e.key===`Dead`&&e.code.startsWith(`Key`)){let t=e.code.slice(3,4);e.shiftKey||(t=t.toLowerCase()),i.key=L.ESC+t,i.cancel=!0}}else n&&!e.altKey&&!e.ctrlKey&&!e.shiftKey&&e.metaKey?e.keyCode===65&&(i.type=1):e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&e.key.length===1?i.key=e.key:e.key&&e.ctrlKey&&(e.key===`_`&&(i.key=L.US),e.key===`@`&&(i.key=L.NUL));break}return i}var Q=0,_s=class{constructor(e){this._getKey=e,this._array=[],this._insertedValues=[],this._flushInsertedTask=new la,this._isFlushingInserted=!1,this._deletedIndices=[],this._flushDeletedTask=new la,this._isFlushingDeleted=!1}clear(){this._array.length=0,this._insertedValues.length=0,this._flushInsertedTask.clear(),this._isFlushingInserted=!1,this._deletedIndices.length=0,this._flushDeletedTask.clear(),this._isFlushingDeleted=!1}insert(e){this._flushCleanupDeleted(),this._insertedValues.length===0&&this._flushInsertedTask.enqueue(()=>this._flushInserted()),this._insertedValues.push(e)}_flushInserted(){let e=this._insertedValues.sort((e,t)=>this._getKey(e)-this._getKey(t)),t=0,n=0,r=Array(this._array.length+this._insertedValues.length);for(let i=0;i=this._array.length||this._getKey(e[t])<=this._getKey(this._array[n])?(r[i]=e[t],t++):r[i]=this._array[n++];this._array=r,this._insertedValues.length=0}_flushCleanupInserted(){!this._isFlushingInserted&&this._insertedValues.length>0&&this._flushInsertedTask.flush()}delete(e){if(this._flushCleanupInserted(),this._array.length===0)return!1;let t=this._getKey(e);if(t===void 0||(Q=this._search(t),Q===-1)||this._getKey(this._array[Q])!==t)return!1;do if(this._array[Q]===e)return this._deletedIndices.length===0&&this._flushDeletedTask.enqueue(()=>this._flushDeleted()),this._deletedIndices.push(Q),!0;while(++Qe-t),t=0,n=Array(this._array.length-e.length),r=0;for(let i=0;i0&&this._flushDeletedTask.flush()}*getKeyIterator(e){if(this._flushCleanupInserted(),this._flushCleanupDeleted(),this._array.length!==0&&(Q=this._search(e),!(Q<0||Q>=this._array.length)&&this._getKey(this._array[Q])===e))do yield this._array[Q];while(++Q=this._array.length)&&this._getKey(this._array[Q])===e))do t(this._array[Q]);while(++Q=t;){let r=t+n>>1,i=this._getKey(this._array[r]);if(i>e)n=r-1;else if(i0&&this._getKey(this._array[r-1])===e;)r--;return r}}return t}},vs=0,ys=0,bs=class extends j{constructor(){super(),this._decorations=new _s(e=>e?.marker.line),this._onDecorationRegistered=this._register(new P),this.onDecorationRegistered=this._onDecorationRegistered.event,this._onDecorationRemoved=this._register(new P),this.onDecorationRemoved=this._onDecorationRemoved.event,this._register(A(()=>this.reset()))}get decorations(){return this._decorations.values()}registerDecoration(e){if(e.marker.isDisposed)return;let t=new xs(e);if(t){let e=t.marker.onDispose(()=>t.dispose()),n=t.onDispose(()=>{n.dispose(),t&&(this._decorations.delete(t)&&this._onDecorationRemoved.fire(t),e.dispose())});this._decorations.insert(t),this._onDecorationRegistered.fire(t)}return t}reset(){for(let e of this._decorations.values())e.dispose();this._decorations.clear()}*getDecorationsAtCell(e,t,n){let r=0,i=0;for(let a of this._decorations.getKeyIterator(t))r=a.options.x??0,i=r+(a.options.width??1),e>=r&&e{vs=t.options.x??0,ys=vs+(t.options.width??1),e>=vs&&e=this._debounceThresholdMS)this._lastRefreshMs=r,this._innerRefresh();else if(!this._additionalRefreshRequested){let e=r-this._lastRefreshMs,t=this._debounceThresholdMS-e;this._additionalRefreshRequested=!0,this._refreshTimeoutID=window.setTimeout(()=>{this._lastRefreshMs=performance.now(),this._innerRefresh(),this._additionalRefreshRequested=!1,this._refreshTimeoutID=void 0},t)}}_innerRefresh(){if(this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return;let e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t)}},ws=20,Ts=class extends j{constructor(e,t,n,r){super(),this._terminal=e,this._coreBrowserService=n,this._renderService=r,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce=``;let i=this._coreBrowserService.mainDocument;this._accessibilityContainer=i.createElement(`div`),this._accessibilityContainer.classList.add(`xterm-accessibility`),this._rowContainer=i.createElement(`div`),this._rowContainer.setAttribute(`role`,`list`),this._rowContainer.classList.add(`xterm-accessibility-tree`),this._rowElements=[];for(let e=0;ethis._handleBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._handleBoundaryFocus(e,1),this._rowElements[0].addEventListener(`focus`,this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=i.createElement(`div`),this._liveRegion.classList.add(`live-region`),this._liveRegion.setAttribute(`aria-live`,`assertive`),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this._register(new Cs(this._renderRows.bind(this))),!this._terminal.element)throw Error(`Cannot enable accessibility before Terminal.open`);this._terminal.element.insertAdjacentElement(`afterbegin`,this._accessibilityContainer),this._register(this._terminal.onResize(e=>this._handleResize(e.rows))),this._register(this._terminal.onRender(e=>this._refreshRows(e.start,e.end))),this._register(this._terminal.onScroll(()=>this._refreshRows())),this._register(this._terminal.onA11yChar(e=>this._handleChar(e))),this._register(this._terminal.onLineFeed(()=>this._handleChar(` -`))),this._register(this._terminal.onA11yTab(e=>this._handleTab(e))),this._register(this._terminal.onKey(e=>this._handleKey(e.key))),this._register(this._terminal.onBlur(()=>this._clearLiveRegion())),this._register(this._renderService.onDimensionsChange(()=>this._refreshRowsDimensions())),this._register(F(i,`selectionchange`,()=>this._handleSelectionChange())),this._register(this._coreBrowserService.onDprChange(()=>this._refreshRowsDimensions())),this._refreshRowsDimensions(),this._refreshRows(),this._register(A(()=>{this._accessibilityContainer.remove(),this._rowElements.length=0}))}_handleTab(e){for(let t=0;t0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,e===` -`&&(this._liveRegionLineCount++,this._liveRegionLineCount===ws+1&&(this._liveRegion.textContent+=S.get())))}_clearLiveRegion(){this._liveRegion.textContent=``,this._liveRegionLineCount=0}_handleKey(e){this._clearLiveRegion(),/\p{Control}/u.test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){let n=this._terminal.buffer,r=n.lines.length.toString();for(let i=e;i<=t;i++){let e=n.lines.get(n.ydisp+i),t=[],a=e?.translateToString(!0,void 0,void 0,t)||``,o=(n.ydisp+i+1).toString(),s=this._rowElements[i];s&&(a.length===0?(s.textContent=`\xA0`,this._rowColumns.set(s,[0,1])):(s.textContent=a,this._rowColumns.set(s,t)),s.setAttribute(`aria-posinset`,o),s.setAttribute(`aria-setsize`,r),this._alignRowWidth(s))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce=``)}_handleBoundaryFocus(e,t){let n=e.target,r=this._rowElements[t===0?1:this._rowElements.length-2];if(n.getAttribute(`aria-posinset`)===(t===0?`1`:`${this._terminal.buffer.lines.length}`)||e.relatedTarget!==r)return;let i,a;if(t===0?(i=n,a=this._rowElements.pop(),this._rowContainer.removeChild(a)):(i=this._rowElements.shift(),a=n,this._rowContainer.removeChild(i)),i.removeEventListener(`focus`,this._topBoundaryFocusListener),a.removeEventListener(`focus`,this._bottomBoundaryFocusListener),t===0){let e=this._createAccessibilityTreeNode();this._rowElements.unshift(e),this._rowContainer.insertAdjacentElement(`afterbegin`,e)}else{let e=this._createAccessibilityTreeNode();this._rowElements.push(e),this._rowContainer.appendChild(e)}this._rowElements[0].addEventListener(`focus`,this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._terminal.scrollLines(t===0?-1:1),this._rowElements[t===0?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleSelectionChange(){if(this._rowElements.length===0)return;let e=this._coreBrowserService.mainDocument.getSelection();if(!e)return;if(e.isCollapsed){this._rowContainer.contains(e.anchorNode)&&this._terminal.clearSelection();return}if(!e.anchorNode||!e.focusNode){console.error(`anchorNode and/or focusNode are null`);return}let t={node:e.anchorNode,offset:e.anchorOffset},n={node:e.focusNode,offset:e.focusOffset};if((t.node.compareDocumentPosition(n.node)&Node.DOCUMENT_POSITION_PRECEDING||t.node===n.node&&t.offset>n.offset)&&([t,n]=[n,t]),t.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(t={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(t.node))return;let r=this._rowElements.slice(-1)[0];if(n.node.compareDocumentPosition(r)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(n={node:r,offset:r.textContent?.length??0}),!this._rowContainer.contains(n.node))return;let i=({node:e,offset:t})=>{let n=e instanceof Text?e.parentNode:e,r=parseInt(n?.getAttribute(`aria-posinset`),10)-1;if(isNaN(r))return console.warn(`row is invalid. Race condition?`),null;let i=this._rowColumns.get(n);if(!i)return console.warn(`columns is null. Race condition?`),null;let a=t=this._terminal.cols&&(++r,a=0),{row:r,column:a}},a=i(t),o=i(n);if(!(!a||!o)){if(a.row>o.row||a.row===o.row&&a.column>=o.column)throw Error(`invalid range`);this._terminal.select(a.column,a.row,(o.row-a.row)*this._terminal.cols-a.column+o.column)}}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener(`focus`,this._bottomBoundaryFocusListener);for(let e=this._rowContainer.children.length;ee;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){let e=this._coreBrowserService.mainDocument.createElement(`div`);return e.setAttribute(`role`,`listitem`),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){Object.assign(this._accessibilityContainer.style,{width:`${this._renderService.dimensions.css.canvas.width}px`,fontSize:`${this._terminal.options.fontSize}px`}),this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e{lt(this._linkCacheDisposables),this._linkCacheDisposables.length=0,this._lastMouseEvent=void 0,this._activeProviderReplies?.clear()})),this._register(this._bufferService.onResize(()=>{this._clearCurrentLink(),this._wasResized=!0})),this._register(F(this._element,`mouseleave`,()=>{this._isMouseOut=!0,this._clearCurrentLink()})),this._register(F(this._element,`mousemove`,this._handleMouseMove.bind(this))),this._register(F(this._element,`mousedown`,this._handleMouseDown.bind(this))),this._register(F(this._element,`mouseup`,this._handleMouseUp.bind(this)))}get currentLink(){return this._currentLink}_handleMouseMove(e){this._lastMouseEvent=e;let t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(!t)return;this._isMouseOut=!1;let n=e.composedPath();for(let e=0;e{e?.forEach(e=>{e.link.dispose&&e.link.dispose()})}),this._activeProviderReplies=new Map,this._activeLine=e.y);let n=!1;for(let[r,i]of this._linkProviderService.linkProviders.entries())t?this._activeProviderReplies?.get(r)&&(n=this._checkLinkProviderResult(r,e,n)):i.provideLinks(e.y,t=>{if(this._isMouseOut)return;let i=t?.map(e=>({link:e}));this._activeProviderReplies?.set(r,i),n=this._checkLinkProviderResult(r,e,n),this._activeProviderReplies?.size===this._linkProviderService.linkProviders.length&&this._removeIntersectingLinks(e.y,this._activeProviderReplies)})}_removeIntersectingLinks(e,t){let n=new Set;for(let r=0;re?this._bufferService.cols:r.link.range.end.x;for(let e=a;e<=o;e++){if(n.has(e)){i.splice(t--,1);break}n.add(e)}}}}_checkLinkProviderResult(e,t,n){if(!this._activeProviderReplies)return n;let r=this._activeProviderReplies.get(e),i=!1;for(let t=0;tthis._linkAtPosition(e.link,t));e&&(n=!0,this._handleNewLink(e))}if(this._activeProviderReplies.size===this._linkProviderService.linkProviders.length&&!n)for(let e=0;ethis._linkAtPosition(e.link,t));if(r){n=!0,this._handleNewLink(r);break}}return n}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(e){if(!this._currentLink)return;let t=this._positionFromMouseEvent(e,this._element,this._mouseService);t&&this._mouseDownLink&&Ds(this._mouseDownLink.link,this._currentLink.link)&&this._linkAtPosition(this._currentLink.link,t)&&this._currentLink.link.activate(e,this._currentLink.link.text)}_clearCurrentLink(e,t){!this._currentLink||!this._lastMouseEvent||(!e||!t||this._currentLink.link.range.start.y>=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,lt(this._linkCacheDisposables),this._linkCacheDisposables.length=0)}_handleNewLink(e){if(!this._lastMouseEvent)return;let t=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);t&&this._linkAtPosition(e.link,t)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:e.link.decorations===void 0||e.link.decorations.underline,pointerCursor:e.link.decorations===void 0||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:()=>this._currentLink?.state?.decorations.pointerCursor,set:e=>{this._currentLink?.state&&this._currentLink.state.decorations.pointerCursor!==e&&(this._currentLink.state.decorations.pointerCursor=e,this._currentLink.state.isHovered&&this._element.classList.toggle(`xterm-cursor-pointer`,e))}},underline:{get:()=>this._currentLink?.state?.decorations.underline,set:t=>{this._currentLink?.state&&this._currentLink?.state?.decorations.underline!==t&&(this._currentLink.state.decorations.underline=t,this._currentLink.state.isHovered&&this._fireUnderlineEvent(e.link,t))}}}),this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e=>{if(!this._currentLink)return;let t=e.start===0?0:e.start+1+this._bufferService.buffer.ydisp,n=this._bufferService.buffer.ydisp+1+e.end;if(this._currentLink.link.range.start.y>=t&&this._currentLink.link.range.end.y<=n&&(this._clearCurrentLink(t,n),this._lastMouseEvent)){let e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._askForLink(e,!1)}})))}_linkHover(e,t,n){this._currentLink?.state&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add(`xterm-cursor-pointer`)),t.hover&&t.hover(n,t.text)}_fireUnderlineEvent(e,t){let n=e.range,r=this._bufferService.buffer.ydisp,i=this._createLinkUnderlineEvent(n.start.x-1,n.start.y-r-1,n.end.x,n.end.y-r-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(i)}_linkLeave(e,t,n){this._currentLink?.state&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove(`xterm-cursor-pointer`)),t.leave&&t.leave(n,t.text)}_linkAtPosition(e,t){let n=e.range.start.y*this._bufferService.cols+e.range.start.x,r=e.range.end.y*this._bufferService.cols+e.range.end.x,i=t.y*this._bufferService.cols+t.x;return n<=i&&i<=r}_positionFromMouseEvent(e,t,n){let r=n.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(r)return{x:r[0],y:r[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(e,t,n,r,i){return{x1:e,y1:t,x2:n,y2:r,cols:this._bufferService.cols,fg:i}}};Es=y([b(1,Me),b(2,Ne),b(3,O),b(4,Le)],Es);function Ds(e,t){return e.text===t.text&&e.range.start.x===t.range.start.x&&e.range.start.y===t.range.start.y&&e.range.end.x===t.range.end.x&&e.range.end.y===t.range.end.y}var Os=class extends ms{constructor(e={}){super(e),this._linkifier=this._register(new pt),this.browser=Ki,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this._register(new pt),this._onCursorMove=this._register(new P),this.onCursorMove=this._onCursorMove.event,this._onKey=this._register(new P),this.onKey=this._onKey.event,this._onRender=this._register(new P),this.onRender=this._onRender.event,this._onSelectionChange=this._register(new P),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this._register(new P),this.onTitleChange=this._onTitleChange.event,this._onBell=this._register(new P),this.onBell=this._onBell.event,this._onFocus=this._register(new P),this._onBlur=this._register(new P),this._onA11yCharEmitter=this._register(new P),this._onA11yTabEmitter=this._register(new P),this._onWillOpen=this._register(new P),this._setup(),this._decorationService=this._instantiationService.createInstance(bs),this._instantiationService.setService(De,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(Vi),this._instantiationService.setService(Le,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(Oe)),this._register(this._inputHandler.onRequestBell(()=>this._onBell.fire())),this._register(this._inputHandler.onRequestRefreshRows(e=>this.refresh(e?.start??0,e?.end??this.rows-1))),this._register(this._inputHandler.onRequestSendFocus(()=>this._reportFocus())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(this._inputHandler.onRequestWindowsOptionsReport(e=>this._reportWindowsOptions(e))),this._register(this._inputHandler.onColor(e=>this._handleColorEvent(e))),this._register(N.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(N.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(N.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(N.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(this._bufferService.onResize(e=>this._afterResize(e.cols,e.rows))),this._register(A(()=>{this._customKeyEventHandler=void 0,this.element?.parentNode?.removeChild(this.element)}))}get linkifier(){return this._linkifier.value}get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}_handleColorEvent(e){if(this._themeService)for(let t of e){let e,n=``;switch(t.index){case 256:e=`foreground`,n=`10`;break;case 257:e=`background`,n=`11`;break;case 258:e=`cursor`,n=`12`;break;default:e=`ansi`,n=`4;`+t.index}switch(t.type){case 0:let r=U.toColorRGB(e===`ansi`?this._themeService.colors.ansi[t.index]:this._themeService.colors[e]);this.coreService.triggerDataEvent(`${L.ESC}]${n};${Qo(r)}${li.ST}`);break;case 1:if(e===`ansi`)this._themeService.modifyColors(e=>e.ansi[t.index]=H.toColor(...t.color));else{let n=e;this._themeService.modifyColors(e=>e[n]=H.toColor(...t.color))}break;case 2:this._themeService.restoreColor(t.index);break}}}_setup(){super._setup(),this._customKeyEventHandler=void 0}get buffer(){return this.buffers.active}focus(){this.textarea&&this.textarea.focus({preventScroll:!0})}_handleScreenReaderModeOptionChange(e){e?!this._accessibilityManager.value&&this._renderService&&(this._accessibilityManager.value=this._instantiationService.createInstance(Ts,this)):this._accessibilityManager.clear()}_handleTextAreaFocus(e){this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(L.ESC+`[I`),this.element.classList.add(`focus`),this._showCursor(),this._onFocus.fire()}blur(){return this.textarea?.blur()}_handleTextAreaBlur(){this.textarea.value=``,this.refresh(this.buffer.y,this.buffer.y),this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(L.ESC+`[O`),this.element.classList.remove(`focus`),this._onBlur.fire()}_syncTextArea(){if(!this.textarea||!this.buffer.isCursorInViewport||this._compositionHelper.isComposing||!this._renderService)return;let e=this.buffer.ybase+this.buffer.y,t=this.buffer.lines.get(e);if(!t)return;let n=Math.min(this.buffer.x,this.cols-1),r=this._renderService.dimensions.css.cell.height,i=t.getWidth(n),a=this._renderService.dimensions.css.cell.width*i,o=this.buffer.y*this._renderService.dimensions.css.cell.height,s=n*this._renderService.dimensions.css.cell.width;this.textarea.style.left=s+`px`,this.textarea.style.top=o+`px`,this.textarea.style.width=a+`px`,this.textarea.style.height=r+`px`,this.textarea.style.lineHeight=r+`px`,this.textarea.style.zIndex=`-5`}_initGlobal(){this._bindKeys(),this._register(F(this.element,`copy`,e=>{this.hasSelection()&&ie(e,this._selectionService)}));let e=e=>w(e,this.textarea,this.coreService,this.optionsService);this._register(F(this.textarea,`paste`,e)),this._register(F(this.element,`paste`,e)),Xi?this._register(F(this.element,`mousedown`,e=>{e.button===2&&se(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})):this._register(F(this.element,`contextmenu`,e=>{se(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})),ia&&this._register(F(this.element,`auxclick`,e=>{e.button===1&&oe(e,this.textarea,this.screenElement)}))}_bindKeys(){this._register(F(this.textarea,`keyup`,e=>this._keyUp(e),!0)),this._register(F(this.textarea,`keydown`,e=>this._keyDown(e),!0)),this._register(F(this.textarea,`keypress`,e=>this._keyPress(e),!0)),this._register(F(this.textarea,`compositionstart`,()=>this._compositionHelper.compositionstart())),this._register(F(this.textarea,`compositionupdate`,e=>this._compositionHelper.compositionupdate(e))),this._register(F(this.textarea,`compositionend`,()=>this._compositionHelper.compositionend())),this._register(F(this.textarea,`input`,e=>this._inputEvent(e),!0)),this._register(this.onRender(()=>this._compositionHelper.updateCompositionElements()))}open(e){if(!e)throw Error(`Terminal requires a parent element.`);if(e.isConnected||this._logService.debug(`Terminal.open was called on an element that was not attached to the DOM`),this.element?.ownerDocument.defaultView&&this._coreBrowserService){this.element.ownerDocument.defaultView!==this._coreBrowserService.window&&(this._coreBrowserService.window=this.element.ownerDocument.defaultView);return}this._document=e.ownerDocument,this.options.documentOverride&&this.options.documentOverride instanceof Document&&(this._document=this.optionsService.rawOptions.documentOverride),this.element=this._document.createElement(`div`),this.element.dir=`ltr`,this.element.classList.add(`terminal`),this.element.classList.add(`xterm`),e.appendChild(this.element);let t=this._document.createDocumentFragment();this._viewportElement=this._document.createElement(`div`),this._viewportElement.classList.add(`xterm-viewport`),t.appendChild(this._viewportElement),this.screenElement=this._document.createElement(`div`),this.screenElement.classList.add(`xterm-screen`),this._register(F(this.screenElement,`mousemove`,e=>this.updateCursorStyle(e))),this._helperContainer=this._document.createElement(`div`),this._helperContainer.classList.add(`xterm-helpers`),this.screenElement.appendChild(this._helperContainer),t.appendChild(this.screenElement);let n=this.textarea=this._document.createElement(`textarea`);this.textarea.classList.add(`xterm-helper-textarea`),this.textarea.setAttribute(`aria-label`,x.get()),aa||this.textarea.setAttribute(`aria-multiline`,`false`),this.textarea.setAttribute(`autocorrect`,`off`),this.textarea.setAttribute(`autocapitalize`,`off`),this.textarea.setAttribute(`spellcheck`,`false`),this.textarea.tabIndex=0,this._register(this.optionsService.onSpecificOptionChange(`disableStdin`,()=>n.readOnly=this.optionsService.rawOptions.disableStdin)),this.textarea.readOnly=this.optionsService.rawOptions.disableStdin,this._coreBrowserService=this._register(this._instantiationService.createInstance(zi,this.textarea,e.ownerDocument.defaultView??window,this._document??typeof window<`u`?window.document:null)),this._instantiationService.setService(je,this._coreBrowserService),this._register(F(this.textarea,`focus`,e=>this._handleTextAreaFocus(e))),this._register(F(this.textarea,`blur`,()=>this._handleTextAreaBlur())),this._helperContainer.appendChild(this.textarea),this._charSizeService=this._instantiationService.createInstance(Fi,this._document,this._helperContainer),this._instantiationService.setService(Ae,this._charSizeService),this._themeService=this._instantiationService.createInstance(Ha),this._instantiationService.setService(Ie,this._themeService),this._characterJoinerService=this._instantiationService.createInstance(gi),this._instantiationService.setService(Fe,this._characterJoinerService),this._renderService=this._register(this._instantiationService.createInstance(da,this.rows,this.screenElement)),this._instantiationService.setService(Ne,this._renderService),this._register(this._renderService.onRenderedViewportChange(e=>this._onRender.fire(e))),this.onResize(e=>this._renderService.resize(e.cols,e.rows)),this._compositionView=this._document.createElement(`div`),this._compositionView.classList.add(`composition-view`),this._compositionHelper=this._instantiationService.createInstance(ui,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this._mouseService=this._instantiationService.createInstance(Wi),this._instantiationService.setService(Me,this._mouseService);let r=this._linkifier.value=this._register(this._instantiationService.createInstance(Es,this.screenElement));this.element.appendChild(t);try{this._onWillOpen.fire(this.element)}catch{}this._renderService.hasRenderer()||this._renderService.setRenderer(this._createRenderer()),this._register(this.onCursorMove(()=>{this._renderService.handleCursorMove(),this._syncTextArea()})),this._register(this.onResize(()=>this._renderService.handleResize(this.cols,this.rows))),this._register(this.onBlur(()=>this._renderService.handleBlur())),this._register(this.onFocus(()=>this._renderService.handleFocus())),this._viewport=this._register(this._instantiationService.createInstance(ti,this.element,this.screenElement)),this._register(this._viewport.onRequestScrollLines(e=>{super.scrollLines(e,!1),this.refresh(0,this.rows-1)})),this._selectionService=this._register(this._instantiationService.createInstance(Na,this.element,this.screenElement,r)),this._instantiationService.setService(Pe,this._selectionService),this._register(this._selectionService.onRequestScrollLines(e=>this.scrollLines(e.amount,e.suppressScrollEvent))),this._register(this._selectionService.onSelectionChange(()=>this._onSelectionChange.fire())),this._register(this._selectionService.onRequestRedraw(e=>this._renderService.handleSelectionChanged(e.start,e.end,e.columnSelectMode))),this._register(this._selectionService.onLinuxMouseSelection(e=>{this.textarea.value=e,this.textarea.focus(),this.textarea.select()})),this._register(N.any(this._onScroll.event,this._inputHandler.onScroll)(()=>{this._selectionService.refresh(),this._viewport?.queueSync()})),this._register(this._instantiationService.createInstance(ni,this.screenElement)),this._register(F(this.element,`mousedown`,e=>this._selectionService.handleMouseDown(e))),this.coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add(`enable-mouse-events`)):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager.value=this._instantiationService.createInstance(Ts,this)),this._register(this.optionsService.onSpecificOptionChange(`screenReaderMode`,e=>this._handleScreenReaderModeOptionChange(e))),this.options.overviewRuler.width&&(this._overviewRulerRenderer=this._register(this._instantiationService.createInstance(si,this._viewportElement,this.screenElement))),this.optionsService.onSpecificOptionChange(`overviewRuler`,e=>{!this._overviewRulerRenderer&&e&&this._viewportElement&&this.screenElement&&(this._overviewRulerRenderer=this._register(this._instantiationService.createInstance(si,this._viewportElement,this.screenElement)))}),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()}_createRenderer(){return this._instantiationService.createInstance(Pi,this,this._document,this.element,this.screenElement,this._viewportElement,this._helperContainer,this.linkifier)}bindMouse(){let e=this,t=this.element;function n(t){let n=e._mouseService.getMouseReportCoords(t,e.screenElement);if(!n)return!1;let r,i;switch(t.overrideType||t.type){case`mousemove`:i=32,t.buttons===void 0?(r=3,t.button!==void 0&&(r=t.button<3?t.button:3)):r=t.buttons&1?0:t.buttons&4?1:t.buttons&2?2:3;break;case`mouseup`:i=0,r=t.button<3?t.button:3;break;case`mousedown`:i=1,r=t.button<3?t.button:3;break;case`wheel`:if(e._customWheelEventHandler&&e._customWheelEventHandler(t)===!1)return!1;let n=t.deltaY;if(n===0||e.coreMouseService.consumeWheelEvent(t,e._renderService?.dimensions?.device?.cell?.height,e._coreBrowserService?.dpr)===0)return!1;i=n<0?0:1,r=4;break;default:return!1}return i===void 0||r===void 0||r>4?!1:e.coreMouseService.triggerMouseEvent({col:n.col,row:n.row,x:n.x,y:n.y,button:r,action:i,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey})}let r={mouseup:null,wheel:null,mousedrag:null,mousemove:null},i={mouseup:e=>(n(e),e.buttons||(this._document.removeEventListener(`mouseup`,r.mouseup),r.mousedrag&&this._document.removeEventListener(`mousemove`,r.mousedrag)),this.cancel(e)),wheel:e=>(n(e),this.cancel(e,!0)),mousedrag:e=>{e.buttons&&n(e)},mousemove:e=>{e.buttons||n(e)}};this._register(this.coreMouseService.onProtocolChange(e=>{e?(this.optionsService.rawOptions.logLevel===`debug`&&this._logService.debug(`Binding to mouse events:`,this.coreMouseService.explainEvents(e)),this.element.classList.add(`enable-mouse-events`),this._selectionService.disable()):(this._logService.debug(`Unbinding from mouse events.`),this.element.classList.remove(`enable-mouse-events`),this._selectionService.enable()),e&8?r.mousemove||=(t.addEventListener(`mousemove`,i.mousemove),i.mousemove):(t.removeEventListener(`mousemove`,r.mousemove),r.mousemove=null),e&16?r.wheel||=(t.addEventListener(`wheel`,i.wheel,{passive:!1}),i.wheel):(t.removeEventListener(`wheel`,r.wheel),r.wheel=null),e&2?r.mouseup||=i.mouseup:(this._document.removeEventListener(`mouseup`,r.mouseup),r.mouseup=null),e&4?r.mousedrag||=i.mousedrag:(this._document.removeEventListener(`mousemove`,r.mousedrag),r.mousedrag=null)})),this.coreMouseService.activeProtocol=this.coreMouseService.activeProtocol,this._register(F(t,`mousedown`,e=>{if(e.preventDefault(),this.focus(),!(!this.coreMouseService.areMouseEventsActive||this._selectionService.shouldForceSelection(e)))return n(e),r.mouseup&&this._document.addEventListener(`mouseup`,r.mouseup),r.mousedrag&&this._document.addEventListener(`mousemove`,r.mousedrag),this.cancel(e)})),this._register(F(t,`wheel`,t=>{if(!r.wheel){if(this._customWheelEventHandler&&this._customWheelEventHandler(t)===!1)return!1;if(!this.buffer.hasScrollback){if(t.deltaY===0)return!1;if(e.coreMouseService.consumeWheelEvent(t,e._renderService?.dimensions?.device?.cell?.height,e._coreBrowserService?.dpr)===0)return this.cancel(t,!0);let n=L.ESC+(this.coreService.decPrivateModes.applicationCursorKeys?`O`:`[`)+(t.deltaY<0?`A`:`B`);return this.coreService.triggerDataEvent(n,!0),this.cancel(t,!0)}}},{passive:!1}))}refresh(e,t){this._renderService?.refreshRows(e,t)}updateCursorStyle(e){this._selectionService?.shouldColumnSelect(e)?this.element.classList.add(`column-select`):this.element.classList.remove(`column-select`)}_showCursor(){this.coreService.isCursorInitialized||(this.coreService.isCursorInitialized=!0,this.refresh(this.buffer.y,this.buffer.y))}scrollLines(e,t){this._viewport?this._viewport.scrollLines(e):super.scrollLines(e,t),this.refresh(0,this.rows-1)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){e&&this._viewport?this._viewport.scrollToLine(this.buffer.ybase,!0):this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}paste(e){ae(e,this.textarea,this.coreService,this.optionsService)}attachCustomKeyEventHandler(e){this._customKeyEventHandler=e}attachCustomWheelEventHandler(e){this._customWheelEventHandler=e}registerLinkProvider(e){return this._linkProviderService.registerLinkProvider(e)}registerCharacterJoiner(e){if(!this._characterJoinerService)throw Error(`Terminal must be opened first`);let t=this._characterJoinerService.register(e);return this.refresh(0,this.rows-1),t}deregisterCharacterJoiner(e){if(!this._characterJoinerService)throw Error(`Terminal must be opened first`);this._characterJoinerService.deregister(e)&&this.refresh(0,this.rows-1)}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}registerDecoration(e){return this._decorationService.registerDecoration(e)}hasSelection(){return this._selectionService?this._selectionService.hasSelection:!1}select(e,t,n){this._selectionService.setSelection(e,t,n)}getSelection(){return this._selectionService?this._selectionService.selectionText:``}getSelectionPosition(){if(!(!this._selectionService||!this._selectionService.hasSelection))return{start:{x:this._selectionService.selectionStart[0],y:this._selectionService.selectionStart[1]},end:{x:this._selectionService.selectionEnd[0],y:this._selectionService.selectionEnd[1]}}}clearSelection(){this._selectionService?.clearSelection()}selectAll(){this._selectionService?.selectAll()}selectLines(e,t){this._selectionService?.selectLines(e,t)}_keyDown(e){if(this._keyDownHandled=!1,this._keyDownSeen=!0,this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)return!1;let t=this.browser.isMac&&this.options.macOptionIsMeta&&e.altKey;if(!t&&!this._compositionHelper.keydown(e))return this.options.scrollOnUserInput&&this.buffer.ybase!==this.buffer.ydisp&&this.scrollToBottom(!0),!1;!t&&(e.key===`Dead`||e.key===`AltGraph`)&&(this._unprocessedDeadKey=!0);let n=gs(e,this.coreService.decPrivateModes.applicationCursorKeys,this.browser.isMac,this.options.macOptionIsMeta);if(this.updateCursorStyle(e),n.type===3||n.type===2){let t=this.rows-1;return this.scrollLines(n.type===2?-t:t),this.cancel(e,!0)}if(n.type===1&&this.selectAll(),this._isThirdLevelShift(this.browser,e)||(n.cancel&&this.cancel(e,!0),!n.key)||e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.key.length===1&&e.key.charCodeAt(0)>=65&&e.key.charCodeAt(0)<=90)return!0;if(this._unprocessedDeadKey)return this._unprocessedDeadKey=!1,!0;if((n.key===L.ETX||n.key===L.CR)&&(this.textarea.value=``),this._onKey.fire({key:n.key,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(n.key,!0),!this.optionsService.rawOptions.screenReaderMode||e.altKey||e.ctrlKey)return this.cancel(e,!0);this._keyDownHandled=!0}_isThirdLevelShift(e,t){let n=e.isMac&&!this.options.macOptionIsMeta&&t.altKey&&!t.ctrlKey&&!t.metaKey||e.isWindows&&t.altKey&&t.ctrlKey&&!t.metaKey||e.isWindows&&t.getModifierState(`AltGraph`);return t.type===`keypress`?n:n&&(!t.keyCode||t.keyCode>47)}_keyUp(e){this._keyDownSeen=!1,!(this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)&&(ks(e)||this.focus(),this.updateCursorStyle(e),this._keyPressHandled=!1)}_keyPress(e){let t;if(this._keyPressHandled=!1,this._keyDownHandled||this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(e.which===null||e.which===void 0)t=e.keyCode;else if(e.which!==0&&e.charCode!==0)t=e.which;else return!1;return!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)?!1:(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(t,!0),this._keyPressHandled=!0,this._unprocessedDeadKey=!1,!0)}_inputEvent(e){if(e.data&&e.inputType===`insertText`&&(!e.composed||!this._keyDownSeen)&&!this.optionsService.rawOptions.screenReaderMode){if(this._keyPressHandled)return!1;this._unprocessedDeadKey=!1;let t=e.data;return this.coreService.triggerDataEvent(t,!0),this.cancel(e),!0}return!1}resize(e,t){if(e===this.cols&&t===this.rows){this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure();return}super.resize(e,t)}_afterResize(e,t){this._charSizeService?.measure()}clear(){if(!(this.buffer.ybase===0&&this.buffer.y===0)){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){let n={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(n),t.dispose=()=>this._wrappedAddonDispose(n),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let n=0;n=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new E)}translateToString(e,t,n){return this._line.translateToString(e,t,n)}},Ms=class{constructor(e,t){this._buffer=e,this.type=t}init(e){return this._buffer=e,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(e){let t=this._buffer.lines.get(e);if(t)return new js(t)}getNullCell(){return new E}},Ns=class extends j{constructor(e){super(),this._core=e,this._onBufferChange=this._register(new P),this.onBufferChange=this._onBufferChange.event,this._normal=new Ms(this._core.buffers.normal,`normal`),this._alternate=new Ms(this._core.buffers.alt,`alternate`),this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw Error(`Active buffer is neither normal nor alternate`)}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}},Ps=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,e=>t(e.toArray()))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,(e,n)=>t(e,n.toArray()))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}},Fs=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}},Is=[`cols`,`rows`],Ls=0,Rs=class extends j{constructor(e){super(),this._core=this._register(new Os(e)),this._addonManager=this._register(new As),this._publicOptions={...this._core.options};let t=e=>this._core.options[e],n=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(let e in this._core.options){let r={get:t.bind(this,e),set:n.bind(this,e)};Object.defineProperty(this._publicOptions,e,r)}}_checkReadonlyOptions(e){if(Is.includes(e))throw Error(`Option "${e}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.rawOptions.allowProposedApi)throw Error(`You must set the allowProposedApi option to true to use proposed API`)}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onKey(){return this._core.onKey}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onSelectionChange(){return this._core.onSelectionChange}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get element(){return this._core.element}get parser(){return this._parser||=new Ps(this._core),this._parser}get unicode(){return this._checkProposedApi(),new Fs(this._core)}get textarea(){return this._core.textarea}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer||=this._register(new Ns(this._core)),this._buffer}get markers(){return this._checkProposedApi(),this._core.markers}get modes(){let e=this._core.coreService.decPrivateModes,t=`none`;switch(this._core.coreMouseService.activeProtocol){case`X10`:t=`x10`;break;case`VT200`:t=`vt200`;break;case`DRAG`:t=`drag`;break;case`ANY`:t=`any`;break}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,synchronizedOutputMode:e.synchronizedOutput,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(let t in e)this._publicOptions[t]=e[t]}blur(){this._core.blur()}focus(){this._core.focus()}input(e,t=!0){this._core.input(e,t)}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}open(e){this._core.open(e)}attachCustomKeyEventHandler(e){this._core.attachCustomKeyEventHandler(e)}attachCustomWheelEventHandler(e){this._core.attachCustomWheelEventHandler(e)}registerLinkProvider(e){return this._core.registerLinkProvider(e)}registerCharacterJoiner(e){return this._checkProposedApi(),this._core.registerCharacterJoiner(e)}deregisterCharacterJoiner(e){this._checkProposedApi(),this._core.deregisterCharacterJoiner(e)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}registerDecoration(e){return this._checkProposedApi(),this._verifyPositiveIntegers(e.x??0,e.width??0,e.height??0),this._core.registerDecoration(e)}hasSelection(){return this._core.hasSelection()}select(e,t,n){this._verifyIntegers(e,t,n),this._core.select(e,t,n)}getSelection(){return this._core.getSelection()}getSelectionPosition(){return this._core.getSelectionPosition()}clearSelection(){this._core.clearSelection()}selectAll(){this._core.selectAll()}selectLines(e,t){this._verifyIntegers(e,t),this._core.selectLines(e,t)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write(`\r -`,t)}paste(e){this._core.paste(e)}refresh(e,t){this._verifyIntegers(e,t),this._core.refresh(e,t)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(e){this._addonManager.loadAddon(this,e)}static get strings(){return{get promptLabel(){return x.get()},set promptLabel(e){x.set(e)},get tooMuchOutput(){return S.get()},set tooMuchOutput(e){S.set(e)}}}_verifyIntegers(...e){for(Ls of e)if(Ls===1/0||isNaN(Ls)||Ls%1!=0)throw Error(`This API only accepts integers`)}_verifyPositiveIntegers(...e){for(Ls of e)if(Ls&&(Ls===1/0||isNaN(Ls)||Ls%1!=0||Ls<0))throw Error(`This API only accepts positive integers`)}},zs=2,Bs=1,Vs=class{activate(e){this._terminal=e}dispose(){}fit(){let e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;let t=this._terminal._core;(this._terminal.rows!==e.rows||this._terminal.cols!==e.cols)&&(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;let e=this._terminal._core._renderService.dimensions;if(e.css.cell.width===0||e.css.cell.height===0)return;let t=this._terminal.options.scrollback===0?0:this._terminal.options.overviewRuler?.width||14,n=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(n.getPropertyValue(`height`)),i=Math.max(0,parseInt(n.getPropertyValue(`width`))),a=window.getComputedStyle(this._terminal.element),o={top:parseInt(a.getPropertyValue(`padding-top`)),bottom:parseInt(a.getPropertyValue(`padding-bottom`)),right:parseInt(a.getPropertyValue(`padding-right`)),left:parseInt(a.getPropertyValue(`padding-left`))},s=o.top+o.bottom,c=o.right+o.left,l=r-s,u=i-c-t;return{cols:Math.max(zs,Math.floor(u/e.css.cell.width)),rows:Math.max(Bs,Math.floor(l/e.css.cell.height))}}};function Hs(){return{fontSize:typeof window<`u`&&typeof window.matchMedia==`function`&&window.matchMedia(`(pointer: coarse)`).matches?13:12,fontFamily:getComputedStyle(document.body).fontFamily}}function Us({panes:e,size:t,zoomed:n,socketRef:r,viewsRef:i,bodyRefs:a,pendingRef:o,setTitles:s}){(0,l.useEffect)(()=>{for(let t of e){if(i.current.has(t))continue;let e=a.current.get(t);if(!e||e.clientHeight===0||e.clientWidth===0)continue;let n=new Rs({...Hs(),theme:{background:`#0b0b0d`,foreground:`#e6e6ec`},cursorBlink:!0}),c=new Vs;n.loadAddon(c),n.onData(e=>r.current?.send(JSON.stringify({type:`input`,pane:t,data:e}))),n.onTitleChange(e=>{let n=e.replace(/\s+/g,` `).trim();n&&s(e=>({...e,[t]:n}))}),n.open(e),i.current.set(t,{term:n,fit:c});let l=o.current.get(t);if(l){for(let e of l)n.write(e);o.current.delete(t)}}for(let[t,n]of i.current)e.includes(t)||(n.term.dispose(),i.current.delete(t))},[e,t,n])}var Ws=60;function Gs({panes:e,size:t,zoomed:n,socketRef:r,viewsRef:i,bodyRefs:a,sentSizesRef:o,ownsSize:s}){let c=(0,l.useRef)(null),u=(0,l.useCallback)(()=>{for(let[e,t]of i.current){let n=a.current.get(e);if(!n||n.clientHeight===0||n.clientWidth===0)continue;let{rows:i,cols:s}=t.term,c=o.current.get(e);c&&c.rows===i&&c.cols===s||(o.current.set(e,{rows:i,cols:s}),r.current?.send(JSON.stringify({type:`resize`,pane:e,rows:i,cols:s})))}},[r,i,a,o]);(0,l.useEffect)(()=>{for(let[e,t]of i.current){let n=a.current.get(e);if(!n||n.clientHeight===0||n.clientWidth===0)continue;if(s){t.fit.fit();continue}let r=o.current.get(e);r&&t.term.resize(r.cols,r.rows)}if(s)return c.current&&clearTimeout(c.current),c.current=setTimeout(u,Ws),()=>{c.current&&clearTimeout(c.current)}},[e,n,t,u,i,a,o,s])}function Ks({pending:e,size:t,socketRef:n,slotRefs:r,panesExist:i,onAnswered:a}){(0,l.useEffect)(()=>{if(e===null)return;let t=n.current;if(!t||t.readyState!==WebSocket.OPEN)return;let o=[];for(let n=0;n{let t=new Rs(Hs()),n=new Vs;t.loadAddon(n),t.open(e);let r=n.proposeDimensions();if(t.dispose(),!r)throw Error(`could not measure the cell`);return{rows:r.rows,cols:r.cols}})}catch{s=[]}t.send(JSON.stringify({type:`start`,sizes:s})),a()},[e,t,n,r,i,a])}var $=e();function qs({pane:e,index:t,label:r,cellStyle:a,isActive:o,isZoomed:s,showZoom:c,isDragged:l,isDropTarget:u,reorderable:d,onFocus:f,onToggleZoom:p,onClose:h,onPaneDragStart:g,onPaneDragMove:_,onPaneDragEnd:v,onPaneDragCancel:ee,bodyRef:y}){return(0,$.jsxs)(`div`,{"data-pane-id":e,onMouseDown:f,style:a,className:`min-h-0 min-w-0 flex-col overflow-hidden rounded-sm border ${u?`border-accent ring-1 ring-accent`:o?`border-accent`:`border-ink-700`} ${l?`opacity-60`:``}`,children:[(0,$.jsxs)(`div`,{onPointerDown:g,onPointerMove:_,onPointerUp:v,onPointerCancel:ee,className:`flex shrink-0 items-center gap-1 select-none bg-ink-900 px-2 py-0.5 text-xs ${d?l?`cursor-grabbing touch-none`:`cursor-grab touch-none`:``}`,children:[(0,$.jsx)(`span`,{title:r,className:`min-w-0 flex-1 truncate ${o?`text-ink-50`:`text-ink-400`}`,children:m(r,20)}),c&&(0,$.jsx)(`button`,{onMouseDown:e=>e.stopPropagation(),onClick:p,"aria-pressed":s,title:s?`Restore the grid`:`Zoom this terminal`,"aria-label":s?`Restore the grid`:`Zoom this terminal`,className:`flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-accent md:h-6 md:w-6`,children:(0,$.jsx)(i,{maximized:s})}),(0,$.jsx)(`button`,{onMouseDown:e=>e.stopPropagation(),onClick:h,title:`Close terminal`,"aria-label":`close terminal ${t+1}`,className:`flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-removed md:h-6 md:w-6`,children:(0,$.jsx)(n,{})})]}),(0,$.jsx)(`div`,{ref:y,className:`min-h-0 flex-1`})]})}function Js({count:e,cells:t,slotRefs:n}){return Array.from({length:e},(e,r)=>{let i=t[r];return(0,$.jsx)(qs,{pane:-1-r,index:r,label:`starting…`,cellStyle:{display:`flex`,gridColumn:`${i.colStart} / span ${i.colSpan}`,gridRow:`${i.row}`},isActive:!1,isZoomed:!1,showZoom:!1,isDragged:!1,isDropTarget:!1,reorderable:!1,onFocus:()=>{},onToggleZoom:()=>{},onClose:()=>{},onPaneDragStart:()=>{},onPaneDragMove:()=>{},onPaneDragEnd:()=>{},onPaneDragCancel:()=>{},bodyRef:e=>{e?n.current.set(r,e):n.current.delete(r)}},`slot-${r}`)})}var Ys={esc:`\x1B`,tab:` `,"shift-tab":`\x1B[Z`,"ctrl-c":``,"ctrl-d":``,"ctrl-z":``,"ctrl-l":`\f`,"ctrl-r":``,up:`\x1B[A`,down:`\x1B[B`,right:`\x1B[C`,left:`\x1B[D`},Xs={up:`\x1BOA`,down:`\x1BOB`,right:`\x1BOC`,left:`\x1BOD`};function Zs(e,t=!1){if(t){let t=Xs[e];if(t)return t}return Ys[e]}var Qs=[{key:`esc`,label:`Esc`,aria:`Escape`},{key:`tab`,label:`Tab`,aria:`Tab`},{key:`shift-tab`,label:`⇧Tab`,aria:`Shift Tab`},{key:`ctrl-c`,label:`^C`,aria:`Control C`},{key:`ctrl-d`,label:`^D`,aria:`Control D`},{key:`ctrl-z`,label:`^Z`,aria:`Control Z`},{key:`ctrl-l`,label:`^L`,aria:`Control L`},{key:`ctrl-r`,label:`^R`,aria:`Control R`},{key:`left`,label:`←`,aria:`Left arrow`},{key:`down`,label:`↓`,aria:`Down arrow`},{key:`up`,label:`↑`,aria:`Up arrow`},{key:`right`,label:`→`,aria:`Right arrow`}];function $s({onKey:e}){return(0,$.jsx)(`div`,{className:`flex shrink-0 items-stretch gap-1 overflow-x-auto border-t border-ink-700 bg-ink-900 px-1 py-1 md:hidden`,children:Qs.map(({key:t,label:n,aria:r})=>(0,$.jsx)(`button`,{onPointerDown:e=>e.preventDefault(),onClick:()=>e(t),"aria-label":r,className:`flex min-h-9 min-w-9 shrink-0 items-center justify-center rounded-sm border border-ink-700 bg-ink-850 px-2 text-xs text-ink-200 active:bg-ink-700 active:text-accent`,children:n},t))})}function ec({showDivider:e,draggingUpper:t,onUpperDragStart:n,onUpperDragMove:r,onUpperDragEnd:i,onUpperDragCancel:a}){return e?(0,$.jsx)(`div`,{role:`separator`,"aria-orientation":`horizontal`,"aria-label":`Resize the terminal panel (double-click to reset)`,title:`Drag to resize · double-click to reset`,onPointerDown:n,onPointerMove:r,onPointerUp:i,onPointerCancel:a,onLostPointerCapture:i,className:`absolute -top-px left-0 z-10 hidden h-1.5 w-full cursor-row-resize touch-none md:block ${t?`bg-accent`:`hover:bg-accent`}`}):null}function tc({ownsSize:e,maximized:t,onClaimSize:n,onCreate:r,onToggleMaximized:a}){let s=`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 text-ink-400 hover:text-accent`;return(0,$.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 bg-ink-900 px-2 py-1`,children:[!e&&(0,$.jsx)(`button`,{onClick:n,title:`These panes are sized for another client. Resize them to fit this screen.`,"aria-label":`Fit the panes to this screen`,className:`ml-auto ${s}`,children:(0,$.jsx)(c,{})}),(0,$.jsx)(`button`,{onClick:r,title:`New terminal`,"aria-label":`New terminal`,className:`${s} ${e?`ml-auto`:``}`,children:(0,$.jsx)(o,{})}),(0,$.jsx)(`button`,{onClick:a,"aria-pressed":t,title:t?`Restore panel height`:`Maximize the panel`,"aria-label":t?`Restore panel height`:`Maximize the panel`,className:`hidden md:flex ${s}`,children:(0,$.jsx)(i,{maximized:t})})]})}function nc({repo:e,maximized:t,onToggleMaximized:n,className:r=``,sectionRef:i,...a}){let o=(0,l.useRef)(null),s=(0,l.useRef)(null),c=(0,l.useRef)(new Map),u=(0,l.useRef)(new Map),d=(0,l.useRef)(new Map),p=(0,l.useRef)(new Map),m=(0,l.useRef)(new Map),_=(0,l.useRef)(0),v=(0,l.useRef)(new Map),[ee,y]=(0,l.useState)(null),[b,te]=(0,l.useState)([]),[x,ne]=(0,l.useState)(null),[S,re]=(0,l.useState)(null),[C,ie]=(0,l.useState)({w:0,h:0}),[w,ae]=(0,l.useState)({}),[oe,se]=(0,l.useState)(!0);g({repo:e,socketRef:s,viewsRef:c,pendingRef:p,sentSizesRef:d,lastActiveByRepoRef:m,expectCreateRef:_,setPending:y,setPanes:te,setActive:ne,setZoomed:re,setTitles:ae,setOwnsSize:se}),Us({panes:b,size:C,zoomed:S,socketRef:s,viewsRef:c,bodyRefs:u,pendingRef:p,setTitles:ae});let T=(0,l.useCallback)(()=>y(null),[]);Ks({pending:ee,size:C,socketRef:s,slotRefs:v,panesExist:b.length>0,onAnswered:T}),Gs({panes:b,size:C,zoomed:S,socketRef:s,viewsRef:c,bodyRefs:u,sentSizesRef:d,ownsSize:oe}),(0,l.useEffect)(()=>{let e=o.current;if(!e)return;let t=new ResizeObserver(()=>{let t=e.clientWidth,n=e.clientHeight;ie(e=>e.w===t&&e.h===n?e:{w:t,h:n})});return t.observe(e),()=>t.disconnect()},[]),(0,l.useEffect)(()=>{if(x===null&&b.length>0){let t=m.current.get(e);ne(t!==void 0&&b.includes(t)?t:b[b.length-1])}},[x,b,e]),(0,l.useEffect)(()=>{x!==null&&c.current.get(x)?.term.focus()},[x]);let ce=t=>{ne(t),m.current.set(e,t)},le=()=>{let e=s.current;e&&(re(null),_.current+=1,e.send(JSON.stringify({type:`create`,rows:24,cols:80})))},ue=()=>{s.current?.send(JSON.stringify({type:`claim_size`}))},de=e=>{s.current?.send(JSON.stringify({type:`close`,pane:e}))},fe=e=>{if(x===null)return;let t=c.current.get(x)?.term.modes.applicationCursorKeysMode??!1;s.current?.send(JSON.stringify({type:`input`,pane:x,data:Zs(e,t)}))},{draggingPane:pe,dragOverPane:me,reorderable:E,endPaneDrag:he,onPaneDragStart:ge,onPaneDragMove:_e,onPaneDragEnd:ve}=h({panes:b,zoomed:S,onFocus:ce,onReorder:e=>s.current?.send(JSON.stringify({type:`reorder`,order:e}))}),D=f(b.length>0?b.length:ee??0,C.w>=C.h);return(0,$.jsxs)(`section`,{ref:i,className:`relative flex min-h-0 min-w-0 flex-col border-t border-ink-700 ${r}`,children:[(0,$.jsx)(ec,{...a}),(0,$.jsx)(tc,{ownsSize:oe,maximized:t,onClaimSize:ue,onCreate:le,onToggleMaximized:n}),(0,$.jsxs)(`div`,{className:`relative min-h-0 flex-1 overflow-hidden bg-ink-950 p-1`,children:[b.length===0&&ee===null&&(0,$.jsxs)(`p`,{className:`p-3 text-ink-400`,children:[`No terminal open. Press `,(0,$.jsx)(`span`,{className:`text-accent`,children:`+`}),` above to start one.`]}),(0,$.jsxs)(`div`,{ref:o,className:`grid h-full gap-1`,style:S===null?{gridTemplateColumns:`repeat(${D.cols}, minmax(0, 1fr))`,gridTemplateRows:`repeat(${D.rows}, minmax(0, 1fr))`}:{gridTemplateColumns:`1fr`,gridTemplateRows:`1fr`},children:[b.length===0&&ee!==null&&(0,$.jsx)(Js,{count:ee,cells:D.cells,slotRefs:v}),b.map((e,t)=>{let n=w[e]??`term ${t+1}`,r=D.cells[t];return(0,$.jsx)(qs,{pane:e,index:t,label:n,cellStyle:S===null?{display:`flex`,gridColumn:`${r.colStart} / span ${r.colSpan}`,gridRow:`${r.row}`}:{display:e===S?`flex`:`none`},isActive:e===x,isZoomed:S===e,showZoom:b.length>1,isDragged:pe===e,isDropTarget:me===e,reorderable:E,onFocus:()=>ce(e),onToggleZoom:()=>re(t=>t===e?null:e),onClose:()=>de(e),onPaneDragStart:t=>ge(t,e),onPaneDragMove:_e,onPaneDragEnd:ve,onPaneDragCancel:he,bodyRef:t=>{t?u.current.set(e,t):u.current.delete(e)}},e)})]})]}),b.length>0&&(0,$.jsx)($s,{onKey:fe})]})}export{nc as TerminalPanel}; \ No newline at end of file diff --git a/viewer-ui/dist/assets/Terminal-DRQzLrr0.js b/viewer-ui/dist/assets/Terminal-DRQzLrr0.js new file mode 100644 index 00000000..719724f6 --- /dev/null +++ b/viewer-ui/dist/assets/Terminal-DRQzLrr0.js @@ -0,0 +1,35 @@ +import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c}from"./index-BN9ccC5R.js";var l=r();function u(e,t){for(;t;)[e,t]=[t,e%t];return e}function d(e,t){switch(e){case 1:return[1];case 2:return t?[2]:[1,1];case 3:return[2,1];case 4:return[2,2];case 5:return[3,2];case 6:return[3,3];case 7:return[4,3];default:return[4,4]}}function f(e,t){let n=d(e,t),r=n.reduce((e,t)=>e*t/u(e,t),1),i=[];return n.forEach((e,t)=>{let n=r/e;for(let r=0;r=4352&&e<=4447||e>=11904&&e<=12350||e>=12353&&e<=13311||e>=13312&&e<=19903||e>=19968&&e<=40959||e>=40960&&e<=42191||e>=44032&&e<=55203||e>=63744&&e<=64255||e>=65072&&e<=65103||e>=65280&&e<=65376||e>=65504&&e<=65510||e>=127744&&e<=129791||e>=131072&&e<=262141}function m(e,t){let n=0;for(let t of e)n+=p(t.codePointAt(0)??0)?2:1;if(n<=t)return e;let r=0,i=``;for(let n of e){let e=p(n.codePointAt(0)??0)?2:1;if(r+e>t-1)break;i+=n,r+=e}return`${i}…`}function h({panes:e,zoomed:t,onFocus:n,onReorder:r}){let i=(0,l.useRef)(null),a=(0,l.useRef)(null),o=(0,l.useRef)(null),c=(0,l.useRef)(!1),[u,d]=(0,l.useState)(null),[f,p]=(0,l.useState)(null),m=t===null&&e.length>1,h=()=>{i.current=null,a.current=null,o.current=null,c.current=!1,d(null),p(null)};return{draggingPane:u,dragOverPane:f,reorderable:m,endPaneDrag:h,onPaneDragStart:(e,t)=>{e.target.closest(`button`)||(n(t),!(e.button!==0||!m)&&(i.current=t,a.current={x:e.clientX,y:e.clientY},c.current=!1,e.currentTarget.setPointerCapture(e.pointerId)))},onPaneDragMove:e=>{let t=i.current,n=a.current;if(t===null||n===null||!c.current&&Math.hypot(e.clientX-n.x,e.clientY-n.y)<4)return;c.current=!0,d(t);let r=document.elementFromPoint(e.clientX,e.clientY)?.closest(`[data-pane-id]`),s=r?Number(r.getAttribute(`data-pane-id`)):null,l=s!==null&&s!==t?s:null;o.current=l,p(l)},onPaneDragEnd:()=>{let t=i.current,n=o.current;t!==null&&c.current&&n!==null&&r(s(e,t,n)),h()}}}function g(e,t){return t.state===`cancelled`?_(e,t.pane):{...e,[t.pane]:{state:t.state,detail:t.detail,deadlineEpoch:t.deadline_epoch,attempt:t.attempt}}}function _(e,t){if(!(t in e))return e;let n={...e};return delete n[t],n}function v(e,t){return Object.keys(e).map(Number).filter(e=>!t.includes(e)).sort((e,t)=>e-t)}function ee(e){if(e===void 0||!Number.isFinite(e))return;let t=new Date(e*1e3);if(!Number.isNaN(t.getTime()))return`${String(t.getHours()).padStart(2,`0`)}:${String(t.getMinutes()).padStart(2,`0`)}`}function y(e){let t=ee(e.deadlineEpoch),n=[e.state];return t&&n.push(`until ${t}`),e.attempt>0&&n.push(`attempt ${e.attempt}`),n.join(` · `)}function te(e){return JSON.stringify({type:`cancel_recovery`,pane:e})}function b(e,t){e?.send(te(t))}function ne({repo:e,socketRef:n,viewsRef:r,pendingRef:i,sentSizesRef:o,lastActiveByRepoRef:s,expectCreateRef:c,setPending:u,setPanes:d,setActive:f,setZoomed:p,setTitles:m,setOwnsSize:h,setRecovery:_}){(0,l.useLayoutEffect)(()=>{c.current=0;let l=!1,v,ee=()=>{r.current.forEach(e=>e.term.dispose()),r.current.clear(),i.current.clear(),o.current.clear()},y=()=>{u(null),d([]),f(null),p(null),m({}),h(!0),_({}),ee();let te=location.protocol===`https:`?`wss:`:`ws:`,b=new WebSocket(`${te}//${location.host}/ws/term?repo=${encodeURIComponent(e)}`);b.binaryType=`arraybuffer`,n.current=b,b.onmessage=l=>{if(n.current!==b)return;if(typeof l.data==`string`){let n=JSON.parse(l.data);if(n.type===`pending`)u(n.count);else if(n.type===`created`){let t=n.pane;o.current.set(t,{rows:n.rows,cols:n.cols}),typeof n.title==`string`&&n.title&&m(e=>({...e,[t]:n.title})),d(e=>[...e,t]),c.current>0?(--c.current,f(t),s.current.set(e,t)):s.current.get(e)===t&&f(t)}else n.type===`exited`?(d(e=>e.filter(e=>e!==n.pane)),f(e=>e===n.pane?null:e),p(e=>e===n.pane?null:e),i.current.delete(n.pane),o.current.delete(n.pane),m(e=>{if(!(n.pane in e))return e;let t={...e};return delete t[n.pane],t})):n.type===`resized`?(o.current.set(n.pane,{rows:n.rows,cols:n.cols}),r.current.get(n.pane)?.term.resize(n.cols,n.rows)):n.type===`recovery`?_(e=>g(e,n)):n.type===`size_owner`?h(n.owned):n.type===`reordered`?d(e=>a(e,n.order)):n.type===`error`&&(c.current=0,t.error(n.message));return}let v=new Uint8Array(l.data);if(v.length<4)return;let ee=new DataView(v.buffer).getUint32(0,!0),y=v.subarray(4),te=r.current.get(ee);if(te)te.term.write(y);else{let e=i.current.get(ee)??[];e.push(y),i.current.set(ee,e)}},b.onclose=()=>{l||(v=setTimeout(y,1e3))}};return y(),()=>{l=!0,v&&clearTimeout(v),n.current?.close(),ee()}},[e])}var re=Object.defineProperty,ie=Object.getOwnPropertyDescriptor,ae=(e,t)=>{for(var n in t)re(e,n,{get:t[n],enumerable:!0})},x=(e,t,n,r)=>{for(var i=r>1?void 0:r?ie(t,n):t,a=e.length-1,o;a>=0;a--)(o=e[a])&&(i=(r?o(t,n,i):o(i))||i);return r&&i&&re(t,n,i),i},S=(e,t)=>(n,r)=>t(n,r,e),C=`Terminal input`,oe={get:()=>C,set:e=>C=e},se=`Too much output to announce, navigate to rows manually to read`,ce={get:()=>se,set:e=>se=e};function w(e){return e.replace(/\r?\n/g,`\r`)}function le(e,t){return t?`\x1B[200~`+e+`\x1B[201~`:e}function ue(e,t){e.clipboardData&&e.clipboardData.setData(`text/plain`,t.selectionText),e.preventDefault()}function de(e,t,n,r){e.stopPropagation(),e.clipboardData&&fe(e.clipboardData.getData(`text/plain`),t,n,r)}function fe(e,t,n,r){e=w(e),e=le(e,n.decPrivateModes.bracketedPasteMode&&r.rawOptions.ignoreBracketedPasteMode!==!0),n.triggerDataEvent(e,!0),t.value=``}function pe(e,t,n){let r=n.getBoundingClientRect(),i=e.clientX-r.left-10,a=e.clientY-r.top-10;t.style.width=`20px`,t.style.height=`20px`,t.style.left=`${i}px`,t.style.top=`${a}px`,t.style.zIndex=`1000`,t.focus()}function me(e,t,n,r,i){pe(e,t,n),i&&r.rightClickSelect(e),t.value=r.selectionText,t.select()}function he(e){return e>65535?(e-=65536,String.fromCharCode((e>>10)+55296)+String.fromCharCode(e%1024+56320)):String.fromCharCode(e)}function T(e,t=0,n=e.length){let r=``;for(let i=t;i65535?(t-=65536,r+=String.fromCharCode((t>>10)+55296)+String.fromCharCode(t%1024+56320)):r+=String.fromCharCode(t)}return r}var ge=class{constructor(){this._interim=0}clear(){this._interim=0}decode(e,t){let n=e.length;if(!n)return 0;let r=0,i=0;if(this._interim){let n=e.charCodeAt(i++);56320<=n&&n<=57343?t[r++]=(this._interim-55296)*1024+n-56320+65536:(t[r++]=this._interim,t[r++]=n),this._interim=0}for(let a=i;a=n)return this._interim=i,r;let o=e.charCodeAt(a);56320<=o&&o<=57343?t[r++]=(i-55296)*1024+o-56320+65536:(t[r++]=i,t[r++]=o);continue}i!==65279&&(t[r++]=i)}return r}},_e=class{constructor(){this.interim=new Uint8Array(3)}clear(){this.interim.fill(0)}decode(e,t){let n=e.length;if(!n)return 0;let r=0,i,a,o,s,c=0,l=0;if(this.interim[0]){let i=!1,a=this.interim[0];a&=(a&224)==192?31:(a&240)==224?15:7;let o=0,s;for(;(s=this.interim[++o]&63)&&o<4;)a<<=6,a|=s;let c=(this.interim[0]&224)==192?2:(this.interim[0]&240)==224?3:4,u=c-o;for(;l=n)return 0;if(s=e[l++],(s&192)!=128){l--,i=!0;break}else this.interim[o++]=s,a<<=6,a|=s&63}i||(c===2?a<128?l--:t[r++]=a:c===3?a<2048||a>=55296&&a<=57343||a===65279||(t[r++]=a):a<65536||a>1114111||(t[r++]=a)),this.interim.fill(0)}let u=n-4,d=l;for(;d=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(c=(i&31)<<6|a&63,c<128){d--;continue}t[r++]=c}else if((i&240)==224){if(d>=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,r;if(o=e[d++],(o&192)!=128){d--;continue}if(c=(i&15)<<12|(a&63)<<6|o&63,c<2048||c>=55296&&c<=57343||c===65279)continue;t[r++]=c}else if((i&248)==240){if(d>=n)return this.interim[0]=i,r;if(a=e[d++],(a&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,r;if(o=e[d++],(o&192)!=128){d--;continue}if(d>=n)return this.interim[0]=i,this.interim[1]=a,this.interim[2]=o,r;if(s=e[d++],(s&192)!=128){d--;continue}if(c=(i&7)<<18|(a&63)<<12|(o&63)<<6|s&63,c<65536||c>1114111)continue;t[r++]=c}}return r}},ve=``,ye=` `,be=class e{constructor(){this.fg=0,this.bg=0,this.extended=new xe}static toColorRGB(e){return[e>>>16&255,e>>>8&255,e&255]}static fromColorRGB(e){return(e[0]&255)<<16|(e[1]&255)<<8|e[2]&255}clone(){let t=new e;return t.fg=this.fg,t.bg=this.bg,t.extended=this.extended.clone(),t}isInverse(){return this.fg&67108864}isBold(){return this.fg&134217728}isUnderline(){return this.hasExtendedAttrs()&&this.extended.underlineStyle!==0?1:this.fg&268435456}isBlink(){return this.fg&536870912}isInvisible(){return this.fg&1073741824}isItalic(){return this.bg&67108864}isDim(){return this.bg&134217728}isStrikethrough(){return this.fg&2147483648}isProtected(){return this.bg&536870912}isOverline(){return this.bg&1073741824}getFgColorMode(){return this.fg&50331648}getBgColorMode(){return this.bg&50331648}isFgRGB(){return(this.fg&50331648)==50331648}isBgRGB(){return(this.bg&50331648)==50331648}isFgPalette(){return(this.fg&50331648)==16777216||(this.fg&50331648)==33554432}isBgPalette(){return(this.bg&50331648)==16777216||(this.bg&50331648)==33554432}isFgDefault(){return(this.fg&50331648)==0}isBgDefault(){return(this.bg&50331648)==0}isAttributeDefault(){return this.fg===0&&this.bg===0}getFgColor(){switch(this.fg&50331648){case 16777216:case 33554432:return this.fg&255;case 50331648:return this.fg&16777215;default:return-1}}getBgColor(){switch(this.bg&50331648){case 16777216:case 33554432:return this.bg&255;case 50331648:return this.bg&16777215;default:return-1}}hasExtendedAttrs(){return this.bg&268435456}updateExtended(){this.extended.isEmpty()?this.bg&=-268435457:this.bg|=268435456}getUnderlineColor(){if(this.bg&268435456&&~this.extended.underlineColor)switch(this.extended.underlineColor&50331648){case 16777216:case 33554432:return this.extended.underlineColor&255;case 50331648:return this.extended.underlineColor&16777215;default:return this.getFgColor()}return this.getFgColor()}getUnderlineColorMode(){return this.bg&268435456&&~this.extended.underlineColor?this.extended.underlineColor&50331648:this.getFgColorMode()}isUnderlineColorRGB(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==50331648:this.isFgRGB()}isUnderlineColorPalette(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==16777216||(this.extended.underlineColor&50331648)==33554432:this.isFgPalette()}isUnderlineColorDefault(){return this.bg&268435456&&~this.extended.underlineColor?(this.extended.underlineColor&50331648)==0:this.isFgDefault()}getUnderlineStyle(){return this.fg&268435456?this.bg&268435456?this.extended.underlineStyle:1:0}getUnderlineVariantOffset(){return this.extended.underlineVariantOffset}},xe=class e{constructor(e=0,t=0){this._ext=0,this._urlId=0,this._ext=e,this._urlId=t}get ext(){return this._urlId?this._ext&-469762049|this.underlineStyle<<26:this._ext}set ext(e){this._ext=e}get underlineStyle(){return this._urlId?5:(this._ext&469762048)>>26}set underlineStyle(e){this._ext&=-469762049,this._ext|=e<<26&469762048}get underlineColor(){return this._ext&67108863}set underlineColor(e){this._ext&=-67108864,this._ext|=e&67108863}get urlId(){return this._urlId}set urlId(e){this._urlId=e}get underlineVariantOffset(){let e=(this._ext&3758096384)>>29;return e<0?e^4294967288:e}set underlineVariantOffset(e){this._ext&=536870911,this._ext|=e<<29&3758096384}clone(){return new e(this._ext,this._urlId)}isEmpty(){return this.underlineStyle===0&&this._urlId===0}},E=class e extends be{constructor(){super(...arguments),this.content=0,this.fg=0,this.bg=0,this.extended=new xe,this.combinedData=``}static fromCharData(t){let n=new e;return n.setFromCharData(t),n}isCombined(){return this.content&2097152}getWidth(){return this.content>>22}getChars(){return this.content&2097152?this.combinedData:this.content&2097151?he(this.content&2097151):``}getCode(){return this.isCombined()?this.combinedData.charCodeAt(this.combinedData.length-1):this.content&2097151}setFromCharData(e){this.fg=e[0],this.bg=0;let t=!1;if(e[1].length>2)t=!0;else if(e[1].length===2){let n=e[1].charCodeAt(0);if(55296<=n&&n<=56319){let r=e[1].charCodeAt(1);56320<=r&&r<=57343?this.content=(n-55296)*1024+r-56320+65536|e[2]<<22:t=!0}else t=!0}else this.content=e[1].charCodeAt(0)|e[2]<<22;t&&(this.combinedData=e[1],this.content=2097152|e[2]<<22)}getAsCharData(){return[this.fg,this.getChars(),this.getWidth(),this.getCode()]}},Se=`di$target`,Ce=`di$dependencies`,we=new Map;function Te(e){return e[Ce]||[]}function D(e){if(we.has(e))return we.get(e);let t=function(e,n,r){if(arguments.length!==3)throw Error(`@IServiceName-decorator can only be used to decorate a parameter`);Ee(t,e,r)};return t._id=e,we.set(e,t),t}function Ee(e,t,n){t[Se]===t?t[Ce].push({id:e,index:n}):(t[Ce]=[{id:e,index:n}],t[Se]=t)}var O=D(`BufferService`),De=D(`CoreMouseService`),Oe=D(`CoreService`),ke=D(`CharsetService`),Ae=D(`InstantiationService`),je=D(`LogService`),k=D(`OptionsService`),Me=D(`OscLinkService`),Ne=D(`UnicodeService`),Pe=D(`DecorationService`),Fe=class{constructor(e,t,n){this._bufferService=e,this._optionsService=t,this._oscLinkService=n}provideLinks(e,t){let n=this._bufferService.buffer.lines.get(e-1);if(!n){t(void 0);return}let r=[],i=this._optionsService.rawOptions.linkHandler,a=new E,o=n.getTrimmedLength(),s=-1,c=-1,l=!1;for(let t=0;ti?i.activate(e,t,a):Ie(e,t),hover:(e,t)=>i?.hover?.(e,t,a),leave:(e,t)=>i?.leave?.(e,t,a)})}l=!1,a.hasExtendedAttrs()&&a.extended.urlId?(c=t,s=a.extended.urlId):(c=-1,s=-1)}}t(r)}};Fe=x([S(0,O),S(1,k),S(2,Me)],Fe);function Ie(e,t){if(confirm(`Do you want to navigate to ${t}? + +WARNING: This link could potentially be dangerous`)){let e=window.open();if(e){try{e.opener=null}catch{}e.location.href=t}else console.warn(`Opening link blocked as opener could not be cleared`)}}var Le=D(`CharSizeService`),Re=D(`CoreBrowserService`),ze=D(`MouseService`),Be=D(`RenderService`),Ve=D(`SelectionService`),He=D(`CharacterJoinerService`),Ue=D(`ThemeService`),We=D(`LinkProviderService`),Ge=new class{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(e){setTimeout(()=>{throw e.stack?Ze.isErrorNoTelemetry(e)?new Ze(e.message+` + +`+e.stack):Error(e.message+` + +`+e.stack):e},0)}}addListener(e){return this.listeners.push(e),()=>{this._removeListener(e)}}emit(e){this.listeners.forEach(t=>{t(e)})}_removeListener(e){this.listeners.splice(this.listeners.indexOf(e),1)}setUnexpectedErrorHandler(e){this.unexpectedErrorHandler=e}getUnexpectedErrorHandler(){return this.unexpectedErrorHandler}onUnexpectedError(e){this.unexpectedErrorHandler(e),this.emit(e)}onUnexpectedExternalError(e){this.unexpectedErrorHandler(e)}};function Ke(e){Je(e)||Ge.onUnexpectedError(e)}var qe=`Canceled`;function Je(e){return e instanceof Ye||e instanceof Error&&e.name===qe&&e.message===qe}var Ye=class extends Error{constructor(){super(qe),this.name=this.message}};function Xe(e){return Error(e?`Illegal argument: ${e}`:`Illegal argument`)}var Ze=class e extends Error{constructor(e){super(e),this.name=`CodeExpectedError`}static fromError(t){if(t instanceof e)return t;let n=new e;return n.message=t.message,n.stack=t.stack,n}static isErrorNoTelemetry(e){return e.name===`CodeExpectedError`}},Qe=class e extends Error{constructor(t){super(t||`An unexpected bug occurred.`),Object.setPrototypeOf(this,e.prototype)}};function $e(e,t,n=0,r=e.length){let i=n,a=r;for(;i{function t(e){return e<0}e.isLessThan=t;function n(e){return e<=0}e.isLessThanOrEqual=n;function r(e){return e>0}e.isGreaterThan=r;function i(e){return e===0}e.isNeitherLessOrGreaterThan=i,e.greaterThan=1,e.lessThan=-1,e.neitherLessOrGreaterThan=0})(nt||={});function rt(e,t){return(n,r)=>t(e(n),e(r))}var it=(e,t)=>e-t,at=class e{constructor(e){this.iterate=e}forEach(e){this.iterate(t=>(e(t),!0))}toArray(){let e=[];return this.iterate(t=>(e.push(t),!0)),e}filter(t){return new e(e=>this.iterate(n=>!t(n)||e(n)))}map(t){return new e(e=>this.iterate(n=>e(t(n))))}some(e){let t=!1;return this.iterate(n=>(t=e(n),!t)),t}findFirst(e){let t;return this.iterate(n=>e(n)?(t=n,!1):!0),t}findLast(e){let t;return this.iterate(n=>(e(n)&&(t=n),!0)),t}findLastMaxBy(e){let t,n=!0;return this.iterate(r=>((n||nt.isGreaterThan(e(r,t)))&&(n=!1,t=r),!0)),t}};at.empty=new at(e=>{});function ot(e,t){let n=Object.create(null);for(let r of e){let e=t(r),i=n[e];i||=n[e]=[],i.push(r)}return n}var st=class{constructor(){this.map=new Map}add(e,t){let n=this.map.get(e);n||(n=new Set,this.map.set(e,n)),n.add(t)}delete(e,t){let n=this.map.get(e);n&&(n.delete(t),n.size===0&&this.map.delete(e))}forEach(e,t){let n=this.map.get(e);n&&n.forEach(t)}get(e){return this.map.get(e)||new Set}};function ct(e,t){let n=this,r=!1,i;return function(){if(r)return i;if(r=!0,t)try{i=e.apply(n,arguments)}finally{t()}else i=e.apply(n,arguments);return i}}var lt;(e=>{function t(e){return e&&typeof e==`object`&&typeof e[Symbol.iterator]==`function`}e.is=t;let n=Object.freeze([]);function r(){return n}e.empty=r;function*i(e){yield e}e.single=i;function a(e){return t(e)?e:i(e)}e.wrap=a;function o(e){return e||n}e.from=o;function*s(e){for(let t=e.length-1;t>=0;t--)yield e[t]}e.reverse=s;function c(e){return!e||e[Symbol.iterator]().next().done===!0}e.isEmpty=c;function l(e){return e[Symbol.iterator]().next().value}e.first=l;function u(e,t){let n=0;for(let r of e)if(t(r,n++))return!0;return!1}e.some=u;function d(e,t){for(let n of e)if(t(n))return n}e.find=d;function*f(e,t){for(let n of e)t(n)&&(yield n)}e.filter=f;function*p(e,t){let n=0;for(let r of e)yield t(r,n++)}e.map=p;function*m(e,t){let n=0;for(let r of e)yield*t(r,n++)}e.flatMap=m;function*h(...e){for(let t of e)yield*t}e.concat=h;function g(e,t,n){let r=n;for(let n of e)r=t(r,n);return r}e.reduce=g;function*_(e,t,n=e.length){for(t<0&&(t+=e.length),n<0?n+=e.length:n>e.length&&(n=e.length);tt.source!==null&&!this.getRootParent(t,e).isSingleton).flatMap(([e])=>e)}computeLeakingDisposables(e=10,t){let n;if(t)n=t;else{let e=new Map,t=[...this.livingDisposables.values()].filter(t=>t.source!==null&&!this.getRootParent(t,e).isSingleton);if(t.length===0)return;let r=new Set(t.map(e=>e.value));if(n=t.filter(e=>!(e.parent&&r.has(e.parent))),n.length===0)throw Error(`There are cyclic diposable chains!`)}if(!n)return;function r(e){function t(e,t){for(;e.length>0&&t.some(t=>typeof t==`string`?t===e[0]:e[0].match(t));)e.shift()}let n=e.source.split(` +`).map(e=>e.trim().replace(`at `,``)).filter(e=>e!==``);return t(n,[`Error`,/^trackDisposable \(.*\)$/,/^DisposableTracker.trackDisposable \(.*\)$/]),n.reverse()}let i=new st;for(let e of n){let t=r(e);for(let n=0;n<=t.length;n++)i.add(t.slice(0,n).join(` +`),e)}n.sort(rt(e=>e.idx,it));let a=``,o=0;for(let t of n.slice(0,e)){o++;let e=r(t),s=[];for(let t=0;tr(e)[t]),e=>e);delete o[e[t]];for(let[e,t]of Object.entries(o))s.unshift(` - stacktraces of ${t.length} other leaks continue with ${e}`);s.unshift(a)}a+=` + + +==================== Leaking disposable ${o}/${n.length}: ${t.value.constructor.name} ==================== +${s.join(` +`)} +============================================================ + +`}return n.length>e&&(a+=` + + +... and ${n.length-e} more leaking disposables + +`),{leaks:n,details:a}}};dt.idx=0;function ft(e){return ut?.trackDisposable(e),e}function pt(e){ut?.markAsDisposed(e)}function mt(e,t){ut?.setParent(e,t)}function ht(e){return ut?.markAsSingleton(e),e}function gt(e){if(lt.is(e)){let t=[];for(let n of e)if(n)try{n.dispose()}catch(e){t.push(e)}if(t.length===1)throw t[0];if(t.length>1)throw AggregateError(t,`Encountered errors while disposing of store`);return Array.isArray(e)?[]:e}else if(e)return e.dispose(),e}function _t(...e){return A(()=>gt(e))}function A(e){let t=ft({dispose:ct(()=>{pt(t),e()})});return t}var vt=class e{constructor(){this._toDispose=new Set,this._isDisposed=!1,ft(this)}dispose(){this._isDisposed||(pt(this),this._isDisposed=!0,this.clear())}get isDisposed(){return this._isDisposed}clear(){if(this._toDispose.size!==0)try{gt(this._toDispose)}finally{this._toDispose.clear()}}add(t){if(!t)return t;if(t===this)throw Error(`Cannot register a disposable on itself!`);return mt(t,this),this._isDisposed?e.DISABLE_DISPOSED_WARNING||console.warn(Error(`Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!`).stack):this._toDispose.add(t),t}delete(e){if(e){if(e===this)throw Error(`Cannot dispose a disposable on itself!`);this._toDispose.delete(e),e.dispose()}}deleteAndLeak(e){e&&this._toDispose.has(e)&&(this._toDispose.delete(e),mt(e,null))}};vt.DISABLE_DISPOSED_WARNING=!1;var yt=vt,j=class{constructor(){this._store=new yt,ft(this),mt(this._store,this)}dispose(){pt(this),this._store.dispose()}_register(e){if(e===this)throw Error(`Cannot register a disposable on itself!`);return this._store.add(e)}};j.None=Object.freeze({dispose(){}});var bt=class{constructor(){this._isDisposed=!1,ft(this)}get value(){return this._isDisposed?void 0:this._value}set value(e){this._isDisposed||e===this._value||(this._value?.dispose(),e&&mt(e,this),this._value=e)}clear(){this.value=void 0}dispose(){this._isDisposed=!0,pt(this),this._value?.dispose(),this._value=void 0}clearAndLeak(){let e=this._value;return this._value=void 0,e&&mt(e,null),e}},xt=typeof window==`object`?window:globalThis,St=class e{constructor(t){this.element=t,this.next=e.Undefined,this.prev=e.Undefined}};St.Undefined=new St(void 0);var M=St,Ct=class{constructor(){this._first=M.Undefined,this._last=M.Undefined,this._size=0}get size(){return this._size}isEmpty(){return this._first===M.Undefined}clear(){let e=this._first;for(;e!==M.Undefined;){let t=e.next;e.prev=M.Undefined,e.next=M.Undefined,e=t}this._first=M.Undefined,this._last=M.Undefined,this._size=0}unshift(e){return this._insert(e,!1)}push(e){return this._insert(e,!0)}_insert(e,t){let n=new M(e);if(this._first===M.Undefined)this._first=n,this._last=n;else if(t){let e=this._last;this._last=n,n.prev=e,e.next=n}else{let e=this._first;this._first=n,n.next=e,e.prev=n}this._size+=1;let r=!1;return()=>{r||(r=!0,this._remove(n))}}shift(){if(this._first!==M.Undefined){let e=this._first.element;return this._remove(this._first),e}}pop(){if(this._last!==M.Undefined){let e=this._last.element;return this._remove(this._last),e}}_remove(e){if(e.prev!==M.Undefined&&e.next!==M.Undefined){let t=e.prev;t.next=e.next,e.next.prev=t}else e.prev===M.Undefined&&e.next===M.Undefined?(this._first=M.Undefined,this._last=M.Undefined):e.next===M.Undefined?(this._last=this._last.prev,this._last.next=M.Undefined):e.prev===M.Undefined&&(this._first=this._first.next,this._first.prev=M.Undefined);--this._size}*[Symbol.iterator](){let e=this._first;for(;e!==M.Undefined;)yield e.element,e=e.next}},wt=globalThis.performance&&typeof globalThis.performance.now==`function`,Tt=class e{static create(t){return new e(t)}constructor(e){this._now=wt&&e===!1?Date.now:globalThis.performance.now.bind(globalThis.performance),this._startTime=this._now(),this._stopTime=-1}stop(){this._stopTime=this._now()}reset(){this._startTime=this._now(),this._stopTime=-1}elapsed(){return this._stopTime===-1?this._now()-this._startTime:this._stopTime-this._startTime}},N;(e=>{e.None=()=>j.None;function t(e,t){return d(e,()=>{},0,void 0,!0,void 0,t)}e.defer=t;function n(e){return(t,n=null,r)=>{let i=!1,a;return a=e(e=>{if(!i)return a?a.dispose():i=!0,t.call(n,e)},null,r),i&&a.dispose(),a}}e.once=n;function r(e,t,n){return l((n,r=null,i)=>e(e=>n.call(r,t(e)),null,i),n)}e.map=r;function i(e,t,n){return l((n,r=null,i)=>e(e=>{t(e),n.call(r,e)},null,i),n)}e.forEach=i;function a(e,t,n){return l((n,r=null,i)=>e(e=>t(e)&&n.call(r,e),null,i),n)}e.filter=a;function o(e){return e}e.signal=o;function s(...e){return(t,n=null,r)=>u(_t(...e.map(e=>e(e=>t.call(n,e)))),r)}e.any=s;function c(e,t,n,i){let a=n;return r(e,e=>(a=t(a,e),a),i)}e.reduce=c;function l(e,t){let n,r=new P({onWillAddFirstListener(){n=e(r.fire,r)},onDidRemoveLastListener(){n?.dispose()}});return t?.add(r),r.event}function u(e,t){return t instanceof Array?t.push(e):t&&t.add(e),e}function d(e,t,n=100,r=!1,i=!1,a,o){let s,c,l,u=0,d,f=new P({leakWarningThreshold:a,onWillAddFirstListener(){s=e(e=>{u++,c=t(c,e),r&&!l&&(f.fire(c),c=void 0),d=()=>{let e=c;c=void 0,l=void 0,(!r||u>1)&&f.fire(e),u=0},typeof n==`number`?(clearTimeout(l),l=setTimeout(d,n)):l===void 0&&(l=0,queueMicrotask(d))})},onWillRemoveListener(){i&&u>0&&d?.()},onDidRemoveLastListener(){d=void 0,s.dispose()}});return o?.add(f),f.event}e.debounce=d;function f(t,n=0,r){return e.debounce(t,(e,t)=>e?(e.push(t),e):[t],n,void 0,!0,void 0,r)}e.accumulate=f;function p(e,t=(e,t)=>e===t,n){let r=!0,i;return a(e,e=>{let n=r||!t(e,i);return r=!1,i=e,n},n)}e.latch=p;function m(t,n,r){return[e.filter(t,n,r),e.filter(t,e=>!n(e),r)]}e.split=m;function h(e,t=!1,n=[],r){let i=n.slice(),a=e(e=>{i?i.push(e):s.fire(e)});r&&r.add(a);let o=()=>{i?.forEach(e=>s.fire(e)),i=null},s=new P({onWillAddFirstListener(){a||(a=e(e=>s.fire(e)),r&&r.add(a))},onDidAddFirstListener(){i&&(t?setTimeout(o):o())},onDidRemoveLastListener(){a&&a.dispose(),a=null}});return r&&r.add(s),s.event}e.buffer=h;function g(e,t){return(n,r,i)=>{let a=t(new v);return e(function(e){let t=a.evaluate(e);t!==_&&n.call(r,t)},void 0,i)}}e.chain=g;let _=Symbol(`HaltChainable`);class v{constructor(){this.steps=[]}map(e){return this.steps.push(e),this}forEach(e){return this.steps.push(t=>(e(t),t)),this}filter(e){return this.steps.push(t=>e(t)?t:_),this}reduce(e,t){let n=t;return this.steps.push(t=>(n=e(n,t),n)),this}latch(e=(e,t)=>e===t){let t=!0,n;return this.steps.push(r=>{let i=t||!e(r,n);return t=!1,n=r,i?r:_}),this}evaluate(e){for(let t of this.steps)if(e=t(e),e===_)break;return e}}function ee(e,t,n=e=>e){let r=(...e)=>i.fire(n(...e)),i=new P({onWillAddFirstListener:()=>e.on(t,r),onDidRemoveLastListener:()=>e.removeListener(t,r)});return i.event}e.fromNodeEventEmitter=ee;function y(e,t,n=e=>e){let r=(...e)=>i.fire(n(...e)),i=new P({onWillAddFirstListener:()=>e.addEventListener(t,r),onDidRemoveLastListener:()=>e.removeEventListener(t,r)});return i.event}e.fromDOMEventEmitter=y;function te(e){return new Promise(t=>n(e)(t))}e.toPromise=te;function b(e){let t=new P;return e.then(e=>{t.fire(e)},()=>{t.fire(void 0)}).finally(()=>{t.dispose()}),t.event}e.fromPromise=b;function ne(e,t){return e(e=>t.fire(e))}e.forward=ne;function re(e,t,n){return t(n),e(e=>t(e))}e.runAndSubscribe=re;class ie{constructor(e,t){this._observable=e,this._counter=0,this._hasChanged=!1;let n={onWillAddFirstListener:()=>{e.addObserver(this)},onDidRemoveLastListener:()=>{e.removeObserver(this)}};this.emitter=new P(n),t&&t.add(this.emitter)}beginUpdate(e){this._counter++}handlePossibleChange(e){}handleChange(e,t){this._hasChanged=!0}endUpdate(e){this._counter--,this._counter===0&&(this._observable.reportChanges(),this._hasChanged&&(this._hasChanged=!1,this.emitter.fire(this._observable.get())))}}function ae(e,t){return new ie(e,t).emitter.event}e.fromObservable=ae;function x(e){return(t,n,r)=>{let i=0,a=!1,o={beginUpdate(){i++},endUpdate(){i--,i===0&&(e.reportChanges(),a&&(a=!1,t.call(n)))},handlePossibleChange(){},handleChange(){a=!0}};e.addObserver(o),e.reportChanges();let s={dispose(){e.removeObserver(o)}};return r instanceof yt?r.add(s):Array.isArray(r)&&r.push(s),s}}e.fromObservableLight=x})(N||={});var Et=class e{constructor(t){this.listenerCount=0,this.invocationCount=0,this.elapsedOverall=0,this.durations=[],this.name=`${t}_${e._idPool++}`,e.all.add(this)}start(e){this._stopWatch=new Tt,this.listenerCount=e}stop(){if(this._stopWatch){let e=this._stopWatch.elapsed();this.durations.push(e),this.elapsedOverall+=e,this.invocationCount+=1,this._stopWatch=void 0}}};Et.all=new Set,Et._idPool=0;var Dt=Et,Ot=-1,kt=class e{constructor(t,n,r=(e._idPool++).toString(16).padStart(3,`0`)){this._errorHandler=t,this.threshold=n,this.name=r,this._warnCountdown=0}dispose(){this._stacks?.clear()}check(e,t){let n=this.threshold;if(n<=0||t{let t=this._stacks.get(e.value)||0;this._stacks.set(e.value,t-1)}}getMostFrequentStack(){if(!this._stacks)return;let e,t=0;for(let[n,r]of this._stacks)(!e||t0||this._options?.leakWarningThreshold?new At(e?.onListenerError??Ke,this._options?.leakWarningThreshold??Ot):void 0,this._perfMon=this._options?._profName?new Dt(this._options._profName):void 0,this._deliveryQueue=this._options?.deliveryQueue}dispose(){this._disposed||(this._disposed=!0,this._deliveryQueue?.current===this&&this._deliveryQueue.reset(),this._listeners&&(this._listeners=void 0,this._size=0),this._options?.onDidRemoveLastListener?.(),this._leakageMon?.dispose())}get event(){return this._event??=(e,t,n)=>{if(this._leakageMon&&this._size>this._leakageMon.threshold**2){let e=`[${this._leakageMon.name}] REFUSES to accept new listeners because it exceeded its threshold by far (${this._size} vs ${this._leakageMon.threshold})`;console.warn(e);let t=this._leakageMon.getMostFrequentStack()??[`UNKNOWN stack`,-1],n=new Nt(`${e}. HINT: Stack shows most frequent listener (${t[1]}-times)`,t[0]);return(this._options?.onListenerError||Ke)(n),j.None}if(this._disposed)return j.None;t&&(e=e.bind(t));let r=new Ft(e),i;this._leakageMon&&this._size>=Math.ceil(this._leakageMon.threshold*.2)&&(r.stack=jt.create(),i=this._leakageMon.check(r.stack,this._size+1)),this._listeners?this._listeners instanceof Ft?(this._deliveryQueue??=new Rt,this._listeners=[this._listeners,r]):this._listeners.push(r):(this._options?.onWillAddFirstListener?.(this),this._listeners=r,this._options?.onDidAddFirstListener?.(this)),this._size++;let a=A(()=>{Lt?.unregister(a),i?.(),this._removeListener(r)});if(n instanceof yt?n.add(a):Array.isArray(n)&&n.push(a),Lt){let e=Error().stack.split(` +`).slice(2,3).join(` +`).trim(),t=/(file:|vscode-file:\/\/vscode-app)?(\/[^:]*:\d+:\d+)/.exec(e);Lt.register(a,t?.[2]??e,a)}return a},this._event}_removeListener(e){if(this._options?.onWillRemoveListener?.(this),!this._listeners)return;if(this._size===1){this._listeners=void 0,this._options?.onDidRemoveLastListener?.(this),this._size=0;return}let t=this._listeners,n=t.indexOf(e);if(n===-1)throw console.log(`disposed?`,this._disposed),console.log(`size?`,this._size),console.log(`arr?`,JSON.stringify(this._listeners)),Error(`Attempted to dispose unknown listener`);this._size--,t[n]=void 0;let r=this._deliveryQueue.current===this;if(this._size*It<=t.length){let e=0;for(let n=0;n0}},Rt=class{constructor(){this.i=-1,this.end=0}enqueue(e,t,n){this.i=0,this.end=n,this.current=e,this.value=t}reset(){this.i=this.end,this.current=void 0,this.value=void 0}},zt=class{constructor(){this.mapWindowIdToZoomLevel=new Map,this._onDidChangeZoomLevel=new P,this.onDidChangeZoomLevel=this._onDidChangeZoomLevel.event,this.mapWindowIdToZoomFactor=new Map,this._onDidChangeFullscreen=new P,this.onDidChangeFullscreen=this._onDidChangeFullscreen.event,this.mapWindowIdToFullScreen=new Map}getZoomLevel(e){return this.mapWindowIdToZoomLevel.get(this.getWindowId(e))??0}setZoomLevel(e,t){if(this.getZoomLevel(t)===e)return;let n=this.getWindowId(t);this.mapWindowIdToZoomLevel.set(n,e),this._onDidChangeZoomLevel.fire(n)}getZoomFactor(e){return this.mapWindowIdToZoomFactor.get(this.getWindowId(e))??1}setZoomFactor(e,t){this.mapWindowIdToZoomFactor.set(this.getWindowId(t),e)}setFullscreen(e,t){if(this.isFullscreen(t)===e)return;let n=this.getWindowId(t);this.mapWindowIdToFullScreen.set(n,e),this._onDidChangeFullscreen.fire(n)}isFullscreen(e){return!!this.mapWindowIdToFullScreen.get(this.getWindowId(e))}getWindowId(e){return e.vscodeWindowId}};zt.INSTANCE=new zt;var Bt=zt;function Vt(e,t,n){typeof t==`string`&&(t=e.matchMedia(t)),t.addEventListener(`change`,n)}Bt.INSTANCE.onDidChangeZoomLevel;function Ht(e){return Bt.INSTANCE.getZoomFactor(e)}Bt.INSTANCE.onDidChangeFullscreen;var Ut=typeof navigator==`object`?navigator.userAgent:``,Wt=Ut.indexOf(`Firefox`)>=0,Gt=Ut.indexOf(`AppleWebKit`)>=0,Kt=Ut.indexOf(`Chrome`)>=0,qt=!Kt&&Ut.indexOf(`Safari`)>=0;Ut.indexOf(`Electron/`),Ut.indexOf(`Android`);var Jt=!1;if(typeof xt.matchMedia==`function`){let e=xt.matchMedia(`(display-mode: standalone) or (display-mode: window-controls-overlay)`),t=xt.matchMedia(`(display-mode: fullscreen)`);Jt=e.matches,Vt(xt,e,({matches:e})=>{Jt&&t.matches||(Jt=e)})}function Yt(){return Jt}var Xt=`en`,Zt=!1,Qt=!1,$t=!1,en=!1,tn=!1,nn=Xt,rn,an=globalThis,on;typeof an.vscode<`u`&&typeof an.vscode.process<`u`?on=an.vscode.process:typeof process<`u`&&typeof process?.versions?.node==`string`&&(on=process);var sn=typeof on?.versions?.electron==`string`&&on?.type===`renderer`;if(typeof on==`object`){Zt=on.platform===`win32`,Qt=on.platform===`darwin`,$t=on.platform===`linux`,$t&&on.env.SNAP&&on.env.SNAP_REVISION,on.env.CI||on.env.BUILD_ARTIFACTSTAGINGDIRECTORY,nn=Xt;let e=on.env.VSCODE_NLS_CONFIG;if(e)try{let t=JSON.parse(e);t.userLocale,t.osLocale,nn=t.resolvedLanguage||Xt,t.languagePack?.translationsConfigFile}catch{}en=!0}else typeof navigator==`object`&&!sn?(rn=navigator.userAgent,Zt=rn.indexOf(`Windows`)>=0,Qt=rn.indexOf(`Macintosh`)>=0,(rn.indexOf(`Macintosh`)>=0||rn.indexOf(`iPad`)>=0||rn.indexOf(`iPhone`)>=0)&&navigator.maxTouchPoints&&navigator.maxTouchPoints,$t=rn.indexOf(`Linux`)>=0,rn?.indexOf(`Mobi`),tn=!0,nn=globalThis._VSCODE_NLS_LANGUAGE||Xt,navigator.language.toLowerCase()):console.error(`Unable to resolve platform.`);var cn=Zt,ln=Qt,un=$t,dn=en;tn&&typeof an.importScripts==`function`&&an.origin;var fn=rn,pn=nn,mn;(e=>{function t(){return pn}e.value=t;function n(){return pn.length===2?pn===`en`:pn.length>=3&&pn[0]===`e`&&pn[1]===`n`&&pn[2]===`-`}e.isDefaultVariant=n;function r(){return pn===`en`}e.isDefault=r})(mn||={});var hn=typeof an.postMessage==`function`&&!an.importScripts;(()=>{if(hn){let e=[];an.addEventListener(`message`,t=>{if(t.data&&t.data.vscodeScheduleAsyncWork)for(let n=0,r=e.length;n{let r=++t;e.push({id:r,callback:n}),an.postMessage({vscodeScheduleAsyncWork:r},`*`)}}return e=>setTimeout(e)})();var gn=!!(fn&&fn.indexOf(`Chrome`)>=0);fn&&fn.indexOf(`Firefox`),!gn&&fn&&fn.indexOf(`Safari`),fn&&fn.indexOf(`Edg/`),fn&&fn.indexOf(`Android`);var _n=typeof navigator==`object`?navigator:{};dn||document.queryCommandSupported&&document.queryCommandSupported(`copy`)||_n&&_n.clipboard&&_n.clipboard.writeText,dn||_n&&_n.clipboard&&_n.clipboard.readText,dn||Yt()||_n.keyboard,`ontouchstart`in xt||_n.maxTouchPoints,xt.PointerEvent&&(`ontouchstart`in xt||navigator.maxTouchPoints);var vn=class{constructor(){this._keyCodeToStr=[],this._strToKeyCode=Object.create(null)}define(e,t){this._keyCodeToStr[e]=t,this._strToKeyCode[t.toLowerCase()]=e}keyCodeToStr(e){return this._keyCodeToStr[e]}strToKeyCode(e){return this._strToKeyCode[e.toLowerCase()]||0}},yn=new vn,bn=new vn,xn=new vn,Sn=Array(230),Cn;(e=>{function t(e){return yn.keyCodeToStr(e)}e.toString=t;function n(e){return yn.strToKeyCode(e)}e.fromString=n;function r(e){return bn.keyCodeToStr(e)}e.toUserSettingsUS=r;function i(e){return xn.keyCodeToStr(e)}e.toUserSettingsGeneral=i;function a(e){return bn.strToKeyCode(e)||xn.strToKeyCode(e)}e.fromUserSettings=a;function o(e){if(e>=98&&e<=113)return null;switch(e){case 16:return`Up`;case 18:return`Down`;case 15:return`Left`;case 17:return`Right`}return yn.keyCodeToStr(e)}e.toElectronAccelerator=o})(Cn||={});var wn=class e{constructor(e,t,n,r,i){this.ctrlKey=e,this.shiftKey=t,this.altKey=n,this.metaKey=r,this.keyCode=i}equals(t){return t instanceof e&&this.ctrlKey===t.ctrlKey&&this.shiftKey===t.shiftKey&&this.altKey===t.altKey&&this.metaKey===t.metaKey&&this.keyCode===t.keyCode}getHashCode(){return`K${this.ctrlKey?`1`:`0`}${this.shiftKey?`1`:`0`}${this.altKey?`1`:`0`}${this.metaKey?`1`:`0`}${this.keyCode}`}isModifierKey(){return this.keyCode===0||this.keyCode===5||this.keyCode===57||this.keyCode===6||this.keyCode===4}toKeybinding(){return new Tn([this])}isDuplicateModifierCase(){return this.ctrlKey&&this.keyCode===5||this.shiftKey&&this.keyCode===4||this.altKey&&this.keyCode===6||this.metaKey&&this.keyCode===57}},Tn=class{constructor(e){if(e.length===0)throw Xe(`chords`);this.chords=e}getHashCode(){let e=``;for(let t=0,n=this.chords.length;t{function t(t){return t===e.None||t===e.Cancelled||t instanceof zn?!0:!t||typeof t!=`object`?!1:typeof t.isCancellationRequested==`boolean`&&typeof t.onCancellationRequested==`function`}e.isCancellationToken=t,e.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:N.None}),e.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:Ln})})(Rn||={});var zn=class{constructor(){this._isCancelled=!1,this._emitter=null}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()))}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?Ln:(this._emitter||=new P,this._emitter.event)}dispose(){this._emitter&&=(this._emitter.dispose(),null)}},Bn=class{constructor(e,t){this._isDisposed=!1,this._token=-1,typeof e==`function`&&typeof t==`number`&&this.setIfNotSet(e,t)}dispose(){this.cancel(),this._isDisposed=!0}cancel(){this._token!==-1&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(e,t){if(this._isDisposed)throw new Qe(`Calling 'cancelAndSet' on a disposed TimeoutTimer`);this.cancel(),this._token=setTimeout(()=>{this._token=-1,e()},t)}setIfNotSet(e,t){if(this._isDisposed)throw new Qe(`Calling 'setIfNotSet' on a disposed TimeoutTimer`);this._token===-1&&(this._token=setTimeout(()=>{this._token=-1,e()},t))}},Vn=class{constructor(){this.disposable=void 0,this.isDisposed=!1}cancel(){this.disposable?.dispose(),this.disposable=void 0}cancelAndSet(e,t,n=globalThis){if(this.isDisposed)throw new Qe(`Calling 'cancelAndSet' on a disposed IntervalTimer`);this.cancel();let r=n.setInterval(()=>{e()},t);this.disposable=A(()=>{n.clearInterval(r),this.disposable=void 0})}dispose(){this.cancel(),this.isDisposed=!0}};(function(){typeof globalThis.requestIdleCallback!=`function`||globalThis.cancelIdleCallback})();var Hn;(e=>{async function t(e){let t,n=await Promise.all(e.map(e=>e.then(e=>e,e=>{t||=e})));if(typeof t<`u`)throw t;return n}e.settled=t;function n(e){return new Promise(async(t,n)=>{try{await e(t,n)}catch(e){n(e)}})}e.withAsyncBody=n})(Hn||={});var Un=class e{static fromArray(t){return new e(e=>{e.emitMany(t)})}static fromPromise(t){return new e(async e=>{e.emitMany(await t)})}static fromPromises(t){return new e(async e=>{await Promise.all(t.map(async t=>e.emitOne(await t)))})}static merge(t){return new e(async e=>{await Promise.all(t.map(async t=>{for await(let n of t)e.emitOne(n)}))})}constructor(e,t){this._state=0,this._results=[],this._error=null,this._onReturn=t,this._onStateChanged=new P,queueMicrotask(async()=>{let t={emitOne:e=>this.emitOne(e),emitMany:e=>this.emitMany(e),reject:e=>this.reject(e)};try{await Promise.resolve(e(t)),this.resolve()}catch(e){this.reject(e)}finally{t.emitOne=void 0,t.emitMany=void 0,t.reject=void 0}})}[Symbol.asyncIterator](){let e=0;return{next:async()=>{do{if(this._state===2)throw this._error;if(e(this._onReturn?.(),{done:!0,value:void 0})}}static map(t,n){return new e(async e=>{for await(let r of t)e.emitOne(n(r))})}map(t){return e.map(this,t)}static filter(t,n){return new e(async e=>{for await(let r of t)n(r)&&e.emitOne(r)})}filter(t){return e.filter(this,t)}static coalesce(t){return e.filter(t,e=>!!e)}coalesce(){return e.coalesce(this)}static async toPromise(e){let t=[];for await(let n of e)t.push(n);return t}toPromise(){return e.toPromise(this)}emitOne(e){this._state===0&&(this._results.push(e),this._onStateChanged.fire())}emitMany(e){this._state===0&&(this._results=this._results.concat(e),this._onStateChanged.fire())}resolve(){this._state===0&&(this._state=1,this._onStateChanged.fire())}reject(e){this._state===0&&(this._state=2,this._error=e,this._onStateChanged.fire())}};Un.EMPTY=Un.fromArray([]);function Wn(e){return 55296<=e&&e<=56319}function Gn(e){return 56320<=e&&e<=57343}function Kn(e,t){return(e-55296<<10)+(t-56320)+65536}function qn(e){return Jn(e,0)}function Jn(e,t){switch(typeof e){case`object`:return e===null?Yn(349,t):Array.isArray(e)?Qn(e,t):$n(e,t);case`string`:return Zn(e,t);case`boolean`:return Xn(e,t);case`number`:return Yn(e,t);case`undefined`:return Yn(937,t);default:return Yn(617,t)}}function Yn(e,t){return(t<<5)-t+e|0}function Xn(e,t){return Yn(e?433:863,t)}function Zn(e,t){t=Yn(149417,t);for(let n=0,r=e.length;nJn(t,e),t)}function $n(e,t){return t=Yn(181387,t),Object.keys(e).sort().reduce((t,n)=>(t=Zn(n,t),Jn(e[n],t)),t)}function er(e,t,n=32){let r=n-t,i=~((1<>>r)>>>0}function tr(e,t=0,n=e.byteLength,r=0){for(let i=0;ie.toString(16).padStart(2,`0`)).join(``):nr((e>>>0).toString(16),t/4)}var ir=class e{constructor(){this._h0=1732584193,this._h1=4023233417,this._h2=2562383102,this._h3=271733878,this._h4=3285377520,this._buff=new Uint8Array(67),this._buffDV=new DataView(this._buff.buffer),this._buffLen=0,this._totalLen=0,this._leftoverHighSurrogate=0,this._finished=!1}update(e){let t=e.length;if(t===0)return;let n=this._buff,r=this._buffLen,i=this._leftoverHighSurrogate,a,o;for(i===0?(a=e.charCodeAt(0),o=0):(a=i,o=-1,i=0);;){let s=a;if(Wn(a))if(o+1>>6,e[t++]=128|(n&63)>>>0):n<65536?(e[t++]=224|(n&61440)>>>12,e[t++]=128|(n&4032)>>>6,e[t++]=128|(n&63)>>>0):(e[t++]=240|(n&1835008)>>>18,e[t++]=128|(n&258048)>>>12,e[t++]=128|(n&4032)>>>6,e[t++]=128|(n&63)>>>0),t>=64&&(this._step(),t-=64,this._totalLen+=64,e[0]=e[64],e[1]=e[65],e[2]=e[66]),t}digest(){return this._finished||(this._finished=!0,this._leftoverHighSurrogate&&(this._leftoverHighSurrogate=0,this._buffLen=this._push(this._buff,this._buffLen,65533)),this._totalLen+=this._buffLen,this._wrapUp()),rr(this._h0)+rr(this._h1)+rr(this._h2)+rr(this._h3)+rr(this._h4)}_wrapUp(){this._buff[this._buffLen++]=128,tr(this._buff,this._buffLen),this._buffLen>56&&(this._step(),tr(this._buff));let e=8*this._totalLen;this._buffDV.setUint32(56,Math.floor(e/4294967296),!1),this._buffDV.setUint32(60,e%4294967296,!1),this._step()}_step(){let t=e._bigBlock32,n=this._buffDV;for(let e=0;e<64;e+=4)t.setUint32(e,n.getUint32(e,!1),!1);for(let e=64;e<320;e+=4)t.setUint32(e,er(t.getUint32(e-12,!1)^t.getUint32(e-32,!1)^t.getUint32(e-56,!1)^t.getUint32(e-64,!1),1),!1);let r=this._h0,i=this._h1,a=this._h2,o=this._h3,s=this._h4,c,l,u;for(let e=0;e<80;e++)e<20?(c=i&a|~i&o,l=1518500249):e<40?(c=i^a^o,l=1859775393):e<60?(c=i&a|i&o|a&o,l=2400959708):(c=i^a^o,l=3395469782),u=er(r,5)+c+s+l+t.getUint32(e*4,!1)&4294967295,s=o,o=a,a=er(i,30),i=r,r=u;this._h0=this._h0+r&4294967295,this._h1=this._h1+i&4294967295,this._h2=this._h2+a&4294967295,this._h3=this._h3+o&4294967295,this._h4=this._h4+s&4294967295}};ir._bigBlock32=new DataView(new ArrayBuffer(320));var{registerWindow:ar,getWindow:or,getDocument:sr,getWindows:cr,getWindowsCount:lr,getWindowId:ur,getWindowById:dr,hasWindow:fr,onDidRegisterWindow:pr,onWillUnregisterWindow:mr,onDidUnregisterWindow:hr}=function(){let e=new Map,t={window:xt,disposables:new yt};e.set(xt.vscodeWindowId,t);let n=new P,r=new P,i=new P;function a(n,r){return(typeof n==`number`?e.get(n):void 0)??(r?t:void 0)}return{onDidRegisterWindow:n.event,onWillUnregisterWindow:i.event,onDidUnregisterWindow:r.event,registerWindow(t){if(e.has(t.vscodeWindowId))return j.None;let a=new yt,o={window:t,disposables:a.add(new yt)};return e.set(t.vscodeWindowId,o),a.add(A(()=>{e.delete(t.vscodeWindowId),r.fire(t)})),a.add(F(t,I.BEFORE_UNLOAD,()=>{i.fire(t)})),n.fire(o),a},getWindows(){return e.values()},getWindowsCount(){return e.size},getWindowId(e){return e.vscodeWindowId},hasWindow(t){return e.has(t)},getWindowById:a,getWindow(e){let t=e;if(t?.ownerDocument?.defaultView)return t.ownerDocument.defaultView.window;let n=e;return n?.view?n.view.window:xt},getDocument(e){return or(e).document}}}(),gr=class{constructor(e,t,n,r){this._node=e,this._type=t,this._handler=n,this._options=r||!1,this._node.addEventListener(this._type,this._handler,this._options)}dispose(){this._handler&&=(this._node.removeEventListener(this._type,this._handler,this._options),this._node=null,null)}};function F(e,t,n,r){return new gr(e,t,n,r)}function _r(e,t){return function(n){return t(new Fn(e,n))}}function vr(e){return function(t){return e(new jn(t))}}var yr=function(e,t,n,r){let i=n;return t===`click`||t===`mousedown`||t===`contextmenu`?i=_r(or(e),n):(t===`keydown`||t===`keypress`||t===`keyup`)&&(i=vr(n)),F(e,t,i,r)},br,xr=class extends Vn{constructor(e){super(),this.defaultTarget=e&&or(e)}cancelAndSet(e,t,n){return super.cancelAndSet(e,t,n??this.defaultTarget)}},Sr=class{constructor(e,t=0){this._runner=e,this.priority=t,this._canceled=!1}dispose(){this._canceled=!0}execute(){if(!this._canceled)try{this._runner()}catch(e){Ke(e)}}static sort(e,t){return t.priority-e.priority}};(function(){let e=new Map,t=new Map,n=new Map,r=new Map,i=i=>{n.set(i,!1);let a=e.get(i)??[];for(t.set(i,a),e.set(i,[]),r.set(i,!0);a.length>0;)a.sort(Sr.sort),a.shift().execute();r.set(i,!1)};br=(t,r,a=0)=>{let o=ur(t),s=new Sr(r,a),c=e.get(o);return c||(c=[],e.set(o,c)),c.push(s),n.get(o)||(n.set(o,!0),t.requestAnimationFrame(()=>i(o))),s}})();var Cr=class e{constructor(e,t){this.width=e,this.height=t}with(t=this.width,n=this.height){return t!==this.width||n!==this.height?new e(t,n):this}static is(e){return typeof e==`object`&&typeof e.height==`number`&&typeof e.width==`number`}static lift(t){return t instanceof e?t:new e(t.width,t.height)}static equals(e,t){return e===t?!0:!e||!t?!1:e.width===t.width&&e.height===t.height}};Cr.None=new Cr(0,0);function wr(e){let t=e.getBoundingClientRect(),n=or(e);return{left:t.left+n.scrollX,top:t.top+n.scrollY,width:t.width,height:t.height}}new class{constructor(){this.mutationObservers=new Map}observe(e,t,n){let r=this.mutationObservers.get(e);r||(r=new Map,this.mutationObservers.set(e,r));let i=qn(n),a=r.get(i);if(a)a.users+=1;else{let o=new P,s=new MutationObserver(e=>o.fire(e));s.observe(e,n);let c=a={users:1,observer:s,onDidMutate:o.event};t.add(A(()=>{--c.users,c.users===0&&(o.dispose(),s.disconnect(),r?.delete(i),r?.size===0&&this.mutationObservers.delete(e))})),r.set(i,a)}return a.onDidMutate}};var I={CLICK:`click`,AUXCLICK:`auxclick`,DBLCLICK:`dblclick`,MOUSE_UP:`mouseup`,MOUSE_DOWN:`mousedown`,MOUSE_OVER:`mouseover`,MOUSE_MOVE:`mousemove`,MOUSE_OUT:`mouseout`,MOUSE_ENTER:`mouseenter`,MOUSE_LEAVE:`mouseleave`,MOUSE_WHEEL:`wheel`,POINTER_UP:`pointerup`,POINTER_DOWN:`pointerdown`,POINTER_MOVE:`pointermove`,POINTER_LEAVE:`pointerleave`,CONTEXT_MENU:`contextmenu`,WHEEL:`wheel`,KEY_DOWN:`keydown`,KEY_PRESS:`keypress`,KEY_UP:`keyup`,LOAD:`load`,BEFORE_UNLOAD:`beforeunload`,UNLOAD:`unload`,PAGE_SHOW:`pageshow`,PAGE_HIDE:`pagehide`,PASTE:`paste`,ABORT:`abort`,ERROR:`error`,RESIZE:`resize`,SCROLL:`scroll`,FULLSCREEN_CHANGE:`fullscreenchange`,WK_FULLSCREEN_CHANGE:`webkitfullscreenchange`,SELECT:`select`,CHANGE:`change`,SUBMIT:`submit`,RESET:`reset`,FOCUS:`focus`,FOCUS_IN:`focusin`,FOCUS_OUT:`focusout`,BLUR:`blur`,INPUT:`input`,STORAGE:`storage`,DRAG_START:`dragstart`,DRAG:`drag`,DRAG_ENTER:`dragenter`,DRAG_LEAVE:`dragleave`,DRAG_OVER:`dragover`,DROP:`drop`,DRAG_END:`dragend`,ANIMATION_START:Gt?`webkitAnimationStart`:`animationstart`,ANIMATION_END:Gt?`webkitAnimationEnd`:`animationend`,ANIMATION_ITERATION:Gt?`webkitAnimationIteration`:`animationiteration`},Tr=/([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;function Er(e,t,n,...r){let i=Tr.exec(t);if(!i)throw Error(`Bad use of emmet`);let a=i[1]||`div`,o;return o=e===`http://www.w3.org/1999/xhtml`?document.createElement(a):document.createElementNS(e,a),i[3]&&(o.id=i[3]),i[4]&&(o.className=i[4].replace(/\./g,` `).trim()),n&&Object.entries(n).forEach(([e,t])=>{typeof t>`u`||(/^on\w+$/.test(e)?o[e]=t:e===`selected`?t&&o.setAttribute(e,`true`):o.setAttribute(e,t))}),o.append(...r),o}function Dr(e,t,...n){return Er(`http://www.w3.org/1999/xhtml`,e,t,...n)}Dr.SVG=function(e,t,...n){return Er(`http://www.w3.org/2000/svg`,e,t,...n)};var Or=class{constructor(e){this.domNode=e,this._maxWidth=``,this._width=``,this._height=``,this._top=``,this._left=``,this._bottom=``,this._right=``,this._paddingTop=``,this._paddingLeft=``,this._paddingBottom=``,this._paddingRight=``,this._fontFamily=``,this._fontWeight=``,this._fontSize=``,this._fontStyle=``,this._fontFeatureSettings=``,this._fontVariationSettings=``,this._textDecoration=``,this._lineHeight=``,this._letterSpacing=``,this._className=``,this._display=``,this._position=``,this._visibility=``,this._color=``,this._backgroundColor=``,this._layerHint=!1,this._contain=`none`,this._boxShadow=``}setMaxWidth(e){let t=kr(e);this._maxWidth!==t&&(this._maxWidth=t,this.domNode.style.maxWidth=this._maxWidth)}setWidth(e){let t=kr(e);this._width!==t&&(this._width=t,this.domNode.style.width=this._width)}setHeight(e){let t=kr(e);this._height!==t&&(this._height=t,this.domNode.style.height=this._height)}setTop(e){let t=kr(e);this._top!==t&&(this._top=t,this.domNode.style.top=this._top)}setLeft(e){let t=kr(e);this._left!==t&&(this._left=t,this.domNode.style.left=this._left)}setBottom(e){let t=kr(e);this._bottom!==t&&(this._bottom=t,this.domNode.style.bottom=this._bottom)}setRight(e){let t=kr(e);this._right!==t&&(this._right=t,this.domNode.style.right=this._right)}setPaddingTop(e){let t=kr(e);this._paddingTop!==t&&(this._paddingTop=t,this.domNode.style.paddingTop=this._paddingTop)}setPaddingLeft(e){let t=kr(e);this._paddingLeft!==t&&(this._paddingLeft=t,this.domNode.style.paddingLeft=this._paddingLeft)}setPaddingBottom(e){let t=kr(e);this._paddingBottom!==t&&(this._paddingBottom=t,this.domNode.style.paddingBottom=this._paddingBottom)}setPaddingRight(e){let t=kr(e);this._paddingRight!==t&&(this._paddingRight=t,this.domNode.style.paddingRight=this._paddingRight)}setFontFamily(e){this._fontFamily!==e&&(this._fontFamily=e,this.domNode.style.fontFamily=this._fontFamily)}setFontWeight(e){this._fontWeight!==e&&(this._fontWeight=e,this.domNode.style.fontWeight=this._fontWeight)}setFontSize(e){let t=kr(e);this._fontSize!==t&&(this._fontSize=t,this.domNode.style.fontSize=this._fontSize)}setFontStyle(e){this._fontStyle!==e&&(this._fontStyle=e,this.domNode.style.fontStyle=this._fontStyle)}setFontFeatureSettings(e){this._fontFeatureSettings!==e&&(this._fontFeatureSettings=e,this.domNode.style.fontFeatureSettings=this._fontFeatureSettings)}setFontVariationSettings(e){this._fontVariationSettings!==e&&(this._fontVariationSettings=e,this.domNode.style.fontVariationSettings=this._fontVariationSettings)}setTextDecoration(e){this._textDecoration!==e&&(this._textDecoration=e,this.domNode.style.textDecoration=this._textDecoration)}setLineHeight(e){let t=kr(e);this._lineHeight!==t&&(this._lineHeight=t,this.domNode.style.lineHeight=this._lineHeight)}setLetterSpacing(e){let t=kr(e);this._letterSpacing!==t&&(this._letterSpacing=t,this.domNode.style.letterSpacing=this._letterSpacing)}setClassName(e){this._className!==e&&(this._className=e,this.domNode.className=this._className)}toggleClassName(e,t){this.domNode.classList.toggle(e,t),this._className=this.domNode.className}setDisplay(e){this._display!==e&&(this._display=e,this.domNode.style.display=this._display)}setPosition(e){this._position!==e&&(this._position=e,this.domNode.style.position=this._position)}setVisibility(e){this._visibility!==e&&(this._visibility=e,this.domNode.style.visibility=this._visibility)}setColor(e){this._color!==e&&(this._color=e,this.domNode.style.color=this._color)}setBackgroundColor(e){this._backgroundColor!==e&&(this._backgroundColor=e,this.domNode.style.backgroundColor=this._backgroundColor)}setLayerHinting(e){this._layerHint!==e&&(this._layerHint=e,this.domNode.style.transform=this._layerHint?`translate3d(0px, 0px, 0px)`:``)}setBoxShadow(e){this._boxShadow!==e&&(this._boxShadow=e,this.domNode.style.boxShadow=e)}setContain(e){this._contain!==e&&(this._contain=e,this.domNode.style.contain=this._contain)}setAttribute(e,t){this.domNode.setAttribute(e,t)}removeAttribute(e){this.domNode.removeAttribute(e)}appendChild(e){this.domNode.appendChild(e.domNode)}removeChild(e){this.domNode.removeChild(e.domNode)}};function kr(e){return typeof e==`number`?`${e}px`:e}function Ar(e){return new Or(e)}var jr=class{constructor(){this._hooks=new yt,this._pointerMoveCallback=null,this._onStopCallback=null}dispose(){this.stopMonitoring(!1),this._hooks.dispose()}stopMonitoring(e,t){if(!this.isMonitoring())return;this._hooks.clear(),this._pointerMoveCallback=null;let n=this._onStopCallback;this._onStopCallback=null,e&&n&&n(t)}isMonitoring(){return!!this._pointerMoveCallback}startMonitoring(e,t,n,r,i){this.isMonitoring()&&this.stopMonitoring(!1),this._pointerMoveCallback=r,this._onStopCallback=i;let a=e;try{e.setPointerCapture(t),this._hooks.add(A(()=>{try{e.releasePointerCapture(t)}catch{}}))}catch{a=or(e)}this._hooks.add(F(a,I.POINTER_MOVE,e=>{if(e.buttons!==n){this.stopMonitoring(!0);return}e.preventDefault(),this._pointerMoveCallback(e)})),this._hooks.add(F(a,I.POINTER_UP,e=>this.stopMonitoring(!0)))}};function Mr(e,t,n){let r=null,i=null;if(typeof n.value==`function`?(r=`value`,i=n.value,i.length!==0&&console.warn(`Memoize should only be used in functions with zero parameters`)):typeof n.get==`function`&&(r=`get`,i=n.get),!i)throw Error(`not supported`);let a=`$memoize$${t}`;n[r]=function(...e){return this.hasOwnProperty(a)||Object.defineProperty(this,a,{configurable:!1,enumerable:!1,writable:!1,value:i.apply(this,e)}),this[a]}}var Nr;(e=>(e.Tap=`-xterm-gesturetap`,e.Change=`-xterm-gesturechange`,e.Start=`-xterm-gesturestart`,e.End=`-xterm-gesturesend`,e.Contextmenu=`-xterm-gesturecontextmenu`))(Nr||={});var Pr=class e extends j{constructor(){super(),this.dispatched=!1,this.targets=new Ct,this.ignoreTargets=new Ct,this.activeTouches={},this.handle=null,this._lastSetTapCountTime=0,this._register(N.runAndSubscribe(pr,({window:e,disposables:t})=>{t.add(F(e.document,`touchstart`,e=>this.onTouchStart(e),{passive:!1})),t.add(F(e.document,`touchend`,t=>this.onTouchEnd(e,t))),t.add(F(e.document,`touchmove`,e=>this.onTouchMove(e),{passive:!1}))},{window:xt,disposables:this._store}))}static addTarget(t){return e.isTouchDevice()?(e.INSTANCE||=ht(new e),A(e.INSTANCE.targets.push(t))):j.None}static ignoreTarget(t){return e.isTouchDevice()?(e.INSTANCE||=ht(new e),A(e.INSTANCE.ignoreTargets.push(t))):j.None}static isTouchDevice(){return`ontouchstart`in xt||navigator.maxTouchPoints>0}dispose(){this.handle&&=(this.handle.dispose(),null),super.dispose()}onTouchStart(e){let t=Date.now();this.handle&&=(this.handle.dispose(),null);for(let n=0,r=e.targetTouches.length;n=e.HOLD_DELAY&&Math.abs(s.initialPageX-tt(s.rollingPageX))<30&&Math.abs(s.initialPageY-tt(s.rollingPageY))<30){let e=this.newGestureEvent(Nr.Contextmenu,s.initialTarget);e.pageX=tt(s.rollingPageX),e.pageY=tt(s.rollingPageY),this.dispatchEvent(e)}else if(i===1){let e=tt(s.rollingPageX),n=tt(s.rollingPageY),i=tt(s.rollingTimestamps)-s.rollingTimestamps[0],a=e-s.rollingPageX[0],o=n-s.rollingPageY[0],c=[...this.targets].filter(e=>s.initialTarget instanceof Node&&e.contains(s.initialTarget));this.inertia(t,c,r,Math.abs(a)/i,a>0?1:-1,e,Math.abs(o)/i,o>0?1:-1,n)}this.dispatchEvent(this.newGestureEvent(Nr.End,s.initialTarget)),delete this.activeTouches[o.identifier]}this.dispatched&&=(n.preventDefault(),n.stopPropagation(),!1)}newGestureEvent(e,t){let n=document.createEvent(`CustomEvent`);return n.initEvent(e,!1,!0),n.initialTarget=t,n.tapCount=0,n}dispatchEvent(t){if(t.type===Nr.Tap){let n=new Date().getTime(),r=0;r=n-this._lastSetTapCountTime>e.CLEAR_TAP_COUNT_TIME?1:2,this._lastSetTapCountTime=n,t.tapCount=r}else(t.type===Nr.Change||t.type===Nr.Contextmenu)&&(this._lastSetTapCountTime=0);if(t.initialTarget instanceof Node){for(let e of this.ignoreTargets)if(e.contains(t.initialTarget))return;let e=[];for(let n of this.targets)if(n.contains(t.initialTarget)){let r=0,i=t.initialTarget;for(;i&&i!==n;)r++,i=i.parentElement;e.push([r,n])}e.sort((e,t)=>e[0]-t[0]);for(let[n,r]of e)r.dispatchEvent(t),this.dispatched=!0}}inertia(t,n,r,i,a,o,s,c,l){this.handle=br(t,()=>{let u=Date.now(),d=u-r,f=0,p=0,m=!0;i+=e.SCROLL_FRICTION*d,s+=e.SCROLL_FRICTION*d,i>0&&(m=!1,f=a*i*d),s>0&&(m=!1,p=c*s*d);let h=this.newGestureEvent(Nr.Change);h.translationX=f,h.translationY=p,n.forEach(e=>e.dispatchEvent(h)),m||this.inertia(t,n,u,i,a,o+f,s,c,l+p)})}onTouchMove(e){let t=Date.now();for(let n=0,r=e.changedTouches.length;n3&&(i.rollingPageX.shift(),i.rollingPageY.shift(),i.rollingTimestamps.shift()),i.rollingPageX.push(r.pageX),i.rollingPageY.push(r.pageY),i.rollingTimestamps.push(t)}this.dispatched&&=(e.preventDefault(),e.stopPropagation(),!1)}};Pr.SCROLL_FRICTION=-.005,Pr.HOLD_DELAY=700,Pr.CLEAR_TAP_COUNT_TIME=400,x([Mr],Pr,`isTouchDevice`,1);var Fr=Pr,Ir=class extends j{onclick(e,t){this._register(F(e,I.CLICK,n=>t(new Fn(or(e),n))))}onmousedown(e,t){this._register(F(e,I.MOUSE_DOWN,n=>t(new Fn(or(e),n))))}onmouseover(e,t){this._register(F(e,I.MOUSE_OVER,n=>t(new Fn(or(e),n))))}onmouseleave(e,t){this._register(F(e,I.MOUSE_LEAVE,n=>t(new Fn(or(e),n))))}onkeydown(e,t){this._register(F(e,I.KEY_DOWN,e=>t(new jn(e))))}onkeyup(e,t){this._register(F(e,I.KEY_UP,e=>t(new jn(e))))}oninput(e,t){this._register(F(e,I.INPUT,t))}onblur(e,t){this._register(F(e,I.BLUR,t))}onfocus(e,t){this._register(F(e,I.FOCUS,t))}onchange(e,t){this._register(F(e,I.CHANGE,t))}ignoreGesture(e){return Fr.ignoreTarget(e)}},Lr=11,Rr=class extends Ir{constructor(e){super(),this._onActivate=e.onActivate,this.bgDomNode=document.createElement(`div`),this.bgDomNode.className=`arrow-background`,this.bgDomNode.style.position=`absolute`,this.bgDomNode.style.width=e.bgWidth+`px`,this.bgDomNode.style.height=e.bgHeight+`px`,typeof e.top<`u`&&(this.bgDomNode.style.top=`0px`),typeof e.left<`u`&&(this.bgDomNode.style.left=`0px`),typeof e.bottom<`u`&&(this.bgDomNode.style.bottom=`0px`),typeof e.right<`u`&&(this.bgDomNode.style.right=`0px`),this.domNode=document.createElement(`div`),this.domNode.className=e.className,this.domNode.style.position=`absolute`,this.domNode.style.width=Lr+`px`,this.domNode.style.height=Lr+`px`,typeof e.top<`u`&&(this.domNode.style.top=e.top+`px`),typeof e.left<`u`&&(this.domNode.style.left=e.left+`px`),typeof e.bottom<`u`&&(this.domNode.style.bottom=e.bottom+`px`),typeof e.right<`u`&&(this.domNode.style.right=e.right+`px`),this._pointerMoveMonitor=this._register(new jr),this._register(yr(this.bgDomNode,I.POINTER_DOWN,e=>this._arrowPointerDown(e))),this._register(yr(this.domNode,I.POINTER_DOWN,e=>this._arrowPointerDown(e))),this._pointerdownRepeatTimer=this._register(new xr),this._pointerdownScheduleRepeatTimer=this._register(new Bn)}_arrowPointerDown(e){!e.target||!(e.target instanceof Element)||(this._onActivate(),this._pointerdownRepeatTimer.cancel(),this._pointerdownScheduleRepeatTimer.cancelAndSet(()=>{this._pointerdownRepeatTimer.cancelAndSet(()=>this._onActivate(),1e3/24,or(e))},200),this._pointerMoveMonitor.startMonitoring(e.target,e.pointerId,e.buttons,e=>{},()=>{this._pointerdownRepeatTimer.cancel(),this._pointerdownScheduleRepeatTimer.cancel()}),e.preventDefault())}},zr=class e{constructor(e,t,n,r,i,a,o){this._forceIntegerValues=e,this._scrollStateBrand=void 0,this._forceIntegerValues&&(t|=0,n|=0,r|=0,i|=0,a|=0,o|=0),this.rawScrollLeft=r,this.rawScrollTop=o,t<0&&(t=0),r+t>n&&(r=n-t),r<0&&(r=0),i<0&&(i=0),o+i>a&&(o=a-i),o<0&&(o=0),this.width=t,this.scrollWidth=n,this.scrollLeft=r,this.height=i,this.scrollHeight=a,this.scrollTop=o}equals(e){return this.rawScrollLeft===e.rawScrollLeft&&this.rawScrollTop===e.rawScrollTop&&this.width===e.width&&this.scrollWidth===e.scrollWidth&&this.scrollLeft===e.scrollLeft&&this.height===e.height&&this.scrollHeight===e.scrollHeight&&this.scrollTop===e.scrollTop}withScrollDimensions(t,n){return new e(this._forceIntegerValues,typeof t.width<`u`?t.width:this.width,typeof t.scrollWidth<`u`?t.scrollWidth:this.scrollWidth,n?this.rawScrollLeft:this.scrollLeft,typeof t.height<`u`?t.height:this.height,typeof t.scrollHeight<`u`?t.scrollHeight:this.scrollHeight,n?this.rawScrollTop:this.scrollTop)}withScrollPosition(t){return new e(this._forceIntegerValues,this.width,this.scrollWidth,typeof t.scrollLeft<`u`?t.scrollLeft:this.rawScrollLeft,this.height,this.scrollHeight,typeof t.scrollTop<`u`?t.scrollTop:this.rawScrollTop)}createScrollEvent(e,t){let n=this.width!==e.width,r=this.scrollWidth!==e.scrollWidth,i=this.scrollLeft!==e.scrollLeft,a=this.height!==e.height,o=this.scrollHeight!==e.scrollHeight,s=this.scrollTop!==e.scrollTop;return{inSmoothScrolling:t,oldWidth:e.width,oldScrollWidth:e.scrollWidth,oldScrollLeft:e.scrollLeft,width:this.width,scrollWidth:this.scrollWidth,scrollLeft:this.scrollLeft,oldHeight:e.height,oldScrollHeight:e.scrollHeight,oldScrollTop:e.scrollTop,height:this.height,scrollHeight:this.scrollHeight,scrollTop:this.scrollTop,widthChanged:n,scrollWidthChanged:r,scrollLeftChanged:i,heightChanged:a,scrollHeightChanged:o,scrollTopChanged:s}}},Br=class extends j{constructor(e){super(),this._scrollableBrand=void 0,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._smoothScrollDuration=e.smoothScrollDuration,this._scheduleAtNextAnimationFrame=e.scheduleAtNextAnimationFrame,this._state=new zr(e.forceIntegerValues,0,0,0,0,0,0),this._smoothScrolling=null}dispose(){this._smoothScrolling&&=(this._smoothScrolling.dispose(),null),super.dispose()}setSmoothScrollDuration(e){this._smoothScrollDuration=e}validateScrollPosition(e){return this._state.withScrollPosition(e)}getScrollDimensions(){return this._state}setScrollDimensions(e,t){let n=this._state.withScrollDimensions(e,t);this._setState(n,!!this._smoothScrolling),this._smoothScrolling?.acceptScrollDimensions(this._state)}getFutureScrollPosition(){return this._smoothScrolling?this._smoothScrolling.to:this._state}getCurrentScrollPosition(){return this._state}setScrollPositionNow(e){let t=this._state.withScrollPosition(e);this._smoothScrolling&&=(this._smoothScrolling.dispose(),null),this._setState(t,!1)}setScrollPositionSmooth(e,t){if(this._smoothScrollDuration===0)return this.setScrollPositionNow(e);if(this._smoothScrolling){e={scrollLeft:typeof e.scrollLeft>`u`?this._smoothScrolling.to.scrollLeft:e.scrollLeft,scrollTop:typeof e.scrollTop>`u`?this._smoothScrolling.to.scrollTop:e.scrollTop};let n=this._state.withScrollPosition(e);if(this._smoothScrolling.to.scrollLeft===n.scrollLeft&&this._smoothScrolling.to.scrollTop===n.scrollTop)return;let r;r=t?new Wr(this._smoothScrolling.from,n,this._smoothScrolling.startTime,this._smoothScrolling.duration):this._smoothScrolling.combine(this._state,n,this._smoothScrollDuration),this._smoothScrolling.dispose(),this._smoothScrolling=r}else{let t=this._state.withScrollPosition(e);this._smoothScrolling=Wr.start(this._state,t,this._smoothScrollDuration)}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{this._smoothScrolling&&(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}hasPendingScrollAnimation(){return!!this._smoothScrolling}_performSmoothScrolling(){if(!this._smoothScrolling)return;let e=this._smoothScrolling.tick(),t=this._state.withScrollPosition(e);if(this._setState(t,!0),this._smoothScrolling){if(e.isDone){this._smoothScrolling.dispose(),this._smoothScrolling=null;return}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{this._smoothScrolling&&(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}}_setState(e,t){let n=this._state;n.equals(e)||(this._state=e,this._onScroll.fire(this._state.createScrollEvent(n,t)))}},Vr=class{constructor(e,t,n){this.scrollLeft=e,this.scrollTop=t,this.isDone=n}};function Hr(e,t){let n=t-e;return function(t){return e+n*Kr(t)}}function Ur(e,t,n){return function(r){return r2.5*n){let r,i;return e{this._domNode?.setClassName(this._visibleClassName)},0))}_hide(e){this._revealTimer.cancel(),this._isVisible&&(this._isVisible=!1,this._domNode?.setClassName(this._invisibleClassName+(e?` fade`:``)))}},Jr=140,Yr=class extends Ir{constructor(e){super(),this._lazyRender=e.lazyRender,this._host=e.host,this._scrollable=e.scrollable,this._scrollByPage=e.scrollByPage,this._scrollbarState=e.scrollbarState,this._visibilityController=this._register(new qr(e.visibility,`visible scrollbar `+e.extraScrollbarClassName,`invisible scrollbar `+e.extraScrollbarClassName)),this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._pointerMoveMonitor=this._register(new jr),this._shouldRender=!0,this.domNode=Ar(document.createElement(`div`)),this.domNode.setAttribute(`role`,`presentation`),this.domNode.setAttribute(`aria-hidden`,`true`),this._visibilityController.setDomNode(this.domNode),this.domNode.setPosition(`absolute`),this._register(F(this.domNode.domNode,I.POINTER_DOWN,e=>this._domNodePointerDown(e)))}_createArrow(e){let t=this._register(new Rr(e));this.domNode.domNode.appendChild(t.bgDomNode),this.domNode.domNode.appendChild(t.domNode)}_createSlider(e,t,n,r){this.slider=Ar(document.createElement(`div`)),this.slider.setClassName(`slider`),this.slider.setPosition(`absolute`),this.slider.setTop(e),this.slider.setLeft(t),typeof n==`number`&&this.slider.setWidth(n),typeof r==`number`&&this.slider.setHeight(r),this.slider.setLayerHinting(!0),this.slider.setContain(`strict`),this.domNode.domNode.appendChild(this.slider.domNode),this._register(F(this.slider.domNode,I.POINTER_DOWN,e=>{e.button===0&&(e.preventDefault(),this._sliderPointerDown(e))})),this.onclick(this.slider.domNode,e=>{e.leftButton&&e.stopPropagation()})}_onElementSize(e){return this._scrollbarState.setVisibleSize(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollSize(e){return this._scrollbarState.setScrollSize(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollPosition(e){return this._scrollbarState.setScrollPosition(e)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}beginReveal(){this._visibilityController.setShouldBeVisible(!0)}beginHide(){this._visibilityController.setShouldBeVisible(!1)}render(){this._shouldRender&&(this._shouldRender=!1,this._renderDomNode(this._scrollbarState.getRectangleLargeSize(),this._scrollbarState.getRectangleSmallSize()),this._updateSlider(this._scrollbarState.getSliderSize(),this._scrollbarState.getArrowSize()+this._scrollbarState.getSliderPosition()))}_domNodePointerDown(e){e.target===this.domNode.domNode&&this._onPointerDown(e)}delegatePointerDown(e){let t=this.domNode.domNode.getClientRects()[0].top,n=t+this._scrollbarState.getSliderPosition(),r=t+this._scrollbarState.getSliderPosition()+this._scrollbarState.getSliderSize(),i=this._sliderPointerPosition(e);n<=i&&i<=r?e.button===0&&(e.preventDefault(),this._sliderPointerDown(e)):this._onPointerDown(e)}_onPointerDown(e){let t,n;if(e.target===this.domNode.domNode&&typeof e.offsetX==`number`&&typeof e.offsetY==`number`)t=e.offsetX,n=e.offsetY;else{let r=wr(this.domNode.domNode);t=e.pageX-r.left,n=e.pageY-r.top}let r=this._pointerDownRelativePosition(t,n);this._setDesiredScrollPositionNow(this._scrollByPage?this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(r):this._scrollbarState.getDesiredScrollPositionFromOffset(r)),e.button===0&&(e.preventDefault(),this._sliderPointerDown(e))}_sliderPointerDown(e){if(!e.target||!(e.target instanceof Element))return;let t=this._sliderPointerPosition(e),n=this._sliderOrthogonalPointerPosition(e),r=this._scrollbarState.clone();this.slider.toggleClassName(`active`,!0),this._pointerMoveMonitor.startMonitoring(e.target,e.pointerId,e.buttons,e=>{let i=this._sliderOrthogonalPointerPosition(e),a=Math.abs(i-n);if(cn&&a>Jr){this._setDesiredScrollPositionNow(r.getScrollPosition());return}let o=this._sliderPointerPosition(e)-t;this._setDesiredScrollPositionNow(r.getDesiredScrollPositionFromDelta(o))},()=>{this.slider.toggleClassName(`active`,!1),this._host.onDragEnd()}),this._host.onDragStart()}_setDesiredScrollPositionNow(e){let t={};this.writeScrollPosition(t,e),this._scrollable.setScrollPositionNow(t)}updateScrollbarSize(e){this._updateScrollbarSize(e),this._scrollbarState.setScrollbarSize(e),this._shouldRender=!0,this._lazyRender||this.render()}isNeeded(){return this._scrollbarState.isNeeded()}},Xr=class e{constructor(e,t,n,r,i,a){this._scrollbarSize=Math.round(t),this._oppositeScrollbarSize=Math.round(n),this._arrowSize=Math.round(e),this._visibleSize=r,this._scrollSize=i,this._scrollPosition=a,this._computedAvailableSize=0,this._computedIsNeeded=!1,this._computedSliderSize=0,this._computedSliderRatio=0,this._computedSliderPosition=0,this._refreshComputedValues()}clone(){return new e(this._arrowSize,this._scrollbarSize,this._oppositeScrollbarSize,this._visibleSize,this._scrollSize,this._scrollPosition)}setVisibleSize(e){let t=Math.round(e);return this._visibleSize===t?!1:(this._visibleSize=t,this._refreshComputedValues(),!0)}setScrollSize(e){let t=Math.round(e);return this._scrollSize===t?!1:(this._scrollSize=t,this._refreshComputedValues(),!0)}setScrollPosition(e){let t=Math.round(e);return this._scrollPosition===t?!1:(this._scrollPosition=t,this._refreshComputedValues(),!0)}setScrollbarSize(e){this._scrollbarSize=Math.round(e)}setOppositeScrollbarSize(e){this._oppositeScrollbarSize=Math.round(e)}static _computeValues(e,t,n,r,i){let a=Math.max(0,n-e),o=Math.max(0,a-2*t),s=r>0&&r>n;if(!s)return{computedAvailableSize:Math.round(a),computedIsNeeded:s,computedSliderSize:Math.round(o),computedSliderRatio:0,computedSliderPosition:0};let c=Math.round(Math.max(20,Math.floor(n*o/r))),l=(o-c)/(r-n),u=i*l;return{computedAvailableSize:Math.round(a),computedIsNeeded:s,computedSliderSize:Math.round(c),computedSliderRatio:l,computedSliderPosition:Math.round(u)}}_refreshComputedValues(){let t=e._computeValues(this._oppositeScrollbarSize,this._arrowSize,this._visibleSize,this._scrollSize,this._scrollPosition);this._computedAvailableSize=t.computedAvailableSize,this._computedIsNeeded=t.computedIsNeeded,this._computedSliderSize=t.computedSliderSize,this._computedSliderRatio=t.computedSliderRatio,this._computedSliderPosition=t.computedSliderPosition}getArrowSize(){return this._arrowSize}getScrollPosition(){return this._scrollPosition}getRectangleLargeSize(){return this._computedAvailableSize}getRectangleSmallSize(){return this._scrollbarSize}isNeeded(){return this._computedIsNeeded}getSliderSize(){return this._computedSliderSize}getSliderPosition(){return this._computedSliderPosition}getDesiredScrollPositionFromOffset(e){if(!this._computedIsNeeded)return 0;let t=e-this._arrowSize-this._computedSliderSize/2;return Math.round(t/this._computedSliderRatio)}getDesiredScrollPositionFromOffsetPaged(e){if(!this._computedIsNeeded)return 0;let t=e-this._arrowSize,n=this._scrollPosition;return t0&&Math.abs(e.deltaY)>0)return 1;let n=.5;if((!this._isAlmostInt(e.deltaX)||!this._isAlmostInt(e.deltaY))&&(n+=.25),t){let r=Math.abs(e.deltaX),i=Math.abs(e.deltaY),a=Math.abs(t.deltaX),o=Math.abs(t.deltaY),s=Math.max(Math.min(r,a),1),c=Math.max(Math.min(i,o),1),l=Math.max(r,a),u=Math.max(i,o);l%s===0&&u%c===0&&(n-=.5)}return Math.min(Math.max(n,0),1)}_isAlmostInt(e){return Math.abs(Math.round(e)-e)<.01}};ri.INSTANCE=new ri;var ii=ri,ai=class extends Ir{constructor(e,t,n){super(),this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._onWillScroll=this._register(new P),this.onWillScroll=this._onWillScroll.event,this._options=si(t),this._scrollable=n,this._register(this._scrollable.onScroll(e=>{this._onWillScroll.fire(e),this._onDidScroll(e),this._onScroll.fire(e)}));let r={onMouseWheel:e=>this._onMouseWheel(e),onDragStart:()=>this._onDragStart(),onDragEnd:()=>this._onDragEnd()};this._verticalScrollbar=this._register(new Qr(this._scrollable,this._options,r)),this._horizontalScrollbar=this._register(new Zr(this._scrollable,this._options,r)),this._domNode=document.createElement(`div`),this._domNode.className=`xterm-scrollable-element `+this._options.className,this._domNode.setAttribute(`role`,`presentation`),this._domNode.style.position=`relative`,this._domNode.appendChild(e),this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode),this._domNode.appendChild(this._verticalScrollbar.domNode.domNode),this._options.useShadows?(this._leftShadowDomNode=Ar(document.createElement(`div`)),this._leftShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._leftShadowDomNode.domNode),this._topShadowDomNode=Ar(document.createElement(`div`)),this._topShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._topShadowDomNode.domNode),this._topLeftShadowDomNode=Ar(document.createElement(`div`)),this._topLeftShadowDomNode.setClassName(`shadow`),this._domNode.appendChild(this._topLeftShadowDomNode.domNode)):(this._leftShadowDomNode=null,this._topShadowDomNode=null,this._topLeftShadowDomNode=null),this._listenOnDomNode=this._options.listenOnDomNode||this._domNode,this._mouseWheelToDispose=[],this._setListeningToMouseWheel(this._options.handleMouseWheel),this.onmouseover(this._listenOnDomNode,e=>this._onMouseOver(e)),this.onmouseleave(this._listenOnDomNode,e=>this._onMouseLeave(e)),this._hideTimeout=this._register(new Bn),this._isDragging=!1,this._mouseIsOver=!1,this._shouldRender=!0,this._revealOnScroll=!0}get options(){return this._options}dispose(){this._mouseWheelToDispose=gt(this._mouseWheelToDispose),super.dispose()}getDomNode(){return this._domNode}getOverviewRulerLayoutInfo(){return{parent:this._domNode,insertBefore:this._verticalScrollbar.domNode.domNode}}delegateVerticalScrollbarPointerDown(e){this._verticalScrollbar.delegatePointerDown(e)}getScrollDimensions(){return this._scrollable.getScrollDimensions()}setScrollDimensions(e){this._scrollable.setScrollDimensions(e,!1)}updateClassName(e){this._options.className=e,ln&&(this._options.className+=` mac`),this._domNode.className=`xterm-scrollable-element `+this._options.className}updateOptions(e){typeof e.handleMouseWheel<`u`&&(this._options.handleMouseWheel=e.handleMouseWheel,this._setListeningToMouseWheel(this._options.handleMouseWheel)),typeof e.mouseWheelScrollSensitivity<`u`&&(this._options.mouseWheelScrollSensitivity=e.mouseWheelScrollSensitivity),typeof e.fastScrollSensitivity<`u`&&(this._options.fastScrollSensitivity=e.fastScrollSensitivity),typeof e.scrollPredominantAxis<`u`&&(this._options.scrollPredominantAxis=e.scrollPredominantAxis),typeof e.horizontal<`u`&&(this._options.horizontal=e.horizontal),typeof e.vertical<`u`&&(this._options.vertical=e.vertical),typeof e.horizontalScrollbarSize<`u`&&(this._options.horizontalScrollbarSize=e.horizontalScrollbarSize),typeof e.verticalScrollbarSize<`u`&&(this._options.verticalScrollbarSize=e.verticalScrollbarSize),typeof e.scrollByPage<`u`&&(this._options.scrollByPage=e.scrollByPage),this._horizontalScrollbar.updateOptions(this._options),this._verticalScrollbar.updateOptions(this._options),this._options.lazyRender||this._render()}setRevealOnScroll(e){this._revealOnScroll=e}delegateScrollFromMouseWheelEvent(e){this._onMouseWheel(new In(e))}_setListeningToMouseWheel(e){this._mouseWheelToDispose.length>0!==e&&(this._mouseWheelToDispose=gt(this._mouseWheelToDispose),e)&&this._mouseWheelToDispose.push(F(this._listenOnDomNode,I.MOUSE_WHEEL,e=>{this._onMouseWheel(new In(e))},{passive:!1}))}_onMouseWheel(e){if(e.browserEvent?.defaultPrevented)return;let t=ii.INSTANCE;ti&&t.acceptStandardWheelEvent(e);let n=!1;if(e.deltaY||e.deltaX){let r=e.deltaY*this._options.mouseWheelScrollSensitivity,i=e.deltaX*this._options.mouseWheelScrollSensitivity;this._options.scrollPredominantAxis&&(this._options.scrollYToX&&i+r===0?i=r=0:Math.abs(r)>=Math.abs(i)?i=0:r=0),this._options.flipAxes&&([r,i]=[i,r]);let a=!ln&&e.browserEvent&&e.browserEvent.shiftKey;(this._options.scrollYToX||a)&&!i&&(i=r,r=0),e.browserEvent&&e.browserEvent.altKey&&(i*=this._options.fastScrollSensitivity,r*=this._options.fastScrollSensitivity);let o=this._scrollable.getFutureScrollPosition(),s={};if(r){let e=ei*r,t=o.scrollTop-(e<0?Math.floor(e):Math.ceil(e));this._verticalScrollbar.writeScrollPosition(s,t)}if(i){let e=ei*i,t=o.scrollLeft-(e<0?Math.floor(e):Math.ceil(e));this._horizontalScrollbar.writeScrollPosition(s,t)}s=this._scrollable.validateScrollPosition(s),(o.scrollLeft!==s.scrollLeft||o.scrollTop!==s.scrollTop)&&(ti&&this._options.mouseWheelSmoothScroll&&t.isPhysicalMouseWheel()?this._scrollable.setScrollPositionSmooth(s):this._scrollable.setScrollPositionNow(s),n=!0)}let r=n;!r&&this._options.alwaysConsumeMouseWheel&&(r=!0),!r&&this._options.consumeMouseWheelIfScrollbarIsNeeded&&(this._verticalScrollbar.isNeeded()||this._horizontalScrollbar.isNeeded())&&(r=!0),r&&(e.preventDefault(),e.stopPropagation())}_onDidScroll(e){this._shouldRender=this._horizontalScrollbar.onDidScroll(e)||this._shouldRender,this._shouldRender=this._verticalScrollbar.onDidScroll(e)||this._shouldRender,this._options.useShadows&&(this._shouldRender=!0),this._revealOnScroll&&this._reveal(),this._options.lazyRender||this._render()}renderNow(){if(!this._options.lazyRender)throw Error("Please use `lazyRender` together with `renderNow`!");this._render()}_render(){if(this._shouldRender&&(this._shouldRender=!1,this._horizontalScrollbar.render(),this._verticalScrollbar.render(),this._options.useShadows)){let e=this._scrollable.getCurrentScrollPosition(),t=e.scrollTop>0,n=e.scrollLeft>0,r=n?` left`:``,i=t?` top`:``,a=n||t?` top-left-corner`:``;this._leftShadowDomNode.setClassName(`shadow${r}`),this._topShadowDomNode.setClassName(`shadow${i}`),this._topLeftShadowDomNode.setClassName(`shadow${a}${i}${r}`)}}_onDragStart(){this._isDragging=!0,this._reveal()}_onDragEnd(){this._isDragging=!1,this._hide()}_onMouseLeave(e){this._mouseIsOver=!1,this._hide()}_onMouseOver(e){this._mouseIsOver=!0,this._reveal()}_reveal(){this._verticalScrollbar.beginReveal(),this._horizontalScrollbar.beginReveal(),this._scheduleHide()}_hide(){!this._mouseIsOver&&!this._isDragging&&(this._verticalScrollbar.beginHide(),this._horizontalScrollbar.beginHide())}_scheduleHide(){!this._mouseIsOver&&!this._isDragging&&this._hideTimeout.cancelAndSet(()=>this._hide(),$r)}},oi=class extends ai{constructor(e,t,n){super(e,t,n)}setScrollPosition(e){e.reuseAnimation?this._scrollable.setScrollPositionSmooth(e,e.reuseAnimation):this._scrollable.setScrollPositionNow(e)}getScrollPosition(){return this._scrollable.getCurrentScrollPosition()}};function si(e){let t={lazyRender:typeof e.lazyRender<`u`&&e.lazyRender,className:typeof e.className<`u`?e.className:``,useShadows:typeof e.useShadows<`u`?e.useShadows:!0,handleMouseWheel:typeof e.handleMouseWheel<`u`?e.handleMouseWheel:!0,flipAxes:typeof e.flipAxes<`u`&&e.flipAxes,consumeMouseWheelIfScrollbarIsNeeded:typeof e.consumeMouseWheelIfScrollbarIsNeeded<`u`&&e.consumeMouseWheelIfScrollbarIsNeeded,alwaysConsumeMouseWheel:typeof e.alwaysConsumeMouseWheel<`u`&&e.alwaysConsumeMouseWheel,scrollYToX:typeof e.scrollYToX<`u`&&e.scrollYToX,mouseWheelScrollSensitivity:typeof e.mouseWheelScrollSensitivity<`u`?e.mouseWheelScrollSensitivity:1,fastScrollSensitivity:typeof e.fastScrollSensitivity<`u`?e.fastScrollSensitivity:5,scrollPredominantAxis:typeof e.scrollPredominantAxis<`u`?e.scrollPredominantAxis:!0,mouseWheelSmoothScroll:typeof e.mouseWheelSmoothScroll<`u`?e.mouseWheelSmoothScroll:!0,arrowSize:typeof e.arrowSize<`u`?e.arrowSize:11,listenOnDomNode:typeof e.listenOnDomNode<`u`?e.listenOnDomNode:null,horizontal:typeof e.horizontal<`u`?e.horizontal:1,horizontalScrollbarSize:typeof e.horizontalScrollbarSize<`u`?e.horizontalScrollbarSize:10,horizontalSliderSize:typeof e.horizontalSliderSize<`u`?e.horizontalSliderSize:0,horizontalHasArrows:typeof e.horizontalHasArrows<`u`&&e.horizontalHasArrows,vertical:typeof e.vertical<`u`?e.vertical:1,verticalScrollbarSize:typeof e.verticalScrollbarSize<`u`?e.verticalScrollbarSize:10,verticalHasArrows:typeof e.verticalHasArrows<`u`&&e.verticalHasArrows,verticalSliderSize:typeof e.verticalSliderSize<`u`?e.verticalSliderSize:0,scrollByPage:typeof e.scrollByPage<`u`&&e.scrollByPage};return t.horizontalSliderSize=typeof e.horizontalSliderSize<`u`?e.horizontalSliderSize:t.horizontalScrollbarSize,t.verticalSliderSize=typeof e.verticalSliderSize<`u`?e.verticalSliderSize:t.verticalScrollbarSize,ln&&(t.className+=` mac`),t}var ci=class extends j{constructor(e,t,n,r,i,a,o,s){super(),this._bufferService=n,this._optionsService=o,this._renderService=s,this._onRequestScrollLines=this._register(new P),this.onRequestScrollLines=this._onRequestScrollLines.event,this._isSyncing=!1,this._isHandlingScroll=!1,this._suppressOnScrollHandler=!1;let c=this._register(new Br({forceIntegerValues:!1,smoothScrollDuration:this._optionsService.rawOptions.smoothScrollDuration,scheduleAtNextAnimationFrame:e=>br(r.window,e)}));this._register(this._optionsService.onSpecificOptionChange(`smoothScrollDuration`,()=>{c.setSmoothScrollDuration(this._optionsService.rawOptions.smoothScrollDuration)})),this._scrollableElement=this._register(new oi(t,{vertical:1,horizontal:2,useShadows:!1,mouseWheelSmoothScroll:!0,...this._getChangeOptions()},c)),this._register(this._optionsService.onMultipleOptionChange([`scrollSensitivity`,`fastScrollSensitivity`,`overviewRuler`],()=>this._scrollableElement.updateOptions(this._getChangeOptions()))),this._register(i.onProtocolChange(e=>{this._scrollableElement.updateOptions({handleMouseWheel:!(e&16)})})),this._scrollableElement.setScrollDimensions({height:0,scrollHeight:0}),this._register(N.runAndSubscribe(a.onChangeColors,()=>{this._scrollableElement.getDomNode().style.backgroundColor=a.colors.background.css})),e.appendChild(this._scrollableElement.getDomNode()),this._register(A(()=>this._scrollableElement.getDomNode().remove())),this._styleElement=r.mainDocument.createElement(`style`),t.appendChild(this._styleElement),this._register(A(()=>this._styleElement.remove())),this._register(N.runAndSubscribe(a.onChangeColors,()=>{this._styleElement.textContent=[`.xterm .xterm-scrollable-element > .scrollbar > .slider {`,` background: ${a.colors.scrollbarSliderBackground.css};`,`}`,`.xterm .xterm-scrollable-element > .scrollbar > .slider:hover {`,` background: ${a.colors.scrollbarSliderHoverBackground.css};`,`}`,`.xterm .xterm-scrollable-element > .scrollbar > .slider.active {`,` background: ${a.colors.scrollbarSliderActiveBackground.css};`,`}`].join(` +`)})),this._register(this._bufferService.onResize(()=>this.queueSync())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._latestYDisp=void 0,this.queueSync()})),this._register(this._bufferService.onScroll(()=>this._sync())),this._register(this._scrollableElement.onScroll(e=>this._handleScroll(e)))}scrollLines(e){let t=this._scrollableElement.getScrollPosition();this._scrollableElement.setScrollPosition({reuseAnimation:!0,scrollTop:t.scrollTop+e*this._renderService.dimensions.css.cell.height})}scrollToLine(e,t){t&&(this._latestYDisp=e),this._scrollableElement.setScrollPosition({reuseAnimation:!t,scrollTop:e*this._renderService.dimensions.css.cell.height})}_getChangeOptions(){return{mouseWheelScrollSensitivity:this._optionsService.rawOptions.scrollSensitivity,fastScrollSensitivity:this._optionsService.rawOptions.fastScrollSensitivity,verticalScrollbarSize:this._optionsService.rawOptions.overviewRuler?.width||14}}queueSync(e){e!==void 0&&(this._latestYDisp=e),this._queuedAnimationFrame===void 0&&(this._queuedAnimationFrame=this._renderService.addRefreshCallback(()=>{this._queuedAnimationFrame=void 0,this._sync(this._latestYDisp)}))}_sync(e=this._bufferService.buffer.ydisp){!this._renderService||this._isSyncing||(this._isSyncing=!0,this._suppressOnScrollHandler=!0,this._scrollableElement.setScrollDimensions({height:this._renderService.dimensions.css.canvas.height,scrollHeight:this._renderService.dimensions.css.cell.height*this._bufferService.buffer.lines.length}),this._suppressOnScrollHandler=!1,e!==this._latestYDisp&&this._scrollableElement.setScrollPosition({scrollTop:e*this._renderService.dimensions.css.cell.height}),this._isSyncing=!1)}_handleScroll(e){if(!this._renderService||this._isHandlingScroll||this._suppressOnScrollHandler)return;this._isHandlingScroll=!0;let t=Math.round(e.scrollTop/this._renderService.dimensions.css.cell.height),n=t-this._bufferService.buffer.ydisp;n!==0&&(this._latestYDisp=t,this._onRequestScrollLines.fire(n)),this._isHandlingScroll=!1}};ci=x([S(2,O),S(3,Re),S(4,De),S(5,Ue),S(6,k),S(7,Be)],ci);var li=class extends j{constructor(e,t,n,r,i){super(),this._screenElement=e,this._bufferService=t,this._coreBrowserService=n,this._decorationService=r,this._renderService=i,this._decorationElements=new Map,this._altBufferIsActive=!1,this._dimensionsChanged=!1,this._container=document.createElement(`div`),this._container.classList.add(`xterm-decoration-container`),this._screenElement.appendChild(this._container),this._register(this._renderService.onRenderedViewportChange(()=>this._doRefreshDecorations())),this._register(this._renderService.onDimensionsChange(()=>{this._dimensionsChanged=!0,this._queueRefresh()})),this._register(this._coreBrowserService.onDprChange(()=>this._queueRefresh())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._altBufferIsActive=this._bufferService.buffer===this._bufferService.buffers.alt})),this._register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh())),this._register(this._decorationService.onDecorationRemoved(e=>this._removeDecoration(e))),this._register(A(()=>{this._container.remove(),this._decorationElements.clear()}))}_queueRefresh(){this._animationFrame===void 0&&(this._animationFrame=this._renderService.addRefreshCallback(()=>{this._doRefreshDecorations(),this._animationFrame=void 0}))}_doRefreshDecorations(){for(let e of this._decorationService.decorations)this._renderDecoration(e);this._dimensionsChanged=!1}_renderDecoration(e){this._refreshStyle(e),this._dimensionsChanged&&this._refreshXPosition(e)}_createElement(e){let t=this._coreBrowserService.mainDocument.createElement(`div`);t.classList.add(`xterm-decoration`),t.classList.toggle(`xterm-decoration-top-layer`,e?.options?.layer===`top`),t.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,t.style.height=`${(e.options.height||1)*this._renderService.dimensions.css.cell.height}px`,t.style.top=`${(e.marker.line-this._bufferService.buffers.active.ydisp)*this._renderService.dimensions.css.cell.height}px`,t.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`;let n=e.options.x??0;return n&&n>this._bufferService.cols&&(t.style.display=`none`),this._refreshXPosition(e,t),t}_refreshStyle(e){let t=e.marker.line-this._bufferService.buffers.active.ydisp;if(t<0||t>=this._bufferService.rows)e.element&&(e.element.style.display=`none`,e.onRenderEmitter.fire(e.element));else{let n=this._decorationElements.get(e);n||(n=this._createElement(e),e.element=n,this._decorationElements.set(e,n),this._container.appendChild(n),e.onDispose(()=>{this._decorationElements.delete(e),n.remove()})),n.style.display=this._altBufferIsActive?`none`:`block`,this._altBufferIsActive||(n.style.width=`${Math.round((e.options.width||1)*this._renderService.dimensions.css.cell.width)}px`,n.style.height=`${(e.options.height||1)*this._renderService.dimensions.css.cell.height}px`,n.style.top=`${t*this._renderService.dimensions.css.cell.height}px`,n.style.lineHeight=`${this._renderService.dimensions.css.cell.height}px`),e.onRenderEmitter.fire(n)}}_refreshXPosition(e,t=e.element){if(!t)return;let n=e.options.x??0;(e.options.anchor||`left`)===`right`?t.style.right=n?`${n*this._renderService.dimensions.css.cell.width}px`:``:t.style.left=n?`${n*this._renderService.dimensions.css.cell.width}px`:``}_removeDecoration(e){this._decorationElements.get(e)?.remove(),this._decorationElements.delete(e),e.dispose()}};li=x([S(1,O),S(2,Re),S(3,Pe),S(4,Be)],li);var ui=class{constructor(){this._zones=[],this._zonePool=[],this._zonePoolIndex=0,this._linePadding={full:0,left:0,center:0,right:0}}get zones(){return this._zonePool.length=Math.min(this._zonePool.length,this._zones.length),this._zones}clear(){this._zones.length=0,this._zonePoolIndex=0}addDecoration(e){if(e.options.overviewRulerOptions){for(let t of this._zones)if(t.color===e.options.overviewRulerOptions.color&&t.position===e.options.overviewRulerOptions.position){if(this._lineIntersectsZone(t,e.marker.line))return;if(this._lineAdjacentToZone(t,e.marker.line,e.options.overviewRulerOptions.position)){this._addLineToZone(t,e.marker.line);return}}if(this._zonePoolIndex=e.startBufferLine&&t<=e.endBufferLine}_lineAdjacentToZone(e,t,n){return t>=e.startBufferLine-this._linePadding[n||`full`]&&t<=e.endBufferLine+this._linePadding[n||`full`]}_addLineToZone(e,t){e.startBufferLine=Math.min(e.startBufferLine,t),e.endBufferLine=Math.max(e.endBufferLine,t)}},di={full:0,left:0,center:0,right:0},fi={full:0,left:0,center:0,right:0},pi={full:0,left:0,center:0,right:0},mi=class extends j{constructor(e,t,n,r,i,a,o,s){super(),this._viewportElement=e,this._screenElement=t,this._bufferService=n,this._decorationService=r,this._renderService=i,this._optionsService=a,this._themeService=o,this._coreBrowserService=s,this._colorZoneStore=new ui,this._shouldUpdateDimensions=!0,this._shouldUpdateAnchor=!0,this._lastKnownBufferLength=0,this._canvas=this._coreBrowserService.mainDocument.createElement(`canvas`),this._canvas.classList.add(`xterm-decoration-overview-ruler`),this._refreshCanvasDimensions(),this._viewportElement.parentElement?.insertBefore(this._canvas,this._viewportElement),this._register(A(()=>this._canvas?.remove()));let c=this._canvas.getContext(`2d`);if(c)this._ctx=c;else throw Error(`Ctx cannot be null`);this._register(this._decorationService.onDecorationRegistered(()=>this._queueRefresh(void 0,!0))),this._register(this._decorationService.onDecorationRemoved(()=>this._queueRefresh(void 0,!0))),this._register(this._renderService.onRenderedViewportChange(()=>this._queueRefresh())),this._register(this._bufferService.buffers.onBufferActivate(()=>{this._canvas.style.display=this._bufferService.buffer===this._bufferService.buffers.alt?`none`:`block`})),this._register(this._bufferService.onScroll(()=>{this._lastKnownBufferLength!==this._bufferService.buffers.normal.lines.length&&(this._refreshDrawHeightConstants(),this._refreshColorZonePadding())})),this._register(this._renderService.onRender(()=>{(!this._containerHeight||this._containerHeight!==this._screenElement.clientHeight)&&(this._queueRefresh(!0),this._containerHeight=this._screenElement.clientHeight)})),this._register(this._coreBrowserService.onDprChange(()=>this._queueRefresh(!0))),this._register(this._optionsService.onSpecificOptionChange(`overviewRuler`,()=>this._queueRefresh(!0))),this._register(this._themeService.onChangeColors(()=>this._queueRefresh())),this._queueRefresh(!0)}get _width(){return this._optionsService.options.overviewRuler?.width||0}_refreshDrawConstants(){let e=Math.floor((this._canvas.width-1)/3),t=Math.ceil((this._canvas.width-1)/3);fi.full=this._canvas.width,fi.left=e,fi.center=t,fi.right=e,this._refreshDrawHeightConstants(),pi.full=1,pi.left=1,pi.center=1+fi.left,pi.right=1+fi.left+fi.center}_refreshDrawHeightConstants(){di.full=Math.round(2*this._coreBrowserService.dpr);let e=this._canvas.height/this._bufferService.buffer.lines.length,t=Math.round(Math.max(Math.min(e,12),6)*this._coreBrowserService.dpr);di.left=t,di.center=t,di.right=t}_refreshColorZonePadding(){this._colorZoneStore.setPadding({full:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*di.full),left:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*di.left),center:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*di.center),right:Math.floor(this._bufferService.buffers.active.lines.length/(this._canvas.height-1)*di.right)}),this._lastKnownBufferLength=this._bufferService.buffers.normal.lines.length}_refreshCanvasDimensions(){this._canvas.style.width=`${this._width}px`,this._canvas.width=Math.round(this._width*this._coreBrowserService.dpr),this._canvas.style.height=`${this._screenElement.clientHeight}px`,this._canvas.height=Math.round(this._screenElement.clientHeight*this._coreBrowserService.dpr),this._refreshDrawConstants(),this._refreshColorZonePadding()}_refreshDecorations(){this._shouldUpdateDimensions&&this._refreshCanvasDimensions(),this._ctx.clearRect(0,0,this._canvas.width,this._canvas.height),this._colorZoneStore.clear();for(let e of this._decorationService.decorations)this._colorZoneStore.addDecoration(e);this._ctx.lineWidth=1,this._renderRulerOutline();let e=this._colorZoneStore.zones;for(let t of e)t.position!==`full`&&this._renderColorZone(t);for(let t of e)t.position===`full`&&this._renderColorZone(t);this._shouldUpdateDimensions=!1,this._shouldUpdateAnchor=!1}_renderRulerOutline(){this._ctx.fillStyle=this._themeService.colors.overviewRulerBorder.css,this._ctx.fillRect(0,0,1,this._canvas.height),this._optionsService.rawOptions.overviewRuler.showTopBorder&&this._ctx.fillRect(1,0,this._canvas.width-1,1),this._optionsService.rawOptions.overviewRuler.showBottomBorder&&this._ctx.fillRect(1,this._canvas.height-1,this._canvas.width-1,this._canvas.height)}_renderColorZone(e){this._ctx.fillStyle=e.color,this._ctx.fillRect(pi[e.position||`full`],Math.round((this._canvas.height-1)*(e.startBufferLine/this._bufferService.buffers.active.lines.length)-di[e.position||`full`]/2),fi[e.position||`full`],Math.round((this._canvas.height-1)*((e.endBufferLine-e.startBufferLine)/this._bufferService.buffers.active.lines.length)+di[e.position||`full`]))}_queueRefresh(e,t){this._shouldUpdateDimensions=e||this._shouldUpdateDimensions,this._shouldUpdateAnchor=t||this._shouldUpdateAnchor,this._animationFrame===void 0&&(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>{this._refreshDecorations(),this._animationFrame=void 0}))}};mi=x([S(2,O),S(3,Pe),S(4,Be),S(5,k),S(6,Ue),S(7,Re)],mi);var L;(e=>(e.NUL=`\0`,e.SOH=``,e.STX=``,e.ETX=``,e.EOT=``,e.ENQ=``,e.ACK=``,e.BEL=`\x07`,e.BS=`\b`,e.HT=` `,e.LF=` +`,e.VT=`\v`,e.FF=`\f`,e.CR=`\r`,e.SO=``,e.SI=``,e.DLE=``,e.DC1=``,e.DC2=``,e.DC3=``,e.DC4=``,e.NAK=``,e.SYN=``,e.ETB=``,e.CAN=``,e.EM=``,e.SUB=``,e.ESC=`\x1B`,e.FS=``,e.GS=``,e.RS=``,e.US=``,e.SP=` `,e.DEL=``))(L||={});var hi;(e=>(e.PAD=`€`,e.HOP=``,e.BPH=`‚`,e.NBH=`ƒ`,e.IND=`„`,e.NEL=`…`,e.SSA=`†`,e.ESA=`‡`,e.HTS=`ˆ`,e.HTJ=`‰`,e.VTS=`Š`,e.PLD=`‹`,e.PLU=`Œ`,e.RI=``,e.SS2=`Ž`,e.SS3=``,e.DCS=``,e.PU1=`‘`,e.PU2=`’`,e.STS=`“`,e.CCH=`”`,e.MW=`•`,e.SPA=`–`,e.EPA=`—`,e.SOS=`˜`,e.SGCI=`™`,e.SCI=`š`,e.CSI=`›`,e.ST=`œ`,e.OSC=``,e.PM=`ž`,e.APC=`Ÿ`))(hi||={});var gi;(e=>e.ST=`${L.ESC}\\`)(gi||={});var _i=class{constructor(e,t,n,r,i,a){this._textarea=e,this._compositionView=t,this._bufferService=n,this._optionsService=r,this._coreService=i,this._renderService=a,this._isComposing=!1,this._isSendingComposition=!1,this._compositionPosition={start:0,end:0},this._dataAlreadySent=``}get isComposing(){return this._isComposing}compositionstart(){this._isComposing=!0,this._compositionPosition.start=this._textarea.value.length,this._compositionView.textContent=``,this._dataAlreadySent=``,this._compositionView.classList.add(`active`)}compositionupdate(e){this._compositionView.textContent=e.data,this.updateCompositionElements(),setTimeout(()=>{this._compositionPosition.end=this._textarea.value.length},0)}compositionend(){this._finalizeComposition(!0)}keydown(e){if(this._isComposing||this._isSendingComposition){if(e.keyCode===20||e.keyCode===229||e.keyCode===16||e.keyCode===17||e.keyCode===18)return!1;this._finalizeComposition(!1)}return e.keyCode===229?(this._handleAnyTextareaChanges(),!1):!0}_finalizeComposition(e){if(this._compositionView.classList.remove(`active`),this._isComposing=!1,e){let e={start:this._compositionPosition.start,end:this._compositionPosition.end};this._isSendingComposition=!0,setTimeout(()=>{if(this._isSendingComposition){this._isSendingComposition=!1;let t;e.start+=this._dataAlreadySent.length,t=this._isComposing?this._textarea.value.substring(e.start,this._compositionPosition.start):this._textarea.value.substring(e.start),t.length>0&&this._coreService.triggerDataEvent(t,!0)}},0)}else{this._isSendingComposition=!1;let e=this._textarea.value.substring(this._compositionPosition.start,this._compositionPosition.end);this._coreService.triggerDataEvent(e,!0)}}_handleAnyTextareaChanges(){let e=this._textarea.value;setTimeout(()=>{if(!this._isComposing){let t=this._textarea.value,n=t.replace(e,``);this._dataAlreadySent=n,t.length>e.length?this._coreService.triggerDataEvent(n,!0):t.lengththis.updateCompositionElements(!0),0)}}};_i=x([S(2,O),S(3,k),S(4,Oe),S(5,Be)],_i);var R=0,z=0,B=0,V=0,vi={css:`#00000000`,rgba:0},H;(e=>{function t(e,t,n,r){return r===void 0?`#${bi(e)}${bi(t)}${bi(n)}`:`#${bi(e)}${bi(t)}${bi(n)}${bi(r)}`}e.toCss=t;function n(e,t,n,r=255){return(e<<24|t<<16|n<<8|r)>>>0}e.toRgba=n;function r(t,n,r,i){return{css:e.toCss(t,n,r,i),rgba:e.toRgba(t,n,r,i)}}e.toColor=r})(H||={});var U;(e=>{function t(e,t){if(V=(t.rgba&255)/255,V===1)return{css:t.css,rgba:t.rgba};let n=t.rgba>>24&255,r=t.rgba>>16&255,i=t.rgba>>8&255,a=e.rgba>>24&255,o=e.rgba>>16&255,s=e.rgba>>8&255;return R=a+Math.round((n-a)*V),z=o+Math.round((r-o)*V),B=s+Math.round((i-s)*V),{css:H.toCss(R,z,B),rgba:H.toRgba(R,z,B)}}e.blend=t;function n(e){return(e.rgba&255)==255}e.isOpaque=n;function r(e,t,n){let r=yi.ensureContrastRatio(e.rgba,t.rgba,n);if(r)return H.toColor(r>>24&255,r>>16&255,r>>8&255)}e.ensureContrastRatio=r;function i(e){let t=(e.rgba|255)>>>0;return[R,z,B]=yi.toChannels(t),{css:H.toCss(R,z,B),rgba:t}}e.opaque=i;function a(e,t){return V=Math.round(t*255),[R,z,B]=yi.toChannels(e.rgba),{css:H.toCss(R,z,B,V),rgba:H.toRgba(R,z,B,V)}}e.opacity=a;function o(e,t){return V=e.rgba&255,a(e,V*t/255)}e.multiplyOpacity=o;function s(e){return[e.rgba>>24&255,e.rgba>>16&255,e.rgba>>8&255]}e.toColorRGB=s})(U||={});var W;(e=>{let t,n;try{let e=document.createElement(`canvas`);e.width=1,e.height=1;let r=e.getContext(`2d`,{willReadFrequently:!0});r&&(t=r,t.globalCompositeOperation=`copy`,n=t.createLinearGradient(0,0,1,1))}catch{}function r(e){if(e.match(/#[\da-f]{3,8}/i))switch(e.length){case 4:return R=parseInt(e.slice(1,2).repeat(2),16),z=parseInt(e.slice(2,3).repeat(2),16),B=parseInt(e.slice(3,4).repeat(2),16),H.toColor(R,z,B);case 5:return R=parseInt(e.slice(1,2).repeat(2),16),z=parseInt(e.slice(2,3).repeat(2),16),B=parseInt(e.slice(3,4).repeat(2),16),V=parseInt(e.slice(4,5).repeat(2),16),H.toColor(R,z,B,V);case 7:return{css:e,rgba:(parseInt(e.slice(1),16)<<8|255)>>>0};case 9:return{css:e,rgba:parseInt(e.slice(1),16)>>>0}}let r=e.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);if(r)return R=parseInt(r[1]),z=parseInt(r[2]),B=parseInt(r[3]),V=Math.round((r[5]===void 0?1:parseFloat(r[5]))*255),H.toColor(R,z,B,V);if(!t||!n||(t.fillStyle=n,t.fillStyle=e,typeof t.fillStyle!=`string`)||(t.fillRect(0,0,1,1),[R,z,B,V]=t.getImageData(0,0,1,1).data,V!==255))throw Error(`css.toColor: Unsupported css format`);return{rgba:H.toRgba(R,z,B,V),css:e}}e.toColor=r})(W||={});var G;(e=>{function t(e){return n(e>>16&255,e>>8&255,e&255)}e.relativeLuminance=t;function n(e,t,n){let r=e/255,i=t/255,a=n/255,o=r<=.03928?r/12.92:((r+.055)/1.055)**2.4,s=i<=.03928?i/12.92:((i+.055)/1.055)**2.4,c=a<=.03928?a/12.92:((a+.055)/1.055)**2.4;return o*.2126+s*.7152+c*.0722}e.relativeLuminance2=n})(G||={});var yi;(e=>{function t(e,t){if(V=(t&255)/255,V===1)return t;let n=t>>24&255,r=t>>16&255,i=t>>8&255,a=e>>24&255,o=e>>16&255,s=e>>8&255;return R=a+Math.round((n-a)*V),z=o+Math.round((r-o)*V),B=s+Math.round((i-s)*V),H.toRgba(R,z,B)}e.blend=t;function n(e,t,n){let a=G.relativeLuminance(e>>8),o=G.relativeLuminance(t>>8);if(xi(a,o)>8));if(sxi(a,G.relativeLuminance(r>>8))?o:r}return o}let s=i(e,t,n),c=xi(a,G.relativeLuminance(s>>8));if(cxi(a,G.relativeLuminance(i>>8))?s:i}return s}}e.ensureContrastRatio=n;function r(e,t,n){let r=e>>24&255,i=e>>16&255,a=e>>8&255,o=t>>24&255,s=t>>16&255,c=t>>8&255,l=xi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));for(;l0||s>0||c>0);)o-=Math.max(0,Math.ceil(o*.1)),s-=Math.max(0,Math.ceil(s*.1)),c-=Math.max(0,Math.ceil(c*.1)),l=xi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));return(o<<24|s<<16|c<<8|255)>>>0}e.reduceLuminance=r;function i(e,t,n){let r=e>>24&255,i=e>>16&255,a=e>>8&255,o=t>>24&255,s=t>>16&255,c=t>>8&255,l=xi(G.relativeLuminance2(o,s,c),G.relativeLuminance2(r,i,a));for(;l>>0}e.increaseLuminance=i;function a(e){return[e>>24&255,e>>16&255,e>>8&255,e&255]}e.toChannels=a})(yi||={});function bi(e){let t=e.toString(16);return t.length<2?`0`+t:t}function xi(e,t){return e1){let e=this._getJoinedRanges(r,o,a,t,i);for(let t=0;t1){let e=this._getJoinedRanges(r,o,a,t,i);for(let t=0;t=ae,ce=C,w=this._workCell;if(f.length>0&&C===f[0][0]&&se){let r=f.shift(),i=this._isCellInSelection(r[0],t);for(v=r[0]+1;v=r[1],se?(oe=!0,w=new Si(this._workCell,e.translateToString(!0,r[0],r[1]),r[1]-r[0]),ce=r[1]-1,m=w.getWidth()):ae=r[1]}let le=this._isCellInSelection(C,t),ue=n&&C===a,de=S&&C>=l&&C<=u,fe=!1;this._decorationService.forEachDecorationAtCell(C,t,void 0,e=>{fe=!0});let pe=w.getChars()||ye;if(pe===` `&&(w.isUnderline()||w.isOverline())&&(pe=`\xA0`),ie=m*s-c.get(pe,w.isBold(),w.isItalic()),!h)h=this._document.createElement(`span`);else if(g&&(le&&re||!le&&!re&&w.bg===ee)&&(le&&re&&p.selectionForeground||w.fg===y)&&w.extended.ext===te&&de===b&&ie===ne&&!ue&&!oe&&!fe&&se){w.isInvisible()?_+=ye:_+=pe,g++;continue}else g&&(h.textContent=_),h=this._document.createElement(`span`),g=0,_=``;if(ee=w.bg,y=w.fg,te=w.extended.ext,b=de,ne=ie,re=le,oe&&a>=C&&a<=ce&&(a=C),!this._coreService.isCursorHidden&&ue&&this._coreService.isCursorInitialized){if(x.push(`xterm-cursor`),this._coreBrowserService.isFocused)o&&x.push(`xterm-cursor-blink`),x.push(r===`bar`?`xterm-cursor-bar`:r===`underline`?`xterm-cursor-underline`:`xterm-cursor-block`);else if(i)switch(i){case`outline`:x.push(`xterm-cursor-outline`);break;case`block`:x.push(`xterm-cursor-block`);break;case`bar`:x.push(`xterm-cursor-bar`);break;case`underline`:x.push(`xterm-cursor-underline`);break;default:break}}if(w.isBold()&&x.push(`xterm-bold`),w.isItalic()&&x.push(`xterm-italic`),w.isDim()&&x.push(`xterm-dim`),_=w.isInvisible()?ye:w.getChars()||ye,w.isUnderline()&&(x.push(`xterm-underline-${w.extended.underlineStyle}`),_===` `&&(_=`\xA0`),!w.isUnderlineColorDefault()))if(w.isUnderlineColorRGB())h.style.textDecorationColor=`rgb(${be.toColorRGB(w.getUnderlineColor()).join(`,`)})`;else{let e=w.getUnderlineColor();this._optionsService.rawOptions.drawBoldTextInBrightColors&&w.isBold()&&e<8&&(e+=8),h.style.textDecorationColor=p.ansi[e].css}w.isOverline()&&(x.push(`xterm-overline`),_===` `&&(_=`\xA0`)),w.isStrikethrough()&&x.push(`xterm-strikethrough`),de&&(h.style.textDecoration=`underline`);let me=w.getFgColor(),he=w.getFgColorMode(),T=w.getBgColor(),ge=w.getBgColorMode(),_e=!!w.isInverse();if(_e){let e=me;me=T,T=e;let t=he;he=ge,ge=t}let ve,xe,E=!1;this._decorationService.forEachDecorationAtCell(C,t,void 0,e=>{e.options.layer!==`top`&&E||(e.backgroundColorRGB&&(ge=50331648,T=e.backgroundColorRGB.rgba>>8&16777215,ve=e.backgroundColorRGB),e.foregroundColorRGB&&(he=50331648,me=e.foregroundColorRGB.rgba>>8&16777215,xe=e.foregroundColorRGB),E=e.options.layer===`top`)}),!E&&le&&(ve=this._coreBrowserService.isFocused?p.selectionBackgroundOpaque:p.selectionInactiveBackgroundOpaque,T=ve.rgba>>8&16777215,ge=50331648,E=!0,p.selectionForeground&&(he=50331648,me=p.selectionForeground.rgba>>8&16777215,xe=p.selectionForeground)),E&&x.push(`xterm-decoration-top`);let Se;switch(ge){case 16777216:case 33554432:Se=p.ansi[T],x.push(`xterm-bg-${T}`);break;case 50331648:Se=H.toColor(T>>16,T>>8&255,T&255),this._addStyle(h,`background-color:#${Ai((T>>>0).toString(16),`0`,6)}`);break;default:_e?(Se=p.foreground,x.push(`xterm-bg-257`)):Se=p.background}switch(ve||w.isDim()&&(ve=U.multiplyOpacity(Se,.5)),he){case 16777216:case 33554432:w.isBold()&&me<8&&this._optionsService.rawOptions.drawBoldTextInBrightColors&&(me+=8),this._applyMinimumContrast(h,Se,p.ansi[me],w,ve,void 0)||x.push(`xterm-fg-${me}`);break;case 50331648:let e=H.toColor(me>>16&255,me>>8&255,me&255);this._applyMinimumContrast(h,Se,e,w,ve,xe)||this._addStyle(h,`color:#${Ai(me.toString(16),`0`,6)}`);break;default:this._applyMinimumContrast(h,Se,p.foreground,w,ve,xe)||_e&&x.push(`xterm-fg-257`)}x.length&&=(h.className=x.join(` `),0),!ue&&!oe&&!fe&&se?g++:h.textContent=_,ie!==this.defaultSpacing&&(h.style.letterSpacing=`${ie}px`),d.push(h),C=ce}return h&&g&&(h.textContent=_),d}_applyMinimumContrast(e,t,n,r,i,a){if(this._optionsService.rawOptions.minimumContrastRatio===1||Ei(r.getCode()))return!1;let o=this._getContrastCache(r),s;if(!i&&!a&&(s=o.getColor(t.rgba,n.rgba)),s===void 0){let e=this._optionsService.rawOptions.minimumContrastRatio/(r.isDim()?2:1);s=U.ensureContrastRatio(i||t,a||n,e),o.setColor((i||t).rgba,(a||n).rgba,s??null)}return s?(this._addStyle(e,`color:${s.css}`),!0):!1}_getContrastCache(e){return e.isDim()?this._themeService.colors.halfContrastCache:this._themeService.colors.contrastCache}_addStyle(e,t){e.setAttribute(`style`,`${e.getAttribute(`style`)||``}${t};`)}_isCellInSelection(e,t){let n=this._selectionStart,r=this._selectionEnd;return!n||!r?!1:this._columnSelectMode?n[0]<=r[0]?e>=n[0]&&t>=n[1]&&e=n[1]&&e>=r[0]&&t<=r[1]:t>n[1]&&t=n[0]&&e=n[0]}};ki=x([S(1,He),S(2,k),S(3,Re),S(4,Oe),S(5,Pe),S(6,Ue)],ki);function Ai(e,t,n){for(;e.length0&&(this._flat[r]=t),t}let i=e;t&&(i+=`B`),n&&(i+=`I`);let a=this._holey.get(i);if(a===void 0){let r=0;t&&(r|=1),n&&(r|=2),a=this._measure(e,r),a>0&&this._holey.set(i,a)}return a}_measure(e,t){let n=this._measureElements[t];return n.textContent=e.repeat(32),n.offsetWidth/32}},Mi=class{constructor(){this.clear()}clear(){this.hasSelection=!1,this.columnSelectMode=!1,this.viewportStartRow=0,this.viewportEndRow=0,this.viewportCappedStartRow=0,this.viewportCappedEndRow=0,this.startCol=0,this.endCol=0,this.selectionStart=void 0,this.selectionEnd=void 0}update(e,t,n,r=!1){if(this.selectionStart=t,this.selectionEnd=n,!t||!n||t[0]===n[0]&&t[1]===n[1]){this.clear();return}let i=e.buffers.active.ydisp,a=t[1]-i,o=n[1]-i,s=Math.max(a,0),c=Math.min(o,e.rows-1);if(s>=e.rows||c<0){this.clear();return}this.hasSelection=!0,this.columnSelectMode=r,this.viewportStartRow=a,this.viewportEndRow=o,this.viewportCappedStartRow=s,this.viewportCappedEndRow=c,this.startCol=t[0],this.endCol=n[0]}isCellSelected(e,t,n){return this.hasSelection?(n-=e.buffer.active.viewportY,this.columnSelectMode?this.startCol<=this.endCol?t>=this.startCol&&n>=this.viewportCappedStartRow&&t=this.viewportCappedStartRow&&t>=this.endCol&&n<=this.viewportCappedEndRow:n>this.viewportStartRow&&n=this.startCol&&t=this.startCol):!1}};function Ni(){return new Mi}var Pi=`xterm-dom-renderer-owner-`,Fi=`xterm-rows`,Ii=`xterm-fg-`,Li=`xterm-bg-`,Ri=`xterm-focus`,zi=`xterm-selection`,Bi=1,Vi=class extends j{constructor(e,t,n,r,i,a,o,s,c,l,u,d,f,p){super(),this._terminal=e,this._document=t,this._element=n,this._screenElement=r,this._viewportElement=i,this._helperContainer=a,this._linkifier2=o,this._charSizeService=c,this._optionsService=l,this._bufferService=u,this._coreService=d,this._coreBrowserService=f,this._themeService=p,this._terminalClass=Bi++,this._rowElements=[],this._selectionRenderModel=Ni(),this.onRequestRedraw=this._register(new P).event,this._rowContainer=this._document.createElement(`div`),this._rowContainer.classList.add(Fi),this._rowContainer.style.lineHeight=`normal`,this._rowContainer.setAttribute(`aria-hidden`,`true`),this._refreshRowElements(this._bufferService.cols,this._bufferService.rows),this._selectionContainer=this._document.createElement(`div`),this._selectionContainer.classList.add(zi),this._selectionContainer.setAttribute(`aria-hidden`,`true`),this.dimensions=Di(),this._updateDimensions(),this._register(this._optionsService.onOptionChange(()=>this._handleOptionsChanged())),this._register(this._themeService.onChangeColors(e=>this._injectCss(e))),this._injectCss(this._themeService.colors),this._rowFactory=s.createInstance(ki,document),this._element.classList.add(Pi+this._terminalClass),this._screenElement.appendChild(this._rowContainer),this._screenElement.appendChild(this._selectionContainer),this._register(this._linkifier2.onShowLinkUnderline(e=>this._handleLinkHover(e))),this._register(this._linkifier2.onHideLinkUnderline(e=>this._handleLinkLeave(e))),this._register(A(()=>{this._element.classList.remove(Pi+this._terminalClass),this._rowContainer.remove(),this._selectionContainer.remove(),this._widthCache.dispose(),this._themeStyleElement.remove(),this._dimensionsStyleElement.remove()})),this._widthCache=new ji(this._document,this._helperContainer),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}_updateDimensions(){let e=this._coreBrowserService.dpr;this.dimensions.device.char.width=this._charSizeService.width*e,this.dimensions.device.char.height=Math.ceil(this._charSizeService.height*e),this.dimensions.device.cell.width=this.dimensions.device.char.width+Math.round(this._optionsService.rawOptions.letterSpacing),this.dimensions.device.cell.height=Math.floor(this.dimensions.device.char.height*this._optionsService.rawOptions.lineHeight),this.dimensions.device.char.left=0,this.dimensions.device.char.top=0,this.dimensions.device.canvas.width=this.dimensions.device.cell.width*this._bufferService.cols,this.dimensions.device.canvas.height=this.dimensions.device.cell.height*this._bufferService.rows,this.dimensions.css.canvas.width=Math.round(this.dimensions.device.canvas.width/e),this.dimensions.css.canvas.height=Math.round(this.dimensions.device.canvas.height/e),this.dimensions.css.cell.width=this.dimensions.css.canvas.width/this._bufferService.cols,this.dimensions.css.cell.height=this.dimensions.css.canvas.height/this._bufferService.rows;for(let e of this._rowElements)e.style.width=`${this.dimensions.css.canvas.width}px`,e.style.height=`${this.dimensions.css.cell.height}px`,e.style.lineHeight=`${this.dimensions.css.cell.height}px`,e.style.overflow=`hidden`;this._dimensionsStyleElement||(this._dimensionsStyleElement=this._document.createElement(`style`),this._screenElement.appendChild(this._dimensionsStyleElement));let t=`${this._terminalSelector} .${Fi} span { display: inline-block; height: 100%; vertical-align: top;}`;this._dimensionsStyleElement.textContent=t,this._selectionContainer.style.height=this._viewportElement.style.height,this._screenElement.style.width=`${this.dimensions.css.canvas.width}px`,this._screenElement.style.height=`${this.dimensions.css.canvas.height}px`}_injectCss(e){this._themeStyleElement||(this._themeStyleElement=this._document.createElement(`style`),this._screenElement.appendChild(this._themeStyleElement));let t=`${this._terminalSelector} .${Fi} { pointer-events: none; color: ${e.foreground.css}; font-family: ${this._optionsService.rawOptions.fontFamily}; font-size: ${this._optionsService.rawOptions.fontSize}px; font-kerning: none; white-space: pre}`;t+=`${this._terminalSelector} .${Fi} .xterm-dim { color: ${U.multiplyOpacity(e.foreground,.5).css};}`,t+=`${this._terminalSelector} span:not(.xterm-bold) { font-weight: ${this._optionsService.rawOptions.fontWeight};}${this._terminalSelector} span.xterm-bold { font-weight: ${this._optionsService.rawOptions.fontWeightBold};}${this._terminalSelector} span.xterm-italic { font-style: italic;}`;let n=`blink_underline_${this._terminalClass}`,r=`blink_bar_${this._terminalClass}`,i=`blink_block_${this._terminalClass}`;t+=`@keyframes ${n} { 50% { border-bottom-style: hidden; }}`,t+=`@keyframes ${r} { 50% { box-shadow: none; }}`,t+=`@keyframes ${i} { 0% { background-color: ${e.cursor.css}; color: ${e.cursorAccent.css}; } 50% { background-color: inherit; color: ${e.cursor.css}; }}`,t+=`${this._terminalSelector} .${Fi}.${Ri} .xterm-cursor.xterm-cursor-blink.xterm-cursor-underline { animation: ${n} 1s step-end infinite;}${this._terminalSelector} .${Fi}.${Ri} .xterm-cursor.xterm-cursor-blink.xterm-cursor-bar { animation: ${r} 1s step-end infinite;}${this._terminalSelector} .${Fi}.${Ri} .xterm-cursor.xterm-cursor-blink.xterm-cursor-block { animation: ${i} 1s step-end infinite;}${this._terminalSelector} .${Fi} .xterm-cursor.xterm-cursor-block { background-color: ${e.cursor.css}; color: ${e.cursorAccent.css};}${this._terminalSelector} .${Fi} .xterm-cursor.xterm-cursor-block:not(.xterm-cursor-blink) { background-color: ${e.cursor.css} !important; color: ${e.cursorAccent.css} !important;}${this._terminalSelector} .${Fi} .xterm-cursor.xterm-cursor-outline { outline: 1px solid ${e.cursor.css}; outline-offset: -1px;}${this._terminalSelector} .${Fi} .xterm-cursor.xterm-cursor-bar { box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${e.cursor.css} inset;}${this._terminalSelector} .${Fi} .xterm-cursor.xterm-cursor-underline { border-bottom: 1px ${e.cursor.css}; border-bottom-style: solid; height: calc(100% - 1px);}`,t+=`${this._terminalSelector} .${zi} { position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;}${this._terminalSelector}.focus .${zi} div { position: absolute; background-color: ${e.selectionBackgroundOpaque.css};}${this._terminalSelector} .${zi} div { position: absolute; background-color: ${e.selectionInactiveBackgroundOpaque.css};}`;for(let[n,r]of e.ansi.entries())t+=`${this._terminalSelector} .${Ii}${n} { color: ${r.css}; }${this._terminalSelector} .${Ii}${n}.xterm-dim { color: ${U.multiplyOpacity(r,.5).css}; }${this._terminalSelector} .${Li}${n} { background-color: ${r.css}; }`;t+=`${this._terminalSelector} .${Ii}257 { color: ${U.opaque(e.background).css}; }${this._terminalSelector} .${Ii}257.xterm-dim { color: ${U.multiplyOpacity(U.opaque(e.background),.5).css}; }${this._terminalSelector} .${Li}257 { background-color: ${e.foreground.css}; }`,this._themeStyleElement.textContent=t}_setDefaultSpacing(){let e=this.dimensions.css.cell.width-this._widthCache.get(`W`,!1,!1);this._rowContainer.style.letterSpacing=`${e}px`,this._rowFactory.defaultSpacing=e}handleDevicePixelRatioChange(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}_refreshRowElements(e,t){for(let e=this._rowElements.length;e<=t;e++){let e=this._document.createElement(`div`);this._rowContainer.appendChild(e),this._rowElements.push(e)}for(;this._rowElements.length>t;)this._rowContainer.removeChild(this._rowElements.pop())}handleResize(e,t){this._refreshRowElements(e,t),this._updateDimensions(),this.handleSelectionChanged(this._selectionRenderModel.selectionStart,this._selectionRenderModel.selectionEnd,this._selectionRenderModel.columnSelectMode)}handleCharSizeChanged(){this._updateDimensions(),this._widthCache.clear(),this._setDefaultSpacing()}handleBlur(){this._rowContainer.classList.remove(Ri),this.renderRows(0,this._bufferService.rows-1)}handleFocus(){this._rowContainer.classList.add(Ri),this.renderRows(this._bufferService.buffer.y,this._bufferService.buffer.y)}handleSelectionChanged(e,t,n){if(this._selectionContainer.replaceChildren(),this._rowFactory.handleSelectionChanged(e,t,n),this.renderRows(0,this._bufferService.rows-1),!e||!t||(this._selectionRenderModel.update(this._terminal,e,t,n),!this._selectionRenderModel.hasSelection))return;let r=this._selectionRenderModel.viewportStartRow,i=this._selectionRenderModel.viewportEndRow,a=this._selectionRenderModel.viewportCappedStartRow,o=this._selectionRenderModel.viewportCappedEndRow,s=this._document.createDocumentFragment();if(n){let n=e[0]>t[0];s.appendChild(this._createSelectionElement(a,n?t[0]:e[0],n?e[0]:t[0],o-a+1))}else{let n=r===a?e[0]:0,c=a===i?t[0]:this._bufferService.cols;s.appendChild(this._createSelectionElement(a,n,c));let l=o-a-1;if(s.appendChild(this._createSelectionElement(a+1,0,this._bufferService.cols,l)),a!==o){let e=i===o?t[0]:this._bufferService.cols;s.appendChild(this._createSelectionElement(o,0,e))}}this._selectionContainer.appendChild(s)}_createSelectionElement(e,t,n,r=1){let i=this._document.createElement(`div`),a=t*this.dimensions.css.cell.width,o=this.dimensions.css.cell.width*(n-t);return a+o>this.dimensions.css.canvas.width&&(o=this.dimensions.css.canvas.width-a),i.style.height=`${r*this.dimensions.css.cell.height}px`,i.style.top=`${e*this.dimensions.css.cell.height}px`,i.style.left=`${a}px`,i.style.width=`${o}px`,i}handleCursorMove(){}_handleOptionsChanged(){this._updateDimensions(),this._injectCss(this._themeService.colors),this._widthCache.setFont(this._optionsService.rawOptions.fontFamily,this._optionsService.rawOptions.fontSize,this._optionsService.rawOptions.fontWeight,this._optionsService.rawOptions.fontWeightBold),this._setDefaultSpacing()}clear(){for(let e of this._rowElements)e.replaceChildren()}renderRows(e,t){let n=this._bufferService.buffer,r=n.ybase+n.y,i=Math.min(n.x,this._bufferService.cols-1),a=this._coreService.decPrivateModes.cursorBlink??this._optionsService.rawOptions.cursorBlink,o=this._coreService.decPrivateModes.cursorStyle??this._optionsService.rawOptions.cursorStyle,s=this._optionsService.rawOptions.cursorInactiveStyle;for(let c=e;c<=t;c++){let e=c+n.ydisp,t=this._rowElements[c],l=n.lines.get(e);if(!t||!l)break;t.replaceChildren(...this._rowFactory.createRow(l,e,e===r,o,s,i,a,this.dimensions.css.cell.width,this._widthCache,-1,-1))}}get _terminalSelector(){return`.${Pi}${this._terminalClass}`}_handleLinkHover(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!0)}_handleLinkLeave(e){this._setCellUnderline(e.x1,e.x2,e.y1,e.y2,e.cols,!1)}_setCellUnderline(e,t,n,r,i,a){n<0&&(e=0),r<0&&(t=0);let o=this._bufferService.rows-1;n=Math.max(Math.min(n,o),0),r=Math.max(Math.min(r,o),0),i=Math.min(i,this._bufferService.cols);let s=this._bufferService.buffer,c=s.ybase+s.y,l=Math.min(s.x,i-1),u=this._optionsService.rawOptions.cursorBlink,d=this._optionsService.rawOptions.cursorStyle,f=this._optionsService.rawOptions.cursorInactiveStyle;for(let o=n;o<=r;++o){let p=o+s.ydisp,m=this._rowElements[o],h=s.lines.get(p);if(!m||!h)break;m.replaceChildren(...this._rowFactory.createRow(h,p,p===c,d,f,l,u,this.dimensions.css.cell.width,this._widthCache,a?o===n?e:0:-1,a?(o===r?t:i)-1:-1))}}};Vi=x([S(7,Ae),S(8,Le),S(9,k),S(10,O),S(11,Oe),S(12,Re),S(13,Ue)],Vi);var Hi=class extends j{constructor(e,t,n){super(),this._optionsService=n,this.width=0,this.height=0,this._onCharSizeChange=this._register(new P),this.onCharSizeChange=this._onCharSizeChange.event;try{this._measureStrategy=this._register(new Gi(this._optionsService))}catch{this._measureStrategy=this._register(new Wi(e,t,this._optionsService))}this._register(this._optionsService.onMultipleOptionChange([`fontFamily`,`fontSize`],()=>this.measure()))}get hasValidSize(){return this.width>0&&this.height>0}measure(){let e=this._measureStrategy.measure();(e.width!==this.width||e.height!==this.height)&&(this.width=e.width,this.height=e.height,this._onCharSizeChange.fire())}};Hi=x([S(2,k)],Hi);var Ui=class extends j{constructor(){super(...arguments),this._result={width:0,height:0}}_validateAndSet(e,t){e!==void 0&&e>0&&t!==void 0&&t>0&&(this._result.width=e,this._result.height=t)}},Wi=class extends Ui{constructor(e,t,n){super(),this._document=e,this._parentElement=t,this._optionsService=n,this._measureElement=this._document.createElement(`span`),this._measureElement.classList.add(`xterm-char-measure-element`),this._measureElement.textContent=`W`.repeat(32),this._measureElement.setAttribute(`aria-hidden`,`true`),this._measureElement.style.whiteSpace=`pre`,this._measureElement.style.fontKerning=`none`,this._parentElement.appendChild(this._measureElement)}measure(){return this._measureElement.style.fontFamily=this._optionsService.rawOptions.fontFamily,this._measureElement.style.fontSize=`${this._optionsService.rawOptions.fontSize}px`,this._validateAndSet(Number(this._measureElement.offsetWidth)/32,Number(this._measureElement.offsetHeight)),this._result}},Gi=class extends Ui{constructor(e){super(),this._optionsService=e,this._canvas=new OffscreenCanvas(100,100),this._ctx=this._canvas.getContext(`2d`);let t=this._ctx.measureText(`W`);if(!(`width`in t&&`fontBoundingBoxAscent`in t&&`fontBoundingBoxDescent`in t))throw Error(`Required font metrics not supported`)}measure(){this._ctx.font=`${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`;let e=this._ctx.measureText(`W`);return this._validateAndSet(e.width,e.fontBoundingBoxAscent+e.fontBoundingBoxDescent),this._result}},Ki=class extends j{constructor(e,t,n){super(),this._textarea=e,this._window=t,this.mainDocument=n,this._isFocused=!1,this._cachedIsFocused=void 0,this._screenDprMonitor=this._register(new qi(this._window)),this._onDprChange=this._register(new P),this.onDprChange=this._onDprChange.event,this._onWindowChange=this._register(new P),this.onWindowChange=this._onWindowChange.event,this._register(this.onWindowChange(e=>this._screenDprMonitor.setWindow(e))),this._register(N.forward(this._screenDprMonitor.onDprChange,this._onDprChange)),this._register(F(this._textarea,`focus`,()=>this._isFocused=!0)),this._register(F(this._textarea,`blur`,()=>this._isFocused=!1))}get window(){return this._window}set window(e){this._window!==e&&(this._window=e,this._onWindowChange.fire(this._window))}get dpr(){return this.window.devicePixelRatio}get isFocused(){return this._cachedIsFocused===void 0&&(this._cachedIsFocused=this._isFocused&&this._textarea.ownerDocument.hasFocus(),queueMicrotask(()=>this._cachedIsFocused=void 0)),this._cachedIsFocused}},qi=class extends j{constructor(e){super(),this._parentWindow=e,this._windowResizeListener=this._register(new bt),this._onDprChange=this._register(new P),this.onDprChange=this._onDprChange.event,this._outerListener=()=>this._setDprAndFireIfDiffers(),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._updateDpr(),this._setWindowResizeListener(),this._register(A(()=>this.clearListener()))}setWindow(e){this._parentWindow=e,this._setWindowResizeListener(),this._setDprAndFireIfDiffers()}_setWindowResizeListener(){this._windowResizeListener.value=F(this._parentWindow,`resize`,()=>this._setDprAndFireIfDiffers())}_setDprAndFireIfDiffers(){this._parentWindow.devicePixelRatio!==this._currentDevicePixelRatio&&this._onDprChange.fire(this._parentWindow.devicePixelRatio),this._updateDpr()}_updateDpr(){this._outerListener&&(this._resolutionMediaMatchList?.removeListener(this._outerListener),this._currentDevicePixelRatio=this._parentWindow.devicePixelRatio,this._resolutionMediaMatchList=this._parentWindow.matchMedia(`screen and (resolution: ${this._parentWindow.devicePixelRatio}dppx)`),this._resolutionMediaMatchList.addListener(this._outerListener))}clearListener(){!this._resolutionMediaMatchList||!this._outerListener||(this._resolutionMediaMatchList.removeListener(this._outerListener),this._resolutionMediaMatchList=void 0,this._outerListener=void 0)}},Ji=class extends j{constructor(){super(),this.linkProviders=[],this._register(A(()=>this.linkProviders.length=0))}registerLinkProvider(e){return this.linkProviders.push(e),{dispose:()=>{let t=this.linkProviders.indexOf(e);t!==-1&&this.linkProviders.splice(t,1)}}}};function Yi(e,t,n){let r=n.getBoundingClientRect(),i=e.getComputedStyle(n),a=parseInt(i.getPropertyValue(`padding-left`)),o=parseInt(i.getPropertyValue(`padding-top`));return[t.clientX-r.left-a,t.clientY-r.top-o]}function Xi(e,t,n,r,i,a,o,s,c){if(!a)return;let l=Yi(e,t,n);if(l)return l[0]=Math.ceil((l[0]+(c?o/2:0))/o),l[1]=Math.ceil(l[1]/s),l[0]=Math.min(Math.max(l[0],1),r+ +!!c),l[1]=Math.min(Math.max(l[1],1),i),l}var Zi=class{constructor(e,t){this._renderService=e,this._charSizeService=t}getCoords(e,t,n,r,i){return Xi(window,e,t,n,r,this._charSizeService.hasValidSize,this._renderService.dimensions.css.cell.width,this._renderService.dimensions.css.cell.height,i)}getMouseReportCoords(e,t){let n=Yi(window,e,t);if(this._charSizeService.hasValidSize)return n[0]=Math.min(Math.max(n[0],0),this._renderService.dimensions.css.canvas.width-1),n[1]=Math.min(Math.max(n[1],0),this._renderService.dimensions.css.canvas.height-1),{col:Math.floor(n[0]/this._renderService.dimensions.css.cell.width),row:Math.floor(n[1]/this._renderService.dimensions.css.cell.height),x:Math.floor(n[0]),y:Math.floor(n[1])}}};Zi=x([S(0,Be),S(1,Le)],Zi);var Qi=class{constructor(e,t){this._renderCallback=e,this._coreBrowserService=t,this._refreshCallbacks=[]}dispose(){this._animationFrame&&=(this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame),void 0)}addRefreshCallback(e){return this._refreshCallbacks.push(e),this._animationFrame||=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()),this._animationFrame}refresh(e,t,n){this._rowCount=n,e=e===void 0?0:e,t=t===void 0?this._rowCount-1:t,this._rowStart=this._rowStart===void 0?e:Math.min(this._rowStart,e),this._rowEnd=this._rowEnd===void 0?t:Math.max(this._rowEnd,t),!this._animationFrame&&(this._animationFrame=this._coreBrowserService.window.requestAnimationFrame(()=>this._innerRefresh()))}_innerRefresh(){if(this._animationFrame=void 0,this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0){this._runRefreshCallbacks();return}let e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t),this._runRefreshCallbacks()}_runRefreshCallbacks(){for(let e of this._refreshCallbacks)e(0);this._refreshCallbacks=[]}},$i={};ae($i,{getSafariVersion:()=>oa,isChromeOS:()=>fa,isFirefox:()=>ra,isIpad:()=>ca,isIphone:()=>la,isLegacyEdge:()=>ia,isLinux:()=>da,isMac:()=>sa,isNode:()=>ea,isSafari:()=>aa,isWindows:()=>ua});var ea=typeof process<`u`&&`title`in process,ta=ea?`node`:navigator.userAgent,na=ea?`node`:navigator.platform,ra=ta.includes(`Firefox`),ia=ta.includes(`Edge`),aa=/^((?!chrome|android).)*safari/i.test(ta);function oa(){if(!aa)return 0;let e=ta.match(/Version\/(\d+)/);return e===null||e.length<2?0:parseInt(e[1])}var sa=[`Macintosh`,`MacIntel`,`MacPPC`,`Mac68K`].includes(na),ca=na===`iPad`,la=na===`iPhone`,ua=[`Windows`,`Win16`,`Win32`,`WinCE`].includes(na),da=na.indexOf(`Linux`)>=0,fa=/\bCrOS\b/.test(ta),pa=class{constructor(){this._tasks=[],this._i=0}enqueue(e){this._tasks.push(e),this._start()}flush(){for(;this._ii){r-t<-20&&console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(r-t))}ms`),this._start();return}r=i}this.clear()}},ma=class extends pa{_requestCallback(e){return setTimeout(()=>e(this._createDeadline(16)))}_cancelCallback(e){clearTimeout(e)}_createDeadline(e){let t=performance.now()+e;return{timeRemaining:()=>Math.max(0,t-performance.now())}}},ha=class extends pa{_requestCallback(e){return requestIdleCallback(e)}_cancelCallback(e){cancelIdleCallback(e)}},ga=!ea&&`requestIdleCallback`in window?ha:ma,_a=class{constructor(){this._queue=new ga}set(e){this._queue.clear(),this._queue.enqueue(e)}flush(){this._queue.flush()}},va=class extends j{constructor(e,t,n,r,i,a,o,s,c){super(),this._rowCount=e,this._optionsService=n,this._charSizeService=r,this._coreService=i,this._coreBrowserService=s,this._renderer=this._register(new bt),this._pausedResizeTask=new _a,this._observerDisposable=this._register(new bt),this._isPaused=!1,this._needsFullRefresh=!1,this._isNextRenderRedrawOnly=!0,this._needsSelectionRefresh=!1,this._canvasWidth=0,this._canvasHeight=0,this._selectionState={start:void 0,end:void 0,columnSelectMode:!1},this._onDimensionsChange=this._register(new P),this.onDimensionsChange=this._onDimensionsChange.event,this._onRenderedViewportChange=this._register(new P),this.onRenderedViewportChange=this._onRenderedViewportChange.event,this._onRender=this._register(new P),this.onRender=this._onRender.event,this._onRefreshRequest=this._register(new P),this.onRefreshRequest=this._onRefreshRequest.event,this._renderDebouncer=new Qi((e,t)=>this._renderRows(e,t),this._coreBrowserService),this._register(this._renderDebouncer),this._syncOutputHandler=new ya(this._coreBrowserService,this._coreService,()=>this._fullRefresh()),this._register(A(()=>this._syncOutputHandler.dispose())),this._register(this._coreBrowserService.onDprChange(()=>this.handleDevicePixelRatioChange())),this._register(o.onResize(()=>this._fullRefresh())),this._register(o.buffers.onBufferActivate(()=>this._renderer.value?.clear())),this._register(this._optionsService.onOptionChange(()=>this._handleOptionsChanged())),this._register(this._charSizeService.onCharSizeChange(()=>this.handleCharSizeChanged())),this._register(a.onDecorationRegistered(()=>this._fullRefresh())),this._register(a.onDecorationRemoved(()=>this._fullRefresh())),this._register(this._optionsService.onMultipleOptionChange([`customGlyphs`,`drawBoldTextInBrightColors`,`letterSpacing`,`lineHeight`,`fontFamily`,`fontSize`,`fontWeight`,`fontWeightBold`,`minimumContrastRatio`,`rescaleOverlappingGlyphs`],()=>{this.clear(),this.handleResize(o.cols,o.rows),this._fullRefresh()})),this._register(this._optionsService.onMultipleOptionChange([`cursorBlink`,`cursorStyle`],()=>this.refreshRows(o.buffer.y,o.buffer.y,!0))),this._register(c.onChangeColors(()=>this._fullRefresh())),this._registerIntersectionObserver(this._coreBrowserService.window,t),this._register(this._coreBrowserService.onWindowChange(e=>this._registerIntersectionObserver(e,t)))}get dimensions(){return this._renderer.value.dimensions}_registerIntersectionObserver(e,t){if(`IntersectionObserver`in e){let n=new e.IntersectionObserver(e=>this._handleIntersectionChange(e[e.length-1]),{threshold:0});n.observe(t),this._observerDisposable.value=A(()=>n.disconnect())}}_handleIntersectionChange(e){this._isPaused=e.isIntersecting===void 0?e.intersectionRatio===0:!e.isIntersecting,!this._isPaused&&!this._charSizeService.hasValidSize&&this._charSizeService.measure(),!this._isPaused&&this._needsFullRefresh&&(this._pausedResizeTask.flush(),this.refreshRows(0,this._rowCount-1),this._needsFullRefresh=!1)}refreshRows(e,t,n=!1){if(this._isPaused){this._needsFullRefresh=!0;return}if(this._coreService.decPrivateModes.synchronizedOutput){this._syncOutputHandler.bufferRows(e,t);return}let r=this._syncOutputHandler.flush();r&&(e=Math.min(e,r.start),t=Math.max(t,r.end)),n||(this._isNextRenderRedrawOnly=!1),this._renderDebouncer.refresh(e,t,this._rowCount)}_renderRows(e,t){if(this._renderer.value){if(this._coreService.decPrivateModes.synchronizedOutput){this._syncOutputHandler.bufferRows(e,t);return}e=Math.min(e,this._rowCount-1),t=Math.min(t,this._rowCount-1),this._renderer.value.renderRows(e,t),this._needsSelectionRefresh&&=(this._renderer.value.handleSelectionChanged(this._selectionState.start,this._selectionState.end,this._selectionState.columnSelectMode),!1),this._isNextRenderRedrawOnly||this._onRenderedViewportChange.fire({start:e,end:t}),this._onRender.fire({start:e,end:t}),this._isNextRenderRedrawOnly=!0}}resize(e,t){this._rowCount=t,this._fireOnCanvasResize()}_handleOptionsChanged(){this._renderer.value&&(this.refreshRows(0,this._rowCount-1),this._fireOnCanvasResize())}_fireOnCanvasResize(){this._renderer.value&&(this._renderer.value.dimensions.css.canvas.width===this._canvasWidth&&this._renderer.value.dimensions.css.canvas.height===this._canvasHeight||this._onDimensionsChange.fire(this._renderer.value.dimensions))}hasRenderer(){return!!this._renderer.value}setRenderer(e){this._renderer.value=e,this._renderer.value&&(this._renderer.value.onRequestRedraw(e=>this.refreshRows(e.start,e.end,!0)),this._needsSelectionRefresh=!0,this._fullRefresh())}addRefreshCallback(e){return this._renderDebouncer.addRefreshCallback(e)}_fullRefresh(){this._isPaused?this._needsFullRefresh=!0:this.refreshRows(0,this._rowCount-1)}clearTextureAtlas(){this._renderer.value&&(this._renderer.value.clearTextureAtlas?.(),this._fullRefresh())}handleDevicePixelRatioChange(){this._charSizeService.measure(),this._renderer.value&&(this._renderer.value.handleDevicePixelRatioChange(),this.refreshRows(0,this._rowCount-1))}handleResize(e,t){this._renderer.value&&(this._isPaused?this._pausedResizeTask.set(()=>this._renderer.value?.handleResize(e,t)):this._renderer.value.handleResize(e,t),this._fullRefresh())}handleCharSizeChanged(){this._renderer.value?.handleCharSizeChanged()}handleBlur(){this._renderer.value?.handleBlur()}handleFocus(){this._renderer.value?.handleFocus()}handleSelectionChanged(e,t,n){this._selectionState.start=e,this._selectionState.end=t,this._selectionState.columnSelectMode=n,this._renderer.value?.handleSelectionChanged(e,t,n)}handleCursorMove(){this._renderer.value?.handleCursorMove()}clear(){this._renderer.value?.clear()}};va=x([S(2,k),S(3,Le),S(4,Oe),S(5,Pe),S(6,O),S(7,Re),S(8,Ue)],va);var ya=class{constructor(e,t,n){this._coreBrowserService=e,this._coreService=t,this._onTimeout=n,this._start=0,this._end=0,this._isBuffering=!1}bufferRows(e,t){this._isBuffering?(this._start=Math.min(this._start,e),this._end=Math.max(this._end,t)):(this._start=e,this._end=t,this._isBuffering=!0),this._timeout===void 0&&(this._timeout=this._coreBrowserService.window.setTimeout(()=>{this._timeout=void 0,this._coreService.decPrivateModes.synchronizedOutput=!1,this._onTimeout()},1e3))}flush(){if(this._timeout!==void 0&&(this._coreBrowserService.window.clearTimeout(this._timeout),this._timeout=void 0),!this._isBuffering)return;let e={start:this._start,end:this._end};return this._isBuffering=!1,e}dispose(){this._timeout!==void 0&&(this._coreBrowserService.window.clearTimeout(this._timeout),this._timeout=void 0)}};function ba(e,t,n,r){let i=n.buffer.x,a=n.buffer.y;if(!n.buffer.hasScrollback)return Ca(i,a,e,t,n,r)+wa(a,t,n,r)+Ta(i,a,e,t,n,r);let o;if(a===t)return o=i>e?`D`:`C`,Ma(Math.abs(i-e),ja(o,r));o=a>t?`D`:`C`;let s=Math.abs(a-t);return Ma(Sa(a>t?e:i,n)+(s-1)*n.cols+1+xa(a>t?i:e,n),ja(o,r))}function xa(e,t){return e-1}function Sa(e,t){return t.cols-e}function Ca(e,t,n,r,i,a){return wa(t,r,i,a).length===0?``:Ma(Aa(e,t,e,t-Da(t,i),!1,i).length,ja(`D`,a))}function wa(e,t,n,r){let i=e-Da(e,n),a=t-Da(t,n);return Ma(Math.abs(i-a)-Ea(e,t,n),ja(ka(e,t),r))}function Ta(e,t,n,r,i,a){let o;o=wa(t,r,i,a).length>0?r-Da(r,i):t;let s=r,c=Oa(e,t,n,r,i,a);return Ma(Aa(e,o,n,s,c===`C`,i).length,ja(c,a))}function Ea(e,t,n){let r=0,i=e-Da(e,n),a=t-Da(t,n);for(let o=0;o=0&&e0?r-Da(r,i):t,e=n&&ot?`A`:`B`}function Aa(e,t,n,r,i,a){let o=e,s=t,c=``;for(;(o!==n||s!==r)&&s>=0&&sa.cols-1?(c+=a.buffer.translateBufferLineToString(s,!1,e,o),o=0,e=0,s++):!i&&o<0&&(c+=a.buffer.translateBufferLineToString(s,!1,0,e+1),o=a.cols-1,e=o,s--);return c+a.buffer.translateBufferLineToString(s,!1,e,o)}function ja(e,t){let n=t?`O`:`[`;return L.ESC+n+e}function Ma(e,t){e=Math.floor(e);let n=``;for(let r=0;rthis._bufferService.cols?e%this._bufferService.cols===0?[this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)-1]:[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[e,this.selectionStart[1]]}if(this.selectionStartLength&&this.selectionEnd[1]===this.selectionStart[1]){let e=this.selectionStart[0]+this.selectionStartLength;return e>this._bufferService.cols?[e%this._bufferService.cols,this.selectionStart[1]+Math.floor(e/this._bufferService.cols)]:[Math.max(e,this.selectionEnd[0]),this.selectionEnd[1]]}return this.selectionEnd}}areSelectionValuesReversed(){let e=this.selectionStart,t=this.selectionEnd;return!e||!t?!1:e[1]>t[1]||e[1]===t[1]&&e[0]>t[0]}handleTrim(e){return this.selectionStart&&(this.selectionStart[1]-=e),this.selectionEnd&&(this.selectionEnd[1]-=e),this.selectionEnd&&this.selectionEnd[1]<0?(this.clearSelection(),!0):(this.selectionStart&&this.selectionStart[1]<0&&(this.selectionStart[1]=0),!1)}};function Pa(e,t){if(e.start.y>e.end.y)throw Error(`Buffer range end (${e.end.x}, ${e.end.y}) cannot be before start (${e.start.x}, ${e.start.y})`);return t*(e.end.y-e.start.y)+(e.end.x-e.start.x+1)}var Fa=50,Ia=15,La=50,Ra=500,za=RegExp(`\xA0`,`g`),Ba=class extends j{constructor(e,t,n,r,i,a,o,s,c){super(),this._element=e,this._screenElement=t,this._linkifier=n,this._bufferService=r,this._coreService=i,this._mouseService=a,this._optionsService=o,this._renderService=s,this._coreBrowserService=c,this._dragScrollAmount=0,this._enabled=!0,this._workCell=new E,this._mouseDownTimeStamp=0,this._oldHasSelection=!1,this._oldSelectionStart=void 0,this._oldSelectionEnd=void 0,this._onLinuxMouseSelection=this._register(new P),this.onLinuxMouseSelection=this._onLinuxMouseSelection.event,this._onRedrawRequest=this._register(new P),this.onRequestRedraw=this._onRedrawRequest.event,this._onSelectionChange=this._register(new P),this.onSelectionChange=this._onSelectionChange.event,this._onRequestScrollLines=this._register(new P),this.onRequestScrollLines=this._onRequestScrollLines.event,this._mouseMoveListener=e=>this._handleMouseMove(e),this._mouseUpListener=e=>this._handleMouseUp(e),this._coreService.onUserInput(()=>{this.hasSelection&&this.clearSelection()}),this._trimListener=this._bufferService.buffer.lines.onTrim(e=>this._handleTrim(e)),this._register(this._bufferService.buffers.onBufferActivate(e=>this._handleBufferActivate(e))),this.enable(),this._model=new Na(this._bufferService),this._activeSelectionMode=0,this._register(A(()=>{this._removeMouseDownListeners()})),this._register(this._bufferService.onResize(e=>{e.rowsChanged&&this.clearSelection()}))}reset(){this.clearSelection()}disable(){this.clearSelection(),this._enabled=!1}enable(){this._enabled=!0}get selectionStart(){return this._model.finalSelectionStart}get selectionEnd(){return this._model.finalSelectionEnd}get hasSelection(){let e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;return!e||!t?!1:e[0]!==t[0]||e[1]!==t[1]}get selectionText(){let e=this._model.finalSelectionStart,t=this._model.finalSelectionEnd;if(!e||!t)return``;let n=this._bufferService.buffer,r=[];if(this._activeSelectionMode===3){if(e[0]===t[0])return``;let i=e[0]e.replace(za,` `)).join(ua?`\r +`:` +`)}clearSelection(){this._model.clearSelection(),this._removeMouseDownListeners(),this.refresh(),this._onSelectionChange.fire()}refresh(e){this._refreshAnimationFrame||=this._coreBrowserService.window.requestAnimationFrame(()=>this._refresh()),da&&e&&this.selectionText.length&&this._onLinuxMouseSelection.fire(this.selectionText)}_refresh(){this._refreshAnimationFrame=void 0,this._onRedrawRequest.fire({start:this._model.finalSelectionStart,end:this._model.finalSelectionEnd,columnSelectMode:this._activeSelectionMode===3})}_isClickInSelection(e){let t=this._getMouseBufferCoords(e),n=this._model.finalSelectionStart,r=this._model.finalSelectionEnd;return!n||!r||!t?!1:this._areCoordsInSelection(t,n,r)}isCellInSelection(e,t){let n=this._model.finalSelectionStart,r=this._model.finalSelectionEnd;return!n||!r?!1:this._areCoordsInSelection([e,t],n,r)}_areCoordsInSelection(e,t,n){return e[1]>t[1]&&e[1]=t[0]&&e[0]=t[0]}_selectWordAtCursor(e,t){let n=this._linkifier.currentLink?.link?.range;if(n)return this._model.selectionStart=[n.start.x-1,n.start.y-1],this._model.selectionStartLength=Pa(n,this._bufferService.cols),this._model.selectionEnd=void 0,!0;let r=this._getMouseBufferCoords(e);return r?(this._selectWordAt(r,t),this._model.selectionEnd=void 0,!0):!1}selectAll(){this._model.isSelectAllActive=!0,this.refresh(),this._onSelectionChange.fire()}selectLines(e,t){this._model.clearSelection(),e=Math.max(e,0),t=Math.min(t,this._bufferService.buffer.lines.length-1),this._model.selectionStart=[0,e],this._model.selectionEnd=[this._bufferService.cols,t],this.refresh(),this._onSelectionChange.fire()}_handleTrim(e){this._model.handleTrim(e)&&this.refresh()}_getMouseBufferCoords(e){let t=this._mouseService.getCoords(e,this._screenElement,this._bufferService.cols,this._bufferService.rows,!0);if(t)return t[0]--,t[1]--,t[1]+=this._bufferService.buffer.ydisp,t}_getMouseEventScrollAmount(e){let t=Yi(this._coreBrowserService.window,e,this._screenElement)[1],n=this._renderService.dimensions.css.canvas.height;return t>=0&&t<=n?0:(t>n&&(t-=n),t=Math.min(Math.max(t,-Fa),Fa),t/=Fa,t/Math.abs(t)+Math.round(t*(Ia-1)))}shouldForceSelection(e){return sa?e.altKey&&this._optionsService.rawOptions.macOptionClickForcesSelection:e.shiftKey}handleMouseDown(e){if(this._mouseDownTimeStamp=e.timeStamp,!(e.button===2&&this.hasSelection)&&e.button===0){if(!this._enabled){if(!this.shouldForceSelection(e))return;e.stopPropagation()}e.preventDefault(),this._dragScrollAmount=0,this._enabled&&e.shiftKey?this._handleIncrementalClick(e):e.detail===1?this._handleSingleClick(e):e.detail===2?this._handleDoubleClick(e):e.detail===3&&this._handleTripleClick(e),this._addMouseDownListeners(),this.refresh(!0)}}_addMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.addEventListener(`mousemove`,this._mouseMoveListener),this._screenElement.ownerDocument.addEventListener(`mouseup`,this._mouseUpListener)),this._dragScrollIntervalTimer=this._coreBrowserService.window.setInterval(()=>this._dragScroll(),La)}_removeMouseDownListeners(){this._screenElement.ownerDocument&&(this._screenElement.ownerDocument.removeEventListener(`mousemove`,this._mouseMoveListener),this._screenElement.ownerDocument.removeEventListener(`mouseup`,this._mouseUpListener)),this._coreBrowserService.window.clearInterval(this._dragScrollIntervalTimer),this._dragScrollIntervalTimer=void 0}_handleIncrementalClick(e){this._model.selectionStart&&(this._model.selectionEnd=this._getMouseBufferCoords(e))}_handleSingleClick(e){if(this._model.selectionStartLength=0,this._model.isSelectAllActive=!1,this._activeSelectionMode=this.shouldColumnSelect(e)?3:0,this._model.selectionStart=this._getMouseBufferCoords(e),!this._model.selectionStart)return;this._model.selectionEnd=void 0;let t=this._bufferService.buffer.lines.get(this._model.selectionStart[1]);t&&t.length!==this._model.selectionStart[0]&&t.hasWidth(this._model.selectionStart[0])===0&&this._model.selectionStart[0]++}_handleDoubleClick(e){this._selectWordAtCursor(e,!0)&&(this._activeSelectionMode=1)}_handleTripleClick(e){let t=this._getMouseBufferCoords(e);t&&(this._activeSelectionMode=2,this._selectLineAt(t[1]))}shouldColumnSelect(e){return e.altKey&&!(sa&&this._optionsService.rawOptions.macOptionClickForcesSelection)}_handleMouseMove(e){if(e.stopImmediatePropagation(),!this._model.selectionStart)return;let t=this._model.selectionEnd?[this._model.selectionEnd[0],this._model.selectionEnd[1]]:null;if(this._model.selectionEnd=this._getMouseBufferCoords(e),!this._model.selectionEnd){this.refresh(!0);return}this._activeSelectionMode===2?this._model.selectionEnd[1]0?this._model.selectionEnd[0]=this._bufferService.cols:this._dragScrollAmount<0&&(this._model.selectionEnd[0]=0));let n=this._bufferService.buffer;if(this._model.selectionEnd[1]0?(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=this._bufferService.cols),this._model.selectionEnd[1]=Math.min(e.ydisp+this._bufferService.rows,e.lines.length-1)):(this._activeSelectionMode!==3&&(this._model.selectionEnd[0]=0),this._model.selectionEnd[1]=e.ydisp),this.refresh()}}_handleMouseUp(e){let t=e.timeStamp-this._mouseDownTimeStamp;if(this._removeMouseDownListeners(),this.selectionText.length<=1&&tthis._handleTrim(e))}_convertViewportColToCharacterIndex(e,t){let n=t;for(let r=0;t>=r;r++){let i=e.loadCell(r,this._workCell).getChars().length;this._workCell.getWidth()===0?n--:i>1&&t!==r&&(n+=i-1)}return n}setSelection(e,t,n){this._model.clearSelection(),this._removeMouseDownListeners(),this._model.selectionStart=[e,t],this._model.selectionStartLength=n,this.refresh(),this._fireEventIfSelectionChanged()}rightClickSelect(e){this._isClickInSelection(e)||(this._selectWordAtCursor(e,!1)&&this.refresh(!0),this._fireEventIfSelectionChanged())}_getWordAt(e,t,n=!0,r=!0){if(e[0]>=this._bufferService.cols)return;let i=this._bufferService.buffer,a=i.lines.get(e[1]);if(!a)return;let o=i.translateBufferLineToString(e[1],!1),s=this._convertViewportColToCharacterIndex(a,e[0]),c=s,l=e[0]-s,u=0,d=0,f=0,p=0;if(o.charAt(s)===` `){for(;s>0&&o.charAt(s-1)===` `;)s--;for(;c1&&(p+=r-1,c+=r-1);t>0&&s>0&&!this._isCharWordSeparator(a.loadCell(t-1,this._workCell));){a.loadCell(t-1,this._workCell);let e=this._workCell.getChars().length;this._workCell.getWidth()===0?(u++,t--):e>1&&(f+=e-1,s-=e-1),s--,t--}for(;n1&&(p+=e-1,c+=e-1),c++,n++}}c++;let m=s+l-u+f,h=Math.min(this._bufferService.cols,c-s+u+d-f-p);if(!(!t&&o.slice(s,c).trim()===``)){if(n&&m===0&&a.getCodePoint(0)!==32){let t=i.lines.get(e[1]-1);if(t&&a.isWrapped&&t.getCodePoint(this._bufferService.cols-1)!==32){let t=this._getWordAt([this._bufferService.cols-1,e[1]-1],!1,!0,!1);if(t){let e=this._bufferService.cols-t.start;m-=e,h+=e}}}if(r&&m+h===this._bufferService.cols&&a.getCodePoint(this._bufferService.cols-1)!==32){let t=i.lines.get(e[1]+1);if(t?.isWrapped&&t.getCodePoint(0)!==32){let t=this._getWordAt([0,e[1]+1],!1,!1,!0);t&&(h+=t.length)}}return{start:m,length:h}}}_selectWordAt(e,t){let n=this._getWordAt(e,t);if(n){for(;n.start<0;)n.start+=this._bufferService.cols,e[1]--;this._model.selectionStart=[n.start,e[1]],this._model.selectionStartLength=n.length}}_selectToWordAt(e){let t=this._getWordAt(e,!0);if(t){let n=e[1];for(;t.start<0;)t.start+=this._bufferService.cols,n--;if(!this._model.areSelectionValuesReversed())for(;t.start+t.length>this._bufferService.cols;)t.length-=this._bufferService.cols,n++;this._model.selectionEnd=[this._model.areSelectionValuesReversed()?t.start:t.start+t.length,n]}}_isCharWordSeparator(e){return e.getWidth()!==0&&this._optionsService.rawOptions.wordSeparator.indexOf(e.getChars())>=0}_selectLineAt(e){let t=this._bufferService.buffer.getWrappedRangeForLine(e),n={start:{x:0,y:t.first},end:{x:this._bufferService.cols-1,y:t.last}};this._model.selectionStart=[0,t.first],this._model.selectionEnd=void 0,this._model.selectionStartLength=Pa(n,this._bufferService.cols)}};Ba=x([S(3,O),S(4,Oe),S(5,ze),S(6,k),S(7,Be),S(8,Re)],Ba);var Va=class{constructor(){this._data={}}set(e,t,n){this._data[e]||(this._data[e]={}),this._data[e][t]=n}get(e,t){return this._data[e]?this._data[e][t]:void 0}clear(){this._data={}}},Ha=class{constructor(){this._color=new Va,this._css=new Va}setCss(e,t,n){this._css.set(e,t,n)}getCss(e,t){return this._css.get(e,t)}setColor(e,t,n){this._color.set(e,t,n)}getColor(e,t){return this._color.get(e,t)}clear(){this._color.clear(),this._css.clear()}},K=Object.freeze((()=>{let e=[W.toColor(`#2e3436`),W.toColor(`#cc0000`),W.toColor(`#4e9a06`),W.toColor(`#c4a000`),W.toColor(`#3465a4`),W.toColor(`#75507b`),W.toColor(`#06989a`),W.toColor(`#d3d7cf`),W.toColor(`#555753`),W.toColor(`#ef2929`),W.toColor(`#8ae234`),W.toColor(`#fce94f`),W.toColor(`#729fcf`),W.toColor(`#ad7fa8`),W.toColor(`#34e2e2`),W.toColor(`#eeeeec`)],t=[0,95,135,175,215,255];for(let n=0;n<216;n++){let r=t[n/36%6|0],i=t[n/6%6|0],a=t[n%6];e.push({css:H.toCss(r,i,a),rgba:H.toRgba(r,i,a)})}for(let t=0;t<24;t++){let n=8+t*10;e.push({css:H.toCss(n,n,n),rgba:H.toRgba(n,n,n)})}return e})()),Ua=W.toColor(`#ffffff`),Wa=W.toColor(`#000000`),Ga=W.toColor(`#ffffff`),Ka=Wa,qa={css:`rgba(255, 255, 255, 0.3)`,rgba:4294967117},Ja=Ua,Ya=class extends j{constructor(e){super(),this._optionsService=e,this._contrastCache=new Ha,this._halfContrastCache=new Ha,this._onChangeColors=this._register(new P),this.onChangeColors=this._onChangeColors.event,this._colors={foreground:Ua,background:Wa,cursor:Ga,cursorAccent:Ka,selectionForeground:void 0,selectionBackgroundTransparent:qa,selectionBackgroundOpaque:U.blend(Wa,qa),selectionInactiveBackgroundTransparent:qa,selectionInactiveBackgroundOpaque:U.blend(Wa,qa),scrollbarSliderBackground:U.opacity(Ua,.2),scrollbarSliderHoverBackground:U.opacity(Ua,.4),scrollbarSliderActiveBackground:U.opacity(Ua,.5),overviewRulerBorder:Ua,ansi:K.slice(),contrastCache:this._contrastCache,halfContrastCache:this._halfContrastCache},this._updateRestoreColors(),this._setTheme(this._optionsService.rawOptions.theme),this._register(this._optionsService.onSpecificOptionChange(`minimumContrastRatio`,()=>this._contrastCache.clear())),this._register(this._optionsService.onSpecificOptionChange(`theme`,()=>this._setTheme(this._optionsService.rawOptions.theme)))}get colors(){return this._colors}_setTheme(e={}){let t=this._colors;if(t.foreground=q(e.foreground,Ua),t.background=q(e.background,Wa),t.cursor=U.blend(t.background,q(e.cursor,Ga)),t.cursorAccent=U.blend(t.background,q(e.cursorAccent,Ka)),t.selectionBackgroundTransparent=q(e.selectionBackground,qa),t.selectionBackgroundOpaque=U.blend(t.background,t.selectionBackgroundTransparent),t.selectionInactiveBackgroundTransparent=q(e.selectionInactiveBackground,t.selectionBackgroundTransparent),t.selectionInactiveBackgroundOpaque=U.blend(t.background,t.selectionInactiveBackgroundTransparent),t.selectionForeground=e.selectionForeground?q(e.selectionForeground,vi):void 0,t.selectionForeground===vi&&(t.selectionForeground=void 0),U.isOpaque(t.selectionBackgroundTransparent)&&(t.selectionBackgroundTransparent=U.opacity(t.selectionBackgroundTransparent,.3)),U.isOpaque(t.selectionInactiveBackgroundTransparent)&&(t.selectionInactiveBackgroundTransparent=U.opacity(t.selectionInactiveBackgroundTransparent,.3)),t.scrollbarSliderBackground=q(e.scrollbarSliderBackground,U.opacity(t.foreground,.2)),t.scrollbarSliderHoverBackground=q(e.scrollbarSliderHoverBackground,U.opacity(t.foreground,.4)),t.scrollbarSliderActiveBackground=q(e.scrollbarSliderActiveBackground,U.opacity(t.foreground,.5)),t.overviewRulerBorder=q(e.overviewRulerBorder,Ja),t.ansi=K.slice(),t.ansi[0]=q(e.black,K[0]),t.ansi[1]=q(e.red,K[1]),t.ansi[2]=q(e.green,K[2]),t.ansi[3]=q(e.yellow,K[3]),t.ansi[4]=q(e.blue,K[4]),t.ansi[5]=q(e.magenta,K[5]),t.ansi[6]=q(e.cyan,K[6]),t.ansi[7]=q(e.white,K[7]),t.ansi[8]=q(e.brightBlack,K[8]),t.ansi[9]=q(e.brightRed,K[9]),t.ansi[10]=q(e.brightGreen,K[10]),t.ansi[11]=q(e.brightYellow,K[11]),t.ansi[12]=q(e.brightBlue,K[12]),t.ansi[13]=q(e.brightMagenta,K[13]),t.ansi[14]=q(e.brightCyan,K[14]),t.ansi[15]=q(e.brightWhite,K[15]),e.extendedAnsi){let n=Math.min(t.ansi.length-16,e.extendedAnsi.length);for(let r=0;re.index-t.index),r=[];for(let t of n){let n=this._services.get(t.id);if(!n)throw Error(`[createInstance] ${e.name} depends on UNKNOWN service ${t.id._id}.`);r.push(n)}let i=n.length>0?n[0].index:t.length;if(t.length!==i)throw Error(`[createInstance] First service dependency of ${e.name} at position ${i+1} conflicts with ${t.length} static arguments`);return new e(...t,...r)}},Qa={trace:0,debug:1,info:2,warn:3,error:4,off:5},$a=`xterm.js: `,eo=class extends j{constructor(e){super(),this._optionsService=e,this._logLevel=5,this._updateLogLevel(),this._register(this._optionsService.onSpecificOptionChange(`logLevel`,()=>this._updateLogLevel())),to=this}get logLevel(){return this._logLevel}_updateLogLevel(){this._logLevel=Qa[this._optionsService.rawOptions.logLevel]}_evalLazyOptionalParams(e){for(let t=0;tthis._length)for(let t=this._length;t=e;t--)this._array[this._getCyclicIndex(t+n.length)]=this._array[this._getCyclicIndex(t)];for(let t=0;tthis._maxLength){let e=this._length+n.length-this._maxLength;this._startIndex+=e,this._length=this._maxLength,this.onTrimEmitter.fire(e)}else this._length+=n.length}trimStart(e){e>this._length&&(e=this._length),this._startIndex+=e,this._length-=e,this.onTrimEmitter.fire(e)}shiftElements(e,t,n){if(!(t<=0)){if(e<0||e>=this._length)throw Error(`start argument out of range`);if(e+n<0)throw Error(`Cannot shift elements in list beyond index 0`);if(n>0){for(let r=t-1;r>=0;r--)this.set(e+r+n,this.get(e+r));let r=e+t+n-this._length;if(r>0)for(this._length+=r;this._length>this._maxLength;)this._length--,this._startIndex++,this.onTrimEmitter.fire(1)}else for(let r=0;r>22,t&2097152?this._combined[e].charCodeAt(this._combined[e].length-1):n]}set(e,t){this._data[e*J+1]=t[0],t[1].length>1?(this._combined[e]=t[1],this._data[e*J+0]=e|2097152|t[2]<<22):this._data[e*J+0]=t[1].charCodeAt(0)|t[2]<<22}getWidth(e){return this._data[e*J+0]>>22}hasWidth(e){return this._data[e*J+0]&12582912}getFg(e){return this._data[e*J+1]}getBg(e){return this._data[e*J+2]}hasContent(e){return this._data[e*J+0]&4194303}getCodePoint(e){let t=this._data[e*J+0];return t&2097152?this._combined[e].charCodeAt(this._combined[e].length-1):t&2097151}isCombined(e){return this._data[e*J+0]&2097152}getString(e){let t=this._data[e*J+0];return t&2097152?this._combined[e]:t&2097151?he(t&2097151):``}isProtected(e){return this._data[e*J+2]&536870912}loadCell(e,t){return ro=e*J,t.content=this._data[ro+0],t.fg=this._data[ro+1],t.bg=this._data[ro+2],t.content&2097152&&(t.combinedData=this._combined[e]),t.bg&268435456&&(t.extended=this._extendedAttrs[e]),t}setCell(e,t){t.content&2097152&&(this._combined[e]=t.combinedData),t.bg&268435456&&(this._extendedAttrs[e]=t.extended),this._data[e*J+0]=t.content,this._data[e*J+1]=t.fg,this._data[e*J+2]=t.bg}setCellFromCodepoint(e,t,n,r){r.bg&268435456&&(this._extendedAttrs[e]=r.extended),this._data[e*J+0]=t|n<<22,this._data[e*J+1]=r.fg,this._data[e*J+2]=r.bg}addCodepointToCell(e,t,n){let r=this._data[e*J+0];r&2097152?this._combined[e]+=he(t):r&2097151?(this._combined[e]=he(r&2097151)+he(t),r&=-2097152,r|=2097152):r=t|1<<22,n&&(r&=-12582913,r|=n<<22),this._data[e*J+0]=r}insertCells(e,t,n){if(e%=this.length,e&&this.getWidth(e-1)===2&&this.setCellFromCodepoint(e-1,0,1,n),t=0;--n)this.setCell(e+t+n,this.loadCell(e+n,r));for(let r=0;rthis.length){if(this._data.buffer.byteLength>=n*4)this._data=new Uint32Array(this._data.buffer,0,n);else{let e=new Uint32Array(n);e.set(this._data),this._data=e}for(let n=this.length;n=e&&delete this._combined[r]}let r=Object.keys(this._extendedAttrs);for(let t=0;t=e&&delete this._extendedAttrs[n]}}return this.length=e,n*4*io=0;--e)if(this._data[e*J+0]&4194303)return e+(this._data[e*J+0]>>22);return 0}getNoBgTrimmedLength(){for(let e=this.length-1;e>=0;--e)if(this._data[e*J+0]&4194303||this._data[e*J+2]&50331648)return e+(this._data[e*J+0]>>22);return 0}copyCellsFrom(e,t,n,r,i){let a=e._data;if(i)for(let i=r-1;i>=0;i--){for(let e=0;e=t&&(this._combined[i-t+n]=e._combined[i])}}translateToString(e,t,n,r){t??=0,n??=this.length,e&&(n=Math.min(n,this.getTrimmedLength())),r&&(r.length=0);let i=``;for(;t>22||1}return r&&r.push(t),i}};function oo(e,t,n,r,i,a){let o=[];for(let s=0;s=s&&r0&&(e>d||u[e].getTrimmedLength()===0);e--)h++;h>0&&(o.push(s+u.length-h),o.push(h)),s+=u.length-1}return o}function so(e,t){let n=[],r=0,i=t[r],a=0;for(let o=0;ouo(e,r,t)).reduce((e,t)=>e+t),a=0,o=0,s=0;for(;sc&&(a-=c,o++);let l=e[o].getWidth(a-1)===2;l&&a--;let u=l?n-1:n;r.push(u),s+=u}return r}function uo(e,t,n){if(t===e.length-1)return e[t].getTrimmedLength();let r=!e[t].hasContent(n-1)&&e[t].getWidth(n-1)===1,i=e[t+1].getWidth(0)===2;return r&&i?n-1:n}var fo=class e{constructor(t){this.line=t,this.isDisposed=!1,this._disposables=[],this._id=e._nextId++,this._onDispose=this.register(new P),this.onDispose=this._onDispose.event}get id(){return this._id}dispose(){this.isDisposed||(this.isDisposed=!0,this.line=-1,this._onDispose.fire(),gt(this._disposables),this._disposables.length=0)}register(e){return this._disposables.push(e),e}};fo._nextId=1;var po=fo,X={},mo=X.B;X[0]={"`":`◆`,a:`▒`,b:`␉`,c:`␌`,d:`␍`,e:`␊`,f:`°`,g:`±`,h:`␤`,i:`␋`,j:`┘`,k:`┐`,l:`┌`,m:`└`,n:`┼`,o:`⎺`,p:`⎻`,q:`─`,r:`⎼`,s:`⎽`,t:`├`,u:`┤`,v:`┴`,w:`┬`,x:`│`,y:`≤`,z:`≥`,"{":`π`,"|":`≠`,"}":`£`,"~":`·`},X.A={"#":`£`},X.B=void 0,X[4]={"#":`£`,"@":`¾`,"[":`ij`,"\\":`½`,"]":`|`,"{":`¨`,"|":`f`,"}":`¼`,"~":`´`},X.C=X[5]={"[":`Ä`,"\\":`Ö`,"]":`Å`,"^":`Ü`,"`":`é`,"{":`ä`,"|":`ö`,"}":`å`,"~":`ü`},X.R={"#":`£`,"@":`à`,"[":`°`,"\\":`ç`,"]":`§`,"{":`é`,"|":`ù`,"}":`è`,"~":`¨`},X.Q={"@":`à`,"[":`â`,"\\":`ç`,"]":`ê`,"^":`î`,"`":`ô`,"{":`é`,"|":`ù`,"}":`è`,"~":`û`},X.K={"@":`§`,"[":`Ä`,"\\":`Ö`,"]":`Ü`,"{":`ä`,"|":`ö`,"}":`ü`,"~":`ß`},X.Y={"#":`£`,"@":`§`,"[":`°`,"\\":`ç`,"]":`é`,"`":`ù`,"{":`à`,"|":`ò`,"}":`è`,"~":`ì`},X.E=X[6]={"@":`Ä`,"[":`Æ`,"\\":`Ø`,"]":`Å`,"^":`Ü`,"`":`ä`,"{":`æ`,"|":`ø`,"}":`å`,"~":`ü`},X.Z={"#":`£`,"@":`§`,"[":`¡`,"\\":`Ñ`,"]":`¿`,"{":`°`,"|":`ñ`,"}":`ç`},X.H=X[7]={"@":`É`,"[":`Ä`,"\\":`Ö`,"]":`Å`,"^":`Ü`,"`":`é`,"{":`ä`,"|":`ö`,"}":`å`,"~":`ü`},X[`=`]={"#":`ù`,"@":`à`,"[":`é`,"\\":`ç`,"]":`ê`,"^":`î`,_:`è`,"`":`ô`,"{":`ä`,"|":`ö`,"}":`ü`,"~":`û`};var ho=4294967295,go=class{constructor(e,t,n){this._hasScrollback=e,this._optionsService=t,this._bufferService=n,this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.tabs={},this.savedY=0,this.savedX=0,this.savedCurAttrData=Y.clone(),this.savedCharset=mo,this.markers=[],this._nullCell=E.fromCharData([0,ve,1,0]),this._whitespaceCell=E.fromCharData([0,ye,1,32]),this._isClearing=!1,this._memoryCleanupQueue=new ga,this._memoryCleanupPosition=0,this._cols=this._bufferService.cols,this._rows=this._bufferService.rows,this.lines=new no(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}getNullCell(e){return e?(this._nullCell.fg=e.fg,this._nullCell.bg=e.bg,this._nullCell.extended=e.extended):(this._nullCell.fg=0,this._nullCell.bg=0,this._nullCell.extended=new xe),this._nullCell}getWhitespaceCell(e){return e?(this._whitespaceCell.fg=e.fg,this._whitespaceCell.bg=e.bg,this._whitespaceCell.extended=e.extended):(this._whitespaceCell.fg=0,this._whitespaceCell.bg=0,this._whitespaceCell.extended=new xe),this._whitespaceCell}getBlankLine(e,t){return new ao(this._bufferService.cols,this.getNullCell(e),t)}get hasScrollback(){return this._hasScrollback&&this.lines.maxLength>this._rows}get isCursorInViewport(){let e=this.ybase+this.y-this.ydisp;return e>=0&&eho?ho:t}fillViewportRows(e){if(this.lines.length===0){e===void 0&&(e=Y);let t=this._rows;for(;t--;)this.lines.push(this.getBlankLine(e))}}clear(){this.ydisp=0,this.ybase=0,this.y=0,this.x=0,this.lines=new no(this._getCorrectBufferLength(this._rows)),this.scrollTop=0,this.scrollBottom=this._rows-1,this.setupTabStops()}resize(e,t){let n=this.getNullCell(Y),r=0,i=this._getCorrectBufferLength(t);if(i>this.lines.maxLength&&(this.lines.maxLength=i),this.lines.length>0){if(this._cols0&&this.lines.length<=this.ybase+this.y+a+1?(this.ybase--,a++,this.ydisp>0&&this.ydisp--):this.lines.push(new ao(e,n)));else for(let e=this._rows;e>t;e--)this.lines.length>t+this.ybase&&(this.lines.length>this.ybase+this.y+1?this.lines.pop():(this.ybase++,this.ydisp++));if(i0&&(this.lines.trimStart(e),this.ybase=Math.max(this.ybase-e,0),this.ydisp=Math.max(this.ydisp-e,0),this.savedY=Math.max(this.savedY-e,0)),this.lines.maxLength=i}this.x=Math.min(this.x,e-1),this.y=Math.min(this.y,t-1),a&&(this.y+=a),this.savedX=Math.min(this.savedX,e-1),this.scrollTop=0}if(this.scrollBottom=t-1,this._isReflowEnabled&&(this._reflow(e,t),this._cols>e))for(let t=0;t.1*this.lines.length&&(this._memoryCleanupPosition=0,this._memoryCleanupQueue.enqueue(()=>this._batchedMemoryCleanup()))}_batchedMemoryCleanup(){let e=!0;this._memoryCleanupPosition>=this.lines.length&&(this._memoryCleanupPosition=0,e=!1);let t=0;for(;this._memoryCleanupPosition100)return!0;return e}get _isReflowEnabled(){let e=this._optionsService.rawOptions.windowsPty;return e&&e.buildNumber?this._hasScrollback&&e.backend===`conpty`&&e.buildNumber>=21376:this._hasScrollback&&!this._optionsService.rawOptions.windowsMode}_reflow(e,t){this._cols!==e&&(e>this._cols?this._reflowLarger(e,t):this._reflowSmaller(e,t))}_reflowLarger(e,t){let n=this._optionsService.rawOptions.reflowCursorLine,r=oo(this.lines,this._cols,e,this.ybase+this.y,this.getNullCell(Y),n);if(r.length>0){let n=so(this.lines,r);co(this.lines,n.layout),this._reflowLargerAdjustViewport(e,t,n.countRemoved)}}_reflowLargerAdjustViewport(e,t,n){let r=this.getNullCell(Y),i=n;for(;i-->0;)this.ybase===0?(this.y>0&&this.y--,this.lines.length=0;o--){let s=this.lines.get(o);if(!s||!s.isWrapped&&s.getTrimmedLength()<=e)continue;let c=[s];for(;s.isWrapped&&o>0;)s=this.lines.get(--o),c.unshift(s);if(!n){let e=this.ybase+this.y;if(e>=o&&e0&&(i.push({start:o+c.length+a,newLines:p}),a+=p.length),c.push(...p);let m=u.length-1,h=u[m];h===0&&(m--,h=u[m]);let g=c.length-d-1,_=l;for(;g>=0;){let e=Math.min(_,h);if(c[m]===void 0)break;c[m].copyCellsFrom(c[g],_-e,h-e,e,!0),h-=e,h===0&&(m--,h=u[m]),_-=e,_===0&&(g--,_=uo(c,Math.max(g,0),this._cols))}for(let t=0;t0;)this.ybase===0?this.y0){let e=[],t=[];for(let e=0;e=0;l--)if(s&&s.start>r+c){for(let e=s.newLines.length-1;e>=0;e--)this.lines.set(l--,s.newLines[e]);l++,e.push({index:r+1,amount:s.newLines.length}),c+=s.newLines.length,s=i[++o]}else this.lines.set(l,t[r--]);let l=0;for(let t=e.length-1;t>=0;t--)e[t].index+=l,this.lines.onInsertEmitter.fire(e[t]),l+=e[t].amount;let u=Math.max(0,n+a-this.lines.maxLength);u>0&&this.lines.onTrimEmitter.fire(u)}}translateBufferLineToString(e,t,n=0,r){let i=this.lines.get(e);return i?i.translateToString(t,n,r):``}getWrappedRangeForLine(e){let t=e,n=e;for(;t>0&&this.lines.get(t).isWrapped;)t--;for(;n+10;);return e>=this._cols?this._cols-1:e<0?0:e}nextStop(e){for(e??=this.x;!this.tabs[++e]&&e=this._cols?this._cols-1:e<0?0:e}clearMarkers(e){this._isClearing=!0;for(let t=0;t{t.line-=e,t.line<0&&t.dispose()})),t.register(this.lines.onInsert(e=>{t.line>=e.index&&(t.line+=e.amount)})),t.register(this.lines.onDelete(e=>{t.line>=e.index&&t.linee.index&&(t.line-=e.amount)})),t.register(t.onDispose(()=>this._removeMarker(t))),t}_removeMarker(e){this._isClearing||this.markers.splice(this.markers.indexOf(e),1)}},_o=class extends j{constructor(e,t){super(),this._optionsService=e,this._bufferService=t,this._onBufferActivate=this._register(new P),this.onBufferActivate=this._onBufferActivate.event,this.reset(),this._register(this._optionsService.onSpecificOptionChange(`scrollback`,()=>this.resize(this._bufferService.cols,this._bufferService.rows))),this._register(this._optionsService.onSpecificOptionChange(`tabStopWidth`,()=>this.setupTabStops()))}reset(){this._normal=new go(!0,this._optionsService,this._bufferService),this._normal.fillViewportRows(),this._alt=new go(!1,this._optionsService,this._bufferService),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}),this.setupTabStops()}get alt(){return this._alt}get active(){return this._activeBuffer}get normal(){return this._normal}activateNormalBuffer(){this._activeBuffer!==this._normal&&(this._normal.x=this._alt.x,this._normal.y=this._alt.y,this._alt.clearAllMarkers(),this._alt.clear(),this._activeBuffer=this._normal,this._onBufferActivate.fire({activeBuffer:this._normal,inactiveBuffer:this._alt}))}activateAltBuffer(e){this._activeBuffer!==this._alt&&(this._alt.fillViewportRows(e),this._alt.x=this._normal.x,this._alt.y=this._normal.y,this._activeBuffer=this._alt,this._onBufferActivate.fire({activeBuffer:this._alt,inactiveBuffer:this._normal}))}resize(e,t){this._normal.resize(e,t),this._alt.resize(e,t),this.setupTabStops(e)}setupTabStops(e){this._normal.setupTabStops(e),this._alt.setupTabStops(e)}},vo=2,yo=1,bo=class extends j{constructor(e){super(),this.isUserScrolling=!1,this._onResize=this._register(new P),this.onResize=this._onResize.event,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this.cols=Math.max(e.rawOptions.cols||0,vo),this.rows=Math.max(e.rawOptions.rows||0,yo),this.buffers=this._register(new _o(e,this)),this._register(this.buffers.onBufferActivate(e=>{this._onScroll.fire(e.activeBuffer.ydisp)}))}get buffer(){return this.buffers.active}resize(e,t){let n=this.cols!==e,r=this.rows!==t;this.cols=e,this.rows=t,this.buffers.resize(e,t),this._onResize.fire({cols:e,rows:t,colsChanged:n,rowsChanged:r})}reset(){this.buffers.reset(),this.isUserScrolling=!1}scroll(e,t=!1){let n=this.buffer,r;r=this._cachedBlankLine,(!r||r.length!==this.cols||r.getFg(0)!==e.fg||r.getBg(0)!==e.bg)&&(r=n.getBlankLine(e,t),this._cachedBlankLine=r),r.isWrapped=t;let i=n.ybase+n.scrollTop,a=n.ybase+n.scrollBottom;if(n.scrollTop===0){let e=n.lines.isFull;a===n.lines.length-1?e?n.lines.recycle().copyFrom(r):n.lines.push(r.clone()):n.lines.splice(a+1,0,r.clone()),e?this.isUserScrolling&&(n.ydisp=Math.max(n.ydisp-1,0)):(n.ybase++,this.isUserScrolling||n.ydisp++)}else{let e=a-i+1;n.lines.shiftElements(i+1,e-1,-1),n.lines.set(a,r.clone())}this.isUserScrolling||(n.ydisp=n.ybase),this._onScroll.fire(n.ydisp)}scrollLines(e,t){let n=this.buffer;if(e<0){if(n.ydisp===0)return;this.isUserScrolling=!0}else e+n.ydisp>=n.ybase&&(this.isUserScrolling=!1);let r=n.ydisp;n.ydisp=Math.max(Math.min(n.ydisp+e,n.ybase),0),r!==n.ydisp&&(t||this._onScroll.fire(n.ydisp))}};bo=x([S(0,k)],bo);var xo={cols:80,rows:24,cursorBlink:!1,cursorStyle:`block`,cursorWidth:1,cursorInactiveStyle:`outline`,customGlyphs:!0,drawBoldTextInBrightColors:!0,documentOverride:null,fastScrollModifier:`alt`,fastScrollSensitivity:5,fontFamily:`monospace`,fontSize:15,fontWeight:`normal`,fontWeightBold:`bold`,ignoreBracketedPasteMode:!1,lineHeight:1,letterSpacing:0,linkHandler:null,logLevel:`info`,logger:null,scrollback:1e3,scrollOnEraseInDisplay:!1,scrollOnUserInput:!0,scrollSensitivity:1,screenReaderMode:!1,smoothScrollDuration:0,macOptionIsMeta:!1,macOptionClickForcesSelection:!1,minimumContrastRatio:1,disableStdin:!1,allowProposedApi:!1,allowTransparency:!1,tabStopWidth:8,theme:{},reflowCursorLine:!1,rescaleOverlappingGlyphs:!1,rightClickSelectsWord:sa,windowOptions:{},windowsMode:!1,windowsPty:{},wordSeparator:` ()[]{}',"\``,altClickMovesCursor:!0,convertEol:!1,termName:`xterm`,cancelEvents:!1,overviewRuler:{}},So=[`normal`,`bold`,`100`,`200`,`300`,`400`,`500`,`600`,`700`,`800`,`900`],Co=class extends j{constructor(e){super(),this._onOptionChange=this._register(new P),this.onOptionChange=this._onOptionChange.event;let t={...xo};for(let n in e)if(n in t)try{let r=e[n];t[n]=this._sanitizeAndValidateOption(n,r)}catch(e){console.error(e)}this.rawOptions=t,this.options={...t},this._setupOptions(),this._register(A(()=>{this.rawOptions.linkHandler=null,this.rawOptions.documentOverride=null}))}onSpecificOptionChange(e,t){return this.onOptionChange(n=>{n===e&&t(this.rawOptions[e])})}onMultipleOptionChange(e,t){return this.onOptionChange(n=>{e.indexOf(n)!==-1&&t()})}_setupOptions(){let e=e=>{if(!(e in xo))throw Error(`No option with key "${e}"`);return this.rawOptions[e]},t=(e,t)=>{if(!(e in xo))throw Error(`No option with key "${e}"`);t=this._sanitizeAndValidateOption(e,t),this.rawOptions[e]!==t&&(this.rawOptions[e]=t,this._onOptionChange.fire(e))};for(let n in this.rawOptions){let r={get:e.bind(this,n),set:t.bind(this,n)};Object.defineProperty(this.options,n,r)}}_sanitizeAndValidateOption(e,t){switch(e){case`cursorStyle`:if(t||=xo[e],!wo(t))throw Error(`"${t}" is not a valid value for ${e}`);break;case`wordSeparator`:t||=xo[e];break;case`fontWeight`:case`fontWeightBold`:if(typeof t==`number`&&1<=t&&t<=1e3)break;t=So.includes(t)?t:xo[e];break;case`cursorWidth`:t=Math.floor(t);case`lineHeight`:case`tabStopWidth`:if(t<1)throw Error(`${e} cannot be less than 1, value: ${t}`);break;case`minimumContrastRatio`:t=Math.max(1,Math.min(21,Math.round(t*10)/10));break;case`scrollback`:if(t=Math.min(t,4294967295),t<0)throw Error(`${e} cannot be less than 0, value: ${t}`);break;case`fastScrollSensitivity`:case`scrollSensitivity`:if(t<=0)throw Error(`${e} cannot be less than or equal to 0, value: ${t}`);break;case`rows`:case`cols`:if(!t&&t!==0)throw Error(`${e} must be numeric, value: ${t}`);break;case`windowsPty`:t??={};break}return t}};function wo(e){return e===`block`||e===`underline`||e===`bar`}function To(e,t=5){if(typeof e!=`object`)return e;let n=Array.isArray(e)?[]:{};for(let r in e)n[r]=t<=1?e[r]:e[r]&&To(e[r],t-1);return n}var Eo=Object.freeze({insertMode:!1}),Do=Object.freeze({applicationCursorKeys:!1,applicationKeypad:!1,bracketedPasteMode:!1,cursorBlink:void 0,cursorStyle:void 0,origin:!1,reverseWraparound:!1,sendFocus:!1,synchronizedOutput:!1,wraparound:!0}),Oo=class extends j{constructor(e,t,n){super(),this._bufferService=e,this._logService=t,this._optionsService=n,this.isCursorInitialized=!1,this.isCursorHidden=!1,this._onData=this._register(new P),this.onData=this._onData.event,this._onUserInput=this._register(new P),this.onUserInput=this._onUserInput.event,this._onBinary=this._register(new P),this.onBinary=this._onBinary.event,this._onRequestScrollToBottom=this._register(new P),this.onRequestScrollToBottom=this._onRequestScrollToBottom.event,this.modes=To(Eo),this.decPrivateModes=To(Do)}reset(){this.modes=To(Eo),this.decPrivateModes=To(Do)}triggerDataEvent(e,t=!1){if(this._optionsService.rawOptions.disableStdin)return;let n=this._bufferService.buffer;t&&this._optionsService.rawOptions.scrollOnUserInput&&n.ybase!==n.ydisp&&this._onRequestScrollToBottom.fire(),t&&this._onUserInput.fire(),this._logService.debug(`sending data "${e}"`),this._logService.trace(`sending data (codes)`,()=>e.split(``).map(e=>e.charCodeAt(0))),this._onData.fire(e)}triggerBinaryEvent(e){this._optionsService.rawOptions.disableStdin||(this._logService.debug(`sending binary "${e}"`),this._logService.trace(`sending binary (codes)`,()=>e.split(``).map(e=>e.charCodeAt(0))),this._onBinary.fire(e))}};Oo=x([S(0,O),S(1,je),S(2,k)],Oo);var ko={NONE:{events:0,restrict:()=>!1},X10:{events:1,restrict:e=>e.button===4||e.action!==1?!1:(e.ctrl=!1,e.alt=!1,e.shift=!1,!0)},VT200:{events:19,restrict:e=>e.action!==32},DRAG:{events:23,restrict:e=>!(e.action===32&&e.button===3)},ANY:{events:31,restrict:e=>!0}};function Ao(e,t){let n=(e.ctrl?16:0)|(e.shift?4:0)|(e.alt?8:0);return e.button===4?(n|=64,n|=e.action):(n|=e.button&3,e.button&4&&(n|=64),e.button&8&&(n|=128),e.action===32?n|=32:e.action===0&&!t&&(n|=3)),n}var jo=String.fromCharCode,Mo={DEFAULT:e=>{let t=[Ao(e,!1)+32,e.col+32,e.row+32];return t[0]>255||t[1]>255||t[2]>255?``:`\x1B[M${jo(t[0])}${jo(t[1])}${jo(t[2])}`},SGR:e=>{let t=e.action===0&&e.button!==4?`m`:`M`;return`\x1B[<${Ao(e,!0)};${e.col};${e.row}${t}`},SGR_PIXELS:e=>{let t=e.action===0&&e.button!==4?`m`:`M`;return`\x1B[<${Ao(e,!0)};${e.x};${e.y}${t}`}},No=class extends j{constructor(e,t,n){super(),this._bufferService=e,this._coreService=t,this._optionsService=n,this._protocols={},this._encodings={},this._activeProtocol=``,this._activeEncoding=``,this._lastEvent=null,this._wheelPartialScroll=0,this._onProtocolChange=this._register(new P),this.onProtocolChange=this._onProtocolChange.event;for(let e of Object.keys(ko))this.addProtocol(e,ko[e]);for(let e of Object.keys(Mo))this.addEncoding(e,Mo[e]);this.reset()}addProtocol(e,t){this._protocols[e]=t}addEncoding(e,t){this._encodings[e]=t}get activeProtocol(){return this._activeProtocol}get areMouseEventsActive(){return this._protocols[this._activeProtocol].events!==0}set activeProtocol(e){if(!this._protocols[e])throw Error(`unknown protocol "${e}"`);this._activeProtocol=e,this._onProtocolChange.fire(this._protocols[e].events)}get activeEncoding(){return this._activeEncoding}set activeEncoding(e){if(!this._encodings[e])throw Error(`unknown encoding "${e}"`);this._activeEncoding=e}reset(){this.activeProtocol=`NONE`,this.activeEncoding=`DEFAULT`,this._lastEvent=null,this._wheelPartialScroll=0}consumeWheelEvent(e,t,n){if(e.deltaY===0||e.shiftKey||t===void 0||n===void 0)return 0;let r=t/n,i=this._applyScrollModifier(e.deltaY,e);return e.deltaMode===WheelEvent.DOM_DELTA_PIXEL?(i/=r+0,Math.abs(e.deltaY)<50&&(i*=.3),this._wheelPartialScroll+=i,i=Math.floor(Math.abs(this._wheelPartialScroll))*(this._wheelPartialScroll>0?1:-1),this._wheelPartialScroll%=1):e.deltaMode===WheelEvent.DOM_DELTA_PAGE&&(i*=this._bufferService.rows),i}_applyScrollModifier(e,t){return t.altKey||t.ctrlKey||t.shiftKey?e*this._optionsService.rawOptions.fastScrollSensitivity*this._optionsService.rawOptions.scrollSensitivity:e*this._optionsService.rawOptions.scrollSensitivity}triggerMouseEvent(e){if(e.col<0||e.col>=this._bufferService.cols||e.row<0||e.row>=this._bufferService.rows||e.button===4&&e.action===32||e.button===3&&e.action!==32||e.button!==4&&(e.action===2||e.action===3)||(e.col++,e.row++,e.action===32&&this._lastEvent&&this._equalEvents(this._lastEvent,e,this._activeEncoding===`SGR_PIXELS`))||!this._protocols[this._activeProtocol].restrict(e))return!1;let t=this._encodings[this._activeEncoding](e);return t&&(this._activeEncoding===`DEFAULT`?this._coreService.triggerBinaryEvent(t):this._coreService.triggerDataEvent(t,!0)),this._lastEvent=e,!0}explainEvents(e){return{down:!!(e&1),up:!!(e&2),drag:!!(e&4),move:!!(e&8),wheel:!!(e&16)}}_equalEvents(e,t,n){if(n){if(e.x!==t.x||e.y!==t.y)return!1}else if(e.col!==t.col||e.row!==t.row)return!1;return!(e.button!==t.button||e.action!==t.action||e.ctrl!==t.ctrl||e.alt!==t.alt||e.shift!==t.shift)}};No=x([S(0,O),S(1,Oe),S(2,k)],No);var Po=[[768,879],[1155,1158],[1160,1161],[1425,1469],[1471,1471],[1473,1474],[1476,1477],[1479,1479],[1536,1539],[1552,1557],[1611,1630],[1648,1648],[1750,1764],[1767,1768],[1770,1773],[1807,1807],[1809,1809],[1840,1866],[1958,1968],[2027,2035],[2305,2306],[2364,2364],[2369,2376],[2381,2381],[2385,2388],[2402,2403],[2433,2433],[2492,2492],[2497,2500],[2509,2509],[2530,2531],[2561,2562],[2620,2620],[2625,2626],[2631,2632],[2635,2637],[2672,2673],[2689,2690],[2748,2748],[2753,2757],[2759,2760],[2765,2765],[2786,2787],[2817,2817],[2876,2876],[2879,2879],[2881,2883],[2893,2893],[2902,2902],[2946,2946],[3008,3008],[3021,3021],[3134,3136],[3142,3144],[3146,3149],[3157,3158],[3260,3260],[3263,3263],[3270,3270],[3276,3277],[3298,3299],[3393,3395],[3405,3405],[3530,3530],[3538,3540],[3542,3542],[3633,3633],[3636,3642],[3655,3662],[3761,3761],[3764,3769],[3771,3772],[3784,3789],[3864,3865],[3893,3893],[3895,3895],[3897,3897],[3953,3966],[3968,3972],[3974,3975],[3984,3991],[3993,4028],[4038,4038],[4141,4144],[4146,4146],[4150,4151],[4153,4153],[4184,4185],[4448,4607],[4959,4959],[5906,5908],[5938,5940],[5970,5971],[6002,6003],[6068,6069],[6071,6077],[6086,6086],[6089,6099],[6109,6109],[6155,6157],[6313,6313],[6432,6434],[6439,6440],[6450,6450],[6457,6459],[6679,6680],[6912,6915],[6964,6964],[6966,6970],[6972,6972],[6978,6978],[7019,7027],[7616,7626],[7678,7679],[8203,8207],[8234,8238],[8288,8291],[8298,8303],[8400,8431],[12330,12335],[12441,12442],[43014,43014],[43019,43019],[43045,43046],[64286,64286],[65024,65039],[65056,65059],[65279,65279],[65529,65531]],Fo=[[68097,68099],[68101,68102],[68108,68111],[68152,68154],[68159,68159],[119143,119145],[119155,119170],[119173,119179],[119210,119213],[119362,119364],[917505,917505],[917536,917631],[917760,917999]],Z;function Io(e,t){let n=0,r=t.length-1,i;if(et[r][1])return!1;for(;r>=n;)if(i=n+r>>1,e>t[i][1])n=i+1;else if(e=131072&&e<=196605||e>=196608&&e<=262141?2:1}charProperties(e,t){let n=this.wcwidth(e),r=n===0&&t!==0;if(r){let e=Ro.extractWidth(t);e===0?r=!1:e>n&&(n=e)}return Ro.createPropertyValue(0,n,r)}},Ro=class e{constructor(){this._providers=Object.create(null),this._active=``,this._onChange=new P,this.onChange=this._onChange.event;let e=new Lo;this.register(e),this._active=e.version,this._activeProvider=e}static extractShouldJoin(e){return(e&1)!=0}static extractWidth(e){return e>>1&3}static extractCharKind(e){return e>>3}static createPropertyValue(e,t,n=!1){return(e&16777215)<<3|(t&3)<<1|!!n}dispose(){this._onChange.dispose()}get versions(){return Object.keys(this._providers)}get activeVersion(){return this._active}set activeVersion(e){if(!this._providers[e])throw Error(`unknown Unicode version "${e}"`);this._active=e,this._activeProvider=this._providers[e],this._onChange.fire(e)}register(e){this._providers[e.version]=e}wcwidth(e){return this._activeProvider.wcwidth(e)}getStringCellWidth(t){let n=0,r=0,i=t.length;for(let a=0;a=i)return n+this.wcwidth(o);let e=t.charCodeAt(a);56320<=e&&e<=57343?o=(o-55296)*1024+e-56320+65536:n+=this.wcwidth(e)}let s=this.charProperties(o,r),c=e.extractWidth(s);e.extractShouldJoin(s)&&(c-=e.extractWidth(r)),n+=c,r=s}return n}charProperties(e,t){return this._activeProvider.charProperties(e,t)}},zo=class{constructor(){this.glevel=0,this._charsets=[]}reset(){this.charset=void 0,this._charsets=[],this.glevel=0}setgLevel(e){this.glevel=e,this.charset=this._charsets[e]}setgCharset(e,t){this._charsets[e]=t,this.glevel===e&&(this.charset=t)}};function Bo(e){let t=e.buffer.lines.get(e.buffer.ybase+e.buffer.y-1)?.get(e.cols-1),n=e.buffer.lines.get(e.buffer.ybase+e.buffer.y);n&&t&&(n.isWrapped=t[3]!==0&&t[3]!==32)}var Vo=2147483647,Ho=256,Uo=class e{constructor(e=32,t=32){if(this.maxLength=e,this.maxSubParamsLength=t,t>Ho)throw Error(`maxSubParamsLength must not be greater than 256`);this.params=new Int32Array(e),this.length=0,this._subParams=new Int32Array(t),this._subParamsLength=0,this._subParamsIdx=new Uint16Array(e),this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}static fromArray(t){let n=new e;if(!t.length)return n;for(let e=+!!Array.isArray(t[0]);e>8,r=this._subParamsIdx[t]&255;r-n>0&&e.push(Array.prototype.slice.call(this._subParams,n,r))}return e}reset(){this.length=0,this._subParamsLength=0,this._rejectDigits=!1,this._rejectSubDigits=!1,this._digitIsSub=!1}addParam(e){if(this._digitIsSub=!1,this.length>=this.maxLength){this._rejectDigits=!0;return}if(e<-1)throw Error(`values lesser than -1 are not allowed`);this._subParamsIdx[this.length]=this._subParamsLength<<8|this._subParamsLength,this.params[this.length++]=e>Vo?Vo:e}addSubParam(e){if(this._digitIsSub=!0,this.length){if(this._rejectDigits||this._subParamsLength>=this.maxSubParamsLength){this._rejectSubDigits=!0;return}if(e<-1)throw Error(`values lesser than -1 are not allowed`);this._subParams[this._subParamsLength++]=e>Vo?Vo:e,this._subParamsIdx[this.length-1]++}}hasSubParams(e){return(this._subParamsIdx[e]&255)-(this._subParamsIdx[e]>>8)>0}getSubParams(e){let t=this._subParamsIdx[e]>>8,n=this._subParamsIdx[e]&255;return n-t>0?this._subParams.subarray(t,n):null}getSubParamsAll(){let e={};for(let t=0;t>8,r=this._subParamsIdx[t]&255;r-n>0&&(e[t]=this._subParams.slice(n,r))}return e}addDigit(e){let t;if(this._rejectDigits||!(t=this._digitIsSub?this._subParamsLength:this.length)||this._digitIsSub&&this._rejectSubDigits)return;let n=this._digitIsSub?this._subParams:this.params,r=n[t-1];n[t-1]=~r?Math.min(r*10+e,Vo):e}},Wo=[],Go=class{constructor(){this._state=0,this._active=Wo,this._id=-1,this._handlers=Object.create(null),this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}registerHandler(e,t){this._handlers[e]===void 0&&(this._handlers[e]=[]);let n=this._handlers[e];return n.push(t),{dispose:()=>{let e=n.indexOf(t);e!==-1&&n.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=Wo}reset(){if(this._state===2)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].end(!1);this._stack.paused=!1,this._active=Wo,this._id=-1,this._state=0}_start(){if(this._active=this._handlers[this._id]||Wo,!this._active.length)this._handlerFb(this._id,`START`);else for(let e=this._active.length-1;e>=0;e--)this._active[e].start()}_put(e,t,n){if(!this._active.length)this._handlerFb(this._id,`PUT`,T(e,t,n));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,n)}start(){this.reset(),this._state=1}put(e,t,n){if(this._state!==3){if(this._state===1)for(;t0&&this._put(e,t,n)}}end(e,t=!0){if(this._state!==0){if(this._state!==3)if(this._state===1&&this._start(),!this._active.length)this._handlerFb(this._id,`END`,e);else{let n=!1,r=this._active.length-1,i=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,n=t,i=this._stack.fallThrough,this._stack.paused=!1),!i&&n===!1){for(;r>=0&&(n=this._active[r].end(e),n!==!0);r--)if(n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,n;r--}for(;r>=0;r--)if(n=this._active[r].end(!1),n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,n}this._active=Wo,this._id=-1,this._state=0}}},Ko=class{constructor(e){this._handler=e,this._data=``,this._hitLimit=!1}start(){this._data=``,this._hitLimit=!1}put(e,t,n){this._hitLimit||(this._data+=T(e,t,n),this._data.length>1e7&&(this._data=``,this._hitLimit=!0))}end(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data),t instanceof Promise))return t.then(e=>(this._data=``,this._hitLimit=!1,e));return this._data=``,this._hitLimit=!1,t}},qo=[],Jo=class{constructor(){this._handlers=Object.create(null),this._active=qo,this._ident=0,this._handlerFb=()=>{},this._stack={paused:!1,loopPosition:0,fallThrough:!1}}dispose(){this._handlers=Object.create(null),this._handlerFb=()=>{},this._active=qo}registerHandler(e,t){this._handlers[e]===void 0&&(this._handlers[e]=[]);let n=this._handlers[e];return n.push(t),{dispose:()=>{let e=n.indexOf(t);e!==-1&&n.splice(e,1)}}}clearHandler(e){this._handlers[e]&&delete this._handlers[e]}setHandlerFallback(e){this._handlerFb=e}reset(){if(this._active.length)for(let e=this._stack.paused?this._stack.loopPosition-1:this._active.length-1;e>=0;--e)this._active[e].unhook(!1);this._stack.paused=!1,this._active=qo,this._ident=0}hook(e,t){if(this.reset(),this._ident=e,this._active=this._handlers[e]||qo,!this._active.length)this._handlerFb(this._ident,`HOOK`,t);else for(let e=this._active.length-1;e>=0;e--)this._active[e].hook(t)}put(e,t,n){if(!this._active.length)this._handlerFb(this._ident,`PUT`,T(e,t,n));else for(let r=this._active.length-1;r>=0;r--)this._active[r].put(e,t,n)}unhook(e,t=!0){if(!this._active.length)this._handlerFb(this._ident,`UNHOOK`,e);else{let n=!1,r=this._active.length-1,i=!1;if(this._stack.paused&&(r=this._stack.loopPosition-1,n=t,i=this._stack.fallThrough,this._stack.paused=!1),!i&&n===!1){for(;r>=0&&(n=this._active[r].unhook(e),n!==!0);r--)if(n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!1,n;r--}for(;r>=0;r--)if(n=this._active[r].unhook(!1),n instanceof Promise)return this._stack.paused=!0,this._stack.loopPosition=r,this._stack.fallThrough=!0,n}this._active=qo,this._ident=0}},Yo=new Uo;Yo.addParam(0);var Xo=class{constructor(e){this._handler=e,this._data=``,this._params=Yo,this._hitLimit=!1}hook(e){this._params=e.length>1||e.params[0]?e.clone():Yo,this._data=``,this._hitLimit=!1}put(e,t,n){this._hitLimit||(this._data+=T(e,t,n),this._data.length>1e7&&(this._data=``,this._hitLimit=!0))}unhook(e){let t=!1;if(this._hitLimit)t=!1;else if(e&&(t=this._handler(this._data,this._params),t instanceof Promise))return t.then(e=>(this._params=Yo,this._data=``,this._hitLimit=!1,e));return this._params=Yo,this._data=``,this._hitLimit=!1,t}},Zo=class{constructor(e){this.table=new Uint8Array(e)}setDefault(e,t){this.table.fill(e<<4|t)}add(e,t,n,r){this.table[t<<8|e]=n<<4|r}addMany(e,t,n,r){for(let i=0;it),n=(e,n)=>t.slice(e,n),r=n(32,127),i=n(0,24);i.push(25),i.push.apply(i,n(28,32));let a=n(0,14),o;for(o in e.setDefault(1,0),e.addMany(r,0,2,0),a)e.addMany([24,26,153,154],o,3,0),e.addMany(n(128,144),o,3,0),e.addMany(n(144,152),o,3,0),e.add(156,o,0,0),e.add(27,o,11,1),e.add(157,o,4,8),e.addMany([152,158,159],o,0,7),e.add(155,o,11,3),e.add(144,o,11,9);return e.addMany(i,0,3,0),e.addMany(i,1,3,1),e.add(127,1,0,1),e.addMany(i,8,0,8),e.addMany(i,3,3,3),e.add(127,3,0,3),e.addMany(i,4,3,4),e.add(127,4,0,4),e.addMany(i,6,3,6),e.addMany(i,5,3,5),e.add(127,5,0,5),e.addMany(i,2,3,2),e.add(127,2,0,2),e.add(93,1,4,8),e.addMany(r,8,5,8),e.add(127,8,5,8),e.addMany([156,27,24,26,7],8,6,0),e.addMany(n(28,32),8,0,8),e.addMany([88,94,95],1,0,7),e.addMany(r,7,0,7),e.addMany(i,7,0,7),e.add(156,7,0,0),e.add(127,7,0,7),e.add(91,1,11,3),e.addMany(n(64,127),3,7,0),e.addMany(n(48,60),3,8,4),e.addMany([60,61,62,63],3,9,4),e.addMany(n(48,60),4,8,4),e.addMany(n(64,127),4,7,0),e.addMany([60,61,62,63],4,0,6),e.addMany(n(32,64),6,0,6),e.add(127,6,0,6),e.addMany(n(64,127),6,0,0),e.addMany(n(32,48),3,9,5),e.addMany(n(32,48),5,9,5),e.addMany(n(48,64),5,0,6),e.addMany(n(64,127),5,7,0),e.addMany(n(32,48),4,9,5),e.addMany(n(32,48),1,9,2),e.addMany(n(32,48),2,9,2),e.addMany(n(48,127),2,10,0),e.addMany(n(48,80),1,10,0),e.addMany(n(81,88),1,10,0),e.addMany([89,90,92],1,10,0),e.addMany(n(96,127),1,10,0),e.add(80,1,11,9),e.addMany(i,9,0,9),e.add(127,9,0,9),e.addMany(n(28,32),9,0,9),e.addMany(n(32,48),9,9,12),e.addMany(n(48,60),9,8,10),e.addMany([60,61,62,63],9,9,10),e.addMany(i,11,0,11),e.addMany(n(32,128),11,0,11),e.addMany(n(28,32),11,0,11),e.addMany(i,10,0,10),e.add(127,10,0,10),e.addMany(n(28,32),10,0,10),e.addMany(n(48,60),10,8,10),e.addMany([60,61,62,63],10,0,11),e.addMany(n(32,48),10,9,12),e.addMany(i,12,0,12),e.add(127,12,0,12),e.addMany(n(28,32),12,0,12),e.addMany(n(32,48),12,9,12),e.addMany(n(48,64),12,0,11),e.addMany(n(64,127),12,12,13),e.addMany(n(64,127),10,12,13),e.addMany(n(64,127),9,12,13),e.addMany(i,13,13,13),e.addMany(r,13,13,13),e.add(127,13,0,13),e.addMany([27,156,24,26],13,14,0),e.add(Qo,0,2,0),e.add(Qo,8,5,8),e.add(Qo,6,0,6),e.add(Qo,11,0,11),e.add(Qo,13,13,13),e}(),es=class extends j{constructor(e=$o){super(),this._transitions=e,this._parseStack={state:0,handlers:[],handlerPos:0,transition:0,chunkPos:0},this.initialState=0,this.currentState=this.initialState,this._params=new Uo,this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._printHandlerFb=(e,t,n)=>{},this._executeHandlerFb=e=>{},this._csiHandlerFb=(e,t)=>{},this._escHandlerFb=e=>{},this._errorHandlerFb=e=>e,this._printHandler=this._printHandlerFb,this._executeHandlers=Object.create(null),this._csiHandlers=Object.create(null),this._escHandlers=Object.create(null),this._register(A(()=>{this._csiHandlers=Object.create(null),this._executeHandlers=Object.create(null),this._escHandlers=Object.create(null)})),this._oscParser=this._register(new Go),this._dcsParser=this._register(new Jo),this._errorHandler=this._errorHandlerFb,this.registerEscHandler({final:`\\`},()=>!0)}_identifier(e,t=[64,126]){let n=0;if(e.prefix){if(e.prefix.length>1)throw Error(`only one byte as prefix supported`);if(n=e.prefix.charCodeAt(0),n&&60>n||n>63)throw Error(`prefix must be in range 0x3c .. 0x3f`)}if(e.intermediates){if(e.intermediates.length>2)throw Error(`only two bytes as intermediates are supported`);for(let t=0;tr||r>47)throw Error(`intermediate must be in range 0x20 .. 0x2f`);n<<=8,n|=r}}if(e.final.length!==1)throw Error(`final must be a single byte`);let r=e.final.charCodeAt(0);if(t[0]>r||r>t[1])throw Error(`final must be in range ${t[0]} .. ${t[1]}`);return n<<=8,n|=r,n}identToString(e){let t=[];for(;e;)t.push(String.fromCharCode(e&255)),e>>=8;return t.reverse().join(``)}setPrintHandler(e){this._printHandler=e}clearPrintHandler(){this._printHandler=this._printHandlerFb}registerEscHandler(e,t){let n=this._identifier(e,[48,126]);this._escHandlers[n]===void 0&&(this._escHandlers[n]=[]);let r=this._escHandlers[n];return r.push(t),{dispose:()=>{let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}}clearEscHandler(e){this._escHandlers[this._identifier(e,[48,126])]&&delete this._escHandlers[this._identifier(e,[48,126])]}setEscHandlerFallback(e){this._escHandlerFb=e}setExecuteHandler(e,t){this._executeHandlers[e.charCodeAt(0)]=t}clearExecuteHandler(e){this._executeHandlers[e.charCodeAt(0)]&&delete this._executeHandlers[e.charCodeAt(0)]}setExecuteHandlerFallback(e){this._executeHandlerFb=e}registerCsiHandler(e,t){let n=this._identifier(e);this._csiHandlers[n]===void 0&&(this._csiHandlers[n]=[]);let r=this._csiHandlers[n];return r.push(t),{dispose:()=>{let e=r.indexOf(t);e!==-1&&r.splice(e,1)}}}clearCsiHandler(e){this._csiHandlers[this._identifier(e)]&&delete this._csiHandlers[this._identifier(e)]}setCsiHandlerFallback(e){this._csiHandlerFb=e}registerDcsHandler(e,t){return this._dcsParser.registerHandler(this._identifier(e),t)}clearDcsHandler(e){this._dcsParser.clearHandler(this._identifier(e))}setDcsHandlerFallback(e){this._dcsParser.setHandlerFallback(e)}registerOscHandler(e,t){return this._oscParser.registerHandler(e,t)}clearOscHandler(e){this._oscParser.clearHandler(e)}setOscHandlerFallback(e){this._oscParser.setHandlerFallback(e)}setErrorHandler(e){this._errorHandler=e}clearErrorHandler(){this._errorHandler=this._errorHandlerFb}reset(){this.currentState=this.initialState,this._oscParser.reset(),this._dcsParser.reset(),this._params.reset(),this._params.addParam(0),this._collect=0,this.precedingJoinState=0,this._parseStack.state!==0&&(this._parseStack.state=2,this._parseStack.handlers=[])}_preserveStack(e,t,n,r,i){this._parseStack.state=e,this._parseStack.handlers=t,this._parseStack.handlerPos=n,this._parseStack.transition=r,this._parseStack.chunkPos=i}parse(e,t,n){let r=0,i=0,a=0,o;if(this._parseStack.state)if(this._parseStack.state===2)this._parseStack.state=0,a=this._parseStack.chunkPos+1;else{if(n===void 0||this._parseStack.state===1)throw this._parseStack.state=1,Error(`improper continuation due to previous async handler, giving up parsing`);let t=this._parseStack.handlers,i=this._parseStack.handlerPos-1;switch(this._parseStack.state){case 3:if(n===!1&&i>-1){for(;i>=0&&(o=t[i](this._params),o!==!0);i--)if(o instanceof Promise)return this._parseStack.handlerPos=i,o}this._parseStack.handlers=[];break;case 4:if(n===!1&&i>-1){for(;i>=0&&(o=t[i](),o!==!0);i--)if(o instanceof Promise)return this._parseStack.handlerPos=i,o}this._parseStack.handlers=[];break;case 6:if(r=e[this._parseStack.chunkPos],o=this._dcsParser.unhook(r!==24&&r!==26,n),o)return o;r===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break;case 5:if(r=e[this._parseStack.chunkPos],o=this._oscParser.end(r!==24&&r!==26,n),o)return o;r===27&&(this._parseStack.transition|=1),this._params.reset(),this._params.addParam(0),this._collect=0;break}this._parseStack.state=0,a=this._parseStack.chunkPos+1,this.precedingJoinState=0,this.currentState=this._parseStack.transition&15}for(let n=a;n>4){case 2:for(let i=n+1;;++i){if(i>=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=t||(r=e[i])<32||r>126&&r=0&&(o=a[s](this._params),o!==!0);s--)if(o instanceof Promise)return this._preserveStack(3,a,s,i,n),o;s<0&&this._csiHandlerFb(this._collect<<8|r,this._params),this.precedingJoinState=0;break;case 8:do switch(r){case 59:this._params.addParam(0);break;case 58:this._params.addSubParam(-1);break;default:this._params.addDigit(r-48)}while(++n47&&r<60);n--;break;case 9:this._collect<<=8,this._collect|=r;break;case 10:let c=this._escHandlers[this._collect<<8|r],l=c?c.length-1:-1;for(;l>=0&&(o=c[l](),o!==!0);l--)if(o instanceof Promise)return this._preserveStack(4,c,l,i,n),o;l<0&&this._escHandlerFb(this._collect<<8|r),this.precedingJoinState=0;break;case 11:this._params.reset(),this._params.addParam(0),this._collect=0;break;case 12:this._dcsParser.hook(this._collect<<8|r,this._params);break;case 13:for(let i=n+1;;++i)if(i>=t||(r=e[i])===24||r===26||r===27||r>127&&r=t||(r=e[i])<32||r>127&&r>4:i>>8}return n}}function is(e,t){let n=e.toString(16),r=n.length<2?`0`+n:n;switch(t){case 4:return n[0];case 8:return r;case 12:return(r+r).slice(0,3);default:return r+r}}function as(e,t=16){let[n,r,i]=e;return`rgb:${is(n,t)}/${is(r,t)}/${is(i,t)}`}var os={"(":0,")":1,"*":2,"+":3,"-":1,".":2},ss=131072,cs=10;function ls(e,t){if(e>24)return t.setWinLines||!1;switch(e){case 1:return!!t.restoreWin;case 2:return!!t.minimizeWin;case 3:return!!t.setWinPosition;case 4:return!!t.setWinSizePixels;case 5:return!!t.raiseWin;case 6:return!!t.lowerWin;case 7:return!!t.refreshWin;case 8:return!!t.setWinSizeChars;case 9:return!!t.maximizeWin;case 10:return!!t.fullscreenWin;case 11:return!!t.getWinState;case 13:return!!t.getWinPosition;case 14:return!!t.getWinSizePixels;case 15:return!!t.getScreenSizePixels;case 16:return!!t.getCellSizePixels;case 18:return!!t.getWinSizeChars;case 19:return!!t.getScreenSizeChars;case 20:return!!t.getIconTitle;case 21:return!!t.getWinTitle;case 22:return!!t.pushTitle;case 23:return!!t.popTitle;case 24:return!!t.setWinLines}return!1}var us=5e3,ds=0,fs=class extends j{constructor(e,t,n,r,i,a,o,s,c=new es){super(),this._bufferService=e,this._charsetService=t,this._coreService=n,this._logService=r,this._optionsService=i,this._oscLinkService=a,this._coreMouseService=o,this._unicodeService=s,this._parser=c,this._parseBuffer=new Uint32Array(4096),this._stringDecoder=new ge,this._utf8Decoder=new _e,this._windowTitle=``,this._iconName=``,this._windowTitleStack=[],this._iconNameStack=[],this._curAttrData=Y.clone(),this._eraseAttrDataInternal=Y.clone(),this._onRequestBell=this._register(new P),this.onRequestBell=this._onRequestBell.event,this._onRequestRefreshRows=this._register(new P),this.onRequestRefreshRows=this._onRequestRefreshRows.event,this._onRequestReset=this._register(new P),this.onRequestReset=this._onRequestReset.event,this._onRequestSendFocus=this._register(new P),this.onRequestSendFocus=this._onRequestSendFocus.event,this._onRequestSyncScrollBar=this._register(new P),this.onRequestSyncScrollBar=this._onRequestSyncScrollBar.event,this._onRequestWindowsOptionsReport=this._register(new P),this.onRequestWindowsOptionsReport=this._onRequestWindowsOptionsReport.event,this._onA11yChar=this._register(new P),this.onA11yChar=this._onA11yChar.event,this._onA11yTab=this._register(new P),this.onA11yTab=this._onA11yTab.event,this._onCursorMove=this._register(new P),this.onCursorMove=this._onCursorMove.event,this._onLineFeed=this._register(new P),this.onLineFeed=this._onLineFeed.event,this._onScroll=this._register(new P),this.onScroll=this._onScroll.event,this._onTitleChange=this._register(new P),this.onTitleChange=this._onTitleChange.event,this._onColor=this._register(new P),this.onColor=this._onColor.event,this._parseStack={paused:!1,cursorStartX:0,cursorStartY:0,decodedLength:0,position:0},this._specialColors=[256,257,258],this._register(this._parser),this._dirtyRowTracker=new ps(this._bufferService),this._activeBuffer=this._bufferService.buffer,this._register(this._bufferService.buffers.onBufferActivate(e=>this._activeBuffer=e.activeBuffer)),this._parser.setCsiHandlerFallback((e,t)=>{this._logService.debug(`Unknown CSI code: `,{identifier:this._parser.identToString(e),params:t.toArray()})}),this._parser.setEscHandlerFallback(e=>{this._logService.debug(`Unknown ESC code: `,{identifier:this._parser.identToString(e)})}),this._parser.setExecuteHandlerFallback(e=>{this._logService.debug(`Unknown EXECUTE code: `,{code:e})}),this._parser.setOscHandlerFallback((e,t,n)=>{this._logService.debug(`Unknown OSC code: `,{identifier:e,action:t,data:n})}),this._parser.setDcsHandlerFallback((e,t,n)=>{t===`HOOK`&&(n=n.toArray()),this._logService.debug(`Unknown DCS code: `,{identifier:this._parser.identToString(e),action:t,payload:n})}),this._parser.setPrintHandler((e,t,n)=>this.print(e,t,n)),this._parser.registerCsiHandler({final:`@`},e=>this.insertChars(e)),this._parser.registerCsiHandler({intermediates:` `,final:`@`},e=>this.scrollLeft(e)),this._parser.registerCsiHandler({final:`A`},e=>this.cursorUp(e)),this._parser.registerCsiHandler({intermediates:` `,final:`A`},e=>this.scrollRight(e)),this._parser.registerCsiHandler({final:`B`},e=>this.cursorDown(e)),this._parser.registerCsiHandler({final:`C`},e=>this.cursorForward(e)),this._parser.registerCsiHandler({final:`D`},e=>this.cursorBackward(e)),this._parser.registerCsiHandler({final:`E`},e=>this.cursorNextLine(e)),this._parser.registerCsiHandler({final:`F`},e=>this.cursorPrecedingLine(e)),this._parser.registerCsiHandler({final:`G`},e=>this.cursorCharAbsolute(e)),this._parser.registerCsiHandler({final:`H`},e=>this.cursorPosition(e)),this._parser.registerCsiHandler({final:`I`},e=>this.cursorForwardTab(e)),this._parser.registerCsiHandler({final:`J`},e=>this.eraseInDisplay(e,!1)),this._parser.registerCsiHandler({prefix:`?`,final:`J`},e=>this.eraseInDisplay(e,!0)),this._parser.registerCsiHandler({final:`K`},e=>this.eraseInLine(e,!1)),this._parser.registerCsiHandler({prefix:`?`,final:`K`},e=>this.eraseInLine(e,!0)),this._parser.registerCsiHandler({final:`L`},e=>this.insertLines(e)),this._parser.registerCsiHandler({final:`M`},e=>this.deleteLines(e)),this._parser.registerCsiHandler({final:`P`},e=>this.deleteChars(e)),this._parser.registerCsiHandler({final:`S`},e=>this.scrollUp(e)),this._parser.registerCsiHandler({final:`T`},e=>this.scrollDown(e)),this._parser.registerCsiHandler({final:`X`},e=>this.eraseChars(e)),this._parser.registerCsiHandler({final:`Z`},e=>this.cursorBackwardTab(e)),this._parser.registerCsiHandler({final:"`"},e=>this.charPosAbsolute(e)),this._parser.registerCsiHandler({final:`a`},e=>this.hPositionRelative(e)),this._parser.registerCsiHandler({final:`b`},e=>this.repeatPrecedingCharacter(e)),this._parser.registerCsiHandler({final:`c`},e=>this.sendDeviceAttributesPrimary(e)),this._parser.registerCsiHandler({prefix:`>`,final:`c`},e=>this.sendDeviceAttributesSecondary(e)),this._parser.registerCsiHandler({final:`d`},e=>this.linePosAbsolute(e)),this._parser.registerCsiHandler({final:`e`},e=>this.vPositionRelative(e)),this._parser.registerCsiHandler({final:`f`},e=>this.hVPosition(e)),this._parser.registerCsiHandler({final:`g`},e=>this.tabClear(e)),this._parser.registerCsiHandler({final:`h`},e=>this.setMode(e)),this._parser.registerCsiHandler({prefix:`?`,final:`h`},e=>this.setModePrivate(e)),this._parser.registerCsiHandler({final:`l`},e=>this.resetMode(e)),this._parser.registerCsiHandler({prefix:`?`,final:`l`},e=>this.resetModePrivate(e)),this._parser.registerCsiHandler({final:`m`},e=>this.charAttributes(e)),this._parser.registerCsiHandler({final:`n`},e=>this.deviceStatus(e)),this._parser.registerCsiHandler({prefix:`?`,final:`n`},e=>this.deviceStatusPrivate(e)),this._parser.registerCsiHandler({intermediates:`!`,final:`p`},e=>this.softReset(e)),this._parser.registerCsiHandler({intermediates:` `,final:`q`},e=>this.setCursorStyle(e)),this._parser.registerCsiHandler({final:`r`},e=>this.setScrollRegion(e)),this._parser.registerCsiHandler({final:`s`},e=>this.saveCursor(e)),this._parser.registerCsiHandler({final:`t`},e=>this.windowOptions(e)),this._parser.registerCsiHandler({final:`u`},e=>this.restoreCursor(e)),this._parser.registerCsiHandler({intermediates:`'`,final:`}`},e=>this.insertColumns(e)),this._parser.registerCsiHandler({intermediates:`'`,final:`~`},e=>this.deleteColumns(e)),this._parser.registerCsiHandler({intermediates:`"`,final:`q`},e=>this.selectProtected(e)),this._parser.registerCsiHandler({intermediates:`$`,final:`p`},e=>this.requestMode(e,!0)),this._parser.registerCsiHandler({prefix:`?`,intermediates:`$`,final:`p`},e=>this.requestMode(e,!1)),this._parser.setExecuteHandler(L.BEL,()=>this.bell()),this._parser.setExecuteHandler(L.LF,()=>this.lineFeed()),this._parser.setExecuteHandler(L.VT,()=>this.lineFeed()),this._parser.setExecuteHandler(L.FF,()=>this.lineFeed()),this._parser.setExecuteHandler(L.CR,()=>this.carriageReturn()),this._parser.setExecuteHandler(L.BS,()=>this.backspace()),this._parser.setExecuteHandler(L.HT,()=>this.tab()),this._parser.setExecuteHandler(L.SO,()=>this.shiftOut()),this._parser.setExecuteHandler(L.SI,()=>this.shiftIn()),this._parser.setExecuteHandler(hi.IND,()=>this.index()),this._parser.setExecuteHandler(hi.NEL,()=>this.nextLine()),this._parser.setExecuteHandler(hi.HTS,()=>this.tabSet()),this._parser.registerOscHandler(0,new Ko(e=>(this.setTitle(e),this.setIconName(e),!0))),this._parser.registerOscHandler(1,new Ko(e=>this.setIconName(e))),this._parser.registerOscHandler(2,new Ko(e=>this.setTitle(e))),this._parser.registerOscHandler(4,new Ko(e=>this.setOrReportIndexedColor(e))),this._parser.registerOscHandler(8,new Ko(e=>this.setHyperlink(e))),this._parser.registerOscHandler(10,new Ko(e=>this.setOrReportFgColor(e))),this._parser.registerOscHandler(11,new Ko(e=>this.setOrReportBgColor(e))),this._parser.registerOscHandler(12,new Ko(e=>this.setOrReportCursorColor(e))),this._parser.registerOscHandler(104,new Ko(e=>this.restoreIndexedColor(e))),this._parser.registerOscHandler(110,new Ko(e=>this.restoreFgColor(e))),this._parser.registerOscHandler(111,new Ko(e=>this.restoreBgColor(e))),this._parser.registerOscHandler(112,new Ko(e=>this.restoreCursorColor(e))),this._parser.registerEscHandler({final:`7`},()=>this.saveCursor()),this._parser.registerEscHandler({final:`8`},()=>this.restoreCursor()),this._parser.registerEscHandler({final:`D`},()=>this.index()),this._parser.registerEscHandler({final:`E`},()=>this.nextLine()),this._parser.registerEscHandler({final:`H`},()=>this.tabSet()),this._parser.registerEscHandler({final:`M`},()=>this.reverseIndex()),this._parser.registerEscHandler({final:`=`},()=>this.keypadApplicationMode()),this._parser.registerEscHandler({final:`>`},()=>this.keypadNumericMode()),this._parser.registerEscHandler({final:`c`},()=>this.fullReset()),this._parser.registerEscHandler({final:`n`},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:`o`},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:`|`},()=>this.setgLevel(3)),this._parser.registerEscHandler({final:`}`},()=>this.setgLevel(2)),this._parser.registerEscHandler({final:`~`},()=>this.setgLevel(1)),this._parser.registerEscHandler({intermediates:`%`,final:`@`},()=>this.selectDefaultCharset()),this._parser.registerEscHandler({intermediates:`%`,final:`G`},()=>this.selectDefaultCharset());for(let e in X)this._parser.registerEscHandler({intermediates:`(`,final:e},()=>this.selectCharset(`(`+e)),this._parser.registerEscHandler({intermediates:`)`,final:e},()=>this.selectCharset(`)`+e)),this._parser.registerEscHandler({intermediates:`*`,final:e},()=>this.selectCharset(`*`+e)),this._parser.registerEscHandler({intermediates:`+`,final:e},()=>this.selectCharset(`+`+e)),this._parser.registerEscHandler({intermediates:`-`,final:e},()=>this.selectCharset(`-`+e)),this._parser.registerEscHandler({intermediates:`.`,final:e},()=>this.selectCharset(`.`+e)),this._parser.registerEscHandler({intermediates:`/`,final:e},()=>this.selectCharset(`/`+e));this._parser.registerEscHandler({intermediates:`#`,final:`8`},()=>this.screenAlignmentPattern()),this._parser.setErrorHandler(e=>(this._logService.error(`Parsing error: `,e),e)),this._parser.registerDcsHandler({intermediates:`$`,final:`q`},new Xo((e,t)=>this.requestStatusString(e,t)))}getAttrData(){return this._curAttrData}_preserveStack(e,t,n,r){this._parseStack.paused=!0,this._parseStack.cursorStartX=e,this._parseStack.cursorStartY=t,this._parseStack.decodedLength=n,this._parseStack.position=r}_logSlowResolvingAsync(e){this._logService.logLevel<=3&&Promise.race([e,new Promise((e,t)=>setTimeout(()=>t(`#SLOW_TIMEOUT`),us))]).catch(e=>{if(e!==`#SLOW_TIMEOUT`)throw e;console.warn(`async parser handler taking longer than ${us} ms`)})}_getCurrentLinkId(){return this._curAttrData.extended.urlId}parse(e,t){let n,r=this._activeBuffer.x,i=this._activeBuffer.y,a=0,o=this._parseStack.paused;if(o){if(n=this._parser.parse(this._parseBuffer,this._parseStack.decodedLength,t))return this._logSlowResolvingAsync(n),n;r=this._parseStack.cursorStartX,i=this._parseStack.cursorStartY,this._parseStack.paused=!1,e.length>ss&&(a=this._parseStack.position+ss)}if(this._logService.logLevel<=1&&this._logService.debug(`parsing data ${typeof e==`string`?` "${e}"`:` "${Array.prototype.map.call(e,e=>String.fromCharCode(e)).join(``)}"`}`),this._logService.logLevel===0&&this._logService.trace(`parsing data (codes)`,typeof e==`string`?e.split(``).map(e=>e.charCodeAt(0)):e),this._parseBuffer.lengthss)for(let t=a;t0&&d.getWidth(this._activeBuffer.x-1)===2&&d.setCellFromCodepoint(this._activeBuffer.x-1,0,1,u);let f=this._parser.precedingJoinState;for(let p=t;ps){if(c){let e=d,t=this._activeBuffer.x-m;for(this._activeBuffer.x=m,this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData(),!0)):(this._activeBuffer.y>=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!0),d=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y),m>0&&d instanceof ao&&d.copyCellsFrom(e,t,0,m,!1);t=0;)d.setCellFromCodepoint(this._activeBuffer.x++,0,0,u);continue}if(l&&(d.insertCells(this._activeBuffer.x,i-m,this._activeBuffer.getNullCell(u)),d.getWidth(s-1)===2&&d.setCellFromCodepoint(s-1,0,1,u)),d.setCellFromCodepoint(this._activeBuffer.x++,r,i,u),i>0)for(;--i;)d.setCellFromCodepoint(this._activeBuffer.x++,0,0,u)}this._parser.precedingJoinState=f,this._activeBuffer.x0&&d.getWidth(this._activeBuffer.x)===0&&!d.hasContent(this._activeBuffer.x)&&d.setCellFromCodepoint(this._activeBuffer.x,0,1,u),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}registerCsiHandler(e,t){return e.final===`t`&&!e.prefix&&!e.intermediates?this._parser.registerCsiHandler(e,e=>!ls(e.params[0],this._optionsService.rawOptions.windowOptions)||t(e)):this._parser.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._parser.registerDcsHandler(e,new Xo(t))}registerEscHandler(e,t){return this._parser.registerEscHandler(e,t)}registerOscHandler(e,t){return this._parser.registerOscHandler(e,new Ko(t))}bell(){return this._onRequestBell.fire(),!0}lineFeed(){return this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._optionsService.rawOptions.convertEol&&(this._activeBuffer.x=0),this._activeBuffer.y++,this._activeBuffer.y===this._activeBuffer.scrollBottom+1?(this._activeBuffer.y--,this._bufferService.scroll(this._eraseAttrData())):this._activeBuffer.y>=this._bufferService.rows?this._activeBuffer.y=this._bufferService.rows-1:this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.x>=this._bufferService.cols&&this._activeBuffer.x--,this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._onLineFeed.fire(),!0}carriageReturn(){return this._activeBuffer.x=0,!0}backspace(){if(!this._coreService.decPrivateModes.reverseWraparound)return this._restrictCursor(),this._activeBuffer.x>0&&this._activeBuffer.x--,!0;if(this._restrictCursor(this._bufferService.cols),this._activeBuffer.x>0)this._activeBuffer.x--;else if(this._activeBuffer.x===0&&this._activeBuffer.y>this._activeBuffer.scrollTop&&this._activeBuffer.y<=this._activeBuffer.scrollBottom&&this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y)?.isWrapped){this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y).isWrapped=!1,this._activeBuffer.y--,this._activeBuffer.x=this._bufferService.cols-1;let e=this._activeBuffer.lines.get(this._activeBuffer.ybase+this._activeBuffer.y);e.hasWidth(this._activeBuffer.x)&&!e.hasContent(this._activeBuffer.x)&&this._activeBuffer.x--}return this._restrictCursor(),!0}tab(){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let e=this._activeBuffer.x;return this._activeBuffer.x=this._activeBuffer.nextStop(),this._optionsService.rawOptions.screenReaderMode&&this._onA11yTab.fire(this._activeBuffer.x-e),!0}shiftOut(){return this._charsetService.setgLevel(1),!0}shiftIn(){return this._charsetService.setgLevel(0),!0}_restrictCursor(e=this._bufferService.cols-1){this._activeBuffer.x=Math.min(e,Math.max(0,this._activeBuffer.x)),this._activeBuffer.y=this._coreService.decPrivateModes.origin?Math.min(this._activeBuffer.scrollBottom,Math.max(this._activeBuffer.scrollTop,this._activeBuffer.y)):Math.min(this._bufferService.rows-1,Math.max(0,this._activeBuffer.y)),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_setCursor(e,t){this._dirtyRowTracker.markDirty(this._activeBuffer.y),this._coreService.decPrivateModes.origin?(this._activeBuffer.x=e,this._activeBuffer.y=this._activeBuffer.scrollTop+t):(this._activeBuffer.x=e,this._activeBuffer.y=t),this._restrictCursor(),this._dirtyRowTracker.markDirty(this._activeBuffer.y)}_moveCursor(e,t){this._restrictCursor(),this._setCursor(this._activeBuffer.x+e,this._activeBuffer.y+t)}cursorUp(e){let t=this._activeBuffer.y-this._activeBuffer.scrollTop;return t>=0?this._moveCursor(0,-Math.min(t,e.params[0]||1)):this._moveCursor(0,-(e.params[0]||1)),!0}cursorDown(e){let t=this._activeBuffer.scrollBottom-this._activeBuffer.y;return t>=0?this._moveCursor(0,Math.min(t,e.params[0]||1)):this._moveCursor(0,e.params[0]||1),!0}cursorForward(e){return this._moveCursor(e.params[0]||1,0),!0}cursorBackward(e){return this._moveCursor(-(e.params[0]||1),0),!0}cursorNextLine(e){return this.cursorDown(e),this._activeBuffer.x=0,!0}cursorPrecedingLine(e){return this.cursorUp(e),this._activeBuffer.x=0,!0}cursorCharAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}cursorPosition(e){return this._setCursor(e.length>=2?(e.params[1]||1)-1:0,(e.params[0]||1)-1),!0}charPosAbsolute(e){return this._setCursor((e.params[0]||1)-1,this._activeBuffer.y),!0}hPositionRelative(e){return this._moveCursor(e.params[0]||1,0),!0}linePosAbsolute(e){return this._setCursor(this._activeBuffer.x,(e.params[0]||1)-1),!0}vPositionRelative(e){return this._moveCursor(0,e.params[0]||1),!0}hVPosition(e){return this.cursorPosition(e),!0}tabClear(e){let t=e.params[0];return t===0?delete this._activeBuffer.tabs[this._activeBuffer.x]:t===3&&(this._activeBuffer.tabs={}),!0}cursorForwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.nextStop();return!0}cursorBackwardTab(e){if(this._activeBuffer.x>=this._bufferService.cols)return!0;let t=e.params[0]||1;for(;t--;)this._activeBuffer.x=this._activeBuffer.prevStop();return!0}selectProtected(e){let t=e.params[0];return t===1&&(this._curAttrData.bg|=536870912),(t===2||t===0)&&(this._curAttrData.bg&=-536870913),!0}_eraseInBufferLine(e,t,n,r=!1,i=!1){let a=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);a.replaceCells(t,n,this._activeBuffer.getNullCell(this._eraseAttrData()),i),r&&(a.isWrapped=!1)}_resetBufferLine(e,t=!1){let n=this._activeBuffer.lines.get(this._activeBuffer.ybase+e);n&&(n.fill(this._activeBuffer.getNullCell(this._eraseAttrData()),t),this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase+e),n.isWrapped=!1)}eraseInDisplay(e,t=!1){this._restrictCursor(this._bufferService.cols);let n;switch(e.params[0]){case 0:for(n=this._activeBuffer.y,this._dirtyRowTracker.markDirty(n),this._eraseInBufferLine(n++,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);n=this._bufferService.cols&&(this._activeBuffer.lines.get(n+1).isWrapped=!1);n--;)this._resetBufferLine(n,t);this._dirtyRowTracker.markDirty(0);break;case 2:if(this._optionsService.rawOptions.scrollOnEraseInDisplay){for(n=this._bufferService.rows,this._dirtyRowTracker.markRangeDirty(0,n-1);n--&&!this._activeBuffer.lines.get(this._activeBuffer.ybase+n)?.getTrimmedLength(););for(;n>=0;n--)this._bufferService.scroll(this._eraseAttrData())}else{for(n=this._bufferService.rows,this._dirtyRowTracker.markDirty(n-1);n--;)this._resetBufferLine(n,t);this._dirtyRowTracker.markDirty(0)}break;case 3:let e=this._activeBuffer.lines.length-this._bufferService.rows;e>0&&(this._activeBuffer.lines.trimStart(e),this._activeBuffer.ybase=Math.max(this._activeBuffer.ybase-e,0),this._activeBuffer.ydisp=Math.max(this._activeBuffer.ydisp-e,0),this._onScroll.fire(0));break}return!0}eraseInLine(e,t=!1){switch(this._restrictCursor(this._bufferService.cols),e.params[0]){case 0:this._eraseInBufferLine(this._activeBuffer.y,this._activeBuffer.x,this._bufferService.cols,this._activeBuffer.x===0,t);break;case 1:this._eraseInBufferLine(this._activeBuffer.y,0,this._activeBuffer.x+1,!1,t);break;case 2:this._eraseInBufferLine(this._activeBuffer.y,0,this._bufferService.cols,!0,t);break}return this._dirtyRowTracker.markDirty(this._activeBuffer.y),!0}insertLines(e){this._restrictCursor();let t=e.params[0]||1;if(this._activeBuffer.y>this._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.ythis._activeBuffer.scrollBottom||this._activeBuffer.y65535?2:1}let c=s;for(let e=1;e0||(this._is(`xterm`)||this._is(`rxvt-unicode`)||this._is(`screen`)?this._coreService.triggerDataEvent(L.ESC+`[?1;2c`):this._is(`linux`)&&this._coreService.triggerDataEvent(L.ESC+`[?6c`)),!0}sendDeviceAttributesSecondary(e){return e.params[0]>0||(this._is(`xterm`)?this._coreService.triggerDataEvent(L.ESC+`[>0;276;0c`):this._is(`rxvt-unicode`)?this._coreService.triggerDataEvent(L.ESC+`[>85;95;0c`):this._is(`linux`)?this._coreService.triggerDataEvent(e.params[0]+`c`):this._is(`screen`)&&this._coreService.triggerDataEvent(L.ESC+`[>83;40003;0c`)),!0}_is(e){return(this._optionsService.rawOptions.termName+``).indexOf(e)===0}setMode(e){for(let t=0;t(e[e.NOT_RECOGNIZED=0]=`NOT_RECOGNIZED`,e[e.SET=1]=`SET`,e[e.RESET=2]=`RESET`,e[e.PERMANENTLY_SET=3]=`PERMANENTLY_SET`,e[e.PERMANENTLY_RESET=4]=`PERMANENTLY_RESET`))(n||={});let r=this._coreService.decPrivateModes,{activeProtocol:i,activeEncoding:a}=this._coreMouseService,o=this._coreService,{buffers:s,cols:c}=this._bufferService,{active:l,alt:u}=s,d=this._optionsService.rawOptions,f=(e,n)=>(o.triggerDataEvent(`${L.ESC}[${t?``:`?`}${e};${n}$y`),!0),p=e=>e?1:2,m=e.params[0];return t?m===2?f(m,4):m===4?f(m,p(o.modes.insertMode)):m===12?f(m,3):m===20?f(m,p(d.convertEol)):f(m,0):m===1?f(m,p(r.applicationCursorKeys)):m===3?f(m,d.windowOptions.setWinLines?c===80?2:+(c===132):0):m===6?f(m,p(r.origin)):m===7?f(m,p(r.wraparound)):m===8?f(m,3):m===9?f(m,p(i===`X10`)):m===12?f(m,p(d.cursorBlink)):m===25?f(m,p(!o.isCursorHidden)):m===45?f(m,p(r.reverseWraparound)):m===66?f(m,p(r.applicationKeypad)):m===67?f(m,4):m===1e3?f(m,p(i===`VT200`)):m===1002?f(m,p(i===`DRAG`)):m===1003?f(m,p(i===`ANY`)):m===1004?f(m,p(r.sendFocus)):m===1005?f(m,4):m===1006?f(m,p(a===`SGR`)):m===1015?f(m,4):m===1016?f(m,p(a===`SGR_PIXELS`)):m===1048?f(m,1):m===47||m===1047||m===1049?f(m,p(l===u)):m===2004?f(m,p(r.bracketedPasteMode)):m===2026?f(m,p(r.synchronizedOutput)):f(m,0)}_updateAttrColor(e,t,n,r,i){return t===2?(e|=50331648,e&=-16777216,e|=be.fromColorRGB([n,r,i])):t===5&&(e&=-50331904,e|=33554432|n&255),e}_extractColor(e,t,n){let r=[0,0,-1,0,0,0],i=0,a=0;do{if(r[a+i]=e.params[t+a],e.hasSubParams(t+a)){let n=e.getSubParams(t+a),o=0;do r[1]===5&&(i=1),r[a+o+1+i]=n[o];while(++o=2||r[1]===2&&a+i>=5)break;r[1]&&(i=1)}while(++a+t5)&&(e=1),t.extended.underlineStyle=e,t.fg|=268435456,e===0&&(t.fg&=-268435457),t.updateExtended()}_processSGR0(e){e.fg=Y.fg,e.bg=Y.bg,e.extended=e.extended.clone(),e.extended.underlineStyle=0,e.extended.underlineColor&=-67108864,e.updateExtended()}charAttributes(e){if(e.length===1&&e.params[0]===0)return this._processSGR0(this._curAttrData),!0;let t=e.length,n,r=this._curAttrData;for(let i=0;i=30&&n<=37?(r.fg&=-50331904,r.fg|=16777216|n-30):n>=40&&n<=47?(r.bg&=-50331904,r.bg|=16777216|n-40):n>=90&&n<=97?(r.fg&=-50331904,r.fg|=n-90|16777224):n>=100&&n<=107?(r.bg&=-50331904,r.bg|=n-100|16777224):n===0?this._processSGR0(r):n===1?r.fg|=134217728:n===3?r.bg|=67108864:n===4?(r.fg|=268435456,this._processUnderline(e.hasSubParams(i)?e.getSubParams(i)[0]:1,r)):n===5?r.fg|=536870912:n===7?r.fg|=67108864:n===8?r.fg|=1073741824:n===9?r.fg|=2147483648:n===2?r.bg|=134217728:n===21?this._processUnderline(2,r):n===22?(r.fg&=-134217729,r.bg&=-134217729):n===23?r.bg&=-67108865:n===24?(r.fg&=-268435457,this._processUnderline(0,r)):n===25?r.fg&=-536870913:n===27?r.fg&=-67108865:n===28?r.fg&=-1073741825:n===29?r.fg&=2147483647:n===39?(r.fg&=-67108864,r.fg|=Y.fg&16777215):n===49?(r.bg&=-67108864,r.bg|=Y.bg&16777215):n===38||n===48||n===58?i+=this._extractColor(e,i,r):n===53?r.bg|=1073741824:n===55?r.bg&=-1073741825:n===59?(r.extended=r.extended.clone(),r.extended.underlineColor=-1,r.updateExtended()):n===100?(r.fg&=-67108864,r.fg|=Y.fg&16777215,r.bg&=-67108864,r.bg|=Y.bg&16777215):this._logService.debug(`Unknown SGR attribute: %d.`,n);return!0}deviceStatus(e){switch(e.params[0]){case 5:this._coreService.triggerDataEvent(`${L.ESC}[0n`);break;case 6:let e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${L.ESC}[${e};${t}R`);break}return!0}deviceStatusPrivate(e){switch(e.params[0]){case 6:let e=this._activeBuffer.y+1,t=this._activeBuffer.x+1;this._coreService.triggerDataEvent(`${L.ESC}[?${e};${t}R`);break;case 15:break;case 25:break;case 26:break;case 53:break}return!0}softReset(e){return this._coreService.isCursorHidden=!1,this._onRequestSyncScrollBar.fire(),this._activeBuffer.scrollTop=0,this._activeBuffer.scrollBottom=this._bufferService.rows-1,this._curAttrData=Y.clone(),this._coreService.reset(),this._charsetService.reset(),this._activeBuffer.savedX=0,this._activeBuffer.savedY=this._activeBuffer.ybase,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,this._coreService.decPrivateModes.origin=!1,!0}setCursorStyle(e){let t=e.length===0?1:e.params[0];if(t===0)this._coreService.decPrivateModes.cursorStyle=void 0,this._coreService.decPrivateModes.cursorBlink=void 0;else{switch(t){case 1:case 2:this._coreService.decPrivateModes.cursorStyle=`block`;break;case 3:case 4:this._coreService.decPrivateModes.cursorStyle=`underline`;break;case 5:case 6:this._coreService.decPrivateModes.cursorStyle=`bar`;break}let e=t%2==1;this._coreService.decPrivateModes.cursorBlink=e}return!0}setScrollRegion(e){let t=e.params[0]||1,n;return(e.length<2||(n=e.params[1])>this._bufferService.rows||n===0)&&(n=this._bufferService.rows),n>t&&(this._activeBuffer.scrollTop=t-1,this._activeBuffer.scrollBottom=n-1,this._setCursor(0,0)),!0}windowOptions(e){if(!ls(e.params[0],this._optionsService.rawOptions.windowOptions))return!0;let t=e.length>1?e.params[1]:0;switch(e.params[0]){case 14:t!==2&&this._onRequestWindowsOptionsReport.fire(0);break;case 16:this._onRequestWindowsOptionsReport.fire(1);break;case 18:this._bufferService&&this._coreService.triggerDataEvent(`${L.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`);break;case 22:(t===0||t===2)&&(this._windowTitleStack.push(this._windowTitle),this._windowTitleStack.length>cs&&this._windowTitleStack.shift()),(t===0||t===1)&&(this._iconNameStack.push(this._iconName),this._iconNameStack.length>cs&&this._iconNameStack.shift());break;case 23:(t===0||t===2)&&this._windowTitleStack.length&&this.setTitle(this._windowTitleStack.pop()),(t===0||t===1)&&this._iconNameStack.length&&this.setIconName(this._iconNameStack.pop());break}return!0}saveCursor(e){return this._activeBuffer.savedX=this._activeBuffer.x,this._activeBuffer.savedY=this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.savedCurAttrData.fg=this._curAttrData.fg,this._activeBuffer.savedCurAttrData.bg=this._curAttrData.bg,this._activeBuffer.savedCharset=this._charsetService.charset,!0}restoreCursor(e){return this._activeBuffer.x=this._activeBuffer.savedX||0,this._activeBuffer.y=Math.max(this._activeBuffer.savedY-this._activeBuffer.ybase,0),this._curAttrData.fg=this._activeBuffer.savedCurAttrData.fg,this._curAttrData.bg=this._activeBuffer.savedCurAttrData.bg,this._charsetService.charset=this._savedCharset,this._activeBuffer.savedCharset&&(this._charsetService.charset=this._activeBuffer.savedCharset),this._restrictCursor(),!0}setTitle(e){return this._windowTitle=e,this._onTitleChange.fire(e),!0}setIconName(e){return this._iconName=e,!0}setOrReportIndexedColor(e){let t=[],n=e.split(`;`);for(;n.length>1;){let e=n.shift(),r=n.shift();if(/^\d+$/.exec(e)){let n=parseInt(e);if(ms(n))if(r===`?`)t.push({type:0,index:n});else{let e=rs(r);e&&t.push({type:1,index:n,color:e})}}}return t.length&&this._onColor.fire(t),!0}setHyperlink(e){let t=e.indexOf(`;`);if(t===-1)return!0;let n=e.slice(0,t).trim(),r=e.slice(t+1);return r?this._createHyperlink(n,r):!n.trim()&&this._finishHyperlink()}_createHyperlink(e,t){this._getCurrentLinkId()&&this._finishHyperlink();let n=e.split(`:`),r,i=n.findIndex(e=>e.startsWith(`id=`));return i!==-1&&(r=n[i].slice(3)||void 0),this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=this._oscLinkService.registerLink({id:r,uri:t}),this._curAttrData.updateExtended(),!0}_finishHyperlink(){return this._curAttrData.extended=this._curAttrData.extended.clone(),this._curAttrData.extended.urlId=0,this._curAttrData.updateExtended(),!0}_setOrReportSpecialColor(e,t){let n=e.split(`;`);for(let e=0;e=this._specialColors.length);++e,++t)if(n[e]===`?`)this._onColor.fire([{type:0,index:this._specialColors[t]}]);else{let r=rs(n[e]);r&&this._onColor.fire([{type:1,index:this._specialColors[t],color:r}])}return!0}setOrReportFgColor(e){return this._setOrReportSpecialColor(e,0)}setOrReportBgColor(e){return this._setOrReportSpecialColor(e,1)}setOrReportCursorColor(e){return this._setOrReportSpecialColor(e,2)}restoreIndexedColor(e){if(!e)return this._onColor.fire([{type:2}]),!0;let t=[],n=e.split(`;`);for(let e=0;e=this._bufferService.rows&&(this._activeBuffer.y=this._bufferService.rows-1),this._restrictCursor(),!0}tabSet(){return this._activeBuffer.tabs[this._activeBuffer.x]=!0,!0}reverseIndex(){if(this._restrictCursor(),this._activeBuffer.y===this._activeBuffer.scrollTop){let e=this._activeBuffer.scrollBottom-this._activeBuffer.scrollTop;this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase+this._activeBuffer.y,e,1),this._activeBuffer.lines.set(this._activeBuffer.ybase+this._activeBuffer.y,this._activeBuffer.getBlankLine(this._eraseAttrData())),this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop,this._activeBuffer.scrollBottom)}else this._activeBuffer.y--,this._restrictCursor();return!0}fullReset(){return this._parser.reset(),this._onRequestReset.fire(),!0}reset(){this._curAttrData=Y.clone(),this._eraseAttrDataInternal=Y.clone()}_eraseAttrData(){return this._eraseAttrDataInternal.bg&=-67108864,this._eraseAttrDataInternal.bg|=this._curAttrData.bg&67108863,this._eraseAttrDataInternal}setgLevel(e){return this._charsetService.setgLevel(e),!0}screenAlignmentPattern(){let e=new E;e.content=4194373,e.fg=this._curAttrData.fg,e.bg=this._curAttrData.bg,this._setCursor(0,0);for(let t=0;t(this._coreService.triggerDataEvent(`${L.ESC}${e}${L.ESC}\\`),!0),r=this._bufferService.buffer,i=this._optionsService.rawOptions;return n(e===`"q`?`P1$r${+!!this._curAttrData.isProtected()}"q`:e===`"p`?`P1$r61;1"p`:e===`r`?`P1$r${r.scrollTop+1};${r.scrollBottom+1}r`:e===`m`?`P1$r0m`:e===` q`?`P1$r${{block:2,underline:4,bar:6}[i.cursorStyle]-+!!i.cursorBlink} q`:`P0$r`)}markRangeDirty(e,t){this._dirtyRowTracker.markRangeDirty(e,t)}},ps=class{constructor(e){this._bufferService=e,this.clearRange()}clearRange(){this.start=this._bufferService.buffer.y,this.end=this._bufferService.buffer.y}markDirty(e){ethis.end&&(this.end=e)}markRangeDirty(e,t){e>t&&(ds=e,e=t,t=ds),ethis.end&&(this.end=t)}markAllDirty(){this.markRangeDirty(0,this._bufferService.rows-1)}};ps=x([S(0,O)],ps);function ms(e){return 0<=e&&e<256}var hs=5e7,gs=12,_s=50,vs=class extends j{constructor(e){super(),this._action=e,this._writeBuffer=[],this._callbacks=[],this._pendingData=0,this._bufferOffset=0,this._isSyncWriting=!1,this._syncCalls=0,this._didUserInput=!1,this._onWriteParsed=this._register(new P),this.onWriteParsed=this._onWriteParsed.event}handleUserInput(){this._didUserInput=!0}writeSync(e,t){if(t!==void 0&&this._syncCalls>t){this._syncCalls=0;return}if(this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(void 0),this._syncCalls++,this._isSyncWriting)return;this._isSyncWriting=!0;let n;for(;n=this._writeBuffer.shift();){this._action(n);let e=this._callbacks.shift();e&&e()}this._pendingData=0,this._bufferOffset=2147483647,this._isSyncWriting=!1,this._syncCalls=0}write(e,t){if(this._pendingData>hs)throw Error(`write data discarded, use flow control to avoid losing data`);if(!this._writeBuffer.length){if(this._bufferOffset=0,this._didUserInput){this._didUserInput=!1,this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t),this._innerWrite();return}setTimeout(()=>this._innerWrite())}this._pendingData+=e.length,this._writeBuffer.push(e),this._callbacks.push(t)}_innerWrite(e=0,t=!0){let n=e||performance.now();for(;this._writeBuffer.length>this._bufferOffset;){let e=this._writeBuffer[this._bufferOffset],r=this._action(e,t);if(r){r.catch(e=>(queueMicrotask(()=>{throw e}),Promise.resolve(!1))).then(e=>performance.now()-n>=gs?setTimeout(()=>this._innerWrite(0,e)):this._innerWrite(n,e));return}let i=this._callbacks[this._bufferOffset];if(i&&i(),this._bufferOffset++,this._pendingData-=e.length,performance.now()-n>=gs)break}this._writeBuffer.length>this._bufferOffset?(this._bufferOffset>_s&&(this._writeBuffer=this._writeBuffer.slice(this._bufferOffset),this._callbacks=this._callbacks.slice(this._bufferOffset),this._bufferOffset=0),setTimeout(()=>this._innerWrite())):(this._writeBuffer.length=0,this._callbacks.length=0,this._pendingData=0,this._bufferOffset=0),this._onWriteParsed.fire()}},ys=class{constructor(e){this._bufferService=e,this._nextId=1,this._entriesWithId=new Map,this._dataByLinkId=new Map}registerLink(e){let t=this._bufferService.buffer;if(e.id===void 0){let n=t.addMarker(t.ybase+t.y),r={data:e,id:this._nextId++,lines:[n]};return n.onDispose(()=>this._removeMarkerFromLink(r,n)),this._dataByLinkId.set(r.id,r),r.id}let n=e,r=this._getEntryIdKey(n),i=this._entriesWithId.get(r);if(i)return this.addLineToLink(i.id,t.ybase+t.y),i.id;let a=t.addMarker(t.ybase+t.y),o={id:this._nextId++,key:this._getEntryIdKey(n),data:n,lines:[a]};return a.onDispose(()=>this._removeMarkerFromLink(o,a)),this._entriesWithId.set(o.key,o),this._dataByLinkId.set(o.id,o),o.id}addLineToLink(e,t){let n=this._dataByLinkId.get(e);if(n&&n.lines.every(e=>e.line!==t)){let e=this._bufferService.buffer.addMarker(t);n.lines.push(e),e.onDispose(()=>this._removeMarkerFromLink(n,e))}}getLinkData(e){return this._dataByLinkId.get(e)?.data}_getEntryIdKey(e){return`${e.id};;${e.uri}`}_removeMarkerFromLink(e,t){let n=e.lines.indexOf(t);n!==-1&&(e.lines.splice(n,1),e.lines.length===0&&(e.data.id!==void 0&&this._entriesWithId.delete(e.key),this._dataByLinkId.delete(e.id)))}};ys=x([S(0,O)],ys);var bs=!1,xs=class extends j{constructor(e){super(),this._windowsWrappingHeuristics=this._register(new bt),this._onBinary=this._register(new P),this.onBinary=this._onBinary.event,this._onData=this._register(new P),this.onData=this._onData.event,this._onLineFeed=this._register(new P),this.onLineFeed=this._onLineFeed.event,this._onResize=this._register(new P),this.onResize=this._onResize.event,this._onWriteParsed=this._register(new P),this.onWriteParsed=this._onWriteParsed.event,this._onScroll=this._register(new P),this._instantiationService=new Za,this.optionsService=this._register(new Co(e)),this._instantiationService.setService(k,this.optionsService),this._bufferService=this._register(this._instantiationService.createInstance(bo)),this._instantiationService.setService(O,this._bufferService),this._logService=this._register(this._instantiationService.createInstance(eo)),this._instantiationService.setService(je,this._logService),this.coreService=this._register(this._instantiationService.createInstance(Oo)),this._instantiationService.setService(Oe,this.coreService),this.coreMouseService=this._register(this._instantiationService.createInstance(No)),this._instantiationService.setService(De,this.coreMouseService),this.unicodeService=this._register(this._instantiationService.createInstance(Ro)),this._instantiationService.setService(Ne,this.unicodeService),this._charsetService=this._instantiationService.createInstance(zo),this._instantiationService.setService(ke,this._charsetService),this._oscLinkService=this._instantiationService.createInstance(ys),this._instantiationService.setService(Me,this._oscLinkService),this._inputHandler=this._register(new fs(this._bufferService,this._charsetService,this.coreService,this._logService,this.optionsService,this._oscLinkService,this.coreMouseService,this.unicodeService)),this._register(N.forward(this._inputHandler.onLineFeed,this._onLineFeed)),this._register(this._inputHandler),this._register(N.forward(this._bufferService.onResize,this._onResize)),this._register(N.forward(this.coreService.onData,this._onData)),this._register(N.forward(this.coreService.onBinary,this._onBinary)),this._register(this.coreService.onRequestScrollToBottom(()=>this.scrollToBottom(!0))),this._register(this.coreService.onUserInput(()=>this._writeBuffer.handleUserInput())),this._register(this.optionsService.onMultipleOptionChange([`windowsMode`,`windowsPty`],()=>this._handleWindowsPtyOptionChange())),this._register(this._bufferService.onScroll(()=>{this._onScroll.fire({position:this._bufferService.buffer.ydisp}),this._inputHandler.markRangeDirty(this._bufferService.buffer.scrollTop,this._bufferService.buffer.scrollBottom)})),this._writeBuffer=this._register(new vs((e,t)=>this._inputHandler.parse(e,t))),this._register(N.forward(this._writeBuffer.onWriteParsed,this._onWriteParsed))}get onScroll(){return this._onScrollApi||(this._onScrollApi=this._register(new P),this._onScroll.event(e=>{this._onScrollApi?.fire(e.position)})),this._onScrollApi.event}get cols(){return this._bufferService.cols}get rows(){return this._bufferService.rows}get buffers(){return this._bufferService.buffers}get options(){return this.optionsService.options}set options(e){for(let t in e)this.optionsService.options[t]=e[t]}write(e,t){this._writeBuffer.write(e,t)}writeSync(e,t){this._logService.logLevel<=3&&!bs&&(this._logService.warn(`writeSync is unreliable and will be removed soon.`),bs=!0),this._writeBuffer.writeSync(e,t)}input(e,t=!0){this.coreService.triggerDataEvent(e,t)}resize(e,t){isNaN(e)||isNaN(t)||(e=Math.max(e,vo),t=Math.max(t,yo),this._bufferService.resize(e,t))}scroll(e,t=!1){this._bufferService.scroll(e,t)}scrollLines(e,t){this._bufferService.scrollLines(e,t)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}registerEscHandler(e,t){return this._inputHandler.registerEscHandler(e,t)}registerDcsHandler(e,t){return this._inputHandler.registerDcsHandler(e,t)}registerCsiHandler(e,t){return this._inputHandler.registerCsiHandler(e,t)}registerOscHandler(e,t){return this._inputHandler.registerOscHandler(e,t)}_setup(){this._handleWindowsPtyOptionChange()}reset(){this._inputHandler.reset(),this._bufferService.reset(),this._charsetService.reset(),this.coreService.reset(),this.coreMouseService.reset()}_handleWindowsPtyOptionChange(){let e=!1,t=this.optionsService.rawOptions.windowsPty;t&&t.buildNumber!==void 0&&t.buildNumber!==void 0?e=t.backend===`conpty`&&t.buildNumber<21376:this.optionsService.rawOptions.windowsMode&&(e=!0),e?this._enableWindowsWrappingHeuristics():this._windowsWrappingHeuristics.clear()}_enableWindowsWrappingHeuristics(){if(!this._windowsWrappingHeuristics.value){let e=[];e.push(this.onLineFeed(Bo.bind(null,this._bufferService))),e.push(this.registerCsiHandler({final:`H`},()=>(Bo(this._bufferService),!1))),this._windowsWrappingHeuristics.value=A(()=>{for(let t of e)t.dispose()})}}},Ss={48:[`0`,`)`],49:[`1`,`!`],50:[`2`,`@`],51:[`3`,`#`],52:[`4`,`$`],53:[`5`,`%`],54:[`6`,`^`],55:[`7`,`&`],56:[`8`,`*`],57:[`9`,`(`],186:[`;`,`:`],187:[`=`,`+`],188:[`,`,`<`],189:[`-`,`_`],190:[`.`,`>`],191:[`/`,`?`],192:["`",`~`],219:[`[`,`{`],220:[`\\`,`|`],221:[`]`,`}`],222:[`'`,`"`]};function Cs(e,t,n,r){let i={type:0,cancel:!1,key:void 0},a=!!e.shiftKey|(e.altKey?2:0)|(e.ctrlKey?4:0)|(e.metaKey?8:0);switch(e.keyCode){case 0:e.key===`UIKeyInputUpArrow`?t?i.key=L.ESC+`OA`:i.key=L.ESC+`[A`:e.key===`UIKeyInputLeftArrow`?t?i.key=L.ESC+`OD`:i.key=L.ESC+`[D`:e.key===`UIKeyInputRightArrow`?t?i.key=L.ESC+`OC`:i.key=L.ESC+`[C`:e.key===`UIKeyInputDownArrow`&&(t?i.key=L.ESC+`OB`:i.key=L.ESC+`[B`);break;case 8:i.key=e.ctrlKey?`\b`:L.DEL,e.altKey&&(i.key=L.ESC+i.key);break;case 9:if(e.shiftKey){i.key=L.ESC+`[Z`;break}i.key=L.HT,i.cancel=!0;break;case 13:i.key=e.altKey?L.ESC+L.CR:L.CR,i.cancel=!0;break;case 27:i.key=L.ESC,e.altKey&&(i.key=L.ESC+L.ESC),i.cancel=!0;break;case 37:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`D`:t?i.key=L.ESC+`OD`:i.key=L.ESC+`[D`;break;case 39:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`C`:t?i.key=L.ESC+`OC`:i.key=L.ESC+`[C`;break;case 38:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`A`:t?i.key=L.ESC+`OA`:i.key=L.ESC+`[A`;break;case 40:if(e.metaKey)break;a?i.key=L.ESC+`[1;`+(a+1)+`B`:t?i.key=L.ESC+`OB`:i.key=L.ESC+`[B`;break;case 45:!e.shiftKey&&!e.ctrlKey&&(i.key=L.ESC+`[2~`);break;case 46:a?i.key=L.ESC+`[3;`+(a+1)+`~`:i.key=L.ESC+`[3~`;break;case 36:a?i.key=L.ESC+`[1;`+(a+1)+`H`:t?i.key=L.ESC+`OH`:i.key=L.ESC+`[H`;break;case 35:a?i.key=L.ESC+`[1;`+(a+1)+`F`:t?i.key=L.ESC+`OF`:i.key=L.ESC+`[F`;break;case 33:e.shiftKey?i.type=2:e.ctrlKey?i.key=L.ESC+`[5;`+(a+1)+`~`:i.key=L.ESC+`[5~`;break;case 34:e.shiftKey?i.type=3:e.ctrlKey?i.key=L.ESC+`[6;`+(a+1)+`~`:i.key=L.ESC+`[6~`;break;case 112:a?i.key=L.ESC+`[1;`+(a+1)+`P`:i.key=L.ESC+`OP`;break;case 113:a?i.key=L.ESC+`[1;`+(a+1)+`Q`:i.key=L.ESC+`OQ`;break;case 114:a?i.key=L.ESC+`[1;`+(a+1)+`R`:i.key=L.ESC+`OR`;break;case 115:a?i.key=L.ESC+`[1;`+(a+1)+`S`:i.key=L.ESC+`OS`;break;case 116:a?i.key=L.ESC+`[15;`+(a+1)+`~`:i.key=L.ESC+`[15~`;break;case 117:a?i.key=L.ESC+`[17;`+(a+1)+`~`:i.key=L.ESC+`[17~`;break;case 118:a?i.key=L.ESC+`[18;`+(a+1)+`~`:i.key=L.ESC+`[18~`;break;case 119:a?i.key=L.ESC+`[19;`+(a+1)+`~`:i.key=L.ESC+`[19~`;break;case 120:a?i.key=L.ESC+`[20;`+(a+1)+`~`:i.key=L.ESC+`[20~`;break;case 121:a?i.key=L.ESC+`[21;`+(a+1)+`~`:i.key=L.ESC+`[21~`;break;case 122:a?i.key=L.ESC+`[23;`+(a+1)+`~`:i.key=L.ESC+`[23~`;break;case 123:a?i.key=L.ESC+`[24;`+(a+1)+`~`:i.key=L.ESC+`[24~`;break;default:if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&!e.metaKey)e.keyCode>=65&&e.keyCode<=90?i.key=String.fromCharCode(e.keyCode-64):e.keyCode===32?i.key=L.NUL:e.keyCode>=51&&e.keyCode<=55?i.key=String.fromCharCode(e.keyCode-51+27):e.keyCode===56?i.key=L.DEL:e.keyCode===219?i.key=L.ESC:e.keyCode===220?i.key=L.FS:e.keyCode===221&&(i.key=L.GS);else if((!n||r)&&e.altKey&&!e.metaKey){let t=Ss[e.keyCode]?.[+!!e.shiftKey];if(t)i.key=L.ESC+t;else if(e.keyCode>=65&&e.keyCode<=90){let t=e.ctrlKey?e.keyCode-64:e.keyCode+32,n=String.fromCharCode(t);e.shiftKey&&(n=n.toUpperCase()),i.key=L.ESC+n}else if(e.keyCode===32)i.key=L.ESC+(e.ctrlKey?L.NUL:` `);else if(e.key===`Dead`&&e.code.startsWith(`Key`)){let t=e.code.slice(3,4);e.shiftKey||(t=t.toLowerCase()),i.key=L.ESC+t,i.cancel=!0}}else n&&!e.altKey&&!e.ctrlKey&&!e.shiftKey&&e.metaKey?e.keyCode===65&&(i.type=1):e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.keyCode>=48&&e.key.length===1?i.key=e.key:e.key&&e.ctrlKey&&(e.key===`_`&&(i.key=L.US),e.key===`@`&&(i.key=L.NUL));break}return i}var Q=0,ws=class{constructor(e){this._getKey=e,this._array=[],this._insertedValues=[],this._flushInsertedTask=new ga,this._isFlushingInserted=!1,this._deletedIndices=[],this._flushDeletedTask=new ga,this._isFlushingDeleted=!1}clear(){this._array.length=0,this._insertedValues.length=0,this._flushInsertedTask.clear(),this._isFlushingInserted=!1,this._deletedIndices.length=0,this._flushDeletedTask.clear(),this._isFlushingDeleted=!1}insert(e){this._flushCleanupDeleted(),this._insertedValues.length===0&&this._flushInsertedTask.enqueue(()=>this._flushInserted()),this._insertedValues.push(e)}_flushInserted(){let e=this._insertedValues.sort((e,t)=>this._getKey(e)-this._getKey(t)),t=0,n=0,r=Array(this._array.length+this._insertedValues.length);for(let i=0;i=this._array.length||this._getKey(e[t])<=this._getKey(this._array[n])?(r[i]=e[t],t++):r[i]=this._array[n++];this._array=r,this._insertedValues.length=0}_flushCleanupInserted(){!this._isFlushingInserted&&this._insertedValues.length>0&&this._flushInsertedTask.flush()}delete(e){if(this._flushCleanupInserted(),this._array.length===0)return!1;let t=this._getKey(e);if(t===void 0||(Q=this._search(t),Q===-1)||this._getKey(this._array[Q])!==t)return!1;do if(this._array[Q]===e)return this._deletedIndices.length===0&&this._flushDeletedTask.enqueue(()=>this._flushDeleted()),this._deletedIndices.push(Q),!0;while(++Qe-t),t=0,n=Array(this._array.length-e.length),r=0;for(let i=0;i0&&this._flushDeletedTask.flush()}*getKeyIterator(e){if(this._flushCleanupInserted(),this._flushCleanupDeleted(),this._array.length!==0&&(Q=this._search(e),!(Q<0||Q>=this._array.length)&&this._getKey(this._array[Q])===e))do yield this._array[Q];while(++Q=this._array.length)&&this._getKey(this._array[Q])===e))do t(this._array[Q]);while(++Q=t;){let r=t+n>>1,i=this._getKey(this._array[r]);if(i>e)n=r-1;else if(i0&&this._getKey(this._array[r-1])===e;)r--;return r}}return t}},Ts=0,Es=0,Ds=class extends j{constructor(){super(),this._decorations=new ws(e=>e?.marker.line),this._onDecorationRegistered=this._register(new P),this.onDecorationRegistered=this._onDecorationRegistered.event,this._onDecorationRemoved=this._register(new P),this.onDecorationRemoved=this._onDecorationRemoved.event,this._register(A(()=>this.reset()))}get decorations(){return this._decorations.values()}registerDecoration(e){if(e.marker.isDisposed)return;let t=new Os(e);if(t){let e=t.marker.onDispose(()=>t.dispose()),n=t.onDispose(()=>{n.dispose(),t&&(this._decorations.delete(t)&&this._onDecorationRemoved.fire(t),e.dispose())});this._decorations.insert(t),this._onDecorationRegistered.fire(t)}return t}reset(){for(let e of this._decorations.values())e.dispose();this._decorations.clear()}*getDecorationsAtCell(e,t,n){let r=0,i=0;for(let a of this._decorations.getKeyIterator(t))r=a.options.x??0,i=r+(a.options.width??1),e>=r&&e{Ts=t.options.x??0,Es=Ts+(t.options.width??1),e>=Ts&&e=this._debounceThresholdMS)this._lastRefreshMs=r,this._innerRefresh();else if(!this._additionalRefreshRequested){let e=r-this._lastRefreshMs,t=this._debounceThresholdMS-e;this._additionalRefreshRequested=!0,this._refreshTimeoutID=window.setTimeout(()=>{this._lastRefreshMs=performance.now(),this._innerRefresh(),this._additionalRefreshRequested=!1,this._refreshTimeoutID=void 0},t)}}_innerRefresh(){if(this._rowStart===void 0||this._rowEnd===void 0||this._rowCount===void 0)return;let e=Math.max(this._rowStart,0),t=Math.min(this._rowEnd,this._rowCount-1);this._rowStart=void 0,this._rowEnd=void 0,this._renderCallback(e,t)}},js=20,Ms=class extends j{constructor(e,t,n,r){super(),this._terminal=e,this._coreBrowserService=n,this._renderService=r,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce=``;let i=this._coreBrowserService.mainDocument;this._accessibilityContainer=i.createElement(`div`),this._accessibilityContainer.classList.add(`xterm-accessibility`),this._rowContainer=i.createElement(`div`),this._rowContainer.setAttribute(`role`,`list`),this._rowContainer.classList.add(`xterm-accessibility-tree`),this._rowElements=[];for(let e=0;ethis._handleBoundaryFocus(e,0),this._bottomBoundaryFocusListener=e=>this._handleBoundaryFocus(e,1),this._rowElements[0].addEventListener(`focus`,this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=i.createElement(`div`),this._liveRegion.classList.add(`live-region`),this._liveRegion.setAttribute(`aria-live`,`assertive`),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this._register(new As(this._renderRows.bind(this))),!this._terminal.element)throw Error(`Cannot enable accessibility before Terminal.open`);this._terminal.element.insertAdjacentElement(`afterbegin`,this._accessibilityContainer),this._register(this._terminal.onResize(e=>this._handleResize(e.rows))),this._register(this._terminal.onRender(e=>this._refreshRows(e.start,e.end))),this._register(this._terminal.onScroll(()=>this._refreshRows())),this._register(this._terminal.onA11yChar(e=>this._handleChar(e))),this._register(this._terminal.onLineFeed(()=>this._handleChar(` +`))),this._register(this._terminal.onA11yTab(e=>this._handleTab(e))),this._register(this._terminal.onKey(e=>this._handleKey(e.key))),this._register(this._terminal.onBlur(()=>this._clearLiveRegion())),this._register(this._renderService.onDimensionsChange(()=>this._refreshRowsDimensions())),this._register(F(i,`selectionchange`,()=>this._handleSelectionChange())),this._register(this._coreBrowserService.onDprChange(()=>this._refreshRowsDimensions())),this._refreshRowsDimensions(),this._refreshRows(),this._register(A(()=>{this._accessibilityContainer.remove(),this._rowElements.length=0}))}_handleTab(e){for(let t=0;t0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,e===` +`&&(this._liveRegionLineCount++,this._liveRegionLineCount===js+1&&(this._liveRegion.textContent+=ce.get())))}_clearLiveRegion(){this._liveRegion.textContent=``,this._liveRegionLineCount=0}_handleKey(e){this._clearLiveRegion(),/\p{Control}/u.test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){let n=this._terminal.buffer,r=n.lines.length.toString();for(let i=e;i<=t;i++){let e=n.lines.get(n.ydisp+i),t=[],a=e?.translateToString(!0,void 0,void 0,t)||``,o=(n.ydisp+i+1).toString(),s=this._rowElements[i];s&&(a.length===0?(s.textContent=`\xA0`,this._rowColumns.set(s,[0,1])):(s.textContent=a,this._rowColumns.set(s,t)),s.setAttribute(`aria-posinset`,o),s.setAttribute(`aria-setsize`,r),this._alignRowWidth(s))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce=``)}_handleBoundaryFocus(e,t){let n=e.target,r=this._rowElements[t===0?1:this._rowElements.length-2];if(n.getAttribute(`aria-posinset`)===(t===0?`1`:`${this._terminal.buffer.lines.length}`)||e.relatedTarget!==r)return;let i,a;if(t===0?(i=n,a=this._rowElements.pop(),this._rowContainer.removeChild(a)):(i=this._rowElements.shift(),a=n,this._rowContainer.removeChild(i)),i.removeEventListener(`focus`,this._topBoundaryFocusListener),a.removeEventListener(`focus`,this._bottomBoundaryFocusListener),t===0){let e=this._createAccessibilityTreeNode();this._rowElements.unshift(e),this._rowContainer.insertAdjacentElement(`afterbegin`,e)}else{let e=this._createAccessibilityTreeNode();this._rowElements.push(e),this._rowContainer.appendChild(e)}this._rowElements[0].addEventListener(`focus`,this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._terminal.scrollLines(t===0?-1:1),this._rowElements[t===0?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleSelectionChange(){if(this._rowElements.length===0)return;let e=this._coreBrowserService.mainDocument.getSelection();if(!e)return;if(e.isCollapsed){this._rowContainer.contains(e.anchorNode)&&this._terminal.clearSelection();return}if(!e.anchorNode||!e.focusNode){console.error(`anchorNode and/or focusNode are null`);return}let t={node:e.anchorNode,offset:e.anchorOffset},n={node:e.focusNode,offset:e.focusOffset};if((t.node.compareDocumentPosition(n.node)&Node.DOCUMENT_POSITION_PRECEDING||t.node===n.node&&t.offset>n.offset)&&([t,n]=[n,t]),t.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(t={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(t.node))return;let r=this._rowElements.slice(-1)[0];if(n.node.compareDocumentPosition(r)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(n={node:r,offset:r.textContent?.length??0}),!this._rowContainer.contains(n.node))return;let i=({node:e,offset:t})=>{let n=e instanceof Text?e.parentNode:e,r=parseInt(n?.getAttribute(`aria-posinset`),10)-1;if(isNaN(r))return console.warn(`row is invalid. Race condition?`),null;let i=this._rowColumns.get(n);if(!i)return console.warn(`columns is null. Race condition?`),null;let a=t=this._terminal.cols&&(++r,a=0),{row:r,column:a}},a=i(t),o=i(n);if(!(!a||!o)){if(a.row>o.row||a.row===o.row&&a.column>=o.column)throw Error(`invalid range`);this._terminal.select(a.column,a.row,(o.row-a.row)*this._terminal.cols-a.column+o.column)}}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener(`focus`,this._bottomBoundaryFocusListener);for(let e=this._rowContainer.children.length;ee;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener(`focus`,this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){let e=this._coreBrowserService.mainDocument.createElement(`div`);return e.setAttribute(`role`,`listitem`),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){Object.assign(this._accessibilityContainer.style,{width:`${this._renderService.dimensions.css.canvas.width}px`,fontSize:`${this._terminal.options.fontSize}px`}),this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e{gt(this._linkCacheDisposables),this._linkCacheDisposables.length=0,this._lastMouseEvent=void 0,this._activeProviderReplies?.clear()})),this._register(this._bufferService.onResize(()=>{this._clearCurrentLink(),this._wasResized=!0})),this._register(F(this._element,`mouseleave`,()=>{this._isMouseOut=!0,this._clearCurrentLink()})),this._register(F(this._element,`mousemove`,this._handleMouseMove.bind(this))),this._register(F(this._element,`mousedown`,this._handleMouseDown.bind(this))),this._register(F(this._element,`mouseup`,this._handleMouseUp.bind(this)))}get currentLink(){return this._currentLink}_handleMouseMove(e){this._lastMouseEvent=e;let t=this._positionFromMouseEvent(e,this._element,this._mouseService);if(!t)return;this._isMouseOut=!1;let n=e.composedPath();for(let e=0;e{e?.forEach(e=>{e.link.dispose&&e.link.dispose()})}),this._activeProviderReplies=new Map,this._activeLine=e.y);let n=!1;for(let[r,i]of this._linkProviderService.linkProviders.entries())t?this._activeProviderReplies?.get(r)&&(n=this._checkLinkProviderResult(r,e,n)):i.provideLinks(e.y,t=>{if(this._isMouseOut)return;let i=t?.map(e=>({link:e}));this._activeProviderReplies?.set(r,i),n=this._checkLinkProviderResult(r,e,n),this._activeProviderReplies?.size===this._linkProviderService.linkProviders.length&&this._removeIntersectingLinks(e.y,this._activeProviderReplies)})}_removeIntersectingLinks(e,t){let n=new Set;for(let r=0;re?this._bufferService.cols:r.link.range.end.x;for(let e=a;e<=o;e++){if(n.has(e)){i.splice(t--,1);break}n.add(e)}}}}_checkLinkProviderResult(e,t,n){if(!this._activeProviderReplies)return n;let r=this._activeProviderReplies.get(e),i=!1;for(let t=0;tthis._linkAtPosition(e.link,t));e&&(n=!0,this._handleNewLink(e))}if(this._activeProviderReplies.size===this._linkProviderService.linkProviders.length&&!n)for(let e=0;ethis._linkAtPosition(e.link,t));if(r){n=!0,this._handleNewLink(r);break}}return n}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(e){if(!this._currentLink)return;let t=this._positionFromMouseEvent(e,this._element,this._mouseService);t&&this._mouseDownLink&&Ps(this._mouseDownLink.link,this._currentLink.link)&&this._linkAtPosition(this._currentLink.link,t)&&this._currentLink.link.activate(e,this._currentLink.link.text)}_clearCurrentLink(e,t){!this._currentLink||!this._lastMouseEvent||(!e||!t||this._currentLink.link.range.start.y>=e&&this._currentLink.link.range.end.y<=t)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,gt(this._linkCacheDisposables),this._linkCacheDisposables.length=0)}_handleNewLink(e){if(!this._lastMouseEvent)return;let t=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);t&&this._linkAtPosition(e.link,t)&&(this._currentLink=e,this._currentLink.state={decorations:{underline:e.link.decorations===void 0||e.link.decorations.underline,pointerCursor:e.link.decorations===void 0||e.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,e.link,this._lastMouseEvent),e.link.decorations={},Object.defineProperties(e.link.decorations,{pointerCursor:{get:()=>this._currentLink?.state?.decorations.pointerCursor,set:e=>{this._currentLink?.state&&this._currentLink.state.decorations.pointerCursor!==e&&(this._currentLink.state.decorations.pointerCursor=e,this._currentLink.state.isHovered&&this._element.classList.toggle(`xterm-cursor-pointer`,e))}},underline:{get:()=>this._currentLink?.state?.decorations.underline,set:t=>{this._currentLink?.state&&this._currentLink?.state?.decorations.underline!==t&&(this._currentLink.state.decorations.underline=t,this._currentLink.state.isHovered&&this._fireUnderlineEvent(e.link,t))}}}),this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e=>{if(!this._currentLink)return;let t=e.start===0?0:e.start+1+this._bufferService.buffer.ydisp,n=this._bufferService.buffer.ydisp+1+e.end;if(this._currentLink.link.range.start.y>=t&&this._currentLink.link.range.end.y<=n&&(this._clearCurrentLink(t,n),this._lastMouseEvent)){let e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._askForLink(e,!1)}})))}_linkHover(e,t,n){this._currentLink?.state&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!0),this._currentLink.state.decorations.pointerCursor&&e.classList.add(`xterm-cursor-pointer`)),t.hover&&t.hover(n,t.text)}_fireUnderlineEvent(e,t){let n=e.range,r=this._bufferService.buffer.ydisp,i=this._createLinkUnderlineEvent(n.start.x-1,n.start.y-r-1,n.end.x,n.end.y-r-1,void 0);(t?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(i)}_linkLeave(e,t,n){this._currentLink?.state&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(t,!1),this._currentLink.state.decorations.pointerCursor&&e.classList.remove(`xterm-cursor-pointer`)),t.leave&&t.leave(n,t.text)}_linkAtPosition(e,t){let n=e.range.start.y*this._bufferService.cols+e.range.start.x,r=e.range.end.y*this._bufferService.cols+e.range.end.x,i=t.y*this._bufferService.cols+t.x;return n<=i&&i<=r}_positionFromMouseEvent(e,t,n){let r=n.getCoords(e,t,this._bufferService.cols,this._bufferService.rows);if(r)return{x:r[0],y:r[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(e,t,n,r,i){return{x1:e,y1:t,x2:n,y2:r,cols:this._bufferService.cols,fg:i}}};Ns=x([S(1,ze),S(2,Be),S(3,O),S(4,We)],Ns);function Ps(e,t){return e.text===t.text&&e.range.start.x===t.range.start.x&&e.range.start.y===t.range.start.y&&e.range.end.x===t.range.end.x&&e.range.end.y===t.range.end.y}var Fs=class extends xs{constructor(e={}){super(e),this._linkifier=this._register(new bt),this.browser=$i,this._keyDownHandled=!1,this._keyDownSeen=!1,this._keyPressHandled=!1,this._unprocessedDeadKey=!1,this._accessibilityManager=this._register(new bt),this._onCursorMove=this._register(new P),this.onCursorMove=this._onCursorMove.event,this._onKey=this._register(new P),this.onKey=this._onKey.event,this._onRender=this._register(new P),this.onRender=this._onRender.event,this._onSelectionChange=this._register(new P),this.onSelectionChange=this._onSelectionChange.event,this._onTitleChange=this._register(new P),this.onTitleChange=this._onTitleChange.event,this._onBell=this._register(new P),this.onBell=this._onBell.event,this._onFocus=this._register(new P),this._onBlur=this._register(new P),this._onA11yCharEmitter=this._register(new P),this._onA11yTabEmitter=this._register(new P),this._onWillOpen=this._register(new P),this._setup(),this._decorationService=this._instantiationService.createInstance(Ds),this._instantiationService.setService(Pe,this._decorationService),this._linkProviderService=this._instantiationService.createInstance(Ji),this._instantiationService.setService(We,this._linkProviderService),this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(Fe)),this._register(this._inputHandler.onRequestBell(()=>this._onBell.fire())),this._register(this._inputHandler.onRequestRefreshRows(e=>this.refresh(e?.start??0,e?.end??this.rows-1))),this._register(this._inputHandler.onRequestSendFocus(()=>this._reportFocus())),this._register(this._inputHandler.onRequestReset(()=>this.reset())),this._register(this._inputHandler.onRequestWindowsOptionsReport(e=>this._reportWindowsOptions(e))),this._register(this._inputHandler.onColor(e=>this._handleColorEvent(e))),this._register(N.forward(this._inputHandler.onCursorMove,this._onCursorMove)),this._register(N.forward(this._inputHandler.onTitleChange,this._onTitleChange)),this._register(N.forward(this._inputHandler.onA11yChar,this._onA11yCharEmitter)),this._register(N.forward(this._inputHandler.onA11yTab,this._onA11yTabEmitter)),this._register(this._bufferService.onResize(e=>this._afterResize(e.cols,e.rows))),this._register(A(()=>{this._customKeyEventHandler=void 0,this.element?.parentNode?.removeChild(this.element)}))}get linkifier(){return this._linkifier.value}get onFocus(){return this._onFocus.event}get onBlur(){return this._onBlur.event}get onA11yChar(){return this._onA11yCharEmitter.event}get onA11yTab(){return this._onA11yTabEmitter.event}get onWillOpen(){return this._onWillOpen.event}_handleColorEvent(e){if(this._themeService)for(let t of e){let e,n=``;switch(t.index){case 256:e=`foreground`,n=`10`;break;case 257:e=`background`,n=`11`;break;case 258:e=`cursor`,n=`12`;break;default:e=`ansi`,n=`4;`+t.index}switch(t.type){case 0:let r=U.toColorRGB(e===`ansi`?this._themeService.colors.ansi[t.index]:this._themeService.colors[e]);this.coreService.triggerDataEvent(`${L.ESC}]${n};${as(r)}${gi.ST}`);break;case 1:if(e===`ansi`)this._themeService.modifyColors(e=>e.ansi[t.index]=H.toColor(...t.color));else{let n=e;this._themeService.modifyColors(e=>e[n]=H.toColor(...t.color))}break;case 2:this._themeService.restoreColor(t.index);break}}}_setup(){super._setup(),this._customKeyEventHandler=void 0}get buffer(){return this.buffers.active}focus(){this.textarea&&this.textarea.focus({preventScroll:!0})}_handleScreenReaderModeOptionChange(e){e?!this._accessibilityManager.value&&this._renderService&&(this._accessibilityManager.value=this._instantiationService.createInstance(Ms,this)):this._accessibilityManager.clear()}_handleTextAreaFocus(e){this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(L.ESC+`[I`),this.element.classList.add(`focus`),this._showCursor(),this._onFocus.fire()}blur(){return this.textarea?.blur()}_handleTextAreaBlur(){this.textarea.value=``,this.refresh(this.buffer.y,this.buffer.y),this.coreService.decPrivateModes.sendFocus&&this.coreService.triggerDataEvent(L.ESC+`[O`),this.element.classList.remove(`focus`),this._onBlur.fire()}_syncTextArea(){if(!this.textarea||!this.buffer.isCursorInViewport||this._compositionHelper.isComposing||!this._renderService)return;let e=this.buffer.ybase+this.buffer.y,t=this.buffer.lines.get(e);if(!t)return;let n=Math.min(this.buffer.x,this.cols-1),r=this._renderService.dimensions.css.cell.height,i=t.getWidth(n),a=this._renderService.dimensions.css.cell.width*i,o=this.buffer.y*this._renderService.dimensions.css.cell.height,s=n*this._renderService.dimensions.css.cell.width;this.textarea.style.left=s+`px`,this.textarea.style.top=o+`px`,this.textarea.style.width=a+`px`,this.textarea.style.height=r+`px`,this.textarea.style.lineHeight=r+`px`,this.textarea.style.zIndex=`-5`}_initGlobal(){this._bindKeys(),this._register(F(this.element,`copy`,e=>{this.hasSelection()&&ue(e,this._selectionService)}));let e=e=>de(e,this.textarea,this.coreService,this.optionsService);this._register(F(this.textarea,`paste`,e)),this._register(F(this.element,`paste`,e)),ra?this._register(F(this.element,`mousedown`,e=>{e.button===2&&me(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})):this._register(F(this.element,`contextmenu`,e=>{me(e,this.textarea,this.screenElement,this._selectionService,this.options.rightClickSelectsWord)})),da&&this._register(F(this.element,`auxclick`,e=>{e.button===1&&pe(e,this.textarea,this.screenElement)}))}_bindKeys(){this._register(F(this.textarea,`keyup`,e=>this._keyUp(e),!0)),this._register(F(this.textarea,`keydown`,e=>this._keyDown(e),!0)),this._register(F(this.textarea,`keypress`,e=>this._keyPress(e),!0)),this._register(F(this.textarea,`compositionstart`,()=>this._compositionHelper.compositionstart())),this._register(F(this.textarea,`compositionupdate`,e=>this._compositionHelper.compositionupdate(e))),this._register(F(this.textarea,`compositionend`,()=>this._compositionHelper.compositionend())),this._register(F(this.textarea,`input`,e=>this._inputEvent(e),!0)),this._register(this.onRender(()=>this._compositionHelper.updateCompositionElements()))}open(e){if(!e)throw Error(`Terminal requires a parent element.`);if(e.isConnected||this._logService.debug(`Terminal.open was called on an element that was not attached to the DOM`),this.element?.ownerDocument.defaultView&&this._coreBrowserService){this.element.ownerDocument.defaultView!==this._coreBrowserService.window&&(this._coreBrowserService.window=this.element.ownerDocument.defaultView);return}this._document=e.ownerDocument,this.options.documentOverride&&this.options.documentOverride instanceof Document&&(this._document=this.optionsService.rawOptions.documentOverride),this.element=this._document.createElement(`div`),this.element.dir=`ltr`,this.element.classList.add(`terminal`),this.element.classList.add(`xterm`),e.appendChild(this.element);let t=this._document.createDocumentFragment();this._viewportElement=this._document.createElement(`div`),this._viewportElement.classList.add(`xterm-viewport`),t.appendChild(this._viewportElement),this.screenElement=this._document.createElement(`div`),this.screenElement.classList.add(`xterm-screen`),this._register(F(this.screenElement,`mousemove`,e=>this.updateCursorStyle(e))),this._helperContainer=this._document.createElement(`div`),this._helperContainer.classList.add(`xterm-helpers`),this.screenElement.appendChild(this._helperContainer),t.appendChild(this.screenElement);let n=this.textarea=this._document.createElement(`textarea`);this.textarea.classList.add(`xterm-helper-textarea`),this.textarea.setAttribute(`aria-label`,oe.get()),fa||this.textarea.setAttribute(`aria-multiline`,`false`),this.textarea.setAttribute(`autocorrect`,`off`),this.textarea.setAttribute(`autocapitalize`,`off`),this.textarea.setAttribute(`spellcheck`,`false`),this.textarea.tabIndex=0,this._register(this.optionsService.onSpecificOptionChange(`disableStdin`,()=>n.readOnly=this.optionsService.rawOptions.disableStdin)),this.textarea.readOnly=this.optionsService.rawOptions.disableStdin,this._coreBrowserService=this._register(this._instantiationService.createInstance(Ki,this.textarea,e.ownerDocument.defaultView??window,this._document??typeof window<`u`?window.document:null)),this._instantiationService.setService(Re,this._coreBrowserService),this._register(F(this.textarea,`focus`,e=>this._handleTextAreaFocus(e))),this._register(F(this.textarea,`blur`,()=>this._handleTextAreaBlur())),this._helperContainer.appendChild(this.textarea),this._charSizeService=this._instantiationService.createInstance(Hi,this._document,this._helperContainer),this._instantiationService.setService(Le,this._charSizeService),this._themeService=this._instantiationService.createInstance(Ya),this._instantiationService.setService(Ue,this._themeService),this._characterJoinerService=this._instantiationService.createInstance(Ci),this._instantiationService.setService(He,this._characterJoinerService),this._renderService=this._register(this._instantiationService.createInstance(va,this.rows,this.screenElement)),this._instantiationService.setService(Be,this._renderService),this._register(this._renderService.onRenderedViewportChange(e=>this._onRender.fire(e))),this.onResize(e=>this._renderService.resize(e.cols,e.rows)),this._compositionView=this._document.createElement(`div`),this._compositionView.classList.add(`composition-view`),this._compositionHelper=this._instantiationService.createInstance(_i,this.textarea,this._compositionView),this._helperContainer.appendChild(this._compositionView),this._mouseService=this._instantiationService.createInstance(Zi),this._instantiationService.setService(ze,this._mouseService);let r=this._linkifier.value=this._register(this._instantiationService.createInstance(Ns,this.screenElement));this.element.appendChild(t);try{this._onWillOpen.fire(this.element)}catch{}this._renderService.hasRenderer()||this._renderService.setRenderer(this._createRenderer()),this._register(this.onCursorMove(()=>{this._renderService.handleCursorMove(),this._syncTextArea()})),this._register(this.onResize(()=>this._renderService.handleResize(this.cols,this.rows))),this._register(this.onBlur(()=>this._renderService.handleBlur())),this._register(this.onFocus(()=>this._renderService.handleFocus())),this._viewport=this._register(this._instantiationService.createInstance(ci,this.element,this.screenElement)),this._register(this._viewport.onRequestScrollLines(e=>{super.scrollLines(e,!1),this.refresh(0,this.rows-1)})),this._selectionService=this._register(this._instantiationService.createInstance(Ba,this.element,this.screenElement,r)),this._instantiationService.setService(Ve,this._selectionService),this._register(this._selectionService.onRequestScrollLines(e=>this.scrollLines(e.amount,e.suppressScrollEvent))),this._register(this._selectionService.onSelectionChange(()=>this._onSelectionChange.fire())),this._register(this._selectionService.onRequestRedraw(e=>this._renderService.handleSelectionChanged(e.start,e.end,e.columnSelectMode))),this._register(this._selectionService.onLinuxMouseSelection(e=>{this.textarea.value=e,this.textarea.focus(),this.textarea.select()})),this._register(N.any(this._onScroll.event,this._inputHandler.onScroll)(()=>{this._selectionService.refresh(),this._viewport?.queueSync()})),this._register(this._instantiationService.createInstance(li,this.screenElement)),this._register(F(this.element,`mousedown`,e=>this._selectionService.handleMouseDown(e))),this.coreMouseService.areMouseEventsActive?(this._selectionService.disable(),this.element.classList.add(`enable-mouse-events`)):this._selectionService.enable(),this.options.screenReaderMode&&(this._accessibilityManager.value=this._instantiationService.createInstance(Ms,this)),this._register(this.optionsService.onSpecificOptionChange(`screenReaderMode`,e=>this._handleScreenReaderModeOptionChange(e))),this.options.overviewRuler.width&&(this._overviewRulerRenderer=this._register(this._instantiationService.createInstance(mi,this._viewportElement,this.screenElement))),this.optionsService.onSpecificOptionChange(`overviewRuler`,e=>{!this._overviewRulerRenderer&&e&&this._viewportElement&&this.screenElement&&(this._overviewRulerRenderer=this._register(this._instantiationService.createInstance(mi,this._viewportElement,this.screenElement)))}),this._charSizeService.measure(),this.refresh(0,this.rows-1),this._initGlobal(),this.bindMouse()}_createRenderer(){return this._instantiationService.createInstance(Vi,this,this._document,this.element,this.screenElement,this._viewportElement,this._helperContainer,this.linkifier)}bindMouse(){let e=this,t=this.element;function n(t){let n=e._mouseService.getMouseReportCoords(t,e.screenElement);if(!n)return!1;let r,i;switch(t.overrideType||t.type){case`mousemove`:i=32,t.buttons===void 0?(r=3,t.button!==void 0&&(r=t.button<3?t.button:3)):r=t.buttons&1?0:t.buttons&4?1:t.buttons&2?2:3;break;case`mouseup`:i=0,r=t.button<3?t.button:3;break;case`mousedown`:i=1,r=t.button<3?t.button:3;break;case`wheel`:if(e._customWheelEventHandler&&e._customWheelEventHandler(t)===!1)return!1;let n=t.deltaY;if(n===0||e.coreMouseService.consumeWheelEvent(t,e._renderService?.dimensions?.device?.cell?.height,e._coreBrowserService?.dpr)===0)return!1;i=n<0?0:1,r=4;break;default:return!1}return i===void 0||r===void 0||r>4?!1:e.coreMouseService.triggerMouseEvent({col:n.col,row:n.row,x:n.x,y:n.y,button:r,action:i,ctrl:t.ctrlKey,alt:t.altKey,shift:t.shiftKey})}let r={mouseup:null,wheel:null,mousedrag:null,mousemove:null},i={mouseup:e=>(n(e),e.buttons||(this._document.removeEventListener(`mouseup`,r.mouseup),r.mousedrag&&this._document.removeEventListener(`mousemove`,r.mousedrag)),this.cancel(e)),wheel:e=>(n(e),this.cancel(e,!0)),mousedrag:e=>{e.buttons&&n(e)},mousemove:e=>{e.buttons||n(e)}};this._register(this.coreMouseService.onProtocolChange(e=>{e?(this.optionsService.rawOptions.logLevel===`debug`&&this._logService.debug(`Binding to mouse events:`,this.coreMouseService.explainEvents(e)),this.element.classList.add(`enable-mouse-events`),this._selectionService.disable()):(this._logService.debug(`Unbinding from mouse events.`),this.element.classList.remove(`enable-mouse-events`),this._selectionService.enable()),e&8?r.mousemove||=(t.addEventListener(`mousemove`,i.mousemove),i.mousemove):(t.removeEventListener(`mousemove`,r.mousemove),r.mousemove=null),e&16?r.wheel||=(t.addEventListener(`wheel`,i.wheel,{passive:!1}),i.wheel):(t.removeEventListener(`wheel`,r.wheel),r.wheel=null),e&2?r.mouseup||=i.mouseup:(this._document.removeEventListener(`mouseup`,r.mouseup),r.mouseup=null),e&4?r.mousedrag||=i.mousedrag:(this._document.removeEventListener(`mousemove`,r.mousedrag),r.mousedrag=null)})),this.coreMouseService.activeProtocol=this.coreMouseService.activeProtocol,this._register(F(t,`mousedown`,e=>{if(e.preventDefault(),this.focus(),!(!this.coreMouseService.areMouseEventsActive||this._selectionService.shouldForceSelection(e)))return n(e),r.mouseup&&this._document.addEventListener(`mouseup`,r.mouseup),r.mousedrag&&this._document.addEventListener(`mousemove`,r.mousedrag),this.cancel(e)})),this._register(F(t,`wheel`,t=>{if(!r.wheel){if(this._customWheelEventHandler&&this._customWheelEventHandler(t)===!1)return!1;if(!this.buffer.hasScrollback){if(t.deltaY===0)return!1;if(e.coreMouseService.consumeWheelEvent(t,e._renderService?.dimensions?.device?.cell?.height,e._coreBrowserService?.dpr)===0)return this.cancel(t,!0);let n=L.ESC+(this.coreService.decPrivateModes.applicationCursorKeys?`O`:`[`)+(t.deltaY<0?`A`:`B`);return this.coreService.triggerDataEvent(n,!0),this.cancel(t,!0)}}},{passive:!1}))}refresh(e,t){this._renderService?.refreshRows(e,t)}updateCursorStyle(e){this._selectionService?.shouldColumnSelect(e)?this.element.classList.add(`column-select`):this.element.classList.remove(`column-select`)}_showCursor(){this.coreService.isCursorInitialized||(this.coreService.isCursorInitialized=!0,this.refresh(this.buffer.y,this.buffer.y))}scrollLines(e,t){this._viewport?this._viewport.scrollLines(e):super.scrollLines(e,t),this.refresh(0,this.rows-1)}scrollPages(e){this.scrollLines(e*(this.rows-1))}scrollToTop(){this.scrollLines(-this._bufferService.buffer.ydisp)}scrollToBottom(e){e&&this._viewport?this._viewport.scrollToLine(this.buffer.ybase,!0):this.scrollLines(this._bufferService.buffer.ybase-this._bufferService.buffer.ydisp)}scrollToLine(e){let t=e-this._bufferService.buffer.ydisp;t!==0&&this.scrollLines(t)}paste(e){fe(e,this.textarea,this.coreService,this.optionsService)}attachCustomKeyEventHandler(e){this._customKeyEventHandler=e}attachCustomWheelEventHandler(e){this._customWheelEventHandler=e}registerLinkProvider(e){return this._linkProviderService.registerLinkProvider(e)}registerCharacterJoiner(e){if(!this._characterJoinerService)throw Error(`Terminal must be opened first`);let t=this._characterJoinerService.register(e);return this.refresh(0,this.rows-1),t}deregisterCharacterJoiner(e){if(!this._characterJoinerService)throw Error(`Terminal must be opened first`);this._characterJoinerService.deregister(e)&&this.refresh(0,this.rows-1)}get markers(){return this.buffer.markers}registerMarker(e){return this.buffer.addMarker(this.buffer.ybase+this.buffer.y+e)}registerDecoration(e){return this._decorationService.registerDecoration(e)}hasSelection(){return this._selectionService?this._selectionService.hasSelection:!1}select(e,t,n){this._selectionService.setSelection(e,t,n)}getSelection(){return this._selectionService?this._selectionService.selectionText:``}getSelectionPosition(){if(!(!this._selectionService||!this._selectionService.hasSelection))return{start:{x:this._selectionService.selectionStart[0],y:this._selectionService.selectionStart[1]},end:{x:this._selectionService.selectionEnd[0],y:this._selectionService.selectionEnd[1]}}}clearSelection(){this._selectionService?.clearSelection()}selectAll(){this._selectionService?.selectAll()}selectLines(e,t){this._selectionService?.selectLines(e,t)}_keyDown(e){if(this._keyDownHandled=!1,this._keyDownSeen=!0,this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)return!1;let t=this.browser.isMac&&this.options.macOptionIsMeta&&e.altKey;if(!t&&!this._compositionHelper.keydown(e))return this.options.scrollOnUserInput&&this.buffer.ybase!==this.buffer.ydisp&&this.scrollToBottom(!0),!1;!t&&(e.key===`Dead`||e.key===`AltGraph`)&&(this._unprocessedDeadKey=!0);let n=Cs(e,this.coreService.decPrivateModes.applicationCursorKeys,this.browser.isMac,this.options.macOptionIsMeta);if(this.updateCursorStyle(e),n.type===3||n.type===2){let t=this.rows-1;return this.scrollLines(n.type===2?-t:t),this.cancel(e,!0)}if(n.type===1&&this.selectAll(),this._isThirdLevelShift(this.browser,e)||(n.cancel&&this.cancel(e,!0),!n.key)||e.key&&!e.ctrlKey&&!e.altKey&&!e.metaKey&&e.key.length===1&&e.key.charCodeAt(0)>=65&&e.key.charCodeAt(0)<=90)return!0;if(this._unprocessedDeadKey)return this._unprocessedDeadKey=!1,!0;if((n.key===L.ETX||n.key===L.CR)&&(this.textarea.value=``),this._onKey.fire({key:n.key,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(n.key,!0),!this.optionsService.rawOptions.screenReaderMode||e.altKey||e.ctrlKey)return this.cancel(e,!0);this._keyDownHandled=!0}_isThirdLevelShift(e,t){let n=e.isMac&&!this.options.macOptionIsMeta&&t.altKey&&!t.ctrlKey&&!t.metaKey||e.isWindows&&t.altKey&&t.ctrlKey&&!t.metaKey||e.isWindows&&t.getModifierState(`AltGraph`);return t.type===`keypress`?n:n&&(!t.keyCode||t.keyCode>47)}_keyUp(e){this._keyDownSeen=!1,!(this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)&&(Is(e)||this.focus(),this.updateCursorStyle(e),this._keyPressHandled=!1)}_keyPress(e){let t;if(this._keyPressHandled=!1,this._keyDownHandled||this._customKeyEventHandler&&this._customKeyEventHandler(e)===!1)return!1;if(this.cancel(e),e.charCode)t=e.charCode;else if(e.which===null||e.which===void 0)t=e.keyCode;else if(e.which!==0&&e.charCode!==0)t=e.which;else return!1;return!t||(e.altKey||e.ctrlKey||e.metaKey)&&!this._isThirdLevelShift(this.browser,e)?!1:(t=String.fromCharCode(t),this._onKey.fire({key:t,domEvent:e}),this._showCursor(),this.coreService.triggerDataEvent(t,!0),this._keyPressHandled=!0,this._unprocessedDeadKey=!1,!0)}_inputEvent(e){if(e.data&&e.inputType===`insertText`&&(!e.composed||!this._keyDownSeen)&&!this.optionsService.rawOptions.screenReaderMode){if(this._keyPressHandled)return!1;this._unprocessedDeadKey=!1;let t=e.data;return this.coreService.triggerDataEvent(t,!0),this.cancel(e),!0}return!1}resize(e,t){if(e===this.cols&&t===this.rows){this._charSizeService&&!this._charSizeService.hasValidSize&&this._charSizeService.measure();return}super.resize(e,t)}_afterResize(e,t){this._charSizeService?.measure()}clear(){if(!(this.buffer.ybase===0&&this.buffer.y===0)){this.buffer.clearAllMarkers(),this.buffer.lines.set(0,this.buffer.lines.get(this.buffer.ybase+this.buffer.y)),this.buffer.lines.length=1,this.buffer.ydisp=0,this.buffer.ybase=0,this.buffer.y=0;for(let e=1;e=0;e--)this._addons[e].instance.dispose()}loadAddon(e,t){let n={instance:t,dispose:t.dispose,isDisposed:!1};this._addons.push(n),t.dispose=()=>this._wrappedAddonDispose(n),t.activate(e)}_wrappedAddonDispose(e){if(e.isDisposed)return;let t=-1;for(let n=0;n=this._line.length))return t?(this._line.loadCell(e,t),t):this._line.loadCell(e,new E)}translateToString(e,t,n){return this._line.translateToString(e,t,n)}},zs=class{constructor(e,t){this._buffer=e,this.type=t}init(e){return this._buffer=e,this}get cursorY(){return this._buffer.y}get cursorX(){return this._buffer.x}get viewportY(){return this._buffer.ydisp}get baseY(){return this._buffer.ybase}get length(){return this._buffer.lines.length}getLine(e){let t=this._buffer.lines.get(e);if(t)return new Rs(t)}getNullCell(){return new E}},Bs=class extends j{constructor(e){super(),this._core=e,this._onBufferChange=this._register(new P),this.onBufferChange=this._onBufferChange.event,this._normal=new zs(this._core.buffers.normal,`normal`),this._alternate=new zs(this._core.buffers.alt,`alternate`),this._core.buffers.onBufferActivate(()=>this._onBufferChange.fire(this.active))}get active(){if(this._core.buffers.active===this._core.buffers.normal)return this.normal;if(this._core.buffers.active===this._core.buffers.alt)return this.alternate;throw Error(`Active buffer is neither normal nor alternate`)}get normal(){return this._normal.init(this._core.buffers.normal)}get alternate(){return this._alternate.init(this._core.buffers.alt)}},Vs=class{constructor(e){this._core=e}registerCsiHandler(e,t){return this._core.registerCsiHandler(e,e=>t(e.toArray()))}addCsiHandler(e,t){return this.registerCsiHandler(e,t)}registerDcsHandler(e,t){return this._core.registerDcsHandler(e,(e,n)=>t(e,n.toArray()))}addDcsHandler(e,t){return this.registerDcsHandler(e,t)}registerEscHandler(e,t){return this._core.registerEscHandler(e,t)}addEscHandler(e,t){return this.registerEscHandler(e,t)}registerOscHandler(e,t){return this._core.registerOscHandler(e,t)}addOscHandler(e,t){return this.registerOscHandler(e,t)}},Hs=class{constructor(e){this._core=e}register(e){this._core.unicodeService.register(e)}get versions(){return this._core.unicodeService.versions}get activeVersion(){return this._core.unicodeService.activeVersion}set activeVersion(e){this._core.unicodeService.activeVersion=e}},Us=[`cols`,`rows`],Ws=0,Gs=class extends j{constructor(e){super(),this._core=this._register(new Fs(e)),this._addonManager=this._register(new Ls),this._publicOptions={...this._core.options};let t=e=>this._core.options[e],n=(e,t)=>{this._checkReadonlyOptions(e),this._core.options[e]=t};for(let e in this._core.options){let r={get:t.bind(this,e),set:n.bind(this,e)};Object.defineProperty(this._publicOptions,e,r)}}_checkReadonlyOptions(e){if(Us.includes(e))throw Error(`Option "${e}" can only be set in the constructor`)}_checkProposedApi(){if(!this._core.optionsService.rawOptions.allowProposedApi)throw Error(`You must set the allowProposedApi option to true to use proposed API`)}get onBell(){return this._core.onBell}get onBinary(){return this._core.onBinary}get onCursorMove(){return this._core.onCursorMove}get onData(){return this._core.onData}get onKey(){return this._core.onKey}get onLineFeed(){return this._core.onLineFeed}get onRender(){return this._core.onRender}get onResize(){return this._core.onResize}get onScroll(){return this._core.onScroll}get onSelectionChange(){return this._core.onSelectionChange}get onTitleChange(){return this._core.onTitleChange}get onWriteParsed(){return this._core.onWriteParsed}get element(){return this._core.element}get parser(){return this._parser||=new Vs(this._core),this._parser}get unicode(){return this._checkProposedApi(),new Hs(this._core)}get textarea(){return this._core.textarea}get rows(){return this._core.rows}get cols(){return this._core.cols}get buffer(){return this._buffer||=this._register(new Bs(this._core)),this._buffer}get markers(){return this._checkProposedApi(),this._core.markers}get modes(){let e=this._core.coreService.decPrivateModes,t=`none`;switch(this._core.coreMouseService.activeProtocol){case`X10`:t=`x10`;break;case`VT200`:t=`vt200`;break;case`DRAG`:t=`drag`;break;case`ANY`:t=`any`;break}return{applicationCursorKeysMode:e.applicationCursorKeys,applicationKeypadMode:e.applicationKeypad,bracketedPasteMode:e.bracketedPasteMode,insertMode:this._core.coreService.modes.insertMode,mouseTrackingMode:t,originMode:e.origin,reverseWraparoundMode:e.reverseWraparound,sendFocusMode:e.sendFocus,synchronizedOutputMode:e.synchronizedOutput,wraparoundMode:e.wraparound}}get options(){return this._publicOptions}set options(e){for(let t in e)this._publicOptions[t]=e[t]}blur(){this._core.blur()}focus(){this._core.focus()}input(e,t=!0){this._core.input(e,t)}resize(e,t){this._verifyIntegers(e,t),this._core.resize(e,t)}open(e){this._core.open(e)}attachCustomKeyEventHandler(e){this._core.attachCustomKeyEventHandler(e)}attachCustomWheelEventHandler(e){this._core.attachCustomWheelEventHandler(e)}registerLinkProvider(e){return this._core.registerLinkProvider(e)}registerCharacterJoiner(e){return this._checkProposedApi(),this._core.registerCharacterJoiner(e)}deregisterCharacterJoiner(e){this._checkProposedApi(),this._core.deregisterCharacterJoiner(e)}registerMarker(e=0){return this._verifyIntegers(e),this._core.registerMarker(e)}registerDecoration(e){return this._checkProposedApi(),this._verifyPositiveIntegers(e.x??0,e.width??0,e.height??0),this._core.registerDecoration(e)}hasSelection(){return this._core.hasSelection()}select(e,t,n){this._verifyIntegers(e,t,n),this._core.select(e,t,n)}getSelection(){return this._core.getSelection()}getSelectionPosition(){return this._core.getSelectionPosition()}clearSelection(){this._core.clearSelection()}selectAll(){this._core.selectAll()}selectLines(e,t){this._verifyIntegers(e,t),this._core.selectLines(e,t)}dispose(){super.dispose()}scrollLines(e){this._verifyIntegers(e),this._core.scrollLines(e)}scrollPages(e){this._verifyIntegers(e),this._core.scrollPages(e)}scrollToTop(){this._core.scrollToTop()}scrollToBottom(){this._core.scrollToBottom()}scrollToLine(e){this._verifyIntegers(e),this._core.scrollToLine(e)}clear(){this._core.clear()}write(e,t){this._core.write(e,t)}writeln(e,t){this._core.write(e),this._core.write(`\r +`,t)}paste(e){this._core.paste(e)}refresh(e,t){this._verifyIntegers(e,t),this._core.refresh(e,t)}reset(){this._core.reset()}clearTextureAtlas(){this._core.clearTextureAtlas()}loadAddon(e){this._addonManager.loadAddon(this,e)}static get strings(){return{get promptLabel(){return oe.get()},set promptLabel(e){oe.set(e)},get tooMuchOutput(){return ce.get()},set tooMuchOutput(e){ce.set(e)}}}_verifyIntegers(...e){for(Ws of e)if(Ws===1/0||isNaN(Ws)||Ws%1!=0)throw Error(`This API only accepts integers`)}_verifyPositiveIntegers(...e){for(Ws of e)if(Ws&&(Ws===1/0||isNaN(Ws)||Ws%1!=0||Ws<0))throw Error(`This API only accepts positive integers`)}},Ks=2,qs=1,Js=class{activate(e){this._terminal=e}dispose(){}fit(){let e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;let t=this._terminal._core;(this._terminal.rows!==e.rows||this._terminal.cols!==e.cols)&&(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;let e=this._terminal._core._renderService.dimensions;if(e.css.cell.width===0||e.css.cell.height===0)return;let t=this._terminal.options.scrollback===0?0:this._terminal.options.overviewRuler?.width||14,n=window.getComputedStyle(this._terminal.element.parentElement),r=parseInt(n.getPropertyValue(`height`)),i=Math.max(0,parseInt(n.getPropertyValue(`width`))),a=window.getComputedStyle(this._terminal.element),o={top:parseInt(a.getPropertyValue(`padding-top`)),bottom:parseInt(a.getPropertyValue(`padding-bottom`)),right:parseInt(a.getPropertyValue(`padding-right`)),left:parseInt(a.getPropertyValue(`padding-left`))},s=o.top+o.bottom,c=o.right+o.left,l=r-s,u=i-c-t;return{cols:Math.max(Ks,Math.floor(u/e.css.cell.width)),rows:Math.max(qs,Math.floor(l/e.css.cell.height))}}};function Ys(){return{fontSize:typeof window<`u`&&typeof window.matchMedia==`function`&&window.matchMedia(`(pointer: coarse)`).matches?13:12,fontFamily:getComputedStyle(document.body).fontFamily}}function Xs({panes:e,size:t,zoomed:n,socketRef:r,viewsRef:i,bodyRefs:a,pendingRef:o,setTitles:s}){(0,l.useEffect)(()=>{for(let t of e){if(i.current.has(t))continue;let e=a.current.get(t);if(!e||e.clientHeight===0||e.clientWidth===0)continue;let n=new Gs({...Ys(),theme:{background:`#0b0b0d`,foreground:`#e6e6ec`},cursorBlink:!0}),c=new Js;n.loadAddon(c),n.onData(e=>r.current?.send(JSON.stringify({type:`input`,pane:t,data:e}))),n.onTitleChange(e=>{let n=e.replace(/\s+/g,` `).trim();n&&s(e=>({...e,[t]:n}))}),n.open(e),i.current.set(t,{term:n,fit:c});let l=o.current.get(t);if(l){for(let e of l)n.write(e);o.current.delete(t)}}for(let[t,n]of i.current)e.includes(t)||(n.term.dispose(),i.current.delete(t))},[e,t,n])}var Zs=60;function Qs({panes:e,size:t,zoomed:n,socketRef:r,viewsRef:i,bodyRefs:a,sentSizesRef:o,ownsSize:s}){let c=(0,l.useRef)(null),u=(0,l.useCallback)(()=>{for(let[e,t]of i.current){let n=a.current.get(e);if(!n||n.clientHeight===0||n.clientWidth===0)continue;let{rows:i,cols:s}=t.term,c=o.current.get(e);c&&c.rows===i&&c.cols===s||(o.current.set(e,{rows:i,cols:s}),r.current?.send(JSON.stringify({type:`resize`,pane:e,rows:i,cols:s})))}},[r,i,a,o]);(0,l.useEffect)(()=>{for(let[e,t]of i.current){let n=a.current.get(e);if(!n||n.clientHeight===0||n.clientWidth===0)continue;if(s){t.fit.fit();continue}let r=o.current.get(e);r&&t.term.resize(r.cols,r.rows)}if(s)return c.current&&clearTimeout(c.current),c.current=setTimeout(u,Zs),()=>{c.current&&clearTimeout(c.current)}},[e,n,t,u,i,a,o,s])}function $s(e){let[t,n]=(0,l.useState)({});return{recovery:t,setRecovery:n,cancelRecovery:(0,l.useCallback)(t=>b(e.current,t),[e])}}function ec({pending:e,size:t,socketRef:n,slotRefs:r,panesExist:i,onAnswered:a}){(0,l.useEffect)(()=>{if(e===null)return;let t=n.current;if(!t||t.readyState!==WebSocket.OPEN)return;let o=[];for(let n=0;n{let t=new Gs(Ys()),n=new Js;t.loadAddon(n),t.open(e);let r=n.proposeDimensions();if(t.dispose(),!r)throw Error(`could not measure the cell`);return{rows:r.rows,cols:r.cols}})}catch{s=[]}t.send(JSON.stringify({type:`start`,sizes:s})),a()},[e,t,n,r,i,a])}var $=e();function tc({report:e,pane:t,onCancel:r}){let i=t===void 0?y(e):`pane ${t} · ${y(e)}`;return(0,$.jsxs)(`span`,{className:`flex min-w-0 shrink items-center gap-1 rounded-sm bg-ink-800 px-1 text-accent`,title:e.detail??i,children:[(0,$.jsx)(`span`,{className:`truncate`,children:i}),e.detail&&(0,$.jsx)(`span`,{className:`hidden min-w-0 truncate text-ink-400 md:inline`,children:e.detail}),(0,$.jsx)(`button`,{onMouseDown:e=>e.stopPropagation(),onClick:r,title:`Stop waiting and release this pane's slot`,"aria-label":`cancel recovery${t===void 0?``:` for pane ${t}`}`,className:`flex h-5 w-5 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-removed`,children:(0,$.jsx)(n,{className:`h-3 w-3`})})]})}function nc({pane:e,index:t,label:r,cellStyle:a,isActive:o,isZoomed:s,showZoom:c,isDragged:l,isDropTarget:u,reorderable:d,recovery:f,onCancelRecovery:p,onFocus:h,onToggleZoom:g,onClose:_,onPaneDragStart:v,onPaneDragMove:ee,onPaneDragEnd:y,onPaneDragCancel:te,bodyRef:b}){return(0,$.jsxs)(`div`,{"data-pane-id":e,onMouseDown:h,style:a,className:`min-h-0 min-w-0 flex-col overflow-hidden rounded-sm border ${u?`border-accent ring-1 ring-accent`:o?`border-accent`:`border-ink-700`} ${l?`opacity-60`:``}`,children:[(0,$.jsxs)(`div`,{onPointerDown:v,onPointerMove:ee,onPointerUp:y,onPointerCancel:te,className:`flex shrink-0 items-center gap-1 select-none bg-ink-900 px-2 py-0.5 text-xs ${d?l?`cursor-grabbing touch-none`:`cursor-grab touch-none`:``}`,children:[(0,$.jsx)(`span`,{title:r,className:`min-w-0 flex-1 truncate ${o?`text-ink-50`:`text-ink-400`}`,children:m(r,20)}),f&&(0,$.jsx)(tc,{report:f,onCancel:p}),c&&(0,$.jsx)(`button`,{onMouseDown:e=>e.stopPropagation(),onClick:g,"aria-pressed":s,title:s?`Restore the grid`:`Zoom this terminal`,"aria-label":s?`Restore the grid`:`Zoom this terminal`,className:`flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-accent md:h-6 md:w-6`,children:(0,$.jsx)(i,{maximized:s})}),(0,$.jsx)(`button`,{onMouseDown:e=>e.stopPropagation(),onClick:_,title:`Close terminal`,"aria-label":`close terminal ${t+1}`,className:`flex h-8 w-8 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-removed md:h-6 md:w-6`,children:(0,$.jsx)(n,{})})]}),(0,$.jsx)(`div`,{ref:b,className:`min-h-0 flex-1`})]})}function rc({count:e,cells:t,slotRefs:n}){return Array.from({length:e},(e,r)=>{let i=t[r];return(0,$.jsx)(nc,{pane:-1-r,index:r,label:`starting…`,cellStyle:{display:`flex`,gridColumn:`${i.colStart} / span ${i.colSpan}`,gridRow:`${i.row}`},isActive:!1,isZoomed:!1,showZoom:!1,isDragged:!1,isDropTarget:!1,reorderable:!1,onCancelRecovery:()=>{},onFocus:()=>{},onToggleZoom:()=>{},onClose:()=>{},onPaneDragStart:()=>{},onPaneDragMove:()=>{},onPaneDragEnd:()=>{},onPaneDragCancel:()=>{},bodyRef:e=>{e?n.current.set(r,e):n.current.delete(r)}},`slot-${r}`)})}var ic={esc:`\x1B`,tab:` `,"shift-tab":`\x1B[Z`,"ctrl-c":``,"ctrl-d":``,"ctrl-z":``,"ctrl-l":`\f`,"ctrl-r":``,up:`\x1B[A`,down:`\x1B[B`,right:`\x1B[C`,left:`\x1B[D`},ac={up:`\x1BOA`,down:`\x1BOB`,right:`\x1BOC`,left:`\x1BOD`};function oc(e,t=!1){if(t){let t=ac[e];if(t)return t}return ic[e]}var sc=[{key:`esc`,label:`Esc`,aria:`Escape`},{key:`tab`,label:`Tab`,aria:`Tab`},{key:`shift-tab`,label:`⇧Tab`,aria:`Shift Tab`},{key:`ctrl-c`,label:`^C`,aria:`Control C`},{key:`ctrl-d`,label:`^D`,aria:`Control D`},{key:`ctrl-z`,label:`^Z`,aria:`Control Z`},{key:`ctrl-l`,label:`^L`,aria:`Control L`},{key:`ctrl-r`,label:`^R`,aria:`Control R`},{key:`left`,label:`←`,aria:`Left arrow`},{key:`down`,label:`↓`,aria:`Down arrow`},{key:`up`,label:`↑`,aria:`Up arrow`},{key:`right`,label:`→`,aria:`Right arrow`}];function cc({onKey:e}){return(0,$.jsx)(`div`,{className:`flex shrink-0 items-stretch gap-1 overflow-x-auto border-t border-ink-700 bg-ink-900 px-1 py-1 md:hidden`,children:sc.map(({key:t,label:n,aria:r})=>(0,$.jsx)(`button`,{onPointerDown:e=>e.preventDefault(),onClick:()=>e(t),"aria-label":r,className:`flex min-h-9 min-w-9 shrink-0 items-center justify-center rounded-sm border border-ink-700 bg-ink-850 px-2 text-xs text-ink-200 active:bg-ink-700 active:text-accent`,children:n},t))})}function lc({showDivider:e,draggingUpper:t,onUpperDragStart:n,onUpperDragMove:r,onUpperDragEnd:i,onUpperDragCancel:a}){return e?(0,$.jsx)(`div`,{role:`separator`,"aria-orientation":`horizontal`,"aria-label":`Resize the terminal panel (double-click to reset)`,title:`Drag to resize · double-click to reset`,onPointerDown:n,onPointerMove:r,onPointerUp:i,onPointerCancel:a,onLostPointerCapture:i,className:`absolute -top-px left-0 z-10 hidden h-1.5 w-full cursor-row-resize touch-none md:block ${t?`bg-accent`:`hover:bg-accent`}`}):null}function uc({ownsSize:e,maximized:t,recovery:n,panes:r,onCancelRecovery:a,onClaimSize:s,onCreate:l,onToggleMaximized:u}){let d=`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 text-ink-400 hover:text-accent`;return(0,$.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 bg-ink-900 px-2 py-1 text-xs`,children:[v(n,r).map(e=>(0,$.jsx)(tc,{pane:e,report:n[e],onCancel:()=>a(e)},e)),!e&&(0,$.jsx)(`button`,{onClick:s,title:`These panes are sized for another client. Resize them to fit this screen.`,"aria-label":`Fit the panes to this screen`,className:`ml-auto ${d}`,children:(0,$.jsx)(c,{})}),(0,$.jsx)(`button`,{onClick:l,title:`New terminal`,"aria-label":`New terminal`,className:`${d} ${e?`ml-auto`:``}`,children:(0,$.jsx)(o,{})}),(0,$.jsx)(`button`,{onClick:u,"aria-pressed":t,title:t?`Restore panel height`:`Maximize the panel`,"aria-label":t?`Restore panel height`:`Maximize the panel`,className:`hidden md:flex ${d}`,children:(0,$.jsx)(i,{maximized:t})})]})}function dc({repo:e,maximized:t,onToggleMaximized:n,className:r=``,sectionRef:i,...a}){let o=(0,l.useRef)(null),s=(0,l.useRef)(null),c=(0,l.useRef)(new Map),u=(0,l.useRef)(new Map),d=(0,l.useRef)(new Map),p=(0,l.useRef)(new Map),m=(0,l.useRef)(new Map),g=(0,l.useRef)(0),_=(0,l.useRef)(new Map),[v,ee]=(0,l.useState)(null),[y,te]=(0,l.useState)([]),[b,re]=(0,l.useState)(null),[ie,ae]=(0,l.useState)(null),[x,S]=(0,l.useState)({w:0,h:0}),[C,oe]=(0,l.useState)({}),[se,ce]=(0,l.useState)(!0),{recovery:w,setRecovery:le,cancelRecovery:ue}=$s(s);ne({repo:e,socketRef:s,viewsRef:c,pendingRef:p,sentSizesRef:d,lastActiveByRepoRef:m,expectCreateRef:g,setPending:ee,setPanes:te,setActive:re,setZoomed:ae,setTitles:oe,setOwnsSize:ce,setRecovery:le}),Xs({panes:y,size:x,zoomed:ie,socketRef:s,viewsRef:c,bodyRefs:u,pendingRef:p,setTitles:oe});let de=(0,l.useCallback)(()=>ee(null),[]);ec({pending:v,size:x,socketRef:s,slotRefs:_,panesExist:y.length>0,onAnswered:de}),Qs({panes:y,size:x,zoomed:ie,socketRef:s,viewsRef:c,bodyRefs:u,sentSizesRef:d,ownsSize:se}),(0,l.useEffect)(()=>{let e=o.current;if(!e)return;let t=new ResizeObserver(()=>{let t=e.clientWidth,n=e.clientHeight;S(e=>e.w===t&&e.h===n?e:{w:t,h:n})});return t.observe(e),()=>t.disconnect()},[]),(0,l.useEffect)(()=>{if(b===null&&y.length>0){let t=m.current.get(e);re(t!==void 0&&y.includes(t)?t:y[y.length-1])}},[b,y,e]),(0,l.useEffect)(()=>{b!==null&&c.current.get(b)?.term.focus()},[b]);let fe=t=>{re(t),m.current.set(e,t)},pe=()=>{let e=s.current;e&&(ae(null),g.current+=1,e.send(JSON.stringify({type:`create`,rows:24,cols:80})))},me=()=>{s.current?.send(JSON.stringify({type:`claim_size`}))},he=e=>{s.current?.send(JSON.stringify({type:`close`,pane:e}))},T=e=>{if(b===null)return;let t=c.current.get(b)?.term.modes.applicationCursorKeysMode??!1;s.current?.send(JSON.stringify({type:`input`,pane:b,data:oc(e,t)}))},{draggingPane:ge,dragOverPane:_e,reorderable:ve,endPaneDrag:ye,onPaneDragStart:be,onPaneDragMove:xe,onPaneDragEnd:E}=h({panes:y,zoomed:ie,onFocus:fe,onReorder:e=>s.current?.send(JSON.stringify({type:`reorder`,order:e}))}),Se=f(y.length>0?y.length:v??0,x.w>=x.h);return(0,$.jsxs)(`section`,{ref:i,className:`relative flex min-h-0 min-w-0 flex-col border-t border-ink-700 ${r}`,children:[(0,$.jsx)(lc,{...a}),(0,$.jsx)(uc,{ownsSize:se,maximized:t,recovery:w,panes:y,onCancelRecovery:ue,onClaimSize:me,onCreate:pe,onToggleMaximized:n}),(0,$.jsxs)(`div`,{className:`relative min-h-0 flex-1 overflow-hidden bg-ink-950 p-1`,children:[y.length===0&&v===null&&(0,$.jsxs)(`p`,{className:`p-3 text-ink-400`,children:[`No terminal open. Press `,(0,$.jsx)(`span`,{className:`text-accent`,children:`+`}),` above to start one.`]}),(0,$.jsxs)(`div`,{ref:o,className:`grid h-full gap-1`,style:ie===null?{gridTemplateColumns:`repeat(${Se.cols}, minmax(0, 1fr))`,gridTemplateRows:`repeat(${Se.rows}, minmax(0, 1fr))`}:{gridTemplateColumns:`1fr`,gridTemplateRows:`1fr`},children:[y.length===0&&v!==null&&(0,$.jsx)(rc,{count:v,cells:Se.cells,slotRefs:_}),y.map((e,t)=>{let n=C[e]??`term ${t+1}`,r=Se.cells[t];return(0,$.jsx)(nc,{pane:e,index:t,label:n,cellStyle:ie===null?{display:`flex`,gridColumn:`${r.colStart} / span ${r.colSpan}`,gridRow:`${r.row}`}:{display:e===ie?`flex`:`none`},isActive:e===b,isZoomed:ie===e,showZoom:y.length>1,isDragged:ge===e,isDropTarget:_e===e,reorderable:ve,recovery:w[e],onCancelRecovery:()=>ue(e),onFocus:()=>fe(e),onToggleZoom:()=>ae(t=>t===e?null:e),onClose:()=>he(e),onPaneDragStart:t=>be(t,e),onPaneDragMove:xe,onPaneDragEnd:E,onPaneDragCancel:ye,bodyRef:t=>{t?u.current.set(e,t):u.current.delete(e)}},e)})]})]}),y.length>0&&(0,$.jsx)(cc,{onKey:T})]})}export{dc as TerminalPanel}; \ No newline at end of file diff --git a/viewer-ui/dist/assets/index-B1AVnrYL.css b/viewer-ui/dist/assets/index-B1AVnrYL.css deleted file mode 100644 index 87d11638..00000000 --- a/viewer-ui/dist/assets/index-B1AVnrYL.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:system-ui, sans-serif;--font-mono:ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink-950:#0b0b0d;--color-ink-900:#121215;--color-ink-850:#17171b;--color-ink-800:#1d1d22;--color-ink-700:#2a2a31;--color-ink-600:#3a3a43;--color-ink-400:#6f6f7d;--color-ink-200:#a8a8b5;--color-ink-50:#e6e6ec;--color-accent:#d9a441;--color-added:#4ba36b;--color-removed:#c85f5f}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.-top-px{top:-1px}.top-0{top:0}.top-3{top:calc(var(--spacing) * 3)}.-right-px{right:-1px}.right-3{right:calc(var(--spacing) * 3)}.left-0{left:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.-my-\[8\.8px\]{margin-block:-8.8px}.my-1{margin-block:var(--spacing)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mr-1{margin-right:var(--spacing)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1{margin-left:var(--spacing)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-72{height:calc(var(--spacing) * 72)}.h-\[22px\]{height:22px}.h-full{height:100%}.max-h-\[70vh\]{max-height:70vh}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:0}.min-h-9{min-height:calc(var(--spacing) * 9)}.min-h-11{min-height:calc(var(--spacing) * 11)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-56{width:calc(var(--spacing) * 56)}.w-80{width:calc(var(--spacing) * 80)}.w-\[17rem\]{width:17rem}.w-\[22px\]{width:22px}.w-\[34rem\]{width:34rem}.w-full{width:100%}.w-max{width:max-content}.max-w-\[6rem\]{max-width:6rem}.max-w-\[9rem\]{max-width:9rem}.max-w-\[80vw\]{max-width:80vw}.max-w-\[86vw\]{max-width:86vw}.max-w-\[calc\(100vw-1\.5rem\)\]{max-width:calc(100vw - 1.5rem)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.min-w-9{min-width:calc(var(--spacing) * 9)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-none{flex:none}.shrink-0{flex-shrink:0}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-row-resize{cursor:row-resize}.touch-none{touch-action:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr}.grid-rows-\[auto_minmax\(0\,1fr\)_auto_auto\]{grid-template-rows:auto minmax(0,1fr) auto auto}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-\[1ch\]{gap:1ch}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[20\.7\%\]{border-radius:20.7%}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-accent{border-color:var(--color-accent)}.border-ink-700{border-color:var(--color-ink-700)}.border-ink-800{border-color:var(--color-ink-800)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-added\/10{background-color:#4ba36b1a}@supports (color:color-mix(in lab, red, red)){.bg-added\/10{background-color:color-mix(in oklab, var(--color-added) 10%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-ink-50{background-color:var(--color-ink-50)}.bg-ink-700{background-color:var(--color-ink-700)}.bg-ink-800{background-color:var(--color-ink-800)}.bg-ink-850{background-color:var(--color-ink-850)}.bg-ink-900{background-color:var(--color-ink-900)}.bg-ink-900\/40{background-color:#12121566}@supports (color:color-mix(in lab, red, red)){.bg-ink-900\/40{background-color:color-mix(in oklab, var(--color-ink-900) 40%, transparent)}}.bg-ink-950{background-color:var(--color-ink-950)}.bg-removed\/10{background-color:#c85f5f1a}@supports (color:color-mix(in lab, red, red)){.bg-removed\/10{background-color:color-mix(in oklab, var(--color-removed) 10%, transparent)}}.bg-white{background-color:var(--color-white)}.p-1{padding:var(--spacing)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-\[1ch\]{padding-inline:1ch}.px-\[12\.8px\]{padding-inline:12.8px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-\[8\.8px\]{padding-block:8.8px}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pl-1{padding-left:var(--spacing)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-sans{font-family:var(--font-sans)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[16px\]{font-size:16px}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.04em\]{--tw-tracking:.04em;letter-spacing:.04em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.text-accent{color:var(--color-accent)}.text-added{color:var(--color-added)}.text-ink-50{color:var(--color-ink-50)}.text-ink-200{color:var(--color-ink-200)}.text-ink-400{color:var(--color-ink-400)}.text-ink-600{color:var(--color-ink-600)}.text-ink-950{color:var(--color-ink-950)}.text-removed{color:var(--color-removed)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline\!{text-decoration-line:underline!important}.opacity-60{opacity:.6}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[inset_0_2px_0_0_var\(--color-accent\)\]{--tw-shadow:inset 0 2px 0 0 var(--tw-shadow-color,var(--color-accent));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-accent{--tw-ring-color:var(--color-accent)}.ring-ink-600{--tw-ring-color:var(--color-ink-600)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.ring-inset{--tw-ring-inset:inset}.placeholder\:text-ink-400::placeholder{color:var(--color-ink-400)}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-ink-700:hover{background-color:var(--color-ink-700)}.hover\:bg-ink-850:hover{background-color:var(--color-ink-850)}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-accent:hover{color:var(--color-accent)}.hover\:text-ink-200:hover{color:var(--color-ink-200)}.hover\:text-removed:hover{color:var(--color-removed)}}.focus\:border-accent:focus{border-color:var(--color-accent)}.focus\:border-ink-600:focus{border-color:var(--color-ink-600)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-\[3px\]:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-accent:focus{--tw-ring-color:var(--color-accent)}.focus\:ring-accent\/15:focus{--tw-ring-color:#d9a44126}@supports (color:color-mix(in lab, red, red)){.focus\:ring-accent\/15:focus{--tw-ring-color:color-mix(in oklab, var(--color-accent) 15%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-ink-700:active{background-color:var(--color-ink-700)}.active\:text-accent:active{color:var(--color-accent)}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:inline{display:inline}}@media (width>=48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:h-6{height:calc(var(--spacing) * 6)}.md\:w-6{width:calc(var(--spacing) * 6)}.md\:flex-1{flex:1}.md\:basis-1\/2{flex-basis:50%}.md\:grid-cols-\[var\(--nc-sidebar\)_1fr\]{grid-template-columns:var(--nc-sidebar) 1fr}.md\:grid-rows-\[auto_minmax\(0\,0fr\)_minmax\(0\,1fr\)_auto\]{grid-template-rows:auto minmax(0,0fr) minmax(0,1fr) auto}.md\:grid-rows-\[auto_minmax\(0\,1fr\)_minmax\(0\,0fr\)_auto\]{grid-template-rows:auto minmax(0,1fr) minmax(0,0fr) auto}.md\:grid-rows-\[auto_minmax\(0\,var\(--nc-upper\)\)_minmax\(0\,var\(--nc-lower\)\)_auto\]{grid-template-rows:auto minmax(0,var(--nc-upper)) minmax(0,var(--nc-lower)) auto}.md\:flex-row{flex-direction:row}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}}}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;-webkit-text-decoration:overline double underline;-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;-webkit-text-decoration:overline wavy underline;-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;-webkit-text-decoration:overline dotted underline;-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;-webkit-text-decoration:overline dashed underline;-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}html,body,#root{height:100%}html{font-size:14px}body{background:var(--color-ink-950);color:var(--color-ink-50);font-family:var(--font-mono);margin:0;font-size:.85rem;line-height:1.4}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}*{scrollbar-width:thin;scrollbar-color:var(--color-ink-600) transparent}.nc-markdown{max-width:52rem;font-family:var(--font-sans);color:var(--color-ink-50);line-height:1.6}.nc-markdown h1,.nc-markdown h2,.nc-markdown h3,.nc-markdown h4,.nc-markdown h5,.nc-markdown h6{margin:1.4em 0 .6em;font-weight:600;line-height:1.25}.nc-markdown h1{border-bottom:1px solid var(--color-ink-700);padding-bottom:.3em;font-size:1.6em}.nc-markdown h2{border-bottom:1px solid var(--color-ink-800);padding-bottom:.25em;font-size:1.35em}.nc-markdown h3{font-size:1.15em}.nc-markdown h4{font-size:1em}.nc-markdown h5,.nc-markdown h6{color:var(--color-ink-200);font-size:.9em}.nc-markdown :first-child{margin-top:0}.nc-markdown p,.nc-markdown ul,.nc-markdown ol,.nc-markdown blockquote,.nc-markdown table,.nc-markdown pre{margin:.75em 0}.nc-markdown ul,.nc-markdown ol{padding-left:1.5em}.nc-markdown ul{list-style:outside}.nc-markdown ol{list-style:decimal}.nc-markdown li{margin:.25em 0}.nc-markdown li::marker{color:var(--color-ink-400)}.nc-markdown li:has(>input[type=checkbox]){margin-left:-1.2em;list-style:none}.nc-markdown a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.nc-markdown strong{font-weight:600}.nc-markdown em{font-style:italic}.nc-markdown blockquote{border-left:3px solid var(--color-ink-700);color:var(--color-ink-200);padding-left:1em}.nc-markdown hr{border:0;border-top:1px solid var(--color-ink-700);margin:1.5em 0}.nc-markdown img{max-width:100%}.nc-markdown :not(pre)>code{font-family:var(--font-mono);background:var(--color-ink-800);border-radius:3px;padding:.1em .35em;font-size:.9em}.nc-markdown pre{background:var(--color-ink-850);border:1px solid var(--color-ink-800);border-radius:4px;padding:.9em 1em;overflow-x:auto}.nc-markdown pre code{font-family:var(--font-mono);background:0 0;padding:0;font-size:.85em}.nc-markdown table{border-collapse:collapse;display:block;overflow-x:auto}.nc-markdown th,.nc-markdown td{border:1px solid var(--color-ink-700);text-align:left;padding:.4em .7em}.nc-markdown th{background:var(--color-ink-850);font-weight:600}@keyframes nc-fade-in{0%{opacity:0}to{opacity:1}}.nc-fade{animation:.16s ease-out nc-fade-in}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}} diff --git a/viewer-ui/dist/assets/index-CQToaW-W.js b/viewer-ui/dist/assets/index-BN9ccC5R.js similarity index 99% rename from viewer-ui/dist/assets/index-CQToaW-W.js rename to viewer-ui/dist/assets/index-BN9ccC5R.js index 364542ca..4b23c261 100644 --- a/viewer-ui/dist/assets/index-CQToaW-W.js +++ b/viewer-ui/dist/assets/index-BN9ccC5R.js @@ -1,4 +1,4 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./Markdown-C0-sv82C.js","./Markdown-C8LL_u4z.css"])))=>i.map(i=>d[i]); +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./Markdown-B9Na3tqX.js","./Markdown-C8LL_u4z.css"])))=>i.map(i=>d[i]); var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),s=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},c=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},l=(n,r,a)=>(a=n==null?{}:e(i(n)),c(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var u=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function T(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function E(e,t){return T(e.type,t,e.props)}function D(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function te(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var O=/\/+/g;function ne(e,t){return typeof e==`object`&&e&&e.key!=null?te(``+e.key):t.toString(36)}function k(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function re(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,re(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+ne(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(O,`$&/`)+`/`),re(o,r,i,``,function(e){return e})):o!=null&&(D(o)&&(o=E(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(O,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=u()})),f=o((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,D());else{var t=n(l);t!==null&&ne(x,t.startTime-e)}}var S=!1,C=-1,w=5,ee=-1;function T(){return g?!0:!(e.unstable_now()-eet&&T());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&ne(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?D():S=!1}}}var D;if(typeof y==`function`)D=function(){y(E)};else if(typeof MessageChannel<`u`){var te=new MessageChannel,O=te.port2;te.port1.onmessage=E,D=function(){O.postMessage(null)}}else D=function(){_(E,0)};function ne(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,ne(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,D()))),r},e.unstable_shouldYield=T,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),p=o(((e,t)=>{t.exports=f()})),m=o((e=>{var t=d();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=m()})),g=o((e=>{var t=p(),n=d(),r=h();function i(e){var t=`https://react.dev/errors/`+e;if(1ce||(e.current=se[ce],se[ce]=null,ce--)}function P(e,t){ce++,se[ce]=e.current,e.current=t}var le=M(null),ue=M(null),de=M(null),fe=M(null);function pe(e,t){switch(P(de,t),P(ue,e),P(le,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}N(le),P(le,e)}function me(){N(le),N(ue),N(de)}function he(e){e.memoizedState!==null&&P(fe,e);var t=le.current,n=Hd(t,e.type);t!==n&&(P(ue,e),P(le,n))}function ge(e){ue.current===e&&(N(le),N(ue)),fe.current===e&&(N(fe),Qf._currentValue=oe)}var _e,ve;function ye(e){if(_e===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);_e=t&&t[1]||``,ve=-1)`:-1>>=0,e===0?32:31-(He(e)/Ue|0)|0}var Ge=256,Ke=262144,qe=4194304;function Je(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Ye(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=Je(n))):i=Je(o):i=Je(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=Je(n))):i=Je(o)):i=Je(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function Xe(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function Ze(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function Qe(){var e=qe;return qe<<=1,!(qe&62914560)&&(qe=4194304),e}function $e(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function et(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function tt(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),pn=!1;if(fn)try{var mn={};Object.defineProperty(mn,"passive",{get:function(){pn=!0}}),window.addEventListener(`test`,mn,mn),window.removeEventListener(`test`,mn,mn)}catch{pn=!1}var hn=null,gn=null,_n=null;function vn(){if(_n)return _n;var e,t=gn,n=t.length,r,i=`value`in hn?hn.value:hn.textContent,a=i.length;for(e=0;e=Xn),$n=` `,er=!1;function tr(e,t){switch(e){case`keyup`:return Jn.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function nr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var rr=!1;function ir(e,t){switch(e){case`compositionend`:return nr(t);case`keypress`:return t.which===32?(er=!0,$n):null;case`textInput`:return e=t.data,e===$n&&er?null:e;default:return null}}function ar(e,t){if(rr)return e===`compositionend`||!Yn&&tr(e,t)?(e=vn(),_n=gn=hn=null,rr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Dr(n)}}function kr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?kr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ar(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=zt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=zt(e.document)}return t}function jr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Mr=fn&&`documentMode`in document&&11>=document.documentMode,Nr=null,Pr=null,Fr=null,Ir=!1;function Lr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Ir||Nr==null||Nr!==zt(r)||(r=Nr,`selectionStart`in r&&jr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Fr&&Er(Fr,r)||(Fr=r,r=Ed(Pr,`onSelect`),0>=o,i-=o,ki=1<<32-Ve(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),L&&ji(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),L&&ji(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return L&&ji(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),L&&ji(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case _:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===D&&Aa(l)===r.type){n(e,r.sibling),c=a(r,o.props),La(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=gi(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=hi(o.type,o.key,o.props,null,e.mode,c),La(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=yi(o,e.mode,c),c.return=e,e=c}return s(e);case D:return o=Aa(o),b(e,r,o,c)}if(ae(o))return h(e,r,o,c);if(k(o)){if(l=k(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),g(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,Ia(o),c);if(o.$$typeof===C)return b(e,r,ia(e,o),c);Ra(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=_i(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Fa=0;var i=b(e,t,n,r);return Pa=null,i}catch(t){if(t===wa||t===Ea)throw t;var a=di(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=za(!0),Va=za(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=ci(e),si(e,null,n),t}return ii(e,r,t,n),ci(e)}function qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,rt(e,n)}}function Ja(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ya=!1;function Xa(){if(Ya){var e=ha;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(J&f)===f:(r&f)===f){f!==0&&f===ma&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;f=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,f);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,f=typeof h==`function`?h.call(_,d,f):h,f==null)break a;d=m({},d,f);break a;case 2:Ha=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=A.T,s={};A.T=s,Fs(e,!1,t,n);try{var c=i(),l=A.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,va(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{j.p=a,o!==null&&s.types!==null&&(o.types=s.types),A.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Es(e).queue;Cs(e,a,t,oe,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:oe,baseState:oe,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:oe},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return ra(Qf)}function ks(){return H().memoizedState}function As(){return H().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(hu(r,t,n),qa(r,t,n)),t={cache:ua()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=ai(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Tr(s,o))return ii(e,t,i,0),K===null&&ri(),!1}catch{}if(n=ai(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(i(479))}else t=ai(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===z||t!==null&&t===z}function Ls(e,t){_o=go=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,rt(e,n)}}var zs={readContext:ra,use:Po,useCallback:V,useContext:V,useEffect:V,useImperativeHandle:V,useLayoutEffect:V,useInsertionEffect:V,useMemo:V,useReducer:V,useRef:V,useState:V,useDebugValue:V,useDeferredValue:V,useTransition:V,useSyncExternalStore:V,useId:V,useHostTransitionStatus:V,useFormState:V,useActionState:V,useOptimistic:V,useMemoCache:V,useCacheRefresh:V};zs.useEffectEvent=V;var Bs={readContext:ra,use:Po,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:ra,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=jo();t=t===void 0?null:t;var r=e();if(vo){Be(!0);try{e()}finally{Be(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=jo();if(n!==void 0){var i=n(t);if(vo){Be(!0);try{n(t)}finally{Be(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,z,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,z,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(jo(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,z,e.queue,!0,!1),jo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=z,a=jo();if(L){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=jo(),t=K.identifierPrefix;if(L){var n=Ai,r=ki;n=(r&~(1<<32-Ve(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=yo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[lt]=t,o[ut]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=de.current,Ui(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Ii,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[lt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Bi(t,!0)}else e=Bd(e).createTextNode(r),e[lt]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=Ui(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[lt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(i(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=Ui(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[lt]=t}else Wi(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),a=!1}else a=Gi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return me(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return Zi(t.type),U(t),null;case 19:if(N(R),r=t.memoizedState,r===null)return U(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=po(e),o!==null){for(t.flags|=128,Rc(r,!1),e=o.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)mi(n,e),n=n.sibling;return P(R,R.current&1|2),L&&ji(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&ke()>tu&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304)}else{if(!a)if(e=po(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!L)return U(t),null}else 2*ke()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ke(),e.sibling=null,n=R.current,P(R,a?n&1|2:n&1),L&&ji(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&N(ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),Zi(la),U(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(Pi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return Zi(la),me(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return ge(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Wi()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return N(R),null;case 4:return me(),null;case 10:return Zi(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&N(ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return Zi(la),null;case 25:return null;default:return null}}function Vc(e,t){switch(Pi(t),t.tag){case 3:Zi(la),me();break;case 26:case 27:case 5:ge(t);break;case 4:me();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:N(R);break;case 10:Zi(t.type);break;case 22:case 23:fo(t),io(),e!==null&&N(ba);break;case 24:Zi(la)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[ut]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=nn));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[lt]=e,t[ut]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Ar(e),jr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[lt]=e,St(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Or(s,h),v=Or(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,A.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),ze&&typeof ze.onPostCommitFiberRoot==`function`)try{ze.onPostCommitFiberRoot(Re,o)}catch{}return!0}finally{j.p=a,A.T=r,Vu(e,t)}}function Wu(e,t,n){t=xi(n,t),t=$s(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(et(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=xi(n,e),n=ec(2),r=Ka(t,n,2),r!==null&&(tc(n,r,t,e),et(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>ke()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=Qe()),e=oi(e,t),e!==null&&(et(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return Te(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-Ve(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=Ye(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||Xe(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=ke(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Vt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),St(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Vt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Vt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Vt(n.imageSizes)+`"]`)):i+=`[href="`+Vt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=m({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),St(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Vt(r)+`"][href="`+Vt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=m({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),St(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=xt(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=m({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);St(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=xt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),St(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=xt(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=m({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),St(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=de.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=xt(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=xt(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=xt(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Vt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return m({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),St(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Vt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Vt(n.href)+`"]`);if(r)return t.instance=r,St(r),r;var a=m({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),St(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,St(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),St(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,St(a),a):(r=n,(a=mf.get(o))&&(r=m({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),St(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,St(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),St(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=d(),y=_(),b=class extends Error{status;constructor(e,t){super(t),this.status=e}},x=e=>e instanceof b&&e.status===401,S=class extends Error{constructor(e){super(`connection lost — check your network`,{cause:e}),this.name=`NetworkError`}},C=e=>e instanceof S;async function w(e,t){try{return await fetch(e,t)}catch(e){throw new S(e)}}async function ee(e){if(!e.ok){let t=`request failed (${e.status})`;try{let n=await e.json();typeof n?.error==`string`&&(t=n.error)}catch{}throw new b(e.status,t)}let t=await e.json();if(t.version!==2)throw new b(e.status,`this page is out of date (server protocol v${t.version}) — reload`);return t}async function T(e,t){return ee(await w(e,{credentials:`same-origin`,signal:t}))}async function E(e,t,n){return ee(await w(e,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/json`},body:JSON.stringify(t),signal:n}))}var D=e=>new URLSearchParams(e).toString(),te=1e4,O={async login(e){let t=await w(`/login`,{method:`POST`,credentials:`same-origin`,headers:{"Content-Type":`application/x-www-form-urlencoded`},body:new URLSearchParams({password:e}).toString()});if(!t.ok)throw new b(t.status,t.status===429?`too many attempts — wait a minute`:`incorrect password`)},repos:e=>T(`/api/repos`,e),setAccent:e=>E(`/api/prefs`,{accent:e}).then(e=>e.accent),setSidebarWidth:e=>E(`/api/prefs`,{sidebar_width:e}).then(e=>e.sidebar_width),setUpperPct:e=>E(`/api/prefs`,{upper_pct:e}).then(e=>e.upper_pct),setActiveRepo:e=>E(`/api/prefs`,{active_repo:e},AbortSignal.timeout(te)).then(e=>e.active_repo),status:e=>T(`/api/status?${D({repo:e})}`),tree:(e,t)=>T(`/api/tree?${D({repo:e,path:t})}`),treeSearch:(e,t)=>T(`/api/tree/search?${D({repo:e,q:t})}`),log:(e,t)=>T(`/api/log?${D(t?{repo:e,from:t.from,skip:String(t.skip)}:{repo:e})}`),diff:(e,t)=>T(`/api/diff?${D({repo:e,path:t})}`),file:(e,t)=>T(`/api/file?${D({repo:e,path:t})}`),commit:(e,t)=>T(`/api/commit?${D({repo:e,oid:t})}`),commitFiles:(e,t)=>T(`/api/commit/files?${D({repo:e,oid:t})}`),commitFileDiff:(e,t,n)=>T(`/api/commit/file-diff?${D({repo:e,oid:t,path:n})}`),browse:e=>T(`/api/browse${e?`?${D({path:e})}`:``}`),mkdir:(e,t)=>E(`/api/mkdir`,{path:e,name:t}).then(e=>e.path),clone:(e,t)=>E(`/api/clone`,{path:e,url:t}),cloneStatus:e=>T(`/api/clone?${D({job:String(e)})}`),runningClone:()=>T(`/api/clone`),open:e=>E(`/api/repos`,{path:e}).then(e=>e.repo),close:async e=>{let t=await w(`/api/repos?${D({repo:e})}`,{method:`DELETE`,credentials:`same-origin`});if(!t.ok)throw new b(t.status,`could not close (${t.status})`)},reorderRepos:e=>E(`/api/repos/order`,{order:e}).then(e=>e.repos)};function ne(e,t){let n=new EventSource(`/api/events?${D({repo:e})}`);return n.addEventListener(`status`,e=>{try{let n=JSON.parse(e.data);n.version===2&&t(n)}catch{}}),()=>n.close()}var k=[],re=1,ie=new Set;function ae(){let e=k;ie.forEach(t=>t(e))}function A(e){return ie.add(e),e(k),()=>{ie.delete(e)}}function j(e){let t=k.filter(t=>t.id!==e);t.length!==k.length&&(k=t,ae())}function oe(e,t){let n=k.findIndex(n=>n.kind===e&&n.message===t);if(n!==-1){let e=k[n];return k=k.map((e,t)=>t===n?{...e,bump:e.bump+1}:e),ae(),e.id}let r=re++;return k=[...k,{id:r,kind:e,message:t,bump:0}].slice(-4),ae(),r}var se={error:e=>oe(`error`,e),info:e=>oe(`info`,e),success:e=>oe(`success`,e)},ce=1e3;function M(e,t){return e===void 0||e<=0?0:e-t}function N(e,t,n){let r=M(t,n);return e===null||Math.abs(r-e)>=1e3?r:e}function P(e,t,n){if(e===void 0)return`cool`;let r=Math.max(0,t-e);return r>=n?`cool`:r<5e3?`fresh`:`warm`}function le(e,t,n){return e.some(e=>P(e,t,n)!==`cool`)}var ue={fresh:`text-accent font-bold`,warm:`text-accent`,cool:``};function de(e,t,n){let[r,i]=(0,v.useState)(()=>Date.now()+n);return(0,v.useEffect)(()=>{if(t<=0||!e)return;let r=e.map(e=>e.mtime),a=Date.now()+n;if(i(a),!le(r,a,t))return;let o=setInterval(()=>{let e=Date.now()+n;i(e),le(r,e,t)||clearInterval(o)},ce);return()=>clearInterval(o)},[e,t,n]),r}var fe=[{name:`yellow`,color:`#d9a441`},{name:`cyan`,color:`#03c4db`},{name:`green`,color:`#77c47a`},{name:`magenta`,color:`#dc8fd5`},{name:`blue`,color:`#87acfd`}],pe=`nightcrow.viewer.accent`;function me(e){if(!Number.isFinite(e))return 0;let t=fe.length;return(Math.trunc(e)%t+t)%t}function he(){try{let e=localStorage.getItem(pe);return e===null?0:me(Number(e))}catch{return 0}}function ge(e){try{localStorage.setItem(pe,String(e))}catch{}}function _e(){let[e,t]=(0,v.useState)(he);(0,v.useLayoutEffect)(()=>{document.documentElement.style.setProperty(`--color-accent`,fe[e].color)},[e]);let n=(0,v.useCallback)(()=>{t(e=>{let t=me(e+1);return ge(t),O.setAccent(t).catch(()=>{}),t})},[]),r=(0,v.useCallback)(e=>{t(t=>{let n=me(e);return n===t?t:(ge(n),n)})},[]);return{accent:fe[e],next:fe[me(e+1)],cycle:n,adopt:r}}var ve=`nightcrow.sidebarWidth`,ye=.5;function be(e){return Math.min(Math.max(Math.round(e),280),720)}function xe(e){let t=720;try{t=Math.min(t,Math.round(window.innerWidth*ye))}catch{}return Math.min(Math.max(Math.round(e),280),Math.max(t,280))}function Se(){try{let e=Number(localStorage.getItem(ve));return Number.isFinite(e)&&e>0?be(e):460}catch{return 460}}function Ce(e){try{localStorage.setItem(ve,String(e))}catch{}}function we(){let[e,t]=(0,v.useState)(Se);return{width:e,resize:(0,v.useCallback)(e=>{let n=xe(e);t(n),Ce(n)},[]),commit:(0,v.useCallback)(e=>{let n=xe(e);t(n),Ce(n),O.setSidebarWidth(n).catch(()=>{})},[]),reset:(0,v.useCallback)(()=>{let e=be(460);t(e),Ce(e),O.setSidebarWidth(e).catch(()=>{})},[]),adopt:(0,v.useCallback)(e=>{t(t=>{let n=be(e);return n===t?t:(Ce(n),n)})},[])}}function Te(e){return Number.isFinite(e)?Math.min(Math.max(e,20),85):55}function Ee(e){return Math.round(Te(e))}function De(e,t,n,r){let i=n-t;return Te(i<=0?r:(e-t)/i*100)}var Oe=`nightcrow.upperPct`;function ke(){try{let e=Number(localStorage.getItem(Oe));return Number.isFinite(e)&&e>0?Ee(e):55}catch{return 55}}function Ae(e){try{localStorage.setItem(Oe,String(e))}catch{}}function je(){let[e,t]=(0,v.useState)(ke);return{pct:e,resize:(0,v.useCallback)(e=>{t(Te(e))},[]),commit:(0,v.useCallback)(e=>{let n=Ee(e);t(n),Ae(n),O.setUpperPct(n).catch(()=>{})},[]),reset:(0,v.useCallback)(()=>{t(55),Ae(55),O.setUpperPct(55).catch(()=>{})},[]),adopt:(0,v.useCallback)(e=>{t(t=>{let n=Ee(e);return n===t?t:(Ae(n),n)})},[])}}function Me(){let{accent:e,next:t,cycle:n,adopt:r}=_e(),{width:i,resize:a,commit:o,reset:s,adopt:c}=we(),{pct:l,resize:u,commit:d,reset:f,adopt:p}=je(),m=(0,v.useRef)(0),h=(0,v.useRef)(0),g=(0,v.useRef)(0);return{accent:e,next:t,cycle:(0,v.useCallback)(()=>{m.current+=1,n()},[n]),adoptAccent:r,accentWrites:m,sidebarWidth:i,resizeSidebar:a,commitSidebarWidth:(0,v.useCallback)(e=>{h.current+=1,o(e)},[o]),resetSidebarWidth:(0,v.useCallback)(()=>{h.current+=1,s()},[s]),bumpSidebarWrites:(0,v.useCallback)(()=>{h.current+=1},[]),adoptSidebarWidth:c,sidebarWrites:h,upperPct:l,resizeUpperPct:u,commitUpperPct:(0,v.useCallback)(e=>{g.current+=1,d(e)},[d]),resetUpperPct:(0,v.useCallback)(()=>{g.current+=1,f()},[f]),bumpUpperPctWrites:(0,v.useCallback)(()=>{g.current+=1},[]),adoptUpperPct:p,upperPctWrites:g}}var Ne=400;function Pe({value:e,valueAt:t,onGestureStart:n,resize:r,commit:i,reset:a,axis:o}){let s=(0,v.useRef)(0),c=(0,v.useRef)(0),l=(0,v.useRef)(!1),u=(0,v.useRef)(!1),d=(0,v.useRef)(0),[f,p]=(0,v.useState)(!1);return{dragging:f,onDragStart:(0,v.useCallback)(t=>{t.button!==0||!t.isPrimary||n()&&(s.current=o===`x`?t.clientX:t.clientY,c.current=e,l.current=!0,u.current=!1,p(!0),t.currentTarget.setPointerCapture(t.pointerId),t.preventDefault())},[e,n,o]),onDragMove:(0,v.useCallback)(e=>{if(!l.current)return;let n=o===`x`?e.clientX:e.clientY;if(!u.current&&Math.abs(n-s.current)<3)return;let i=t(e);i!==null&&(u.current=!0,c.current=i,r(i))},[t,r,o]),onDragEnd:(0,v.useCallback)(()=>{if(!l.current)return;if(l.current=!1,p(!1),u.current){i(c.current),d.current=0;return}let e=Date.now();e-d.current{l.current=!1,u.current=!1,d.current=0,p(!1)},[]),draggingRef:l}}function Fe({sidebarRef:e,sidebarWidth:t,resizeSidebar:n,commitSidebarWidth:r,resetSidebarWidth:i,bumpSidebarWrites:a}){let o=(0,v.useRef)(0),s=(0,v.useCallback)(()=>{let t=e.current?.getBoundingClientRect().left;return t===void 0?!1:(o.current=t,a(),!0)},[e,a]),{dragging:c,onDragStart:l,onDragMove:u,onDragEnd:d,onDragCancel:f,draggingRef:p}=Pe({value:t,valueAt:(0,v.useCallback)(e=>e.clientX-o.current,[]),onGestureStart:s,resize:n,commit:r,reset:i,axis:`x`});return{draggingSidebar:c,onSidebarDragStart:l,onSidebarDragMove:u,onSidebarDragEnd:d,onSidebarDragCancel:f,draggingRef:p}}function Ie({upperRef:e,lowerRef:t,upperPct:n,resizeUpperPct:r,commitUpperPct:i,resetUpperPct:a,bumpUpperPctWrites:o}){let s=(0,v.useRef)(0),c=(0,v.useRef)(0),l=(0,v.useCallback)(()=>{let n=e.current?.getBoundingClientRect().top,r=t.current?.getBoundingClientRect().bottom;return n===void 0||r===void 0?!1:(s.current=n,c.current=r,o(),!0)},[e,t,o]),{dragging:u,onDragStart:d,onDragMove:f,onDragEnd:p,onDragCancel:m,draggingRef:h}=Pe({value:n,valueAt:(0,v.useCallback)(e=>De(e.clientY,s.current,c.current,n),[n]),onGestureStart:l,resize:r,commit:i,reset:a,axis:`y`});return{draggingUpper:u,onUpperDragStart:d,onUpperDragMove:f,onUpperDragEnd:p,onUpperDragCancel:m,upperDraggingRef:h}}function Le(){let{accent:e,next:t,cycle:n,adoptAccent:r,accentWrites:i,sidebarWidth:a,resizeSidebar:o,commitSidebarWidth:s,resetSidebarWidth:c,bumpSidebarWrites:l,adoptSidebarWidth:u,sidebarWrites:d,upperPct:f,resizeUpperPct:p,commitUpperPct:m,resetUpperPct:h,bumpUpperPctWrites:g,adoptUpperPct:_,upperPctWrites:y}=Me(),b=(0,v.useRef)(null),x=(0,v.useRef)(null),S=(0,v.useRef)(null),C=Fe({sidebarRef:b,sidebarWidth:a,resizeSidebar:o,commitSidebarWidth:s,resetSidebarWidth:c,bumpSidebarWrites:l}),w=Ie({upperRef:x,lowerRef:S,upperPct:f,resizeUpperPct:p,commitUpperPct:m,resetUpperPct:h,bumpUpperPctWrites:g});return{accent:e,next:t,cycle:n,upperPct:f,shell:{sidebarWidth:a,sidebarRef:b,upperRef:x,lowerRef:S,draggingSidebar:C.draggingSidebar,onSidebarDragStart:C.onSidebarDragStart,onSidebarDragMove:C.onSidebarDragMove,onSidebarDragEnd:C.onSidebarDragEnd,onSidebarDragCancel:C.onSidebarDragCancel,draggingUpper:w.draggingUpper,onUpperDragStart:w.onUpperDragStart,onUpperDragMove:w.onUpperDragMove,onUpperDragEnd:w.onUpperDragEnd,onUpperDragCancel:w.onUpperDragCancel},guards:{adoptAccent:r,adoptSidebarWidth:u,adoptUpperPct:_,accentWrites:i,sidebarWrites:d,upperPctWrites:y,draggingRef:C.draggingRef,upperDraggingRef:w.upperDraggingRef}}}function Re(e,t,n,r=!1){return r&&n&&t.includes(n)?n:e&&t.includes(e)?e:n&&t.includes(n)?n:t[0]??null}function ze(e){let t=!1,n=null,r=()=>{if(t||n===null)return;let i=n;n=null,t=!0,e(i).catch(()=>{}).finally(()=>{t=!1,r()})};return e=>{n=e,r()}}function Be(e,t,n){if(t===n)return e;let r=e.indexOf(t),i=e.indexOf(n);if(r===-1||i===-1)return e;let a=e.filter(e=>e!==t),o=a.indexOf(n),s=r{let e=!1,d,g=new AbortController,v=()=>{let y=c.current,S=l.current,ee=u.current,E=f.current;return O.repos(g.signal).then(n=>{let{repos:g,hot:x,accent:C,sidebar_width:te,upper_pct:O,active_repo:k,now_ms:ie,can_clone:ae}=n;if(e)return;w(x),ne(ae),T(e=>N(e,ie,Date.now())),c.current===y&&r(C),l.current===S&&!o.current&&i(te),u.current===ee&&!s.current&&a(O),t(!0),D(!0);let A=m.current||h.current!==null;f.current===E&&!p.current&&!A?_(g):_(e=>{let t=Ve(g.map(e=>e.id),e.map(e=>e.id)),n=new Map(g.map(e=>[e.id,e]));return t.map(e=>n.get(e)).filter(Boolean)});let j=k!==re.current;re.current=k??null,b(e=>Re(e,g.map(e=>e.id),k,j)),e||(d=setTimeout(v,He))}).catch(r=>{e||(x(r)?(t(!1),D(!1)):C(r)||n(r),d=setTimeout(v,He))})};return v(),()=>{e=!0,g.abort(),d&&clearTimeout(d)}},[e,t,n,r,i,a,d,c,l,u,o,s,f,p,m,h]),(0,v.useEffect)(()=>{y&&k(y)},[y,k]),{repos:g,setRepos:_,repo:y,setRepo:b,hot:S,clockSkewMs:ee,reposLoaded:E,canClone:te}}var We=4;function Ge({ids:e,onReorder:t,draggingRef:n}){let r=(0,v.useRef)(null),i=(0,v.useRef)(null),a=(0,v.useRef)(null),[o,s]=(0,v.useState)(null),[c,l]=(0,v.useState)(null);return{dragging:o,target:c,onStart:(t,a)=>{t.target.closest(`button[data-tab-close]`)||t.button!==0||e.length<2||(r.current=a,i.current={x:t.clientX,y:t.clientY},n.current=!1)},onMove:e=>{let t=r.current,o=i.current;if(t===null||o===null)return;if(!n.current&&e.buttons===0){r.current=null,i.current=null;return}if(!n.current&&Math.hypot(e.clientX-o.x,e.clientY-o.y){let o=r.current,c=a.current;o!==null&&n.current&&c!==null&&t(Be(e,o,c)),r.current=null,i.current=null,a.current=null,n.current=!1,s(null),l(null)}}}function Ke({repos:e,setRepos:t,handle:n,writesRef:r,draggingRef:i,inFlightRef:a,pendingRef:o}){let s=(0,v.useCallback)(()=>{if(a.current||o.current===null)return;let e=o.current;o.current=null,a.current=!0;let i=r.current;O.reorderRepos(e).then(e=>{r.current===i&&t(e)}).catch(n).finally(()=>{a.current=!1,s()})},[n,t]),c=(0,v.useCallback)(e=>{r.current+=1,t(t=>{let n=Ve(t.map(e=>e.id),e),r=new Map(t.map(e=>[e.id,e]));return n.map(e=>r.get(e)).filter(Boolean)}),o.current=e,s()},[s,t]);return{...Ge({ids:e.map(e=>e.id),onReorder:c,draggingRef:i}),writesRef:r,draggingRef:i,inFlightRef:a,pendingRef:o}}function qe({authed:e,setAuthed:t,handle:n,resumeTick:r,adoptAccent:i,adoptSidebarWidth:a,adoptUpperPct:o,accentWrites:s,sidebarWrites:c,upperPctWrites:l,draggingRef:u,upperDraggingRef:d}){let f=(0,v.useRef)(0),p=(0,v.useRef)(!1),m=(0,v.useRef)(!1),h=(0,v.useRef)(null),g=Ue({authed:e,setAuthed:t,handle:n,adoptAccent:i,adoptSidebarWidth:a,adoptUpperPct:o,draggingRef:u,upperDraggingRef:d,accentWrites:s,sidebarWrites:c,upperPctWrites:l,resumeTick:r,orderWrites:f,repoDraggingRef:p,reorderInFlightRef:m,pendingReorderRef:h}),{dragging:_,target:y,onStart:b,onMove:x,onEnd:S}=Ke({repos:g.repos,setRepos:g.setRepos,handle:n,writesRef:f,draggingRef:p,inFlightRef:m,pendingRef:h});return{...g,orderWrites:f,draggingRepo:_,dragOverRepo:y,onRepoDragStart:b,onRepoDragMove:x,onRepoDragEnd:S}}function Je({repo:e,authed:t,resumeTick:n,tab:r,pane:i,setPane:a,handle:o,paneRequestRef:s}){let[c,l]=(0,v.useState)(null),u=(0,v.useRef)(i);u.current=i;let d=(0,v.useRef)(r);return d.current=r,(0,v.useLayoutEffect)(()=>{l(null)},[e,t]),(0,v.useEffect)(()=>{if(!(!e||!t))return ne(e,l)},[e,t,n]),(0,v.useEffect)(()=>{if(!e||!c)return;let t=u.current;if(d.current!==`status`||t.kind!==`diff`)return;let n=t.value.path;if(!c.files.some(e=>e.path===n)){a({kind:`empty`});return}let r=s.current,i=!0,l=()=>{let e=u.current;return i&&r===s.current&&e.kind===`diff`&&e.value.path===n};return O.diff(e,n).then(e=>{l()&&a({kind:`diff`,value:e})}).catch(e=>{l()&&o(e)}),()=>{i=!1}},[c,e,o,u,d,s,a]),{status:c,paneRef:u,tabRef:d}}function Ye({repo:e,handle:t,setPane:n,paneRequestRef:r,setCommitDrillDown:i,setMobileView:a,setPreviewRendered:o}){return{openDiff:(0,v.useCallback)(i=>{if(!e)return;a(`diff`);let o=r.current+=1;O.diff(e,i).then(e=>{o===r.current&&n({kind:`diff`,value:e})}).catch(e=>{o===r.current&&t(e)})},[e,t,n,r,a]),openFile:(0,v.useCallback)(i=>{if(!e)return;a(`diff`),o(!0);let s=r.current+=1;O.file(e,i).then(e=>{s===r.current&&n({kind:`file`,value:e})}).catch(e=>{s===r.current&&t(e)})},[e,t,n,r,a,o]),openCommit:(0,v.useCallback)(i=>{if(!e)return;a(`diff`);let o=r.current+=1;O.commit(e,i).then(e=>{o===r.current&&n({kind:`diff`,value:e})}).catch(e=>{o===r.current&&t(e)})},[e,t,n,r,a]),openCommitFileDiff:(0,v.useCallback)((i,o)=>{if(!e)return;a(`diff`);let s=r.current+=1;O.commitFileDiff(e,i,o).then(e=>{s===r.current&&n({kind:`diff`,value:e})}).catch(e=>{s===r.current&&t(e)})},[e,t,n,r,a]),openCommitFiles:(0,v.useCallback)(async o=>{if(!e)return;a(`diff`);let s=r.current+=1;try{let t=await O.commitFiles(e,o.oid);if(s!==r.current)return;if(i({commit:o,...t}),t.files.length===0){n({kind:`empty`});return}let a=await O.commit(e,o.oid);s===r.current&&n({kind:`diff`,value:a})}catch(e){s===r.current&&t(e)}},[e,t,n,r,i,a])}}function Xe({repo:e,authed:t,tab:n,filter:r,handle:i}){let[a,o]=(0,v.useState)([]),[s,c]=(0,v.useState)(!1),[l,u]=(0,v.useState)(!1),d=(0,v.useRef)(null),f=(0,v.useRef)(!1),p=(0,v.useRef)(0),m=(0,v.useCallback)(()=>{p.current+=1,f.current=!1,o([]),d.current=null,c(!1),u(!1)},[]),[h,g]=(0,v.useState)(null),_=(0,v.useRef)(a);_.current=a;let y=(0,v.useCallback)(async()=>{if(!e||f.current)return;f.current=!0;let t=p.current;try{let n=d.current,r=await O.log(e,n===null?void 0:{from:n,skip:_.current.length});if(t!==p.current)return;o(e=>[...e,...r.commits]),d.current=r.head??null,c(!r.truncated||r.head===void 0)}catch(e){t===p.current&&(i(e),u(!0))}finally{t===p.current&&(f.current=!1)}},[e,i]);(0,v.useEffect)(()=>{!e||!t||n!==`log`||a.length===0&&!s&&!l&&y()},[e,t,n,a.length,s,l,y]);let b=a.filter(e=>e.summary.toLowerCase().includes(r.toLowerCase())),x=r!==``,S=(0,v.useRef)(null);return(0,v.useEffect)(()=>{let e=S.current;if(!e)return;let t=new IntersectionObserver(e=>{e.some(e=>e.isIntersecting)&&y()},{root:e.closest(`ul`),rootMargin:`400px`});return t.observe(e),()=>t.disconnect()},[y,s,l,x,h,n,b.length]),{commits:a,logDone:s,logStalled:l,setLogStalled:u,commitDrillDown:h,setCommitDrillDown:g,resetLog:m,logSentinelRef:S,visibleCommits:b,logPagingPaused:x}}function Ze(){let[e,t]=(0,v.useState)(0);return(0,v.useEffect)(()=>{let e=()=>{document.visibilityState===`visible`&&t(e=>e+1)};return document.addEventListener(`visibilitychange`,e),window.addEventListener(`online`,e),()=>{document.removeEventListener(`visibilitychange`,e),window.removeEventListener(`online`,e)}},[]),e}function Qe(e){let[t,n]=(0,v.useState)({}),r=(0,v.useCallback)(t=>{e!=null&&n(n=>{let r=n[e]??`none`,i=typeof t==`function`?t(r):t;return{...n,[e]:i}})},[e]);return{maximized:e!=null&&t[e]||`none`,setMaximized:r,dropMaximized:(0,v.useCallback)(e=>{n(({[e]:t,...n})=>n)},[])}}function $e({repos:e,setRepos:t,setRepo:n,setPane:r,setTab:i,setPickerOpen:a,dropMaximized:o,handle:s,orderWrites:c}){return{selectOpenedRepo:(0,v.useCallback)(e=>{c.current+=1,t(t=>t.some(t=>t.id===e.id)?t:[...t,e]),n(e.id),r({kind:`empty`}),i(`status`),a(!1)},[t,n,r,i,a,c]),closeRepo:(0,v.useCallback)(async r=>{try{await O.close(r),c.current+=1;let i=e.filter(e=>e.id!==r);t(i),n(e=>e===r?i[0]?.id??null:e),o(r)}catch(e){s(e)}},[e,t,n,o,s,c])}}var et=1e3,tt=3,nt=2e3;function rt(e,t){let[n,r]=(0,v.useState)(!1),i=(0,v.useRef)(!1),a=(0,v.useRef)(!1);(0,v.useEffect)(()=>(a.current=!1,()=>{a.current=!0}),[]);let o=(0,v.useCallback)(async t=>{for(;!a.current;){if(await new Promise(e=>setTimeout(e,et)),a.current)return;let n;try{n=await O.cloneStatus(t)}catch(e){if(x(e)){i.current=!1,a.current||r(!1);return}if(e instanceof b&&e.status===404){if(a.current)return;se.error(`the clone's progress is no longer available`),i.current=!1,r(!1);return}continue}if(a.current)return;if(n.state===`done`){try{let t=await O.open(n.path);if(a.current)return;e(t)}catch(e){if(a.current)return;se.error(e instanceof Error?e.message:`could not open`)}finally{i.current=!1,a.current||r(!1)}return}if(n.state===`failed`){se.error(n.message),i.current=!1,r(!1);return}}},[e]),s=(0,v.useCallback)(async(e=()=>!1)=>{for(let t=0;t0&&await new Promise(e=>setTimeout(e,nt)),i.current||e()||a.current)return;let n;try{({job:n}=await O.runningClone())}catch(e){if(x(e))return;continue}if(n===null||e()||a.current||i.current)return;i.current=!0,r(!0),o(n);return}},[o]);return(0,v.useEffect)(()=>{if(!t)return;let e=!1;return s(()=>e),()=>{e=!0}},[t,s]),{busy:n,start:(0,v.useCallback)(async(e,t)=>{if(!(!t.trim()||i.current)){i.current=!0,r(!0);try{let{job:n}=await O.clone(e,t.trim());await o(n)}catch(e){if(i.current=!1,a.current)return;let t=e instanceof b&&e.status>=400;se.error(t?e.message:`could not confirm the clone started — check this folder before retrying`),r(!1),s()}}},[o,s])}}var it=`data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='utf-8'?%3e%3csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='12%2057%201150%201150'%20role='img'%20aria-label='Black%20crow'%3e%3ctitle%3eBlack%20crow%3c/title%3e%3cdesc%3eMonochrome%20black%20crow%20silhouette%20on%20a%20transparent%20background,%20framed%20so%20the%20bird%20sits%20centred%20for%20use%20as%20an%20inline%20mark%20on%20a%20square%20tile.%3c/desc%3e%3cg%20fill-rule='evenodd'%20clip-rule='evenodd'%3e%3cpath%20fill='%23000000'%20d='M%20882%20147%20L%20859%20136%20L%20844%20131%20L%20831%20129%20L%20830%20128%20L%20815%20127%20L%20814%20126%20L%20796%20126%20L%20795%20127%20L%20786%20127%20L%20785%20128%20L%20775%20129%20L%20752%20136%20L%20732%20146%20L%20713%20160%20L%20701%20172%20L%20684%20172%20L%20683%20173%20L%20673%20173%20L%20672%20174%20L%20650%20176%20L%20649%20177%20L%20627%20181%20L%20602%20190%20L%20589%20197%20L%20579%20204%20L%20562%20221%20L%20562%20223%20L%20565%20223%20L%20578%20228%20L%20581%20228%20L%20612%20238%20L%20672%20252%20L%20684%20258%20L%20698%20271%20L%20702%20278%20L%20705%20288%20L%20705%20294%20L%20703%20301%20L%20699%20308%20L%20688%20318%20L%20630%20347%20L%20593%20372%20L%20561%20399%20L%20544%20416%20L%20522%20441%20L%20492%20481%20L%20461%20531%20L%20438%20576%20L%20431%20594%20L%20425%20602%20L%20405%20635%20L%20387%20668%20L%20385%20676%20L%20390%20679%20L%20368%20705%20L%20330%20755%20L%20306%20790%20L%20296%20808%20L%20289%20818%20L%20280%20838%20L%20280%20843%20L%20283%20845%20L%20292%20843%20L%20297%20840%20L%20299%20840%20L%20321%20828%20L%20322%20830%20L%20311%20844%20L%20288%20878%20L%20287%20881%20L%20259%20924%20L%20235%20965%20L%20205%201023%20L%20205%201025%20L%20197%201042%20L%20191%201061%20L%20191%201071%20L%20192%201072%20L%20198%201071%20L%20220%201056%20L%20242%201038%20L%20300%20986%20L%20302%20987%20L%20265%201040%20L%20264%201043%20L%20246%201070%20L%20235%201090%20L%20227%201112%20L%20227%201123%20L%20229%201128%20L%20234%201133%20L%20239%201135%20L%20255%201135%20L%20274%201129%20L%20279%201134%20L%20286%201137%20L%20290%201137%20L%20291%201138%20L%20310%201138%20L%20311%201137%20L%20317%201137%20L%20318%201136%20L%20326%201135%20L%20344%201129%20L%20369%201116%20L%20395%201097%20L%20420%201073%20L%20445%201042%20L%20457%201024%20L%20461%201016%20L%20464%201013%20L%20468%201011%20L%20489%20994%20L%20595%20901%20L%20601%20906%20L%20606%20913%20L%20614%20921%20L%20637%20949%20L%20639%20953%20L%20639%20956%20L%20636%20960%20L%20634%20961%20L%20619%20962%20L%20613%20965%20L%20605%20974%20L%20602%20982%20L%20602%20994%20L%20605%201001%20L%20608%201004%20L%20609%201004%20L%20609%20999%20L%20612%20992%20L%20616%20988%20L%20620%20986%20L%20627%20986%20L%20635%20983%20L%20645%20983%20L%20646%20982%20L%20655%20982%20L%20668%20986%20L%20676%20990%20L%20682%20996%20L%20685%201003%20L%20688%201006%20L%20696%201009%20L%20697%201012%20L%20697%201024%20L%20693%201033%20L%20693%201035%20L%20695%201035%20L%20700%201032%20L%20707%201025%20L%20710%201020%20L%20713%201010%20L%20713%201003%20L%20711%20998%20L%20711%20994%20L%20712%20993%20L%20719%201003%20L%20723%201005%20L%20727%201005%20L%20730%201011%20L%20730%201022%20L%20727%201031%20L%20728%201033%20L%20740%201021%20L%20743%201014%20L%20744%201003%20L%20747%20999%20L%20749%20992%20L%20748%20977%20L%20744%20968%20L%20740%20963%20L%20741%20962%20L%20755%20961%20L%20768%20964%20L%20777%20969%20L%20783%20975%20L%20786%20981%20L%20789%20984%20L%20795%20987%20L%20799%20987%20L%20801%20991%20L%20801%20997%20L%20802%20998%20L%20799%201013%20L%20802%201012%20L%20808%201007%20L%20813%201000%20L%20816%20991%20L%20816%20981%20L%20814%20976%20L%20814%20968%20L%20815%20967%20L%20819%20970%20L%20823%20970%20L%20826%20973%20L%20829%20980%20L%20830%20993%20L%20834%20990%20L%20838%20979%20L%20838%20968%20L%20832%20951%20L%20822%20940%20L%20815%20936%20L%20803%20933%20L%20776%20935%20L%20763%20931%20L%20753%20922%20L%20731%20898%20L%20703%20865%20L%20703%20863%20L%20710%20853%20L%20711%20855%20L%20707%20862%20L%20709%20862%20L%20718%20857%20L%20754%20832%20L%20793%20799%20L%20818%20774%20L%20849%20737%20L%20850%20741%20L%20845%20755%20L%20847%20755%20L%20861%20743%20L%20881%20721%20L%20906%20686%20L%20918%20665%20L%20933%20635%20L%20951%20590%20L%20971%20525%20L%20971%20521%20L%20974%20512%20L%20974%20508%20L%20978%20493%20L%20979%20482%20L%20980%20481%20L%20981%20466%20L%20982%20465%20L%20983%20441%20L%20982%20440%20L%20982%20428%20L%20981%20427%20L%20980%20414%20L%20978%20409%20L%20976%20397%20L%20970%20381%20L%20971%20379%20L%20974%20383%20L%20976%20381%20L%20977%20334%20L%20976%20333%20L%20976%20322%20L%20975%20321%20L%20974%20307%20L%20973%20306%20L%20973%20301%20L%20972%20300%20L%20969%20280%20L%20958%20243%20L%20949%20224%20L%20949%20222%20L%20939%20204%20L%20922%20181%20L%20903%20162%20Z%20M%20625%20888%20L%20656%20874%20L%20658%20874%20L%20665%20870%20L%20725%20930%20L%20728%20934%20L%20728%20940%20L%20723%20943%20L%20714%20944%20L%20707%20950%20L%20683%20951%20L%20673%20947%20L%20659%20932%20Z%20M%20787%20182%20L%20792%20182%20L%20796%20187%20L%20795%20192%20L%20791%20195%20L%20788%20195%20L%20783%20191%20L%20783%20186%20Z'/%3e%3cpath%20fill='%23000000'%20d='M%20895%20156%20L%20863%20138%20L%20838%20130%20L%20819%20127%20L%20789%20127%20L%20777%20129%20L%20753%20136%20L%20736%20144%20L%20717%20157%20L%20702%20172%20L%20652%20176%20L%20628%20181%20L%20607%20188%20L%20585%20200%20L%20562%20222%20L%20613%20238%20L%20670%20251%20L%20683%20257%20L%20697%20269%20L%20705%20286%20L%20705%20296%20L%20703%20302%20L%20698%20310%20L%20687%20319%20L%20631%20347%20L%20591%20374%20L%20568%20393%20L%20538%20423%20L%20520%20444%20L%20481%20498%20L%20457%20539%20L%20437%20579%20L%20432%20593%20L%20413%20622%20L%20386%20671%20L%20386%20677%20L%20390%20677%20L%20391%20679%20L%20374%20698%20L%20334%20750%20L%20307%20789%20L%20290%20817%20L%20280%20839%20L%20281%20844%20L%20291%20843%20L%20323%20826%20L%20325%20827%20L%20289%20877%20L%20237%20962%20L%20209%201015%20L%20198%201040%20L%20191%201063%20L%20191%201070%20L%20194%201072%20L%20213%201061%20L%20259%201023%20L%20302%20984%20L%20303%20985%20L%20262%201045%20L%20245%201072%20L%20233%201095%20L%20227%201114%20L%20228%201126%20L%20233%201132%20L%20242%201135%20L%20252%201135%20L%20274%201128%20L%20278%201133%20L%20288%201137%20L%20313%201137%20L%20343%201129%20L%20373%201113%20L%20398%201094%20L%20424%201068%20L%20441%201047%20L%20465%201012%20L%20520%20967%20L%20595%20900%20L%20611%20917%20L%20639%20952%20L%20639%20957%20L%20637%20960%20L%20632%20962%20L%20618%20963%20L%20611%20967%20L%20606%20973%20L%20602%20983%20L%20602%20992%20L%20608%201004%20L%20611%20993%20L%20619%20986%20L%20626%20986%20L%20643%20982%20L%20656%20982%20L%20675%20989%20L%20683%20997%20L%20688%201006%20L%20696%201009%20L%20697%201025%20L%20693%201034%20L%20694%201035%20L%20701%201031%20L%20710%201019%20L%20712%201013%20L%20712%20993%20L%20720%201003%20L%20727%201005%20L%20730%201009%20L%20730%201025%20L%20727%201032%20L%20732%201030%20L%20739%201022%20L%20743%201013%20L%20743%201004%20L%20749%20991%20L%20748%20978%20L%20740%20964%20L%20743%20961%20L%20757%20961%20L%20769%20964%20L%20781%20972%20L%20791%20985%20L%20798%20986%20L%20802%20994%20L%20802%201003%20L%20799%201013%20L%20810%201004%20L%20815%20994%20L%20816%20983%20L%20814%20977%20L%20814%20965%20L%20818%20969%20L%20825%20971%20L%20830%20983%20L%20830%20993%20L%20833%20991%20L%20837%20982%20L%20838%20969%20L%20834%20956%20L%20825%20943%20L%20820%20939%20L%20807%20934%20L%20774%20935%20L%20762%20931%20L%20736%20904%20L%20702%20864%20L%20715%20845%20L%20716%20847%20L%20707%20862%20L%20719%20856%20L%20744%20839%20L%20786%20805%20L%20824%20767%20L%20851%20733%20L%20852%20735%20L%20846%20755%20L%20853%20750%20L%20883%20718%20L%20906%20685%20L%20927%20647%20L%20951%20589%20L%20968%20535%20L%20976%20502%20L%20982%20461%20L%20981%20422%20L%20976%20398%20L%20968%20378%20L%20969%20376%20L%20975%20383%20L%20977%20341%20L%20970%20286%20L%20958%20244%20L%20945%20215%20L%20925%20185%20L%20906%20165%20Z%20M%20625%20888%20L%20665%20870%20L%20728%20933%20L%20729%20940%20L%20726%20943%20L%20715%20944%20L%20708%20950%20L%20692%20952%20L%20678%20950%20L%20672%20947%20L%20662%20936%20Z%20M%20785%20183%20L%20792%20182%20L%20796%20186%20L%20796%20191%20L%20791%20195%20L%20785%20194%20L%20783%20191%20L%20783%20186%20Z'/%3e%3cpath%20fill='%23000000'%20d='M%20896%20157%20L%20869%20141%20L%20837%20130%20L%20817%20127%20L%20792%20127%20L%20778%20129%20L%20751%20137%20L%20733%20146%20L%20715%20159%20L%20702%20172%20L%20653%20176%20L%20610%20187%20L%20581%20203%20L%20562%20222%20L%20625%20241%20L%20671%20251%20L%20683%20257%20L%20698%20270%20L%20705%20285%20L%20704%20300%20L%20699%20309%20L%20689%20318%20L%20628%20349%20L%20581%20382%20L%20540%20421%20L%20517%20448%20L%20478%20503%20L%20456%20541%20L%20439%20575%20L%20432%20593%20L%20412%20624%20L%20388%20667%20L%20386%20676%20L%20390%20677%20L%20391%20679%20L%20363%20712%20L%20332%20753%20L%20289%20819%20L%20281%20836%20L%20281%20844%20L%20293%20842%20L%20323%20826%20L%20325%20827%20L%20284%20885%20L%20236%20964%20L%20205%201024%20L%20197%201043%20L%20191%201064%20L%20191%201070%20L%20197%201071%20L%20210%201063%20L%20253%201028%20L%20303%20983%20L%20304%20984%20L%20259%201050%20L%20235%201091%20L%20227%201115%20L%20228%201125%20L%20232%201131%20L%20238%201134%20L%20249%201135%20L%20261%201133%20L%20274%201128%20L%20278%201133%20L%20289%201137%20L%20312%201137%20L%20342%201129%20L%20368%201116%20L%20399%201093%20L%20423%201069%20L%20443%201044%20L%20463%201013%20L%20493%20990%20L%20595%20900%20L%20612%20918%20L%20639%20952%20L%20639%20957%20L%20636%20961%20L%20616%20964%20L%20606%20973%20L%20602%20984%20L%20602%20991%20L%20608%201004%20L%20611%20993%20L%20621%20985%20L%20625%20986%20L%20639%20982%20L%20657%20982%20L%20677%20990%20L%20685%201002%20L%20690%201007%20L%20697%201010%20L%20698%201021%20L%20693%201034%20L%20694%201035%20L%20705%201027%20L%20710%201018%20L%20712%201011%20L%20712%201001%20L%20710%20994%20L%20712%20993%20L%20720%201003%20L%20729%201006%20L%20731%201020%20L%20728%201032%20L%20738%201023%20L%20743%201012%20L%20743%201003%20L%20748%20994%20L%20747%20976%20L%20740%20964%20L%20743%20961%20L%20759%20961%20L%20767%20963%20L%20780%20971%20L%20790%20984%20L%20799%20986%20L%20802%20992%20L%20802%201004%20L%20799%201012%20L%20803%201011%20L%20810%201004%20L%20815%20994%20L%20814%20965%20L%20818%20969%20L%20823%20969%20L%20830%20982%20L%20830%20992%20L%20832%20992%20L%20837%20982%20L%20837%20965%20L%20831%20950%20L%20821%20940%20L%20806%20934%20L%20772%20935%20L%20762%20931%20L%20733%20901%20L%20702%20863%20L%20717%20842%20L%20718%20844%20L%20707%20862%20L%20738%20843%20L%20780%20810%20L%20822%20769%20L%20851%20733%20L%20852%20735%20L%20846%20755%20L%20855%20748%20L%20882%20719%20L%20904%20688%20L%20930%20640%20L%20950%20591%20L%20967%20538%20L%20978%20490%20L%20982%20459%20L%20982%20434%20L%20976%20399%20L%20967%20376%20L%20969%20375%20L%20975%20383%20L%20976%20329%20L%20971%20293%20L%20960%20250%20L%20942%20210%20L%20923%20183%20Z%20M%20625%20888%20L%20665%20870%20L%20729%20934%20L%20729%20940%20L%20726%20943%20L%20716%20944%20L%20708%20950%20L%20695%20952%20L%20677%20950%20L%20666%20941%20Z%20M%20786%20182%20L%20790%20181%20L%20794%20183%20L%20797%20188%20L%20792%20195%20L%20787%20195%20L%20782%20190%20L%20782%20187%20Z'/%3e%3cpath%20fill='%23000000'%20d='M%20893%20155%20L%20873%20143%20L%20841%20131%20L%20816%20127%20L%20794%20127%20L%20779%20129%20L%20754%20136%20L%20735%20145%20L%20715%20159%20L%20702%20172%20L%20648%20177%20L%20629%20181%20L%20608%20188%20L%20584%20201%20L%20562%20222%20L%20614%20238%20L%20671%20251%20L%20685%20258%20L%20698%20270%20L%20704%20281%20L%20706%20292%20L%20703%20303%20L%20699%20309%20L%20686%20320%20L%20628%20349%20L%20590%20375%20L%20565%20396%20L%20543%20418%20L%20518%20447%20L%20490%20485%20L%20460%20534%20L%20441%20571%20L%20431%20595%20L%20405%20636%20L%20386%20672%20L%20386%20676%20L%20390%20677%20L%20391%20679%20L%20371%20702%20L%20335%20749%20L%20291%20816%20L%20280%20840%20L%20282%20844%20L%20298%20840%20L%20324%20825%20L%20326%20826%20L%20294%20870%20L%20235%20966%20L%20206%201022%20L%20198%201041%20L%20191%201065%20L%20191%201070%20L%20196%201071%20L%20214%201060%20L%20254%201027%20L%20303%20983%20L%20304%20984%20L%20256%201055%20L%20232%201098%20L%20227%201115%20L%20228%201125%20L%20232%201131%20L%20239%201134%20L%20248%201135%20L%20264%201132%20L%20274%201128%20L%20280%201134%20L%20289%201137%20L%20311%201137%20L%20339%201130%20L%20371%201114%20L%20401%201091%20L%20422%201070%20L%20440%201048%20L%20463%201013%20L%20487%20995%20L%20595%20900%20L%20613%20919%20L%20639%20951%20L%20639%20958%20L%20636%20961%20L%20619%20963%20L%20613%20966%20L%20605%20975%20L%20602%20991%20L%20604%20998%20L%20608%201003%20L%20611%20993%20L%20620%20985%20L%20624%20986%20L%20632%20983%20L%20647%20981%20L%20658%20982%20L%20677%20990%20L%20689%201006%20L%20697%201009%20L%20698%201021%20L%20694%201035%20L%20703%201029%20L%20710%201018%20L%20712%201010%20L%20712%201002%20L%20710%20997%20L%20711%20992%20L%20720%201003%20L%20728%201005%20L%20730%201008%20L%20731%201021%20L%20728%201032%20L%20738%201023%20L%20742%201014%20L%20743%201003%20L%20748%20994%20L%20748%20980%20L%20742%20966%20L%20739%20963%20L%20741%20961%20L%20753%20960%20L%20770%20964%20L%20780%20971%20L%20790%20984%20L%20799%20986%20L%20802%20992%20L%20802%201005%20L%20799%201012%20L%20804%201010%20L%20809%201005%20L%20815%20993%20L%20813%20964%20L%20817%20968%20L%20823%20969%20L%20826%20972%20L%20830%20982%20L%20831%20992%20L%20834%20989%20L%20838%20975%20L%20837%20966%20L%20831%20950%20L%20821%20940%20L%20804%20934%20L%20779%20936%20L%20763%20932%20L%20731%20899%20L%20702%20865%20L%20702%20863%20L%20717%20842%20L%20718%20844%20L%20708%20862%20L%20749%20835%20L%20781%20809%20L%20820%20771%20L%20851%20733%20L%20852%20736%20L%20846%20755%20L%20857%20746%20L%20881%20720%20L%20907%20683%20L%20928%20644%20L%20945%20604%20L%20967%20537%20L%20978%20489%20L%20982%20455%20L%20982%20437%20L%20979%20413%20L%20975%20396%20L%20967%20377%20L%20968%20374%20L%20975%20382%20L%20976%20332%20L%20972%20299%20L%20962%20257%20L%20954%20235%20L%20943%20212%20L%20923%20183%20Z%20M%20624%20888%20L%20666%20870%20L%20729%20934%20L%20729%20940%20L%20726%20943%20L%20716%20944%20L%20706%20951%20L%20684%20952%20L%20677%20950%20L%20665%20940%20Z%20M%20701%20220%20L%20710%20219%20L%20717%20221%20L%20704%20223%20L%20704%20221%20Z%20M%20666%20217%20L%20679%20216%20L%20689%20218%20L%20685%20220%20L%20675%20220%20Z%20M%20658%20210%20L%20661%20208%20L%20686%20205%20L%20706%20206%20L%20725%20209%20L%20733%20217%20L%20741%20220%20L%20738%20221%20L%20696%20214%20L%20661%20212%20Z%20M%20788%20181%20L%20793%20182%20L%20797%20188%20L%20792%20195%20L%20786%20195%20L%20782%20190%20L%20783%20185%20Z'/%3e%3cpath%20fill='%23000000'%20d='M%20893%20155%20L%20864%20139%20L%20836%20130%20L%20813%20127%20L%20796%20127%20L%20774%20130%20L%20749%20138%20L%20735%20145%20L%20720%20155%20L%20702%20172%20L%20687%20173%20L%20700%20174%20L%20694%20181%20L%20674%20184%20L%20670%20180%20L%20665%20183%20L%20657%20184%20L%20639%20196%20L%20641%20198%20L%20647%20198%20L%20648%20194%20L%20654%20193%20L%20686%20204%20L%20724%20208%20L%20740%20213%20L%20751%20222%20L%20750%20223%20L%20693%20214%20L%20671%20212%20L%20639%20212%20L%20636%20211%20L%20634%20207%20L%20629%20207%20L%20626%20210%20L%20617%20210%20L%20614%20208%20L%20604%20207%20L%20594%20213%20L%20582%20216%20L%20580%20214%20L%20581%20210%20L%20576%20208%20L%20586%20200%20L%20565%20218%20L%20563%20222%20L%20618%20239%20L%20671%20251%20L%20682%20256%20L%20689%20261%20L%20702%20276%20L%20706%20288%20L%20704%20301%20L%20700%20308%20L%20683%20322%20L%20630%20348%20L%20585%20379%20L%20563%20398%20L%20523%20441%20L%20497%20475%20L%20459%20536%20L%20443%20567%20L%20431%20595%20L%20411%20626%20L%20387%20670%20L%20386%20676%20L%20390%20677%20L%20391%20679%20L%20368%20706%20L%20333%20752%20L%20290%20818%20L%20281%20837%20L%20282%20844%20L%20300%20839%20L%20324%20825%20L%20326%20826%20L%20285%20884%20L%20242%20954%20L%20207%201020%20L%20192%201060%20L%20192%201071%20L%20196%201071%20L%20211%201062%20L%20240%201039%20L%20303%20983%20L%20305%20984%20L%20277%201023%20L%20242%201078%20L%20228%201110%20L%20227%201119%20L%20231%201130%20L%20239%201134%20L%20255%201134%20L%20274%201128%20L%20280%201134%20L%20285%201136%20L%20310%201137%20L%20339%201130%20L%20374%201112%20L%20394%201097%20L%20421%201071%20L%20445%201041%20L%20463%201013%20L%20488%20994%20L%20595%20900%20L%20614%20920%20L%20639%20951%20L%20639%20958%20L%20636%20961%20L%20619%20963%20L%20613%20966%20L%20607%20972%20L%20603%20980%20L%20602%20989%20L%20603%20995%20L%20608%201003%20L%20610%20994%20L%20620%20985%20L%20623%20986%20L%20631%20983%20L%20647%20981%20L%20665%20984%20L%20676%20989%20L%20689%201006%20L%20695%201007%20L%20697%201009%20L%20698%201023%20L%20694%201035%20L%20699%201032%20L%20709%201020%20L%20712%201010%20L%20710%20997%20L%20711%20992%20L%20719%201002%20L%20725%201005%20L%20727%201004%20L%20730%201008%20L%20731%201022%20L%20728%201032%20L%20739%201021%20L%20743%201010%20L%20743%201003%20L%20748%20993%20L%20748%20981%20L%20746%20974%20L%20739%20963%20L%20741%20961%20L%20755%20960%20L%20770%20964%20L%20779%20970%20L%20789%20983%20L%20799%20986%20L%20802%20992%20L%20802%201005%20L%20799%201012%20L%20808%201006%20L%20815%20993%20L%20813%20964%20L%20817%20968%20L%20825%20970%20L%20829%20978%20L%20831%20992%20L%20837%20981%20L%20837%20966%20L%20832%20952%20L%20822%20941%20L%20813%20936%20L%20803%20934%20L%20776%20936%20L%20763%20932%20L%20723%20890%20L%20702%20865%20L%20702%20863%20L%20717%20841%20L%20719%20842%20L%20708%20862%20L%20742%20840%20L%20782%20808%20L%20818%20773%20L%20851%20733%20L%20852%20736%20L%20846%20754%20L%20850%20752%20L%20881%20720%20L%20905%20686%20L%20932%20635%20L%20950%20590%20L%20969%20529%20L%20977%20494%20L%20982%20454%20L%20980%20419%20L%20975%20396%20L%20967%20377%20L%20968%20374%20L%20975%20382%20L%20976%20334%20L%20969%20284%20L%20959%20248%20L%20941%20209%20L%20921%20181%20Z%20M%20624%20888%20L%20666%20870%20L%20729%20934%20L%20729%20941%20L%20724%20944%20L%20716%20944%20L%20707%20951%20L%20683%20952%20L%20673%20948%20L%20659%20933%20Z%20M%20563%20618%20L%20569%20614%20L%20579%20621%20L%20576%20626%20L%20572%20627%20L%20568%20623%20L%20565%20623%20Z%20M%20575%20603%20L%20578%20603%20L%20588%20613%20L%20588%20619%20L%20585%20621%20L%20582%20620%20L%20575%20613%20L%20576%20612%20L%20573%20605%20Z%20M%20748%20398%20L%20752%20412%20L%20752%20432%20L%20748%20444%20L%20732%20470%20L%20716%20484%20L%20698%20493%20L%20685%20494%20L%20682%20491%20L%20707%20445%20L%20685%20473%20L%20667%20492%20L%20647%20508%20L%20632%20517%20L%20619%20519%20L%20615%20517%20L%20615%20513%20L%20643%20470%20L%20603%20514%20L%20589%20526%20L%20569%20538%20L%20558%20540%20L%20554%20539%20L%20552%20535%20L%20577%20498%20L%20549%20528%20L%20526%20546%20L%20508%20554%20L%20498%20555%20L%20495%20552%20L%20501%20541%20L%20482%20555%20L%20471%20558%20L%20464%20558%20L%20461%20560%20L%20460%20559%20L%20461%20555%20L%20469%20547%20L%20473%20537%20L%20491%20512%20L%20511%20497%20L%20582%20434%20L%20617%20408%20L%20632%20399%20L%20659%20386%20L%20677%20380%20L%20695%20377%20L%20712%20377%20L%20725%20380%20L%20739%20388%20Z%20M%20625%20216%20L%20683%20216%20L%20737%20222%20L%20742%20224%20L%20733%20226%20L%20721%20225%20L%20713%20230%20L%20711%20228%20L%20706%20228%20L%20694%20237%20L%20690%20233%20L%20690%20225%20L%20679%20228%20L%20674%20232%20L%20668%20232%20L%20659%20229%20L%20649%20222%20L%20632%20219%20L%20632%20217%20Z%20M%20788%20181%20L%20793%20182%20L%20797%20187%20L%20795%20193%20L%20792%20195%20L%20786%20195%20L%20782%20190%20L%20783%20185%20Z'/%3e%3cpath%20fill='%23000000'%20d='M%20607%20972%20L%20603%20981%20L%20603%20995%20L%20608%201003%20L%20610%20994%20L%20616%20987%20L%20620%20985%20L%20627%20985%20L%20635%20982%20L%20655%20981%20L%20676%20989%20L%20683%20996%20L%20689%201006%20L%20695%201007%20L%20697%201009%20L%20698%201024%20L%20694%201034%20L%20699%201032%20L%20706%201025%20L%20712%201010%20L%20710%20994%20L%20709%20997%20L%20706%20994%20L%20700%20994%20L%20694%20997%20L%20672%20976%20L%20658%20974%20L%20647%20967%20L%20642%20974%20L%20638%20974%20L%20636%20972%20L%20636%20961%20L%20629%20963%20L%20631%20972%20L%20625%20977%20L%20620%20977%20L%20613%20967%20L%20614%20966%20Z%20M%20686%20966%20L%20704%20980%20L%20719%201002%20L%20727%201004%20L%20730%201007%20L%20731%201023%20L%20728%201032%20L%20731%201030%20L%20741%201017%20L%20743%201003%20L%20748%20992%20L%20748%20981%20L%20742%20967%20L%20740%20965%20L%20742%20968%20L%20735%20972%20L%20726%20965%20L%20705%20960%20L%20724%20968%20L%20740%20983%20L%20742%20987%20L%20740%20991%20L%20732%20991%20L%20728%20996%20L%20726%20996%20L%20721%20992%20L%20713%20979%20Z%20M%20784%20943%20L%20784%20946%20L%20798%20950%20L%20819%20969%20L%20825%20970%20L%20830%20981%20L%20831%20992%20L%20837%20981%20L%20837%20966%20L%20829%20948%20L%20825%20944%20L%20830%20950%20L%20821%20957%20L%20813%20951%20L%20794%20942%20Z%20M%20659%20933%20L%20662%20937%20L%20658%20942%20L%20658%20945%20L%20663%20947%20L%20666%20945%20L%20667%20946%20L%20668%20944%20L%20670%20946%20Z%20M%20643%20913%20L%20647%20918%20L%20644%20921%20L%20642%20928%20L%20629%20938%20L%20627%20936%20L%20636%20947%20L%20634%20945%20L%20641%20933%20L%20651%20923%20L%20657%20930%20Z%20M%20936%20310%20L%20934%20309%20L%20945%20338%20L%20949%20368%20L%20949%20381%20L%20947%20383%20L%20935%20367%20L%20914%20348%20L%20929%20377%20L%20934%20397%20L%20936%20414%20L%20935%20440%20L%20933%20442%20L%20930%20440%20L%20924%20419%20L%20912%20395%20L%20909%20393%20L%20910%20410%20L%20907%20433%20L%20899%20458%20L%20895%20462%20L%20893%20460%20L%20892%20445%20L%20888%20427%20L%20873%20390%20L%20873%20416%20L%20871%20430%20L%20866%20448%20L%20862%20454%20L%20859%20452%20L%20853%20433%20L%20842%20410%20L%20823%20382%20L%20807%20365%20L%20805%20366%20L%20811%20394%20L%20812%20416%20L%20810%20425%20L%20806%20428%20L%20801%20423%20L%20790%20403%20L%20772%20383%20L%20759%20372%20L%20733%20357%20L%20724%20355%20L%20705%20346%20L%20693%20344%20L%20669%20344%20L%20646%20349%20L%20636%20353%20L%20662%20349%20L%20677%20349%20L%20699%20353%20L%20708%20356%20L%20725%20366%20L%20736%20376%20L%20745%20389%20L%20753%20415%20L%20752%20442%20L%20743%20476%20L%20723%20520%20L%20693%20569%20L%20646%20631%20L%20605%20676%20L%20568%20709%20L%20565%20710%20L%20562%20706%20L%20562%20692%20L%20567%20665%20L%20578%20636%20L%20602%20619%20L%20622%20602%20L%20645%20578%20L%20670%20546%20L%20630%20588%20L%20603%20610%20L%20574%20628%20L%20560%20633%20L%20556%20633%20L%20555%20631%20L%20563%20617%20L%20596%20572%20L%20647%20509%20L%20636%20515%20L%20562%20609%20L%20541%20630%20L%20520%20645%20L%20502%20653%20L%20490%20653%20L%20536%20590%20L%20513%20617%20L%20478%20651%20L%20454%20665%20L%20440%20669%20L%20436%20667%20L%20488%20596%20L%20459%20630%20L%20436%20653%20L%20411%20671%20L%20392%20678%20L%20372%20701%20L%20319%20772%20L%20289%20820%20L%20281%20838%20L%20282%20844%20L%20295%20841%20L%20324%20825%20L%20326%20826%20L%20289%20878%20L%20242%20954%20L%20209%201016%20L%20193%201056%20L%20191%201069%20L%20192%201071%20L%20196%201071%20L%20219%201056%20L%20303%20983%20L%20305%20984%20L%20255%201057%20L%20234%201094%20L%20228%201111%20L%20228%201124%20L%20234%201132%20L%20240%201134%20L%20254%201134%20L%20275%201128%20L%20279%201133%20L%20291%201137%20L%20316%201136%20L%20344%201128%20L%20364%201118%20L%20389%201101%20L%20403%201089%20L%20427%201064%20L%20447%201038%20L%20463%201013%20L%20489%20993%20L%20594%20901%20L%20595%20899%20L%20593%20894%20L%20588%20891%20L%20567%20868%20L%20569%20865%20L%20576%20867%20L%20586%20873%20L%20589%20867%20L%20607%20867%20L%20619%20880%20L%20622%20887%20L%20626%20891%20L%20624%20889%20L%20626%20886%20L%20666%20870%20L%20705%20909%20L%20710%20905%20L%20713%20905%20L%20716%20908%20L%20716%20911%20L%20712%20916%20L%20728%20932%20L%20730%20939%20L%20727%20943%20L%20717%20944%20L%20710%20949%20L%20712%20948%20L%20725%20951%20L%20739%20963%20L%20740%20961%20L%20756%20960%20L%20774%20966%20L%20784%20975%20L%20789%20983%20L%20800%20987%20L%20802%20991%20L%20802%201005%20L%20799%201012%20L%20806%201008%20L%20811%201002%20L%20815%20993%20L%20813%20968%20L%20812%20973%20L%20807%20971%20L%20796%20976%20L%20791%20973%20L%20788%20967%20L%20779%20958%20L%20767%20952%20L%20762%20941%20L%20756%20940%20L%20751%20932%20L%20749%20931%20L%20743%20936%20L%20738%20936%20L%20736%20933%20L%20741%20925%20L%20739%20915%20L%20742%20912%20L%20745%20914%20L%20702%20865%20L%20702%20863%20L%20717%20841%20L%20719%20842%20L%20709%20861%20L%20746%20837%20L%20783%20807%20L%20826%20764%20L%20851%20732%20L%20853%20734%20L%20846%20754%20L%20851%20751%20L%20874%20728%20L%20890%20708%20L%20908%20681%20L%20938%20621%20L%20960%20560%20L%20971%20521%20L%20979%20481%20L%20982%20444%20L%20977%20405%20L%20970%20382%20L%20966%20375%20L%20968%20374%20L%20974%20381%20L%20975%20368%20L%20974%20372%20L%20968%20364%20L%20966%20354%20L%20957%20337%20Z%20M%20247%201077%20L%20251%201085%20L%20246%201088%20L%20245%201091%20L%20240%201092%20L%20239%201085%20L%20241%201081%20Z%20M%20370%20935%20L%20371%20937%20L%20326%20997%20L%20301%201034%20L%20289%201044%20L%20286%201042%20L%20283%201045%20L%20277%201045%20L%20273%201043%20L%20264%201052%20L%20261%201049%20L%20270%201037%20L%20271%201038%20L%20281%201027%20L%20293%201010%20L%20326%20971%20Z%20M%20726%20921%20L%20728%20921%20L%20731%20926%20L%20729%20933%20L%20722%20926%20Z%20M%20729%20897%20L%20734%20902%20L%20732%20906%20L%20726%20902%20L%20726%20899%20Z%20M%20715%20882%20L%20720%20886%20L%20718%20890%20L%20711%20887%20Z%20M%20605%20762%20L%20608%20761%20L%20607%20760%20L%20609%20757%20L%20610%20759%20L%20617%20761%20L%20620%20764%20L%20611%20770%20L%20607%20770%20L%20606%20765%20L%20608%20765%20Z%20M%20611%20754%20L%20617%20750%20L%20620%20751%20L%20621%20749%20L%20625%20749%20L%20628%20752%20L%20628%20756%20L%20623%20761%20Z%20M%20549%20710%20L%20551%20713%20L%20551%20722%20L%20506%20774%20L%20403%20878%20L%20309%20963%20L%20307%20961%20L%20313%20953%20L%20322%20945%20L%20340%20922%20L%20375%20885%20Z%20M%20695%20704%20L%20696%20708%20L%20698%20709%20L%20695%20710%20L%20695%20713%20L%20692%20715%20L%20688%20710%20Z%20M%20704%20694%20L%20706%20695%20L%20706%20698%20L%20710%20698%20L%20713%20703%20L%20708%20708%20L%20707%20714%20L%20705%20716%20L%20701%20716%20L%20697%20708%20L%20699%20706%20L%20697%20705%20L%20701%20701%20L%20704%20705%20L%20706%20704%20L%20706%20700%20L%20702%20697%20Z%20M%20552%20678%20L%20554%20681%20L%20552%20701%20L%20465%20787%20L%20361%20883%20L%20316%20927%20L%20253%20993%20L%20211%201042%20L%20211%201036%20L%20222%201011%20L%20261%20942%20L%20292%20898%20L%20338%20843%20L%20374%20807%20L%20387%20807%20L%20403%20801%20L%20419%20792%20L%20456%20766%20L%20500%20728%20Z%20M%20704%20654%20L%20706%20658%20L%20711%20658%20L%20714%20661%20L%20715%20673%20L%20713%20676%20L%20705%20676%20L%20702%20674%20L%20704%20677%20L%20704%20681%20L%20702%20682%20L%20704%20685%20L%20700%20686%20L%20694%20678%20L%20691%20678%20L%20688%20674%20L%20692%20670%20L%20692%20664%20L%20694%20661%20Z%20M%20565%20642%20L%20558%20663%20L%20545%20677%20L%20494%20726%20L%20443%20767%20L%20401%20792%20L%20392%20795%20L%20387%20794%20L%20405%20766%20L%20455%20707%20L%20388%20767%20L%20355%20794%20L%20304%20827%20L%20299%20826%20L%20302%20817%20L%20316%20795%20L%20360%20739%20L%20398%20700%20L%20425%20677%20L%20428%20679%20L%20443%20679%20L%20461%20673%20L%20476%20664%20L%20493%20666%20L%20517%20657%20L%20542%20641%20L%20546%20643%20Z%20M%20563%20221%20L%20572%20225%20L%20571%20223%20L%20574%20220%20L%20585%20217%20L%20596%20218%20L%20607%20215%20L%20653%20214%20L%20706%20218%20L%20749%20224%20L%20749%20226%20L%20735%20233%20L%20732%20238%20L%20732%20249%20L%20693%20242%20L%20620%20239%20L%20672%20251%20L%20688%20260%20L%20701%20274%20L%20706%20287%20L%20704%20301%20L%20707%20297%20L%20711%20300%20L%20713%20309%20L%20720%20324%20L%20723%20327%20L%20724%20308%20L%20726%20304%20L%20735%20318%20L%20757%20341%20L%20753%20319%20L%20754%20313%20L%20760%20317%20L%20786%20345%20L%20784%20324%20L%20775%20300%20L%20779%20300%20L%20800%20317%20L%20800%20312%20L%20787%20283%20L%20772%20264%20L%20778%20263%20L%20800%20271%20L%20803%20270%20L%20770%20236%20L%20772%20234%20L%20797%20234%20L%20807%20230%20L%20801%20230%20L%20800%20228%20L%20811%20218%20L%20818%20203%20L%20818%20193%20L%20812%20179%20L%20798%20168%20L%20784%20166%20L%20760%20173%20L%20747%20173%20L%20712%20163%20L%20718%20157%20L%20703%20171%20L%20711%20173%20L%20720%20178%20L%20723%20182%20L%20720%20186%20L%20695%20190%20L%20680%20190%20L%20670%20193%20L%20656%20193%20L%20687%20204%20L%20729%20209%20L%20742%20214%20L%20753%20223%20L%20752%20224%20L%20729%20219%20L%20669%20212%20L%20610%20213%20L%20576%20218%20L%20565%20221%20L%20565%20219%20Z%20M%20771%20189%20L%20772%20201%20L%20775%20207%20L%20780%20212%20L%20787%20215%20L%20796%20216%20L%20796%20218%20L%20784%20219%20L%20776%20215%20L%20771%20210%20L%20768%20204%20L%20768%20194%20Z%20M%20788%20181%20L%20793%20182%20L%20797%20187%20L%20796%20192%20L%20790%20196%20L%20786%20195%20L%20782%20191%20L%20782%20186%20Z'/%3e%3c/g%3e%3c/svg%3e`,at=o((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),ot=o(((e,t)=>{t.exports=at()})),F=ot();function st({className:e}){return(0,F.jsx)(`span`,{className:`block overflow-hidden rounded-[20.7%] bg-accent ${e??``}`,children:(0,F.jsx)(`img`,{src:it,alt:``,"aria-hidden":`true`,className:`h-full w-full`})})}function ct({maximized:e}){return(0,F.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`h-4 w-4`,children:e?(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`path`,{d:`M8 3v3a2 2 0 0 1-2 2H3`}),(0,F.jsx)(`path`,{d:`M21 8h-3a2 2 0 0 1-2-2V3`}),(0,F.jsx)(`path`,{d:`M3 16h3a2 2 0 0 1 2 2v3`}),(0,F.jsx)(`path`,{d:`M16 21v-3a2 2 0 0 1 2-2h3`})]}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`path`,{d:`M8 3H5a2 2 0 0 0-2 2v3`}),(0,F.jsx)(`path`,{d:`M21 8V5a2 2 0 0 0-2-2h-3`}),(0,F.jsx)(`path`,{d:`M3 16v3a2 2 0 0 0 2 2h3`}),(0,F.jsx)(`path`,{d:`M16 21h3a2 2 0 0 0 2-2v-3`})]})})}function lt({open:e}){return(0,F.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2.5`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`h-3.5 w-3.5 shrink-0 transition-transform ${e?`rotate-90`:``}`,children:(0,F.jsx)(`path`,{d:`m9 18 6-6-6-6`})})}function ut({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`M18 6 6 18`}),(0,F.jsx)(`path`,{d:`m6 6 12 12`})]})}function dt({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`M5 12h14`}),(0,F.jsx)(`path`,{d:`M12 5v14`})]})}function ft(){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`h-4 w-4`,children:[(0,F.jsx)(`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`}),(0,F.jsx)(`path`,{d:`M12 3v18`})]})}function pt(){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`h-4 w-4`,children:[(0,F.jsx)(`path`,{d:`M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z`}),(0,F.jsx)(`circle`,{cx:`12`,cy:`12`,r:`3`})]})}function mt(){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`h-4 w-4`,children:[(0,F.jsx)(`circle`,{cx:`11`,cy:`11`,r:`8`}),(0,F.jsx)(`path`,{d:`m21 21-4.3-4.3`})]})}function ht({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`M3 6h.01`}),(0,F.jsx)(`path`,{d:`M3 12h.01`}),(0,F.jsx)(`path`,{d:`M3 18h.01`}),(0,F.jsx)(`path`,{d:`M8 6h13`}),(0,F.jsx)(`path`,{d:`M8 12h13`}),(0,F.jsx)(`path`,{d:`M8 18h13`})]})}function gt({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z`}),(0,F.jsx)(`path`,{d:`M14 2v4a2 2 0 0 0 2 2h4`}),(0,F.jsx)(`path`,{d:`M10 9H8`}),(0,F.jsx)(`path`,{d:`M16 13H8`}),(0,F.jsx)(`path`,{d:`M16 17H8`})]})}function _t({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`m4 17 6-6-6-6`}),(0,F.jsx)(`path`,{d:`M12 19h8`})]})}function vt({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`rect`,{x:`2`,y:`3`,width:`20`,height:`14`,rx:`2`}),(0,F.jsx)(`path`,{d:`M12 17v4`}),(0,F.jsx)(`path`,{d:`M8 21h8`}),(0,F.jsx)(`path`,{d:`m9 13 6-6`}),(0,F.jsx)(`path`,{d:`M9 10v3h3`}),(0,F.jsx)(`path`,{d:`M15 10V7h-3`})]})}function yt({className:e=`h-4 w-4`}){return(0,F.jsxs)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`2`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":`true`,focusable:`false`,className:`shrink-0 ${e}`,children:[(0,F.jsx)(`path`,{d:`M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4`}),(0,F.jsx)(`path`,{d:`m16 17 5-5-5-5`}),(0,F.jsx)(`path`,{d:`M21 12H9`})]})}function bt({repos:e,currentId:t,onSelect:n,onCloseProject:r,onOpenPicker:i,className:a=``}){let[o,s]=(0,v.useState)(!1),c=(0,v.useRef)(null),l=e.find(e=>e.id===t);return(0,v.useEffect)(()=>{if(!o)return;let e=e=>{e.key===`Escape`&&(s(!1),c.current?.focus())};return document.addEventListener(`keydown`,e),()=>document.removeEventListener(`keydown`,e)},[o]),(0,F.jsxs)(`div`,{className:`relative ${a}`,children:[(0,F.jsxs)(`button`,{ref:c,onClick:()=>s(e=>!e),"aria-haspopup":`menu`,"aria-expanded":o,title:l?.display_path??`Select a project`,className:`flex max-w-[9rem] items-center gap-1 rounded-sm bg-ink-700 py-0.5 pl-2 pr-1 text-ink-50`,children:[(0,F.jsx)(`span`,{className:`truncate`,children:l?.name??`No project`}),(0,F.jsx)(lt,{open:o})]}),o&&(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`div`,{className:`fixed inset-0 z-40`,onClick:()=>s(!1)}),(0,F.jsxs)(`div`,{role:`menu`,className:`absolute left-0 z-50 mt-1 max-h-[70vh] w-56 max-w-[80vw] overflow-y-auto rounded-md border border-ink-700 bg-ink-900 py-1 shadow-lg`,children:[e.length===0&&(0,F.jsx)(`p`,{className:`px-3 py-1.5 text-ink-400`,children:`No projects open.`}),e.map(e=>(0,F.jsxs)(`div`,{className:`flex items-center ${e.id===t?`bg-ink-700 text-ink-50`:`text-ink-200`}`,children:[(0,F.jsx)(`button`,{role:`menuitem`,onClick:()=>{n(e.id),s(!1)},title:e.display_path,className:`min-w-0 flex-1 truncate py-1.5 pl-3 pr-1 text-left hover:text-accent`,children:e.name}),(0,F.jsx)(`button`,{onClick:()=>r(e.id),"aria-label":`close ${e.name}`,title:`Close project`,className:`mr-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:text-removed`,children:(0,F.jsx)(ut,{className:`h-3.5 w-3.5`})})]},e.id)),(0,F.jsx)(`div`,{className:`my-1 border-t border-ink-800`}),(0,F.jsxs)(`button`,{role:`menuitem`,onClick:()=>{i(),s(!1)},className:`flex w-full items-center gap-1 px-3 py-1.5 text-left text-ink-400 hover:text-ink-200`,children:[(0,F.jsx)(dt,{className:`h-3.5 w-3.5`}),`open`]})]})]})]})}function xt({repos:e,repo:t,setRepo:n,setPane:r,closeRepo:i,setPickerOpen:a,cloning:o,accent:s,next:c,cycle:l,draggingRepo:u,dragOverRepo:d,onRepoDragStart:f,onRepoDragMove:p,onRepoDragEnd:m}){return(0,F.jsxs)(`header`,{className:`flex items-center gap-2 border-b border-ink-700 bg-ink-900 px-[12.8px] py-[8.8px]`,children:[(0,F.jsx)(st,{className:`h-[22px] w-[22px] shrink-0`}),(0,F.jsx)(`span`,{className:`text-[16px] font-medium tracking-[0.04em] text-ink-50`,children:`nightcrow`}),(0,F.jsx)(`span`,{className:`hidden font-sans text-[10px] uppercase tracking-[0.18em] text-ink-400 sm:inline`,children:`web viewer`}),(0,F.jsx)(bt,{className:`md:hidden`,repos:e,currentId:t,onSelect:e=>{n(e),r()},onCloseProject:i,onOpenPicker:()=>a(!0)}),(0,F.jsx)(`nav`,{className:`-my-[8.8px] hidden items-stretch self-stretch overflow-x-auto pl-1 md:flex`,children:e.map(a=>(0,F.jsxs)(`div`,{"data-repo-id":a.id,onPointerDown:e=>f(e,a.id),onPointerMove:p,onPointerUp:m,onPointerCancel:m,onLostPointerCapture:m,className:`flex items-center border-r border-ink-700 whitespace-nowrap ${e.length>1?`touch-none`:``} ${u===a.id?`opacity-60`:``} ${d===a.id?`bg-ink-800 ring-1 ring-inset ring-accent`:``} ${a.id===t?`bg-ink-950 text-ink-50 shadow-[inset_0_2px_0_0_var(--color-accent)]`:`text-ink-400 hover:bg-ink-850 hover:text-ink-200`}`,title:a.display_path,children:[(0,F.jsx)(`button`,{onClick:()=>{n(a.id),r()},className:`self-stretch pl-3 pr-1`,children:a.name}),(0,F.jsx)(`button`,{onClick:e=>{e.stopPropagation(),i(a.id)},"data-tab-close":!0,title:`Close project`,"aria-label":`close ${a.name}`,className:`mr-1 flex h-5 w-5 items-center justify-center rounded-sm text-ink-400 hover:bg-ink-700 hover:text-removed`,children:(0,F.jsx)(ut,{className:`h-3.5 w-3.5`})})]},a.id))}),(0,F.jsxs)(`button`,{onClick:()=>a(!0),title:`Open a project`,className:`hidden shrink-0 items-center gap-1 rounded-sm px-2 py-0.5 text-ink-400 hover:text-ink-200 md:inline-flex`,children:[(0,F.jsx)(dt,{className:`h-3.5 w-3.5`}),`open`]}),o&&(0,F.jsxs)(`span`,{role:`status`,title:`A clone is running on the server`,className:`flex shrink-0 items-center gap-1.5 px-2 py-0.5 text-ink-400`,children:[(0,F.jsx)(`span`,{"aria-hidden":`true`,className:`h-1.5 w-1.5 animate-pulse rounded-full bg-accent`}),`Cloning…`]}),(0,F.jsx)(`button`,{onClick:l,title:`Accent: ${s.name} (click for ${c.name})`,"aria-label":`accent colour: ${s.name}, click for ${c.name}`,className:`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded-sm`,children:(0,F.jsx)(`span`,{"aria-hidden":`true`,className:`h-3 w-3 rounded-full bg-accent ring-1 ring-ink-600`})}),(0,F.jsx)(`a`,{href:`/logout`,title:`Sign out`,"aria-label":`sign out`,className:`ml-1 flex h-6 w-6 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:bg-ink-700 hover:text-ink-200`,children:(0,F.jsx)(yt,{className:`h-3.5 w-3.5`})})]})}function St(){let e=new Map;return{start(t){let n=(e.get(t)??0)+1;return e.set(t,n),n},isCurrent(t,n){return e.get(t)===n}}}var Ct={children:{},expanded:new Set};function wt(e,t,n){return{...e,children:{...e.children,[t]:n}}}function Tt(e,t){let n=new Set(e.expanded);return n.delete(t)||n.add(t),{...e,expanded:n}}function Et(e,t){let n=new Set(e.expanded);return t.forEach(e=>n.add(e)),{...e,expanded:n}}function Dt(e){let t=[],n=``;for(let r of e.split(`/`))n=n?`${n}/${r}`:r,t.push(n);return t}var Ot=180,kt={items:[],truncated:!1};function At({repo:e,authed:t,tab:n,filter:r,filterOpen:i,handle:a}){let[o,s]=(0,v.useState)(Ct),[c,l]=(0,v.useState)(kt),[u,d]=(0,v.useState)(!1),[f]=(0,v.useState)(St);(0,v.useEffect)(()=>{if(!e||!t||n!==`tree`||!i||!r){l(kt),d(!1);return}d(!0);let o=!0,s=setTimeout(()=>{O.treeSearch(e,r).then(e=>{o&&l({items:e.matches,truncated:e.truncated})}).catch(e=>{o&&a(e)}).finally(()=>{o&&d(!1)})},Ot);return()=>{o=!1,clearTimeout(s)}},[e,t,n,r,i,a]);let p=(0,v.useCallback)(t=>{if(!e)return;let n=f.start(t);O.tree(e,t).then(e=>{f.isCurrent(t,n)&&s(n=>wt(n,t,e.entries))}).catch(e=>{f.isCurrent(t,n)&&a(e)})},[e,a,f]);(0,v.useEffect)(()=>{!e||!t||n!==`tree`||p(``)},[e,t,n,p]);let m=(0,v.useCallback)(e=>{let t=!o.expanded.has(e);s(t=>Tt(t,e)),t&&!(e in o.children)&&p(e)},[o,p]),h=(0,v.useCallback)(e=>{let t=Dt(e);s(e=>Et(e,t)),t.forEach(e=>{e in o.children||p(e)})},[o,p]);return{treeChildren:o.children,treeExpanded:o.expanded,treeMatches:c.items,treeTruncated:c.truncated,treeSearchLoading:u,loadTreeChildren:p,toggleTreeDir:m,revealTreeDir:h}}function jt(e,t){let n=[],r=(i,a)=>{for(let o of e[i]??[]){let e=i?`${i}/${o.name}`:o.name;n.push({path:e,name:o.name,is_dir:o.is_dir,depth:a}),o.is_dir&&t.has(e)&&r(e,a+1)}};return r(``,0),n}function Mt({path:e,from:t,className:n}){return(0,F.jsx)(`span`,{className:`whitespace-nowrap ${n??``}`,title:t?`${t} → ${e}`:e,children:t?`${t} → ${e}`:e})}function Nt(e){let t=Math.max(0,Math.floor(Date.now()/1e3-e));return t<60?`${t}s`:t<3600?`${Math.floor(t/60)}m`:t<86400?`${Math.floor(t/3600)}h`:t<86400*30?`${Math.floor(t/86400)}d`:t<86400*365?`${Math.floor(t/(86400*30))}mo`:`${Math.floor(t/(86400*365))}y`}function Pt(e){return e===`+`?`bg-added/10`:e===`-`?`bg-removed/10`:``}function Ft(e){return e===`?`?`text-ink-400`:e===`D`?`text-removed`:e===`A`?`text-added`:`text-accent`}function It({status:e,files:t,now:n,hotWindowMs:r,openDiff:i}){return e===null?(0,F.jsx)(`li`,{className:`px-3 py-2 text-ink-400`,children:`Loading…`}):(0,F.jsxs)(F.Fragment,{children:[t.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>i(e.path),className:`flex w-max min-w-full gap-2 px-3 py-0.5 text-left hover:bg-ink-850`,children:[(0,F.jsxs)(`span`,{className:`shrink-0`,children:[(0,F.jsx)(`span`,{className:Ft(e.index),children:e.index===` `?` `:e.index}),(0,F.jsx)(`span`,{className:Ft(e.worktree),children:e.worktree===` `?` `:e.worktree})]}),(0,F.jsx)(Mt,{path:e.path,from:e.old_path,className:ue[P(e.mtime,n,r)]})]})},e.path)),e.truncated&&(0,F.jsxs)(`li`,{className:`px-3 py-1 text-accent`,children:[`Showing the first `,e.files.length,` changed files.`]})]})}function Lt({visibleCommits:e,commits:t,aheadOids:n,commitDrillDown:r,visibleCommitFiles:i,logDone:a,logStalled:o,logPagingPaused:s,setLogStalled:c,logSentinelRef:l,openCommitFiles:u,openCommit:d,openCommitFileDiff:f,setCommitDrillDown:p,setPaneEmpty:m,bumpPaneRequest:h}){return(0,F.jsxs)(F.Fragment,{children:[!r&&e.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>void u(e),title:`${e.author} · ${e.summary}`,className:`flex w-max min-w-full items-baseline gap-2 px-3 py-0.5 text-left hover:bg-ink-850`,children:[(0,F.jsx)(`span`,{className:`w-2 shrink-0 text-added`,children:n.has(e.oid)?`↑`:``}),(0,F.jsx)(`span`,{className:`shrink-0 text-accent`,children:e.short_id}),(0,F.jsx)(`span`,{className:`w-10 shrink-0 text-right text-ink-400`,children:Nt(e.time)}),(0,F.jsx)(`span`,{className:`max-w-[6rem] shrink-0 truncate text-ink-400`,children:e.author}),(0,F.jsx)(`span`,{className:`whitespace-nowrap`,children:e.summary})]})},e.oid)),!r&&!a&&!o&&!s&&(0,F.jsx)(`li`,{ref:l,className:`px-3 py-1 text-ink-400`,"aria-hidden":`true`,children:`loading…`}),!r&&!a&&!o&&s&&(0,F.jsxs)(`li`,{className:`px-3 py-1 text-ink-400`,children:[`filtering `,t.length,` loaded commits — clear the filter to load more`]}),!r&&o&&(0,F.jsx)(`li`,{className:`px-3 py-1`,children:(0,F.jsx)(`button`,{onClick:()=>c(!1),className:`text-ink-400 hover:text-accent`,children:`could not load more — retry`})}),r&&(0,F.jsxs)(F.Fragment,{children:[(0,F.jsxs)(`li`,{className:`sticky top-0 z-10 flex w-max min-w-full items-center gap-1 bg-ink-900 px-2 py-1 text-ink-400`,children:[(0,F.jsx)(`button`,{onClick:()=>{h(),p(null),m()},className:`rounded-sm px-1 hover:text-accent`,title:`Back to commit log`,children:`< log`}),(0,F.jsx)(`span`,{className:`text-ink-600`,children:`·`}),(0,F.jsx)(`span`,{className:`shrink-0 text-accent`,children:r.commit.short_id}),(0,F.jsx)(`button`,{onClick:()=>d(r.commit.oid),className:`rounded-sm px-1 hover:text-accent`,title:`Show the complete commit diff`,children:`all changes`})]}),i.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>f(r.commit.oid,e.path),className:`flex w-max min-w-full gap-2 px-3 py-0.5 text-left hover:bg-ink-850`,children:[(0,F.jsx)(`span`,{className:Ft(e.index),children:e.index}),(0,F.jsx)(Mt,{path:e.path,from:e.old_path})]})},e.path)),r.files.length===0&&(0,F.jsx)(`li`,{className:`px-3 py-2 text-ink-400`,children:`No changed files.`}),r.files.length>0&&i.length===0&&(0,F.jsx)(`li`,{className:`px-3 py-2 text-ink-400`,children:`No matching files.`}),r.truncated&&(0,F.jsxs)(`li`,{className:`px-3 py-1 text-accent`,children:[`Showing the first `,r.files.length,` files.`]})]})]})}function Rt({treeSearching:e,treeMatches:t,treeTruncated:n,treeSearchLoading:r,treeRows:i,treeExpanded:a,openFile:o,revealTreeDir:s,toggleTreeDir:c}){return e?(0,F.jsxs)(F.Fragment,{children:[t.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsx)(`button`,{onClick:()=>{e.is_dir?s(e.path):o(e.path)},title:e.path,className:`w-max min-w-full whitespace-nowrap px-3 py-0.5 text-left hover:bg-ink-850`,children:e.is_dir?(0,F.jsxs)(`span`,{className:`text-accent`,children:[e.path,`/`]}):e.path})},e.path)),t.length===0&&(0,F.jsx)(`li`,{className:`px-3 py-0.5 text-ink-400`,children:r?`searching…`:`no matches`}),n&&(0,F.jsxs)(`li`,{className:`px-3 py-0.5 text-ink-400`,children:[`showing the first `,t.length,` matches`]})]}):(0,F.jsx)(F.Fragment,{children:i.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>e.is_dir?c(e.path):o(e.path),title:e.path,style:{paddingLeft:`${e.depth*.75+.5}rem`},className:`flex w-max min-w-full items-center gap-1 py-0.5 pr-3 text-left hover:bg-ink-850`,children:[e.is_dir?(0,F.jsx)(lt,{open:a.has(e.path)}):(0,F.jsx)(`span`,{className:`h-3.5 w-3.5 shrink-0`}),(0,F.jsx)(`span`,{className:`whitespace-nowrap ${e.is_dir?`text-accent`:``}`,children:e.is_dir?`${e.name}/`:e.name})]})},e.path))})}function zt(e){let{tab:t,setTab:n,filter:r,setFilter:i,filterOpen:a,setFilterOpen:o,status:s,files:c,now:l,hotWindowMs:u,setPane:d,openDiff:f,openFile:p,openCommit:m,openCommitFileDiff:h,openCommitFiles:g,repo:_,authed:v,handle:y,sidebarRef:b,draggingSidebar:x,onSidebarDragStart:S,onSidebarDragMove:C,onSidebarDragEnd:w,onSidebarDragCancel:ee,filesMax:T,bumpPaneRequest:E,commits:D,logDone:te,logStalled:O,setLogStalled:ne,commitDrillDown:k,setCommitDrillDown:re,resetLog:ie,logSentinelRef:ae,visibleCommits:A,logPagingPaused:j,aheadOids:oe,visibleCommitFiles:se,mobileView:ce}=e,M=At({repo:_,authed:v,tab:t,filter:r,filterOpen:a,handle:y}),N=t===`tree`&&a&&r!==``,P=jt(M.treeChildren,M.treeExpanded);return(0,F.jsxs)(`section`,{ref:b,className:`relative min-h-0 flex-col overflow-hidden ${ce===`files`?`flex`:`hidden md:flex`} ${T?`md:flex`:`border-ink-700 md:border-r`}`,children:[!T&&(0,F.jsx)(`div`,{role:`separator`,"aria-orientation":`vertical`,"aria-label":`Resize the file sidebar (double-click to reset)`,title:`Drag to resize · double-click to reset`,onPointerDown:S,onPointerMove:C,onPointerUp:w,onPointerCancel:ee,onLostPointerCapture:w,className:`absolute -right-px top-0 z-10 hidden h-full w-1.5 cursor-col-resize touch-none md:block ${x?`bg-accent`:`hover:bg-accent`}`}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-stretch border-b border-ink-700 px-2`,children:[[`status`,`log`,`tree`].map(e=>(0,F.jsx)(`button`,{onClick:()=>{e!==t&&(E(),t===`log`&&(re(null),ie()),n(e),d({kind:`empty`}))},"aria-current":e===t?`page`:void 0,className:`-mb-px border-b-2 px-2 py-1 ${e===t?`border-accent text-ink-50`:`border-transparent text-ink-400 hover:text-ink-200`}`,children:e},e)),(0,F.jsx)(`button`,{onClick:()=>{a&&i(``),o(e=>!e)},"aria-pressed":a,title:a?`Hide the filter`:`Filter the list`,"aria-label":a?`Hide the filter`:`Filter the list`,className:`my-1 ml-auto flex shrink-0 items-center rounded-sm px-1.5 hover:text-accent ${a?`text-ink-50`:`text-ink-400`}`,children:(0,F.jsx)(mt,{})})]}),a&&(0,F.jsx)(`input`,{value:r,onChange:e=>i(e.target.value),placeholder:`filter…`,autoFocus:!0,className:`mx-2 mb-1 shrink-0 rounded-sm bg-ink-850 px-2 py-1 outline-none placeholder:text-ink-400 focus:ring-1 focus:ring-accent`}),(0,F.jsxs)(`ul`,{className:`min-h-0 flex-1 overflow-auto`,children:[t===`status`&&(0,F.jsx)(It,{status:s,files:c,now:l,hotWindowMs:u,openDiff:f}),t===`log`&&(0,F.jsx)(Lt,{visibleCommits:A,commits:D,aheadOids:oe,commitDrillDown:k,visibleCommitFiles:se,logDone:te,logStalled:O,logPagingPaused:j,setLogStalled:ne,logSentinelRef:ae,openCommitFiles:g,openCommit:m,openCommitFileDiff:h,setCommitDrillDown:re,setPaneEmpty:()=>d({kind:`empty`}),bumpPaneRequest:E}),t===`tree`&&(0,F.jsx)(Rt,{treeSearching:N,treeMatches:M.treeMatches,treeTruncated:M.treeTruncated,treeSearchLoading:M.treeSearchLoading,treeRows:P,treeExpanded:M.treeExpanded,openFile:p,revealTreeDir:M.revealTreeDir,toggleTreeDir:M.toggleTreeDir})]})]})}function Bt(e){let t=[],n=[],r=[],i=()=>{let e=Math.max(n.length,r.length);for(let i=0;i{t(e=>e===`split`?`unified`:`split`)},[])}}var Ht=[`.md`,`.markdown`],Ut=[`.html`,`.htm`];function Wt(e){let t=e.toLowerCase();return Ht.some(e=>t.endsWith(e))}function Gt(e){let t=e.toLowerCase();return Ut.some(e=>t.endsWith(e))}function Kt(e){return Wt(e)||Gt(e)}function qt(e){return e.map(e=>e.map(e=>e.t).join(``)).join(` -`)}var Jt=3;function Yt(e){let t=e<1?1:String(Math.floor(e)).length;return Math.max(t,Jt)}function Xt(e){let t=0;for(let n of e)for(let e of n.lines)t=Math.max(t,e.old_lineno??0,e.new_lineno??0);return Yt(t)}function Zt({nos:e,digits:t,tint:n=``}){return(0,F.jsx)(`span`,{className:`sticky left-0 shrink-0 select-none bg-ink-950`,children:(0,F.jsx)(`span`,{className:`flex gap-[1ch] px-[1ch] text-ink-400 ${n}`,children:e.map((e,n)=>(0,F.jsx)(`span`,{className:`text-right`,style:{width:`${t}ch`},children:e??``},n))})})}function Qt({line:e}){return(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`span`,{className:`text-ink-400 select-none`,children:e.kind}),e.spans.map((e,t)=>(0,F.jsx)(`span`,{style:{color:e.c},children:e.t},t))]})}function $t({line:e,digits:t,side:n}){if(e===null)return(0,F.jsxs)(`div`,{className:`flex bg-ink-900/40`,children:[(0,F.jsx)(Zt,{nos:[void 0],digits:t,tint:`bg-ink-900/40`}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:` `})]});let r=Pt(e.kind);return(0,F.jsxs)(`div`,{className:`flex ${r}`,children:[(0,F.jsx)(Zt,{nos:[n===`old`?e.old_lineno:e.new_lineno],digits:t,tint:r}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:(0,F.jsx)(Qt,{line:e})})]})}function en({cells:e,digits:t,side:n,border:r}){return(0,F.jsx)(`div`,{className:`min-w-0 flex-none overflow-x-auto md:flex-1 md:basis-1/2 ${r?`border-t border-ink-800 md:border-t-0 md:border-l`:``}`,children:(0,F.jsx)(`div`,{className:`w-max min-w-full`,children:e.map((e,r)=>(0,F.jsx)($t,{line:e,digits:t,side:n},r))})})}function tn({lines:e,digits:t}){let n=Bt(e);return(0,F.jsxs)(`div`,{className:`flex flex-col md:flex-row`,children:[(0,F.jsx)(en,{cells:n.map(e=>e.left),digits:t,side:`old`,border:!1}),(0,F.jsx)(en,{cells:n.map(e=>e.right),digits:t,side:`new`,border:!0})]})}function nn({diff:e,split:t}){let n=Xt(e.hunks);return(0,F.jsxs)(`div`,{className:`p-1`,children:[e.hunks.length===0&&(0,F.jsx)(`p`,{className:`p-3 text-ink-400`,children:`No changes.`}),e.hunks.map((e,r)=>{let i=(0,F.jsxs)(`div`,{className:`bg-ink-850 px-3 py-0.5 text-ink-400`,children:[e.file_path?`${e.file_path} `:``,e.header]});return(0,F.jsx)(`div`,{className:`mb-2`,children:t?(0,F.jsxs)(F.Fragment,{children:[i,(0,F.jsx)(tn,{lines:e.lines,digits:n})]}):(0,F.jsxs)(`div`,{className:`w-max min-w-full`,children:[i,e.lines.map((e,t)=>{let r=Pt(e.kind);return(0,F.jsxs)(`div`,{className:`flex ${r}`,children:[(0,F.jsx)(Zt,{nos:[e.old_lineno,e.new_lineno],digits:n,tint:r}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:(0,F.jsx)(Qt,{line:e})})]},t)})]})},r)}),e.truncated&&(0,F.jsx)(`p`,{className:`p-3 text-accent`,children:`Diff truncated — it exceeded the server's size ceiling.`})]})}var rn=`modulepreload`,an=function(e,t){return new URL(e,t).href},on={},sn=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=an(t,n),t=s(t),t in on)return;on[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:rn,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},cn=(0,v.lazy)(()=>sn(()=>import(`./Markdown-C0-sv82C.js`).then(e=>({default:e.MarkdownView})),__vite__mapDeps([0,1]),import.meta.url)),ln=(0,v.lazy)(()=>sn(()=>import(`./Html-CCQq4xAj.js`).then(e=>({default:e.HtmlView})),[],import.meta.url));function un({lines:e}){let t=Yt(e.length);return(0,F.jsx)(`pre`,{className:`w-max min-w-full py-2 text-ink-200`,children:e.map((e,n)=>(0,F.jsxs)(`div`,{className:`flex`,children:[(0,F.jsx)(Zt,{nos:[n+1],digits:t}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:e.length===0?` `:e.map((e,t)=>(0,F.jsx)(`span`,{style:{color:e.c},children:e.t},t))})]},n))})}function dn({pane:e,previewRendered:t,setPreviewRendered:n,filesMax:r,setMaximized:i,status:a,className:o=``}){let s=Vt();return(0,F.jsxs)(`section`,{className:`min-h-0 min-w-0 flex-col ${o}`,children:[(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 bg-ink-850 px-3 py-0.5 text-ink-400`,children:[e.kind===`file`&&(0,F.jsx)(Mt,{path:e.value.path}),(0,F.jsxs)(`div`,{className:`ml-auto flex shrink-0 items-center gap-1`,children:[e.kind===`diff`&&(0,F.jsx)(`button`,{onClick:s.toggle,"aria-pressed":s.layout===`split`,title:s.layout===`split`?`Switch to unified diff`:`Switch to split diff`,"aria-label":s.layout===`split`?`Switch to unified diff`:`Switch to split diff`,className:`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent ${s.layout===`split`?`text-accent`:``}`,children:(0,F.jsx)(ft,{})}),e.kind===`file`&&Kt(e.value.path)&&(0,F.jsx)(`button`,{onClick:()=>n(e=>!e),"aria-pressed":t,title:t?`Show raw source`:`Show the rendered page`,"aria-label":t?`Show raw source`:`Show the rendered page`,className:`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent ${t?`text-accent`:``}`,children:(0,F.jsx)(pt,{})}),(0,F.jsx)(`button`,{onClick:()=>i(r?`none`:`files`),"aria-pressed":r,title:r?`Restore the layout`:`Maximize the file pane`,"aria-label":r?`Restore the layout`:`Maximize the file pane`,className:`hidden shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent md:flex`,children:(0,F.jsx)(ct,{maximized:r})})]})]}),(0,F.jsxs)(`div`,{className:`min-h-0 flex-1 overflow-auto`,children:[e.kind===`empty`&&(0,F.jsx)(`p`,{className:`p-4 text-ink-400`,children:a===null?`Loading…`:`Select a file or commit.`}),e.kind===`file`&&(0,F.jsxs)(F.Fragment,{children:[Kt(e.value.path)&&t?(0,F.jsx)(v.Suspense,{fallback:(0,F.jsx)(`p`,{className:`p-4 text-ink-400`,children:`Rendering…`}),children:Gt(e.value.path)?(0,F.jsx)(ln,{source:qt(e.value.lines)}):(0,F.jsx)(cn,{source:qt(e.value.lines)})}):(0,F.jsx)(un,{lines:e.value.lines}),e.value.truncated&&(0,F.jsx)(`p`,{className:`p-3 text-accent`,children:`File truncated — it exceeded the server's size ceiling.`})]}),e.kind===`diff`&&(0,F.jsx)(nn,{diff:e.value,split:s.layout===`split`})]})]})}var fn=(0,v.lazy)(()=>sn(()=>import(`./Terminal-C_7faHIE.js`).then(e=>({default:e.TerminalPanel})),[],import.meta.url));function pn(e){let{repo:t,current:n,status:r,files:i,now:a,hotWindowMs:o,pane:s,setPane:c,tab:l,setTab:u,filter:d,setFilter:f,filterOpen:p,setFilterOpen:m,openDiff:h,openFile:g,openCommit:_,openCommitFileDiff:y,openCommitFiles:b,authed:x,handle:S,sidebarWidth:C,sidebarRef:w,draggingSidebar:ee,onSidebarDragStart:T,onSidebarDragMove:E,onSidebarDragEnd:D,onSidebarDragCancel:te,upperRef:O,lowerRef:ne,draggingUpper:k,onUpperDragStart:re,onUpperDragMove:ie,onUpperDragEnd:ae,onUpperDragCancel:A,filesMax:j,bumpPaneRequest:oe,commits:se,logDone:ce,logStalled:M,setLogStalled:N,commitDrillDown:P,setCommitDrillDown:le,resetLog:ue,logSentinelRef:de,visibleCommits:fe,logPagingPaused:pe,aheadOids:me,visibleCommitFiles:he,previewRendered:ge,setPreviewRendered:_e,maximized:ve,setMaximized:be,mobileView:xe,setMobileView:Se}=e;return(0,v.useEffect)(()=>te,[t,te]),(0,v.useEffect)(()=>A,[A]),(0,F.jsxs)(F.Fragment,{children:[ee&&(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 cursor-col-resize`}),k&&(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 cursor-row-resize`}),(0,F.jsxs)(`main`,{ref:O,className:`grid min-h-0 grid-cols-1 md:grid-cols-[var(--nc-sidebar)_1fr] ${xe===`terminal`?`hidden md:grid`:``} ${ee||k?`select-none`:``}`,style:{"--nc-sidebar":j?`0px`:`min(${C}px, ${ye*100}vw)`},children:[(0,F.jsx)(zt,{tab:l,setTab:u,filter:d,setFilter:f,filterOpen:p,setFilterOpen:m,status:r,files:i,now:a,hotWindowMs:o,setPane:c,openDiff:h,openFile:g,openCommit:_,openCommitFileDiff:y,openCommitFiles:b,repo:t,authed:x,handle:S,sidebarRef:w,draggingSidebar:ee,onSidebarDragStart:T,onSidebarDragMove:E,onSidebarDragEnd:D,onSidebarDragCancel:te,filesMax:j,bumpPaneRequest:oe,commits:se,logDone:ce,logStalled:M,setLogStalled:N,commitDrillDown:P,setCommitDrillDown:le,resetLog:ue,logSentinelRef:de,visibleCommits:fe,logPagingPaused:pe,aheadOids:me,visibleCommitFiles:he,mobileView:xe},t),(0,F.jsx)(dn,{pane:s,previewRendered:ge,setPreviewRendered:_e,filesMax:j,setMaximized:be,status:r,className:xe===`diff`?`flex`:`hidden md:flex`})]}),(0,F.jsx)(v.Suspense,{fallback:null,children:(0,F.jsx)(fn,{repo:t,maximized:ve===`terminal`,onToggleMaximized:()=>be(e=>e===`terminal`?`none`:`terminal`),className:xe===`terminal`?`flex`:`hidden md:flex`,sectionRef:ne,showDivider:ve===`none`,draggingUpper:k,onUpperDragStart:re,onUpperDragMove:ie,onUpperDragEnd:ae,onUpperDragCancel:A})}),(0,F.jsx)(`nav`,{"aria-label":`Switch view`,className:`flex shrink-0 items-stretch border-t border-ink-700 bg-ink-900 md:hidden`,children:[[`files`,`Files`,ht],[`diff`,`Diff`,gt],[`terminal`,`Terminal`,_t]].map(([e,t,n])=>(0,F.jsxs)(`button`,{onClick:()=>Se(e),"aria-current":xe===e?`page`:void 0,className:`flex min-h-11 flex-1 flex-col items-center justify-center gap-0.5 py-1 text-[11px] ${xe===e?`text-accent shadow-[inset_0_2px_0_0_var(--color-accent)]`:`text-ink-400`}`,children:[(0,F.jsx)(n,{className:`h-5 w-5`}),t]},e))}),(0,F.jsxs)(`footer`,{className:`flex shrink-0 items-center gap-3 border-t border-ink-700 bg-ink-900 px-3 py-1 text-ink-400`,children:[(0,F.jsx)(`span`,{className:`truncate`,children:n?.display_path}),r?.branch&&(0,F.jsx)(`span`,{className:`text-accent`,children:r.branch}),r?.tracking&&(0,F.jsxs)(`span`,{children:[`↑`,r.tracking.ahead,` ↓`,r.tracking.behind]}),(0,F.jsx)(`span`,{className:`ml-auto`,children:r?(0,F.jsx)(`span`,{className:`text-added`,children:`● live`}):`connecting…`})]})]})}function mn({onClose:e,onOpened:t,canClone:n,cloning:r,onClone:i}){let[a,o]=(0,v.useState)(null),[s,c]=(0,v.useState)(null),[l,u]=(0,v.useState)(null),[d,f]=(0,v.useState)(!1),[p,m]=(0,v.useState)(``),[h,g]=(0,v.useState)(!1),[_,y]=(0,v.useState)(``),[b,x]=(0,v.useState)(0);(0,v.useEffect)(()=>{let e=!1;return O.browse(a??void 0).then(t=>{e||(c(t),u(null))}).catch(t=>{e||u(t instanceof Error?t.message:`could not browse`)}),()=>{e=!0}},[a,b]);let S=e=>o(`${s.path.replace(/\/$/,``)}/${e}`),C=async()=>{if(s){f(!0);try{t(await O.open(s.path))}catch(e){se.error(e instanceof Error?e.message:`could not open`),f(!1)}}},w=async()=>{if(!s)return;let e=p.trim();if(e){g(!0);try{await O.mkdir(s.path,e),m(``),x(e=>e+1)}catch(e){se.error(e instanceof Error?e.message:`could not create folder`)}finally{g(!1)}}};return(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4`,onClick:e,children:(0,F.jsxs)(`div`,{className:`flex max-h-[80vh] w-[34rem] max-w-full flex-col rounded-md border border-ink-700 bg-ink-900`,onClick:e=>e.stopPropagation(),children:[(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-b border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`span`,{className:`font-medium text-ink-50`,children:`Open a project`}),(0,F.jsx)(`button`,{onClick:e,"aria-label":`close`,className:`ml-auto flex h-6 w-6 items-center justify-center rounded-sm text-ink-400 hover:text-ink-200`,children:(0,F.jsx)(ut,{})})]}),(0,F.jsx)(`div`,{className:`shrink-0 truncate border-b border-ink-700 px-3 py-1.5 text-ink-400`,children:s?.path??`…`}),(0,F.jsxs)(`ul`,{className:`h-72 min-h-0 overflow-y-auto`,children:[s?.parent&&(0,F.jsx)(`li`,{children:(0,F.jsx)(`button`,{onClick:()=>o(s.parent),className:`w-full px-3 py-1 text-left text-ink-400 hover:bg-ink-850`,children:`../`})}),s?.entries.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>S(e.name),className:`flex w-full items-center gap-2 px-3 py-1 text-left hover:bg-ink-850`,children:[(0,F.jsxs)(`span`,{className:`truncate text-accent`,children:[e.name,`/`]}),e.is_repo&&(0,F.jsx)(`span`,{className:`rounded-sm bg-ink-700 px-1 text-[0.65rem] text-ink-200`,children:`git`})]})},e.name)),s&&s.entries.length===0&&(0,F.jsx)(`li`,{className:`px-3 py-1 text-ink-400`,children:`No sub-folders.`})]}),l&&(0,F.jsx)(`p`,{className:`shrink-0 px-3 py-1 text-removed`,children:l}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),onKeyDown:e=>{e.key===`Enter`&&w()},placeholder:`New folder name`,"aria-label":`new folder name`,className:`min-w-0 flex-1 rounded-sm border border-ink-700 bg-ink-950 px-2 py-1 text-ink-50 placeholder:text-ink-400 focus:border-ink-600 focus:outline-none`}),(0,F.jsx)(`button`,{onClick:w,disabled:!s||!p.trim()||h,className:`shrink-0 rounded-sm border border-ink-700 px-2 py-1 text-ink-200 hover:bg-ink-850 disabled:opacity-50`,children:h?`Creating…`:`Create`})]}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`input`,{value:_,onChange:e=>y(e.target.value),onKeyDown:e=>{e.key===`Enter`&&s&&i(s.path,_)},disabled:!n,placeholder:n?`Clone a git URL here`:`git is not installed on the server`,"aria-label":`git URL to clone`,spellCheck:!1,autoCapitalize:`none`,autoCorrect:`off`,className:`min-w-0 flex-1 rounded-sm border border-ink-700 bg-ink-950 px-2 py-1 text-ink-50 placeholder:text-ink-400 focus:border-ink-600 focus:outline-none disabled:opacity-50`}),(0,F.jsx)(`button`,{onClick:()=>s&&i(s.path,_),disabled:!n||!s||!_.trim()||r,title:n?void 0:`the server has no git on its PATH`,className:`shrink-0 rounded-sm border border-ink-700 px-2 py-1 text-ink-200 hover:bg-ink-850 disabled:opacity-50`,children:r?`Cloning…`:`Clone`})]}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`span`,{className:`truncate text-ink-400`,children:s?s.path:``}),(0,F.jsx)(`button`,{onClick:C,disabled:!s||d,className:`ml-auto shrink-0 rounded-md bg-ink-50 px-3 py-1 font-semibold text-ink-950 hover:bg-white disabled:opacity-50`,children:d?`Opening…`:`Open`})]})]})})}function hn(){return(0,F.jsx)(`div`,{className:`flex h-full items-center justify-center p-6`,children:(0,F.jsxs)(`div`,{className:`flex flex-col items-center gap-3 text-ink-400`,children:[(0,F.jsx)(st,{className:`h-12 w-12 animate-pulse`}),(0,F.jsx)(`span`,{className:`text-[0.72rem] tracking-[0.18em] uppercase`,children:`Loading…`})]})})}function gn({onSuccess:e}){let[t,n]=(0,v.useState)(``),[r,i]=(0,v.useState)(null),[a,o]=(0,v.useState)(!1);return(0,F.jsx)(`div`,{className:`flex h-full items-center justify-center p-6`,children:(0,F.jsxs)(`form`,{onSubmit:async n=>{n.preventDefault(),o(!0),i(null);try{await O.login(t),e()}catch(e){i(e instanceof Error?e.message:`login failed`)}finally{o(!1)}},className:`w-[17rem] max-w-[86vw]`,children:[(0,F.jsx)(st,{className:`mx-auto mb-3 block h-10 w-10`}),(0,F.jsx)(`h1`,{className:`text-center text-lg font-medium tracking-wide text-ink-50`,children:`nightcrow`}),(0,F.jsx)(`p`,{className:`mt-1 mb-5 text-center text-[0.62rem] tracking-[0.18em] text-ink-400 uppercase`,children:`web viewer`}),r&&(0,F.jsx)(`p`,{className:`mb-2.5 text-center text-removed`,children:r}),(0,F.jsx)(`input`,{type:`password`,autoFocus:!0,value:t,onChange:e=>n(e.target.value),placeholder:`password`,className:`mb-2 w-full rounded-md border border-ink-700 bg-ink-900 px-2.5 py-1.5 outline-none placeholder:text-ink-400 focus:border-accent focus:ring-[3px] focus:ring-accent/15`}),(0,F.jsx)(`button`,{type:`submit`,disabled:a,className:`w-full rounded-md bg-ink-50 py-1.5 font-semibold text-ink-950 hover:bg-white disabled:opacity-50`,children:a?`Signing in…`:`Sign in`})]})})}function _n(e,t){return e?`grid-rows-[auto_minmax(0,1fr)_auto_auto] ${t===`terminal`?`md:grid-rows-[auto_minmax(0,0fr)_minmax(0,1fr)_auto]`:t===`files`?`md:grid-rows-[auto_minmax(0,1fr)_minmax(0,0fr)_auto]`:`md:grid-rows-[auto_minmax(0,var(--nc-upper))_minmax(0,var(--nc-lower))_auto]`}`:`grid-rows-[auto_1fr]`}function vn(){let[e,t]=(0,v.useState)(null),[n,r]=(0,v.useState)(`status`),[i,a]=(0,v.useState)(``),[o,s]=(0,v.useState)(!1),[c,l]=(0,v.useState)({kind:`empty`}),[u,d]=(0,v.useState)(`files`),f=(0,v.useRef)(0),p=(0,v.useCallback)(()=>{f.current+=1},[]),[m,h]=(0,v.useState)(!1),{accent:g,next:_,cycle:y,upperPct:b,shell:S,guards:C}=Le(),[w,ee]=(0,v.useState)(!0),T=(0,v.useCallback)(e=>{if(x(e)){t(!1);return}se.error(e instanceof Error?e.message:`request failed`)},[]),E=Ze(),{repos:D,setRepos:te,repo:O,setRepo:ne,hot:k,clockSkewMs:re,reposLoaded:ie,canClone:ae,orderWrites:A,draggingRepo:j,dragOverRepo:oe,onRepoDragStart:ce,onRepoDragMove:M,onRepoDragEnd:N}=qe({authed:e,setAuthed:t,handle:T,resumeTick:E,...C}),P=k?.enabled?k.window_secs*1e3:0;de(void 0,P,re??0);let{status:le}=Je({repo:O,authed:e,resumeTick:E,tab:n,pane:c,setPane:l,handle:T,paneRequestRef:f}),ue=de(le?.files,P,re??0),{maximized:fe,setMaximized:pe,dropMaximized:me}=Qe(O),{commits:he,logDone:ge,logStalled:_e,setLogStalled:ve,commitDrillDown:ye,setCommitDrillDown:be,resetLog:xe,logSentinelRef:Se,visibleCommits:Ce,logPagingPaused:we}=Xe({repo:O,authed:e,tab:n,filter:i,handle:T}),{openDiff:Te,openFile:Ee,openCommit:De,openCommitFileDiff:Oe,openCommitFiles:ke}=Ye({repo:O,handle:T,setPane:l,paneRequestRef:f,setCommitDrillDown:be,setMobileView:d,setPreviewRendered:ee}),{selectOpenedRepo:Ae,closeRepo:je}=$e({repos:D,setRepos:te,setRepo:ne,setPane:l,setTab:r,setPickerOpen:h,dropMaximized:me,handle:T,orderWrites:A}),{busy:Me,start:Ne}=rt(Ae,e===!0);if((0,v.useLayoutEffect)(()=>{p(),be(null),l({kind:`empty`}),xe()},[O,xe,p,be]),e===null)return(0,F.jsx)(hn,{});if(!e)return(0,F.jsx)(gn,{onSuccess:()=>t(!0)});if(!ie)return(0,F.jsx)(hn,{});let Pe=D.find(e=>e.id===O),Fe=i.toLowerCase(),Ie=(le?.files??[]).filter(e=>e.path.toLowerCase().includes(Fe)),Re=(ye?.files??[]).filter(e=>e.path.toLowerCase().includes(Fe)||e.old_path?.toLowerCase().includes(Fe)),ze=new Set(he.slice(0,le?.tracking?.ahead??0).map(e=>e.oid)),Be=fe===`files`;return(0,F.jsxs)(`div`,{className:`nc-fade grid h-full ${_n(O,fe)}`,style:{"--nc-upper":`${b}fr`,"--nc-lower":`${100-b}fr`},children:[(0,F.jsx)(xt,{repos:D,repo:O,setRepo:e=>{ne(e),l({kind:`empty`})},setPane:()=>l({kind:`empty`}),closeRepo:je,setPickerOpen:h,cloning:Me,accent:g,next:_,cycle:y,draggingRepo:j,dragOverRepo:oe,onRepoDragStart:ce,onRepoDragMove:M,onRepoDragEnd:N}),O?(0,F.jsx)(pn,{repo:O,repos:D,current:Pe,status:le,files:Ie,now:ue,hotWindowMs:P,pane:c,setPane:l,tab:n,setTab:r,filter:i,setFilter:a,filterOpen:o,setFilterOpen:s,openDiff:Te,openFile:Ee,openCommit:De,openCommitFileDiff:Oe,openCommitFiles:ke,authed:e,handle:T,...S,filesMax:Be,bumpPaneRequest:p,commits:he,logDone:ge,logStalled:_e,setLogStalled:ve,commitDrillDown:ye,setCommitDrillDown:be,resetLog:xe,logSentinelRef:Se,visibleCommits:Ce,logPagingPaused:we,aheadOids:ze,visibleCommitFiles:Re,previewRendered:w,setPreviewRendered:ee,maximized:fe,setMaximized:pe,mobileView:u,setMobileView:d}):(0,F.jsx)(`div`,{className:`flex items-center justify-center p-6 text-center text-ink-400`,children:(0,F.jsxs)(`span`,{children:[`No repository open. Click`,` `,(0,F.jsx)(`span`,{className:`text-ink-200`,children:`+ open`}),` above to add one.`]})}),m&&(0,F.jsx)(mn,{onClose:()=>h(!1),onOpened:Ae,canClone:ae,cloning:Me,onClone:Ne})]})}var yn={error:7e3,info:5e3,success:5e3},bn={error:`text-removed`,info:`text-accent`,success:`text-added`};function xn(){let[e,t]=(0,v.useState)([]);return(0,v.useEffect)(()=>A(t),[]),e.length===0?null:(0,F.jsx)(`div`,{className:`pointer-events-none fixed right-3 top-3 z-[60] flex w-80 max-w-[calc(100vw-1.5rem)] flex-col gap-2`,"aria-live":`polite`,children:e.map(e=>(0,F.jsx)(Sn,{toast:e},e.id))})}function Sn({toast:e}){let[t,n]=(0,v.useState)(!1);return(0,v.useEffect)(()=>{if(t)return;let n=setTimeout(()=>j(e.id),yn[e.kind]);return()=>clearTimeout(n)},[e.id,e.kind,e.bump,t]),(0,F.jsxs)(`div`,{role:e.kind===`error`?`alert`:`status`,className:`nc-fade pointer-events-auto flex items-start gap-2 rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-xs shadow-lg`,onMouseEnter:()=>n(!0),onMouseLeave:()=>n(!1),children:[(0,F.jsx)(`span`,{className:`min-w-0 flex-1 break-words ${bn[e.kind]}`,children:e.message}),(0,F.jsx)(`button`,{type:`button`,onClick:()=>j(e.id),"aria-label":`dismiss`,className:`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:bg-ink-700 hover:text-ink-200`,children:(0,F.jsx)(ut,{className:`h-3 w-3`})})]})}(0,y.createRoot)(document.getElementById(`root`)).render((0,F.jsxs)(v.StrictMode,{children:[(0,F.jsx)(vn,{}),(0,F.jsx)(xn,{})]}));export{ot as a,se as c,s as d,l as f,ut as i,d as l,ct as n,Ve as o,dt as r,Be as s,vt as t,o as u}; \ No newline at end of file +`)}var Jt=3;function Yt(e){let t=e<1?1:String(Math.floor(e)).length;return Math.max(t,Jt)}function Xt(e){let t=0;for(let n of e)for(let e of n.lines)t=Math.max(t,e.old_lineno??0,e.new_lineno??0);return Yt(t)}function Zt({nos:e,digits:t,tint:n=``}){return(0,F.jsx)(`span`,{className:`sticky left-0 shrink-0 select-none bg-ink-950`,children:(0,F.jsx)(`span`,{className:`flex gap-[1ch] px-[1ch] text-ink-400 ${n}`,children:e.map((e,n)=>(0,F.jsx)(`span`,{className:`text-right`,style:{width:`${t}ch`},children:e??``},n))})})}function Qt({line:e}){return(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(`span`,{className:`text-ink-400 select-none`,children:e.kind}),e.spans.map((e,t)=>(0,F.jsx)(`span`,{style:{color:e.c},children:e.t},t))]})}function $t({line:e,digits:t,side:n}){if(e===null)return(0,F.jsxs)(`div`,{className:`flex bg-ink-900/40`,children:[(0,F.jsx)(Zt,{nos:[void 0],digits:t,tint:`bg-ink-900/40`}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:` `})]});let r=Pt(e.kind);return(0,F.jsxs)(`div`,{className:`flex ${r}`,children:[(0,F.jsx)(Zt,{nos:[n===`old`?e.old_lineno:e.new_lineno],digits:t,tint:r}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:(0,F.jsx)(Qt,{line:e})})]})}function en({cells:e,digits:t,side:n,border:r}){return(0,F.jsx)(`div`,{className:`min-w-0 flex-none overflow-x-auto md:flex-1 md:basis-1/2 ${r?`border-t border-ink-800 md:border-t-0 md:border-l`:``}`,children:(0,F.jsx)(`div`,{className:`w-max min-w-full`,children:e.map((e,r)=>(0,F.jsx)($t,{line:e,digits:t,side:n},r))})})}function tn({lines:e,digits:t}){let n=Bt(e);return(0,F.jsxs)(`div`,{className:`flex flex-col md:flex-row`,children:[(0,F.jsx)(en,{cells:n.map(e=>e.left),digits:t,side:`old`,border:!1}),(0,F.jsx)(en,{cells:n.map(e=>e.right),digits:t,side:`new`,border:!0})]})}function nn({diff:e,split:t}){let n=Xt(e.hunks);return(0,F.jsxs)(`div`,{className:`p-1`,children:[e.hunks.length===0&&(0,F.jsx)(`p`,{className:`p-3 text-ink-400`,children:`No changes.`}),e.hunks.map((e,r)=>{let i=(0,F.jsxs)(`div`,{className:`bg-ink-850 px-3 py-0.5 text-ink-400`,children:[e.file_path?`${e.file_path} `:``,e.header]});return(0,F.jsx)(`div`,{className:`mb-2`,children:t?(0,F.jsxs)(F.Fragment,{children:[i,(0,F.jsx)(tn,{lines:e.lines,digits:n})]}):(0,F.jsxs)(`div`,{className:`w-max min-w-full`,children:[i,e.lines.map((e,t)=>{let r=Pt(e.kind);return(0,F.jsxs)(`div`,{className:`flex ${r}`,children:[(0,F.jsx)(Zt,{nos:[e.old_lineno,e.new_lineno],digits:n,tint:r}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:(0,F.jsx)(Qt,{line:e})})]},t)})]})},r)}),e.truncated&&(0,F.jsx)(`p`,{className:`p-3 text-accent`,children:`Diff truncated — it exceeded the server's size ceiling.`})]})}var rn=`modulepreload`,an=function(e,t){return new URL(e,t).href},on={},sn=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=an(t,n),t=s(t),t in on)return;on[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:rn,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},cn=(0,v.lazy)(()=>sn(()=>import(`./Markdown-B9Na3tqX.js`).then(e=>({default:e.MarkdownView})),__vite__mapDeps([0,1]),import.meta.url)),ln=(0,v.lazy)(()=>sn(()=>import(`./Html-Bzz2sKDk.js`).then(e=>({default:e.HtmlView})),[],import.meta.url));function un({lines:e}){let t=Yt(e.length);return(0,F.jsx)(`pre`,{className:`w-max min-w-full py-2 text-ink-200`,children:e.map((e,n)=>(0,F.jsxs)(`div`,{className:`flex`,children:[(0,F.jsx)(Zt,{nos:[n+1],digits:t}),(0,F.jsx)(`span`,{className:`whitespace-pre pr-3`,children:e.length===0?` `:e.map((e,t)=>(0,F.jsx)(`span`,{style:{color:e.c},children:e.t},t))})]},n))})}function dn({pane:e,previewRendered:t,setPreviewRendered:n,filesMax:r,setMaximized:i,status:a,className:o=``}){let s=Vt();return(0,F.jsxs)(`section`,{className:`min-h-0 min-w-0 flex-col ${o}`,children:[(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 bg-ink-850 px-3 py-0.5 text-ink-400`,children:[e.kind===`file`&&(0,F.jsx)(Mt,{path:e.value.path}),(0,F.jsxs)(`div`,{className:`ml-auto flex shrink-0 items-center gap-1`,children:[e.kind===`diff`&&(0,F.jsx)(`button`,{onClick:s.toggle,"aria-pressed":s.layout===`split`,title:s.layout===`split`?`Switch to unified diff`:`Switch to split diff`,"aria-label":s.layout===`split`?`Switch to unified diff`:`Switch to split diff`,className:`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent ${s.layout===`split`?`text-accent`:``}`,children:(0,F.jsx)(ft,{})}),e.kind===`file`&&Kt(e.value.path)&&(0,F.jsx)(`button`,{onClick:()=>n(e=>!e),"aria-pressed":t,title:t?`Show raw source`:`Show the rendered page`,"aria-label":t?`Show raw source`:`Show the rendered page`,className:`flex shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent ${t?`text-accent`:``}`,children:(0,F.jsx)(pt,{})}),(0,F.jsx)(`button`,{onClick:()=>i(r?`none`:`files`),"aria-pressed":r,title:r?`Restore the layout`:`Maximize the file pane`,"aria-label":r?`Restore the layout`:`Maximize the file pane`,className:`hidden shrink-0 items-center rounded-sm px-1.5 py-0.5 hover:text-accent md:flex`,children:(0,F.jsx)(ct,{maximized:r})})]})]}),(0,F.jsxs)(`div`,{className:`min-h-0 flex-1 overflow-auto`,children:[e.kind===`empty`&&(0,F.jsx)(`p`,{className:`p-4 text-ink-400`,children:a===null?`Loading…`:`Select a file or commit.`}),e.kind===`file`&&(0,F.jsxs)(F.Fragment,{children:[Kt(e.value.path)&&t?(0,F.jsx)(v.Suspense,{fallback:(0,F.jsx)(`p`,{className:`p-4 text-ink-400`,children:`Rendering…`}),children:Gt(e.value.path)?(0,F.jsx)(ln,{source:qt(e.value.lines)}):(0,F.jsx)(cn,{source:qt(e.value.lines)})}):(0,F.jsx)(un,{lines:e.value.lines}),e.value.truncated&&(0,F.jsx)(`p`,{className:`p-3 text-accent`,children:`File truncated — it exceeded the server's size ceiling.`})]}),e.kind===`diff`&&(0,F.jsx)(nn,{diff:e.value,split:s.layout===`split`})]})]})}var fn=(0,v.lazy)(()=>sn(()=>import(`./Terminal-DRQzLrr0.js`).then(e=>({default:e.TerminalPanel})),[],import.meta.url));function pn(e){let{repo:t,current:n,status:r,files:i,now:a,hotWindowMs:o,pane:s,setPane:c,tab:l,setTab:u,filter:d,setFilter:f,filterOpen:p,setFilterOpen:m,openDiff:h,openFile:g,openCommit:_,openCommitFileDiff:y,openCommitFiles:b,authed:x,handle:S,sidebarWidth:C,sidebarRef:w,draggingSidebar:ee,onSidebarDragStart:T,onSidebarDragMove:E,onSidebarDragEnd:D,onSidebarDragCancel:te,upperRef:O,lowerRef:ne,draggingUpper:k,onUpperDragStart:re,onUpperDragMove:ie,onUpperDragEnd:ae,onUpperDragCancel:A,filesMax:j,bumpPaneRequest:oe,commits:se,logDone:ce,logStalled:M,setLogStalled:N,commitDrillDown:P,setCommitDrillDown:le,resetLog:ue,logSentinelRef:de,visibleCommits:fe,logPagingPaused:pe,aheadOids:me,visibleCommitFiles:he,previewRendered:ge,setPreviewRendered:_e,maximized:ve,setMaximized:be,mobileView:xe,setMobileView:Se}=e;return(0,v.useEffect)(()=>te,[t,te]),(0,v.useEffect)(()=>A,[A]),(0,F.jsxs)(F.Fragment,{children:[ee&&(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 cursor-col-resize`}),k&&(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 cursor-row-resize`}),(0,F.jsxs)(`main`,{ref:O,className:`grid min-h-0 grid-cols-1 md:grid-cols-[var(--nc-sidebar)_1fr] ${xe===`terminal`?`hidden md:grid`:``} ${ee||k?`select-none`:``}`,style:{"--nc-sidebar":j?`0px`:`min(${C}px, ${ye*100}vw)`},children:[(0,F.jsx)(zt,{tab:l,setTab:u,filter:d,setFilter:f,filterOpen:p,setFilterOpen:m,status:r,files:i,now:a,hotWindowMs:o,setPane:c,openDiff:h,openFile:g,openCommit:_,openCommitFileDiff:y,openCommitFiles:b,repo:t,authed:x,handle:S,sidebarRef:w,draggingSidebar:ee,onSidebarDragStart:T,onSidebarDragMove:E,onSidebarDragEnd:D,onSidebarDragCancel:te,filesMax:j,bumpPaneRequest:oe,commits:se,logDone:ce,logStalled:M,setLogStalled:N,commitDrillDown:P,setCommitDrillDown:le,resetLog:ue,logSentinelRef:de,visibleCommits:fe,logPagingPaused:pe,aheadOids:me,visibleCommitFiles:he,mobileView:xe},t),(0,F.jsx)(dn,{pane:s,previewRendered:ge,setPreviewRendered:_e,filesMax:j,setMaximized:be,status:r,className:xe===`diff`?`flex`:`hidden md:flex`})]}),(0,F.jsx)(v.Suspense,{fallback:null,children:(0,F.jsx)(fn,{repo:t,maximized:ve===`terminal`,onToggleMaximized:()=>be(e=>e===`terminal`?`none`:`terminal`),className:xe===`terminal`?`flex`:`hidden md:flex`,sectionRef:ne,showDivider:ve===`none`,draggingUpper:k,onUpperDragStart:re,onUpperDragMove:ie,onUpperDragEnd:ae,onUpperDragCancel:A})}),(0,F.jsx)(`nav`,{"aria-label":`Switch view`,className:`flex shrink-0 items-stretch border-t border-ink-700 bg-ink-900 md:hidden`,children:[[`files`,`Files`,ht],[`diff`,`Diff`,gt],[`terminal`,`Terminal`,_t]].map(([e,t,n])=>(0,F.jsxs)(`button`,{onClick:()=>Se(e),"aria-current":xe===e?`page`:void 0,className:`flex min-h-11 flex-1 flex-col items-center justify-center gap-0.5 py-1 text-[11px] ${xe===e?`text-accent shadow-[inset_0_2px_0_0_var(--color-accent)]`:`text-ink-400`}`,children:[(0,F.jsx)(n,{className:`h-5 w-5`}),t]},e))}),(0,F.jsxs)(`footer`,{className:`flex shrink-0 items-center gap-3 border-t border-ink-700 bg-ink-900 px-3 py-1 text-ink-400`,children:[(0,F.jsx)(`span`,{className:`truncate`,children:n?.display_path}),r?.branch&&(0,F.jsx)(`span`,{className:`text-accent`,children:r.branch}),r?.tracking&&(0,F.jsxs)(`span`,{children:[`↑`,r.tracking.ahead,` ↓`,r.tracking.behind]}),(0,F.jsx)(`span`,{className:`ml-auto`,children:r?(0,F.jsx)(`span`,{className:`text-added`,children:`● live`}):`connecting…`})]})]})}function mn({onClose:e,onOpened:t,canClone:n,cloning:r,onClone:i}){let[a,o]=(0,v.useState)(null),[s,c]=(0,v.useState)(null),[l,u]=(0,v.useState)(null),[d,f]=(0,v.useState)(!1),[p,m]=(0,v.useState)(``),[h,g]=(0,v.useState)(!1),[_,y]=(0,v.useState)(``),[b,x]=(0,v.useState)(0);(0,v.useEffect)(()=>{let e=!1;return O.browse(a??void 0).then(t=>{e||(c(t),u(null))}).catch(t=>{e||u(t instanceof Error?t.message:`could not browse`)}),()=>{e=!0}},[a,b]);let S=e=>o(`${s.path.replace(/\/$/,``)}/${e}`),C=async()=>{if(s){f(!0);try{t(await O.open(s.path))}catch(e){se.error(e instanceof Error?e.message:`could not open`),f(!1)}}},w=async()=>{if(!s)return;let e=p.trim();if(e){g(!0);try{await O.mkdir(s.path,e),m(``),x(e=>e+1)}catch(e){se.error(e instanceof Error?e.message:`could not create folder`)}finally{g(!1)}}};return(0,F.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4`,onClick:e,children:(0,F.jsxs)(`div`,{className:`flex max-h-[80vh] w-[34rem] max-w-full flex-col rounded-md border border-ink-700 bg-ink-900`,onClick:e=>e.stopPropagation(),children:[(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-b border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`span`,{className:`font-medium text-ink-50`,children:`Open a project`}),(0,F.jsx)(`button`,{onClick:e,"aria-label":`close`,className:`ml-auto flex h-6 w-6 items-center justify-center rounded-sm text-ink-400 hover:text-ink-200`,children:(0,F.jsx)(ut,{})})]}),(0,F.jsx)(`div`,{className:`shrink-0 truncate border-b border-ink-700 px-3 py-1.5 text-ink-400`,children:s?.path??`…`}),(0,F.jsxs)(`ul`,{className:`h-72 min-h-0 overflow-y-auto`,children:[s?.parent&&(0,F.jsx)(`li`,{children:(0,F.jsx)(`button`,{onClick:()=>o(s.parent),className:`w-full px-3 py-1 text-left text-ink-400 hover:bg-ink-850`,children:`../`})}),s?.entries.map(e=>(0,F.jsx)(`li`,{children:(0,F.jsxs)(`button`,{onClick:()=>S(e.name),className:`flex w-full items-center gap-2 px-3 py-1 text-left hover:bg-ink-850`,children:[(0,F.jsxs)(`span`,{className:`truncate text-accent`,children:[e.name,`/`]}),e.is_repo&&(0,F.jsx)(`span`,{className:`rounded-sm bg-ink-700 px-1 text-[0.65rem] text-ink-200`,children:`git`})]})},e.name)),s&&s.entries.length===0&&(0,F.jsx)(`li`,{className:`px-3 py-1 text-ink-400`,children:`No sub-folders.`})]}),l&&(0,F.jsx)(`p`,{className:`shrink-0 px-3 py-1 text-removed`,children:l}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`input`,{value:p,onChange:e=>m(e.target.value),onKeyDown:e=>{e.key===`Enter`&&w()},placeholder:`New folder name`,"aria-label":`new folder name`,className:`min-w-0 flex-1 rounded-sm border border-ink-700 bg-ink-950 px-2 py-1 text-ink-50 placeholder:text-ink-400 focus:border-ink-600 focus:outline-none`}),(0,F.jsx)(`button`,{onClick:w,disabled:!s||!p.trim()||h,className:`shrink-0 rounded-sm border border-ink-700 px-2 py-1 text-ink-200 hover:bg-ink-850 disabled:opacity-50`,children:h?`Creating…`:`Create`})]}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`input`,{value:_,onChange:e=>y(e.target.value),onKeyDown:e=>{e.key===`Enter`&&s&&i(s.path,_)},disabled:!n,placeholder:n?`Clone a git URL here`:`git is not installed on the server`,"aria-label":`git URL to clone`,spellCheck:!1,autoCapitalize:`none`,autoCorrect:`off`,className:`min-w-0 flex-1 rounded-sm border border-ink-700 bg-ink-950 px-2 py-1 text-ink-50 placeholder:text-ink-400 focus:border-ink-600 focus:outline-none disabled:opacity-50`}),(0,F.jsx)(`button`,{onClick:()=>s&&i(s.path,_),disabled:!n||!s||!_.trim()||r,title:n?void 0:`the server has no git on its PATH`,className:`shrink-0 rounded-sm border border-ink-700 px-2 py-1 text-ink-200 hover:bg-ink-850 disabled:opacity-50`,children:r?`Cloning…`:`Clone`})]}),(0,F.jsxs)(`div`,{className:`flex shrink-0 items-center gap-2 border-t border-ink-700 px-3 py-2`,children:[(0,F.jsx)(`span`,{className:`truncate text-ink-400`,children:s?s.path:``}),(0,F.jsx)(`button`,{onClick:C,disabled:!s||d,className:`ml-auto shrink-0 rounded-md bg-ink-50 px-3 py-1 font-semibold text-ink-950 hover:bg-white disabled:opacity-50`,children:d?`Opening…`:`Open`})]})]})})}function hn(){return(0,F.jsx)(`div`,{className:`flex h-full items-center justify-center p-6`,children:(0,F.jsxs)(`div`,{className:`flex flex-col items-center gap-3 text-ink-400`,children:[(0,F.jsx)(st,{className:`h-12 w-12 animate-pulse`}),(0,F.jsx)(`span`,{className:`text-[0.72rem] tracking-[0.18em] uppercase`,children:`Loading…`})]})})}function gn({onSuccess:e}){let[t,n]=(0,v.useState)(``),[r,i]=(0,v.useState)(null),[a,o]=(0,v.useState)(!1);return(0,F.jsx)(`div`,{className:`flex h-full items-center justify-center p-6`,children:(0,F.jsxs)(`form`,{onSubmit:async n=>{n.preventDefault(),o(!0),i(null);try{await O.login(t),e()}catch(e){i(e instanceof Error?e.message:`login failed`)}finally{o(!1)}},className:`w-[17rem] max-w-[86vw]`,children:[(0,F.jsx)(st,{className:`mx-auto mb-3 block h-10 w-10`}),(0,F.jsx)(`h1`,{className:`text-center text-lg font-medium tracking-wide text-ink-50`,children:`nightcrow`}),(0,F.jsx)(`p`,{className:`mt-1 mb-5 text-center text-[0.62rem] tracking-[0.18em] text-ink-400 uppercase`,children:`web viewer`}),r&&(0,F.jsx)(`p`,{className:`mb-2.5 text-center text-removed`,children:r}),(0,F.jsx)(`input`,{type:`password`,autoFocus:!0,value:t,onChange:e=>n(e.target.value),placeholder:`password`,className:`mb-2 w-full rounded-md border border-ink-700 bg-ink-900 px-2.5 py-1.5 outline-none placeholder:text-ink-400 focus:border-accent focus:ring-[3px] focus:ring-accent/15`}),(0,F.jsx)(`button`,{type:`submit`,disabled:a,className:`w-full rounded-md bg-ink-50 py-1.5 font-semibold text-ink-950 hover:bg-white disabled:opacity-50`,children:a?`Signing in…`:`Sign in`})]})})}function _n(e,t){return e?`grid-rows-[auto_minmax(0,1fr)_auto_auto] ${t===`terminal`?`md:grid-rows-[auto_minmax(0,0fr)_minmax(0,1fr)_auto]`:t===`files`?`md:grid-rows-[auto_minmax(0,1fr)_minmax(0,0fr)_auto]`:`md:grid-rows-[auto_minmax(0,var(--nc-upper))_minmax(0,var(--nc-lower))_auto]`}`:`grid-rows-[auto_1fr]`}function vn(){let[e,t]=(0,v.useState)(null),[n,r]=(0,v.useState)(`status`),[i,a]=(0,v.useState)(``),[o,s]=(0,v.useState)(!1),[c,l]=(0,v.useState)({kind:`empty`}),[u,d]=(0,v.useState)(`files`),f=(0,v.useRef)(0),p=(0,v.useCallback)(()=>{f.current+=1},[]),[m,h]=(0,v.useState)(!1),{accent:g,next:_,cycle:y,upperPct:b,shell:S,guards:C}=Le(),[w,ee]=(0,v.useState)(!0),T=(0,v.useCallback)(e=>{if(x(e)){t(!1);return}se.error(e instanceof Error?e.message:`request failed`)},[]),E=Ze(),{repos:D,setRepos:te,repo:O,setRepo:ne,hot:k,clockSkewMs:re,reposLoaded:ie,canClone:ae,orderWrites:A,draggingRepo:j,dragOverRepo:oe,onRepoDragStart:ce,onRepoDragMove:M,onRepoDragEnd:N}=qe({authed:e,setAuthed:t,handle:T,resumeTick:E,...C}),P=k?.enabled?k.window_secs*1e3:0;de(void 0,P,re??0);let{status:le}=Je({repo:O,authed:e,resumeTick:E,tab:n,pane:c,setPane:l,handle:T,paneRequestRef:f}),ue=de(le?.files,P,re??0),{maximized:fe,setMaximized:pe,dropMaximized:me}=Qe(O),{commits:he,logDone:ge,logStalled:_e,setLogStalled:ve,commitDrillDown:ye,setCommitDrillDown:be,resetLog:xe,logSentinelRef:Se,visibleCommits:Ce,logPagingPaused:we}=Xe({repo:O,authed:e,tab:n,filter:i,handle:T}),{openDiff:Te,openFile:Ee,openCommit:De,openCommitFileDiff:Oe,openCommitFiles:ke}=Ye({repo:O,handle:T,setPane:l,paneRequestRef:f,setCommitDrillDown:be,setMobileView:d,setPreviewRendered:ee}),{selectOpenedRepo:Ae,closeRepo:je}=$e({repos:D,setRepos:te,setRepo:ne,setPane:l,setTab:r,setPickerOpen:h,dropMaximized:me,handle:T,orderWrites:A}),{busy:Me,start:Ne}=rt(Ae,e===!0);if((0,v.useLayoutEffect)(()=>{p(),be(null),l({kind:`empty`}),xe()},[O,xe,p,be]),e===null)return(0,F.jsx)(hn,{});if(!e)return(0,F.jsx)(gn,{onSuccess:()=>t(!0)});if(!ie)return(0,F.jsx)(hn,{});let Pe=D.find(e=>e.id===O),Fe=i.toLowerCase(),Ie=(le?.files??[]).filter(e=>e.path.toLowerCase().includes(Fe)),Re=(ye?.files??[]).filter(e=>e.path.toLowerCase().includes(Fe)||e.old_path?.toLowerCase().includes(Fe)),ze=new Set(he.slice(0,le?.tracking?.ahead??0).map(e=>e.oid)),Be=fe===`files`;return(0,F.jsxs)(`div`,{className:`nc-fade grid h-full ${_n(O,fe)}`,style:{"--nc-upper":`${b}fr`,"--nc-lower":`${100-b}fr`},children:[(0,F.jsx)(xt,{repos:D,repo:O,setRepo:e=>{ne(e),l({kind:`empty`})},setPane:()=>l({kind:`empty`}),closeRepo:je,setPickerOpen:h,cloning:Me,accent:g,next:_,cycle:y,draggingRepo:j,dragOverRepo:oe,onRepoDragStart:ce,onRepoDragMove:M,onRepoDragEnd:N}),O?(0,F.jsx)(pn,{repo:O,repos:D,current:Pe,status:le,files:Ie,now:ue,hotWindowMs:P,pane:c,setPane:l,tab:n,setTab:r,filter:i,setFilter:a,filterOpen:o,setFilterOpen:s,openDiff:Te,openFile:Ee,openCommit:De,openCommitFileDiff:Oe,openCommitFiles:ke,authed:e,handle:T,...S,filesMax:Be,bumpPaneRequest:p,commits:he,logDone:ge,logStalled:_e,setLogStalled:ve,commitDrillDown:ye,setCommitDrillDown:be,resetLog:xe,logSentinelRef:Se,visibleCommits:Ce,logPagingPaused:we,aheadOids:ze,visibleCommitFiles:Re,previewRendered:w,setPreviewRendered:ee,maximized:fe,setMaximized:pe,mobileView:u,setMobileView:d}):(0,F.jsx)(`div`,{className:`flex items-center justify-center p-6 text-center text-ink-400`,children:(0,F.jsxs)(`span`,{children:[`No repository open. Click`,` `,(0,F.jsx)(`span`,{className:`text-ink-200`,children:`+ open`}),` above to add one.`]})}),m&&(0,F.jsx)(mn,{onClose:()=>h(!1),onOpened:Ae,canClone:ae,cloning:Me,onClone:Ne})]})}var yn={error:7e3,info:5e3,success:5e3},bn={error:`text-removed`,info:`text-accent`,success:`text-added`};function xn(){let[e,t]=(0,v.useState)([]);return(0,v.useEffect)(()=>A(t),[]),e.length===0?null:(0,F.jsx)(`div`,{className:`pointer-events-none fixed right-3 top-3 z-[60] flex w-80 max-w-[calc(100vw-1.5rem)] flex-col gap-2`,"aria-live":`polite`,children:e.map(e=>(0,F.jsx)(Sn,{toast:e},e.id))})}function Sn({toast:e}){let[t,n]=(0,v.useState)(!1);return(0,v.useEffect)(()=>{if(t)return;let n=setTimeout(()=>j(e.id),yn[e.kind]);return()=>clearTimeout(n)},[e.id,e.kind,e.bump,t]),(0,F.jsxs)(`div`,{role:e.kind===`error`?`alert`:`status`,className:`nc-fade pointer-events-auto flex items-start gap-2 rounded-md border border-ink-700 bg-ink-850 px-3 py-2 text-xs shadow-lg`,onMouseEnter:()=>n(!0),onMouseLeave:()=>n(!1),children:[(0,F.jsx)(`span`,{className:`min-w-0 flex-1 break-words ${bn[e.kind]}`,children:e.message}),(0,F.jsx)(`button`,{type:`button`,onClick:()=>j(e.id),"aria-label":`dismiss`,className:`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-ink-400 hover:bg-ink-700 hover:text-ink-200`,children:(0,F.jsx)(ut,{className:`h-3 w-3`})})]})}(0,y.createRoot)(document.getElementById(`root`)).render((0,F.jsxs)(v.StrictMode,{children:[(0,F.jsx)(vn,{}),(0,F.jsx)(xn,{})]}));export{ot as a,se as c,s as d,l as f,ut as i,d as l,ct as n,Ve as o,dt as r,Be as s,vt as t,o as u}; \ No newline at end of file diff --git a/viewer-ui/dist/assets/index-CEPamnLX.css b/viewer-ui/dist/assets/index-CEPamnLX.css new file mode 100644 index 00000000..bc3fb697 --- /dev/null +++ b/viewer-ui/dist/assets/index-CEPamnLX.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:system-ui, sans-serif;--font-mono:ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--radius-sm:.25rem;--radius-md:.375rem;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink-950:#0b0b0d;--color-ink-900:#121215;--color-ink-850:#17171b;--color-ink-800:#1d1d22;--color-ink-700:#2a2a31;--color-ink-600:#3a3a43;--color-ink-400:#6f6f7d;--color-ink-200:#a8a8b5;--color-ink-50:#e6e6ec;--color-accent:#d9a441;--color-added:#4ba36b;--color-removed:#c85f5f}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.-top-px{top:-1px}.top-0{top:0}.top-3{top:calc(var(--spacing) * 3)}.-right-px{right:-1px}.right-3{right:calc(var(--spacing) * 3)}.left-0{left:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.z-\[60\]{z-index:60}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.-my-\[8\.8px\]{margin-block:-8.8px}.my-1{margin-block:var(--spacing)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mr-1{margin-right:var(--spacing)}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.ml-1{margin-left:var(--spacing)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.table{display:table}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-72{height:calc(var(--spacing) * 72)}.h-\[22px\]{height:22px}.h-full{height:100%}.max-h-\[70vh\]{max-height:70vh}.max-h-\[80vh\]{max-height:80vh}.min-h-0{min-height:0}.min-h-9{min-height:calc(var(--spacing) * 9)}.min-h-11{min-height:calc(var(--spacing) * 11)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-10{width:calc(var(--spacing) * 10)}.w-12{width:calc(var(--spacing) * 12)}.w-56{width:calc(var(--spacing) * 56)}.w-80{width:calc(var(--spacing) * 80)}.w-\[17rem\]{width:17rem}.w-\[22px\]{width:22px}.w-\[34rem\]{width:34rem}.w-full{width:100%}.w-max{width:max-content}.max-w-\[6rem\]{max-width:6rem}.max-w-\[9rem\]{max-width:9rem}.max-w-\[80vw\]{max-width:80vw}.max-w-\[86vw\]{max-width:86vw}.max-w-\[calc\(100vw-1\.5rem\)\]{max-width:calc(100vw - 1.5rem)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.min-w-9{min-width:calc(var(--spacing) * 9)}.min-w-full{min-width:100%}.flex-1{flex:1}.flex-none{flex:none}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.rotate-90{rotate:90deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-pulse{animation:var(--animate-pulse)}.cursor-col-resize{cursor:col-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-row-resize{cursor:row-resize}.touch-none{touch-action:none}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-rows-\[auto_1fr\]{grid-template-rows:auto 1fr}.grid-rows-\[auto_minmax\(0\,1fr\)_auto_auto\]{grid-template-rows:auto minmax(0,1fr) auto auto}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-\[1ch\]{gap:1ch}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[20\.7\%\]{border-radius:20.7%}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-accent{border-color:var(--color-accent)}.border-ink-700{border-color:var(--color-ink-700)}.border-ink-800{border-color:var(--color-ink-800)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-added\/10{background-color:#4ba36b1a}@supports (color:color-mix(in lab, red, red)){.bg-added\/10{background-color:color-mix(in oklab, var(--color-added) 10%, transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-ink-50{background-color:var(--color-ink-50)}.bg-ink-700{background-color:var(--color-ink-700)}.bg-ink-800{background-color:var(--color-ink-800)}.bg-ink-850{background-color:var(--color-ink-850)}.bg-ink-900{background-color:var(--color-ink-900)}.bg-ink-900\/40{background-color:#12121566}@supports (color:color-mix(in lab, red, red)){.bg-ink-900\/40{background-color:color-mix(in oklab, var(--color-ink-900) 40%, transparent)}}.bg-ink-950{background-color:var(--color-ink-950)}.bg-removed\/10{background-color:#c85f5f1a}@supports (color:color-mix(in lab, red, red)){.bg-removed\/10{background-color:color-mix(in oklab, var(--color-removed) 10%, transparent)}}.bg-white{background-color:var(--color-white)}.p-1{padding:var(--spacing)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-\[1ch\]{padding-inline:1ch}.px-\[12\.8px\]{padding-inline:12.8px}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-\[8\.8px\]{padding-block:8.8px}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pl-1{padding-left:var(--spacing)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-sans{font-family:var(--font-sans)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.62rem\]{font-size:.62rem}.text-\[0\.65rem\]{font-size:.65rem}.text-\[0\.72rem\]{font-size:.72rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[16px\]{font-size:16px}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.04em\]{--tw-tracking:.04em;letter-spacing:.04em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.text-accent{color:var(--color-accent)}.text-added{color:var(--color-added)}.text-ink-50{color:var(--color-ink-50)}.text-ink-200{color:var(--color-ink-200)}.text-ink-400{color:var(--color-ink-400)}.text-ink-600{color:var(--color-ink-600)}.text-ink-950{color:var(--color-ink-950)}.text-removed{color:var(--color-removed)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.underline\!{text-decoration-line:underline!important}.opacity-60{opacity:.6}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[inset_0_2px_0_0_var\(--color-accent\)\]{--tw-shadow:inset 0 2px 0 0 var(--tw-shadow-color,var(--color-accent));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-accent{--tw-ring-color:var(--color-accent)}.ring-ink-600{--tw-ring-color:var(--color-ink-600)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.ring-inset{--tw-ring-inset:inset}.placeholder\:text-ink-400::placeholder{color:var(--color-ink-400)}@media (hover:hover){.hover\:bg-accent:hover{background-color:var(--color-accent)}.hover\:bg-ink-700:hover{background-color:var(--color-ink-700)}.hover\:bg-ink-850:hover{background-color:var(--color-ink-850)}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:text-accent:hover{color:var(--color-accent)}.hover\:text-ink-200:hover{color:var(--color-ink-200)}.hover\:text-removed:hover{color:var(--color-removed)}}.focus\:border-accent:focus{border-color:var(--color-accent)}.focus\:border-ink-600:focus{border-color:var(--color-ink-600)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-\[3px\]:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus\:ring-accent:focus{--tw-ring-color:var(--color-accent)}.focus\:ring-accent\/15:focus{--tw-ring-color:#d9a44126}@supports (color:color-mix(in lab, red, red)){.focus\:ring-accent\/15:focus{--tw-ring-color:color-mix(in oklab, var(--color-accent) 15%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:bg-ink-700:active{background-color:var(--color-ink-700)}.active\:text-accent:active{color:var(--color-accent)}.disabled\:opacity-50:disabled{opacity:.5}@media (width>=40rem){.sm\:inline{display:inline}}@media (width>=48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-flex{display:inline-flex}.md\:h-6{height:calc(var(--spacing) * 6)}.md\:w-6{width:calc(var(--spacing) * 6)}.md\:flex-1{flex:1}.md\:basis-1\/2{flex-basis:50%}.md\:grid-cols-\[var\(--nc-sidebar\)_1fr\]{grid-template-columns:var(--nc-sidebar) 1fr}.md\:grid-rows-\[auto_minmax\(0\,0fr\)_minmax\(0\,1fr\)_auto\]{grid-template-rows:auto minmax(0,0fr) minmax(0,1fr) auto}.md\:grid-rows-\[auto_minmax\(0\,1fr\)_minmax\(0\,0fr\)_auto\]{grid-template-rows:auto minmax(0,1fr) minmax(0,0fr) auto}.md\:grid-rows-\[auto_minmax\(0\,var\(--nc-upper\)\)_minmax\(0\,var\(--nc-lower\)\)_auto\]{grid-template-rows:auto minmax(0,var(--nc-upper)) minmax(0,var(--nc-lower)) auto}.md\:flex-row{flex-direction:row}.md\:border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.md\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.md\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}}}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;-webkit-text-decoration:overline double underline;-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;-webkit-text-decoration:overline wavy underline;-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;-webkit-text-decoration:overline dotted underline;-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;-webkit-text-decoration:overline dashed underline;-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}html,body,#root{height:100%}html{font-size:14px}body{background:var(--color-ink-950);color:var(--color-ink-50);font-family:var(--font-mono);margin:0;font-size:.85rem;line-height:1.4}button:not(:disabled),[role=button]:not(:disabled){cursor:pointer}*{scrollbar-width:thin;scrollbar-color:var(--color-ink-600) transparent}.nc-markdown{max-width:52rem;font-family:var(--font-sans);color:var(--color-ink-50);line-height:1.6}.nc-markdown h1,.nc-markdown h2,.nc-markdown h3,.nc-markdown h4,.nc-markdown h5,.nc-markdown h6{margin:1.4em 0 .6em;font-weight:600;line-height:1.25}.nc-markdown h1{border-bottom:1px solid var(--color-ink-700);padding-bottom:.3em;font-size:1.6em}.nc-markdown h2{border-bottom:1px solid var(--color-ink-800);padding-bottom:.25em;font-size:1.35em}.nc-markdown h3{font-size:1.15em}.nc-markdown h4{font-size:1em}.nc-markdown h5,.nc-markdown h6{color:var(--color-ink-200);font-size:.9em}.nc-markdown :first-child{margin-top:0}.nc-markdown p,.nc-markdown ul,.nc-markdown ol,.nc-markdown blockquote,.nc-markdown table,.nc-markdown pre{margin:.75em 0}.nc-markdown ul,.nc-markdown ol{padding-left:1.5em}.nc-markdown ul{list-style:outside}.nc-markdown ol{list-style:decimal}.nc-markdown li{margin:.25em 0}.nc-markdown li::marker{color:var(--color-ink-400)}.nc-markdown li:has(>input[type=checkbox]){margin-left:-1.2em;list-style:none}.nc-markdown a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.nc-markdown strong{font-weight:600}.nc-markdown em{font-style:italic}.nc-markdown blockquote{border-left:3px solid var(--color-ink-700);color:var(--color-ink-200);padding-left:1em}.nc-markdown hr{border:0;border-top:1px solid var(--color-ink-700);margin:1.5em 0}.nc-markdown img{max-width:100%}.nc-markdown :not(pre)>code{font-family:var(--font-mono);background:var(--color-ink-800);border-radius:3px;padding:.1em .35em;font-size:.9em}.nc-markdown pre{background:var(--color-ink-850);border:1px solid var(--color-ink-800);border-radius:4px;padding:.9em 1em;overflow-x:auto}.nc-markdown pre code{font-family:var(--font-mono);background:0 0;padding:0;font-size:.85em}.nc-markdown table{border-collapse:collapse;display:block;overflow-x:auto}.nc-markdown th,.nc-markdown td{border:1px solid var(--color-ink-700);text-align:left;padding:.4em .7em}.nc-markdown th{background:var(--color-ink-850);font-weight:600}@keyframes nc-fade-in{0%{opacity:0}to{opacity:1}}.nc-fade{animation:.16s ease-out nc-fade-in}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes pulse{50%{opacity:.5}} diff --git a/viewer-ui/dist/index.html b/viewer-ui/dist/index.html index c1ee50c8..2b897e07 100644 --- a/viewer-ui/dist/index.html +++ b/viewer-ui/dist/index.html @@ -14,8 +14,8 @@ nightcrow - - + +
diff --git a/viewer-ui/src/components/terminal/PanelToolbar.tsx b/viewer-ui/src/components/terminal/PanelToolbar.tsx index 9397cfc5..2c5c8a9d 100644 --- a/viewer-ui/src/components/terminal/PanelToolbar.tsx +++ b/viewer-ui/src/components/terminal/PanelToolbar.tsx @@ -1,10 +1,18 @@ import { FitScreenIcon, MaximizeIcon, PlusIcon } from "../icons"; +import { RecoveryChip } from "./RecoveryChip"; +import { orphanRecovery, type RecoveryByPane } from "../../lib/recovery"; export interface PanelToolbarProps { /** Whether this page's layout is what sets the pane sizes. When it is not, * the button that takes the sizing back appears. */ ownsSize: boolean; maximized: boolean; + /** Every pane's recovery report, and the panes this page still lists. Reports + * for panes it does not — a process that ended while its slot is held for a + * relaunch — have no cell to sit in, so they surface here. */ + recovery: RecoveryByPane; + panes: number[]; + onCancelRecovery: (pane: number) => void; onClaimSize: () => void; onCreate: () => void; onToggleMaximized: () => void; @@ -16,6 +24,9 @@ export interface PanelToolbarProps { export function PanelToolbar({ ownsSize, maximized, + recovery, + panes, + onCancelRecovery, onClaimSize, onCreate, onToggleMaximized, @@ -23,7 +34,15 @@ export function PanelToolbar({ const button = "flex shrink-0 items-center rounded-sm px-1.5 py-0.5 text-ink-400 hover:text-accent"; return ( -
+
+ {orphanRecovery(recovery, panes).map((pane) => ( + onCancelRecovery(pane)} + /> + ))} {!ownsSize && ( + + ); +} diff --git a/viewer-ui/src/components/terminal/StartupSlots.tsx b/viewer-ui/src/components/terminal/StartupSlots.tsx index 98300f7a..db3372eb 100644 --- a/viewer-ui/src/components/terminal/StartupSlots.tsx +++ b/viewer-ui/src/components/terminal/StartupSlots.tsx @@ -40,6 +40,7 @@ export function StartupSlots({ isDragged={false} isDropTarget={false} reorderable={false} + onCancelRecovery={() => {}} onFocus={() => {}} onToggleZoom={() => {}} onClose={() => {}} diff --git a/viewer-ui/src/components/terminal/Terminal.tsx b/viewer-ui/src/components/terminal/Terminal.tsx index ce56a725..53fa0d12 100644 --- a/viewer-ui/src/components/terminal/Terminal.tsx +++ b/viewer-ui/src/components/terminal/Terminal.tsx @@ -10,6 +10,7 @@ import { usePaneDrag } from "../../hooks/terminal/usePaneDrag"; import { useTerminalSocket } from "../../hooks/terminal/useTerminalSocket"; import { useTerminalViews } from "../../hooks/terminal/useTerminalViews"; import { usePaneSizes } from "../../hooks/terminal/usePaneSizes"; +import { usePaneRecovery } from "../../hooks/terminal/usePaneRecovery"; import { useStartupSizes } from "../../hooks/terminal/useStartupSizes"; import { TerminalCell } from "./TerminalCell"; import { StartupSlots } from "./StartupSlots"; @@ -58,6 +59,7 @@ export function TerminalPanel({ // and the child cannot be re-flowed afterwards, so one client at a time // decides it; the rest render the grid they are given. const [ownsSize, setOwnsSize] = useState(true); + const { recovery, setRecovery, cancelRecovery } = usePaneRecovery(socketRef); useTerminalSocket({ repo, @@ -73,6 +75,7 @@ export function TerminalPanel({ setZoomed, setTitles, setOwnsSize, + setRecovery, }); useTerminalViews({ @@ -207,6 +210,9 @@ export function TerminalPanel({ cancelRecovery(pane)} onFocus={() => focusPane(pane)} onToggleZoom={() => setZoomed((z) => (z === pane ? null : pane)) diff --git a/viewer-ui/src/components/terminal/TerminalCell.tsx b/viewer-ui/src/components/terminal/TerminalCell.tsx index 7e063448..d6a7ea5e 100644 --- a/viewer-ui/src/components/terminal/TerminalCell.tsx +++ b/viewer-ui/src/components/terminal/TerminalCell.tsx @@ -1,5 +1,7 @@ import type { CSSProperties } from "react"; import { MaximizeIcon, XIcon } from "../icons"; +import { RecoveryChip } from "./RecoveryChip"; +import type { PaneRecovery } from "../../lib/recovery"; import { TAB_TITLE_MAX_CELLS, truncateCells } from "../../lib/terminalLayout"; interface TerminalCellProps { @@ -13,6 +15,9 @@ interface TerminalCellProps { isDragged: boolean; isDropTarget: boolean; reorderable: boolean; + /** What this pane's plugin last reported about recovering it, if anything. */ + recovery?: PaneRecovery; + onCancelRecovery: () => void; onFocus: () => void; onToggleZoom: () => void; onClose: () => void; @@ -34,6 +39,8 @@ export function TerminalCell({ isDragged, isDropTarget, reorderable, + recovery, + onCancelRecovery, onFocus, onToggleZoom, onClose, @@ -78,6 +85,9 @@ export function TerminalCell({ > {truncateCells(label, TAB_TITLE_MAX_CELLS)} + {recovery && ( + + )} {showZoom && (