@brimveyn/aimux 1.19.6 → 1.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +6 -0
  2. package/package.json +3 -2
  3. package/skills/aimux-orchestrator/SKILL.md +93 -0
  4. package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
  5. package/skills/aimux-orchestrator/references/prompts.md +57 -0
  6. package/skills/aimux-orchestrator/references/review.md +18 -0
  7. package/src/app-runtime/backend-runtime-events.ts +16 -0
  8. package/src/app-runtime/side-effects.ts +15 -2
  9. package/src/auto-rename/coordinator.ts +97 -0
  10. package/src/auto-rename/prompt-capture.ts +211 -0
  11. package/src/auto-rename/title-runner.ts +96 -0
  12. package/src/cli/client/daemon-client.ts +16 -0
  13. package/src/cli/commands/tab/create.ts +236 -124
  14. package/src/cli/commands/tab/prompt-io.ts +2 -1
  15. package/src/cli/commands/worker/await.ts +33 -0
  16. package/src/cli/commands/worker/doctor.ts +129 -0
  17. package/src/cli/commands/worker/list.ts +25 -0
  18. package/src/cli/commands/worker/prompt.ts +49 -0
  19. package/src/cli/commands/worker/run.ts +97 -0
  20. package/src/cli/commands/worker/shared.ts +255 -0
  21. package/src/cli/commands/worker/stop.ts +84 -0
  22. package/src/cli/commands/worktree/remove.ts +29 -9
  23. package/src/cli/index.ts +21 -9
  24. package/src/cli/registry.ts +12 -0
  25. package/src/daemon/daemon.ts +173 -12
  26. package/src/daemon/session-manager.ts +8 -0
  27. package/src/daemon/session-registry.ts +22 -1
  28. package/src/git/worktree.ts +8 -0
  29. package/src/index.tsx +21 -82
  30. package/src/input/modes/types.ts +1 -0
  31. package/src/ipc/manager-protocol.ts +52 -2
  32. package/src/ipc/protocol.ts +74 -3
  33. package/src/session-backend/bootstrap.ts +3 -1
  34. package/src/session-backend/local-session-backend.ts +63 -2
  35. package/src/session-backend/remote-session-backend.ts +17 -0
  36. package/src/session-backend/types.ts +7 -0
  37. package/src/state/reducers/tab-state.ts +14 -1
  38. package/src/state/session-persistence.ts +4 -0
  39. package/src/state/types.ts +18 -1
  40. package/src/state/validation.ts +5 -1
  41. package/src/terminal-manager/manager-client.ts +33 -1
  42. package/src/terminal-manager/terminal-manager.ts +9 -0
package/README.md CHANGED
@@ -157,6 +157,8 @@ The help modal reflects the resolved keymap, so it includes your overrides.
157
157
 
158
158
  ```bash
159
159
  aimux
160
+ aimux worker doctor
161
+ aimux worker run --name investigate --assistant claude "inspect this repository"
160
162
  aimux version
161
163
  aimux doctor
162
164
  aimux update
@@ -166,6 +168,10 @@ aimux restart-terminal-manager
166
168
 
167
169
  See [`docs/reference/cli.md`](docs/reference/cli.md) for behavior details.
168
170
 
171
+ For agent orchestration, prefer the named `aimux worker` commands. They combine
172
+ isolated worktree creation, prompt dispatch, authoritative turn waiting, fleet
173
+ inspection, and guarded cleanup without shell wrappers or `jq`.
174
+
169
175
  ## Runtime Model
170
176
 
171
177
  `aimux` is split into:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.19.6",
3
+ "version": "1.20.1",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, Kimi side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -34,6 +34,7 @@
34
34
  ],
35
35
  "files": [
36
36
  "src",
37
+ "skills",
37
38
  "README.md",
38
39
  "LICENSE"
39
40
  ],
@@ -65,7 +66,7 @@
65
66
  "bump:terminal_manager": "bun run scripts/bump-protocol.ts terminal-manager"
66
67
  },
67
68
  "dependencies": {
68
- "@brimveyn/aimux-config": "0.8.3",
69
+ "@brimveyn/aimux-config": "0.8.5",
69
70
  "@opentui/core": "^0.1.90",
70
71
  "@opentui/react": "^0.1.90",
71
72
  "@resvg/resvg-wasm": "^2.6.2",
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: aimux-orchestrator
3
+ description: Orchestrate implementation plans with parallel Claude, Codex, OpenCode, Grok, or Kimi workers through the aimux worker CLI. Use when an agent must decompose a plan, launch isolated workers, supervise questions and failures, review each worker's diff and checks, integrate accepted work, and clean up safely.
4
+ ---
5
+
6
+ # aimux Orchestrator
7
+
8
+ Own the plan and quality gate. Let aimux own worker transport and lifecycle.
9
+
10
+ ## Preflight
11
+
12
+ Run once:
13
+
14
+ ```bash
15
+ aimux worker doctor
16
+ aimux worker --help
17
+ ```
18
+
19
+ Stop if the doctor reports missing worker capabilities, the wrong workspace, or
20
+ an unavailable assistant. Restart/update aimux as instructed; do not fall back
21
+ to screen scraping or the legacy shell wrappers.
22
+
23
+ ## Orchestration loop
24
+
25
+ 1. Parse the plan into independently reviewable units and dependencies.
26
+ 2. Record units in a ledger. Copy `assets/ledger.template.md` for multi-unit work.
27
+ 3. Select only units whose dependencies are accepted.
28
+ 4. Launch no more workers than you can review:
29
+
30
+ ```bash
31
+ aimux worker run \
32
+ --name feat-auth \
33
+ --assistant claude \
34
+ --prompt-file /tmp/feat-auth.md \
35
+ --detach
36
+ ```
37
+
38
+ 5. Inspect the fleet with `aimux worker list`.
39
+ 6. Await a detached worker with `aimux worker await feat-auth`.
40
+ 7. If a worker asks a question, answer only when the plan already determines the
41
+ answer:
42
+
43
+ ```bash
44
+ aimux worker prompt feat-auth --prompt-file /tmp/answer.md
45
+ ```
46
+
47
+ Escalate design choices, irreversible operations, deployments, pushes,
48
+ migrations, spending, and external communication to the human.
49
+
50
+ 8. Review the worktree path returned in the worker JSON. Read the diff and run
51
+ the repository's required tests, typecheck, lint, and build. Worker claims
52
+ are not evidence.
53
+ 9. Accept, request a precise correction with `worker prompt`, or escalate.
54
+ 10. After integration, close and clean up:
55
+
56
+ ```bash
57
+ aimux worker stop feat-auth --cleanup-worktree
58
+ ```
59
+
60
+ ## Isolation
61
+
62
+ `worker run` creates a fresh worktree by default. Keep that default when writes
63
+ may overlap or scope is uncertain.
64
+
65
+ - Use `--worktree <id>` only for workers intentionally sharing one review unit.
66
+ - Use `--no-worktree` only for sequential work or provably disjoint writes in
67
+ the active tree.
68
+ - Review co-located workers together because their diff is shared.
69
+
70
+ ## Dispatch rules
71
+
72
+ - Give each worker one bounded unit with goal, scope, exclusions, acceptance
73
+ criteria, repository conventions, and required verification.
74
+ - Read `references/prompts.md` when drafting task or correction prompts.
75
+ - Use provider defaults unless the plan or user requires a model. Set
76
+ `--model`/`--effort` only when `worker doctor` says the assistant supports the
77
+ control.
78
+ - Prefer `--prompt-file` for multiline prompts; it avoids shell quoting hazards.
79
+ - Use `--detach` for parallel launch. Without it, `worker run` waits and returns
80
+ a completed/question/timeout/error outcome.
81
+
82
+ ## Quality and safety
83
+
84
+ Read `references/review.md` before accepting the first unit.
85
+
86
+ - `completed` means the turn ended, not that the change is correct.
87
+ - Never accept a failing or unrun verification gate.
88
+ - Cap correction attempts at three; then escalate with evidence.
89
+ - Never use `--force` cleanup unless the human authorized discarding dirty work.
90
+ - Keep the ledger truthful after every dispatch, outcome, review, and integration.
91
+
92
+ The CLI is the command reference. Use `aimux worker <verb> --help` instead of
93
+ copying flags or JSON shapes into this skill.
@@ -0,0 +1,18 @@
1
+ # Orchestration ledger
2
+
3
+ Base: `<branch / sha>`
4
+
5
+ | unit | goal | deps | status | worker | worktree / branch | attempts | review evidence |
6
+ | ---- | ---- | ---- | ------- | ------ | ----------------- | -------: | --------------- |
7
+ | 1.1 | | — | pending | | | 0 | |
8
+
9
+ Statuses: `pending`, `dispatched`, `in-review`, `needs-changes`, `accepted`,
10
+ `blocked`, `escalated`.
11
+
12
+ ## Decisions
13
+
14
+ - `<date>` — `<decision and reason>`
15
+
16
+ ## Human questions
17
+
18
+ - [ ] `<unit>` — `<decision needed, evidence, and options>`
@@ -0,0 +1,57 @@
1
+ # Worker prompts
2
+
3
+ ## Task
4
+
5
+ ```markdown
6
+ # Task: <id> — <title>
7
+
8
+ ## Goal
9
+
10
+ <observable outcome>
11
+
12
+ ## Scope
13
+
14
+ - In: <files or subsystem>
15
+ - Out: <explicit exclusions>
16
+
17
+ ## Requirements
18
+
19
+ 1. <behavior>
20
+ 2. <edge case>
21
+
22
+ ## Acceptance
23
+
24
+ - <machine-checkable criterion>
25
+
26
+ ## Verification
27
+
28
+ Run the repository's tests, typecheck, lint, and build commands that apply.
29
+ Report changed files, commands run, results, and remaining risks.
30
+ ```
31
+
32
+ Tell the worker to read repository instructions before editing. Do not ask it to
33
+ merge, push, deploy, or clean up its worktree.
34
+
35
+ ## Correction
36
+
37
+ ```markdown
38
+ # Correction for <id>
39
+
40
+ The review found:
41
+
42
+ - <specific failing behavior with file/test evidence>
43
+
44
+ Required change:
45
+
46
+ - <bounded correction>
47
+
48
+ Do not broaden scope. Re-run:
49
+
50
+ - <exact verification commands>
51
+ ```
52
+
53
+ ## Answer
54
+
55
+ Answer the exact question and restate any relevant boundary. If the question
56
+ introduces a product decision or risky action not settled by the plan, escalate
57
+ instead of improvising.
@@ -0,0 +1,18 @@
1
+ # Worker review gate
2
+
3
+ Review inside the returned worktree path.
4
+
5
+ 1. Read repository instructions and the dispatched acceptance criteria.
6
+ 2. Inspect status and the complete diff against the unit's base.
7
+ 3. Confirm every changed hunk belongs to the requested scope.
8
+ 4. Check correctness, edge cases, error paths, tests, debug leftovers, secrets,
9
+ stubs, and repository conventions.
10
+ 5. Run the required test, typecheck, lint, format-check, and build commands
11
+ yourself.
12
+ 6. Record evidence in the ledger.
13
+
14
+ Accept only when the behavior and verification pass. If a check cannot run,
15
+ record why and escalate when it prevents a trustworthy decision.
16
+
17
+ Request changes with concrete evidence: location, observed behavior, expected
18
+ behavior, and the exact gate to rerun. Do not send a vague "fix the tests" prompt.
@@ -239,6 +239,11 @@ export function bindBackendRuntimeEvents({
239
239
  const current = appStore.getState()
240
240
  if (current.tabs.some((t) => t.id === tab.id)) {
241
241
  logInputDebug('app.backend.event.tabAdded.skipDuplicate', { sessionId, tabId: tab.id })
242
+ dispatch({
243
+ autoRenameStatus: tab.autoRenameStatus,
244
+ tabId: tab.id,
245
+ type: 'update-tab-metadata',
246
+ })
242
247
  return
243
248
  }
244
249
  if (current.currentSessionId !== sessionId) {
@@ -253,12 +258,22 @@ export function bindBackendRuntimeEvents({
253
258
  dispatch({ tab, type: 'add-tab' })
254
259
  }
255
260
 
261
+ const handleTabMetadataUpdated = (
262
+ sessionId: string,
263
+ tabId: string,
264
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
265
+ ) => {
266
+ if (appStore.getState().currentSessionId !== sessionId) return
267
+ dispatch({ ...patch, tabId, type: 'update-tab-metadata' })
268
+ }
269
+
256
270
  backend.on('render', handleRender)
257
271
  backend.on('exit', handleExit)
258
272
  backend.on('error', handleError)
259
273
  backend.on('sessionActivity', handleSessionActivity)
260
274
  backend.on('tabActivity', handleTabActivity)
261
275
  backend.on('tabAdded', handleTabAdded)
276
+ backend.on('tabMetadataUpdated', handleTabMetadataUpdated)
262
277
  backend.on('workspaceCreateRequested', handleWorkspaceCreateRequested)
263
278
  backend.on('workspaceSwitchRequested', handleWorkspaceSwitchRequested)
264
279
  backend.on('workspaceCloseRequested', handleWorkspaceCloseRequested)
@@ -274,6 +289,7 @@ export function bindBackendRuntimeEvents({
274
289
  backend.off('sessionActivity', handleSessionActivity)
275
290
  backend.off('tabActivity', handleTabActivity)
276
291
  backend.off('tabAdded', handleTabAdded)
292
+ backend.off('tabMetadataUpdated', handleTabMetadataUpdated)
277
293
  backend.off('workspaceCreateRequested', handleWorkspaceCreateRequested)
278
294
  backend.off('workspaceSwitchRequested', handleWorkspaceSwitchRequested)
279
295
  backend.off('workspaceCloseRequested', handleWorkspaceCloseRequested)
@@ -329,7 +329,8 @@ export function startTabSession(
329
329
  tab: Pick<TabSession, 'id' | 'assistant' | 'title' | 'command' | 'worktreeId'>,
330
330
  cols: number,
331
331
  rows: number,
332
- cwd?: string
332
+ cwd?: string,
333
+ autoRenameCandidate = true
333
334
  ): void {
334
335
  logInputDebug('app.tab.start.request', {
335
336
  cols,
@@ -357,6 +358,7 @@ export function startTabSession(
357
358
  backend.createSession({
358
359
  args,
359
360
  assistant: tab.assistant,
361
+ autoRenameCandidate,
360
362
  cols,
361
363
  command: executable,
362
364
  cwd,
@@ -613,7 +615,8 @@ function startExistingTab(ctx: SideEffectContext, tab: TabSession): void {
613
615
  tab,
614
616
  state.layout.terminalCols,
615
617
  state.layout.terminalRows,
616
- getTabProjectPath(ctx, tab)
618
+ getTabProjectPath(ctx, tab),
619
+ false
617
620
  )
618
621
  }
619
622
 
@@ -874,6 +877,16 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
874
877
  handleRenameSessionEffect(state.sessions, dispatch, effect.sessionId, effect.name)
875
878
  return
876
879
  }
880
+ case 'rename-tab': {
881
+ dispatch({
882
+ autoRenameStatus: 'attempted',
883
+ tabId: effect.tabId,
884
+ title: effect.title,
885
+ type: 'rename-tab',
886
+ })
887
+ backend.renameTab(effect.tabId, effect.title)
888
+ return
889
+ }
877
890
  case 'split-pane': {
878
891
  const sourceTab =
879
892
  effect.sourceTabId != null && effect.sourceTabId !== ''
@@ -0,0 +1,97 @@
1
+ import type { AssistantId } from '../state/types'
2
+
3
+ import { isSupportedProvider } from '../auto-commit/headless-commands'
4
+ import { PromptCapture } from './prompt-capture'
5
+ import { generateTabTitle, type TitleSpawnFn } from './title-runner'
6
+
7
+ export interface AutoRenameConfigSnapshot {
8
+ enabled: boolean
9
+ timeoutMs: number
10
+ models: Partial<Record<string, string>>
11
+ }
12
+
13
+ export interface AutoRenameTab {
14
+ id: string
15
+ assistant: AssistantId
16
+ title: string
17
+ autoRenameStatus?: 'eligible' | 'attempted'
18
+ }
19
+
20
+ export interface AutoRenameCoordinatorOptions {
21
+ config: AutoRenameConfigSnapshot
22
+ getTab: (tabId: string) => AutoRenameTab | undefined
23
+ updateTab: (
24
+ tabId: string,
25
+ patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' }
26
+ ) => void
27
+ spawn?: TitleSpawnFn
28
+ }
29
+
30
+ export function initialAutoRenameStatus(
31
+ config: AutoRenameConfigSnapshot,
32
+ assistant: AssistantId,
33
+ candidate: boolean
34
+ ): 'eligible' | undefined {
35
+ return config.enabled && candidate && isSupportedProvider(assistant) ? 'eligible' : undefined
36
+ }
37
+
38
+ export class AutoRenameCoordinator {
39
+ private readonly captures = new Map<string, PromptCapture>()
40
+ private readonly controllers = new Map<string, AbortController>()
41
+
42
+ constructor(private readonly options: AutoRenameCoordinatorOptions) {}
43
+
44
+ register(tab: AutoRenameTab): void {
45
+ if (tab.autoRenameStatus === 'eligible' && !this.captures.has(tab.id)) {
46
+ this.captures.set(tab.id, new PromptCapture())
47
+ }
48
+ }
49
+
50
+ unregister(tabId: string): void {
51
+ this.controllers.get(tabId)?.abort()
52
+ this.controllers.delete(tabId)
53
+ this.captures.delete(tabId)
54
+ }
55
+
56
+ manualRename(tabId: string): void {
57
+ this.unregister(tabId)
58
+ }
59
+
60
+ observeWrite(tabId: string, input: string): void {
61
+ const tab = this.options.getTab(tabId)
62
+ if (!tab || tab.autoRenameStatus !== 'eligible') return
63
+ let capture = this.captures.get(tabId)
64
+ if (!capture) {
65
+ capture = new PromptCapture()
66
+ this.captures.set(tabId, capture)
67
+ }
68
+ const result = capture.feed(input)
69
+ if (result.type === 'pending') return
70
+
71
+ this.captures.delete(tabId)
72
+ this.options.updateTab(tabId, { autoRenameStatus: 'attempted' })
73
+ const prompt = result.prompt
74
+ if (prompt === null) return
75
+ const controller = new AbortController()
76
+ this.controllers.set(tabId, controller)
77
+ const originalTitle = tab.title
78
+
79
+ void (async () => {
80
+ const title = await generateTabTitle({
81
+ firstPrompt: prompt,
82
+ model: this.options.config.models[tab.assistant],
83
+ provider: tab.assistant,
84
+ signal: controller.signal,
85
+ spawn: this.options.spawn,
86
+ timeoutMs: this.options.config.timeoutMs,
87
+ })
88
+ if (this.controllers.get(tabId) !== controller) return
89
+ this.controllers.delete(tabId)
90
+ if (title == null || title === '') return
91
+ const current = this.options.getTab(tabId)
92
+ if (!current || current.autoRenameStatus !== 'attempted' || current.title !== originalTitle)
93
+ return
94
+ this.options.updateTab(tabId, { autoRenameStatus: 'attempted', title })
95
+ })()
96
+ }
97
+ }
@@ -0,0 +1,211 @@
1
+ const MAX_PROMPT_LENGTH = 8_000
2
+
3
+ export type PromptCaptureResult = { type: 'pending' } | { type: 'submitted'; prompt: string | null }
4
+
5
+ export class PromptCapture {
6
+ private chars: string[] = []
7
+ private cursor = 0
8
+ private escapeBuffer = ''
9
+ private bracketedPaste = false
10
+ private unreliable = false
11
+
12
+ feed(input: string): PromptCaptureResult {
13
+ for (const char of input) {
14
+ if (this.escapeBuffer !== '') {
15
+ this.feedEscape(char)
16
+ continue
17
+ }
18
+
19
+ if (char === '\x1b') {
20
+ this.escapeBuffer = char
21
+ continue
22
+ }
23
+
24
+ if (this.bracketedPaste) {
25
+ this.insert(char === '\r' ? '\n' : char)
26
+ continue
27
+ }
28
+
29
+ if (char === '\r' || char === '\n') {
30
+ if (this.unreliable) {
31
+ this.reset()
32
+ return { prompt: null, type: 'submitted' }
33
+ }
34
+ const prompt = this.value().trim()
35
+ this.reset()
36
+ if (prompt !== '') return { prompt, type: 'submitted' }
37
+ continue
38
+ }
39
+
40
+ if (char === '\x7f' || char === '\b') {
41
+ this.backspace()
42
+ continue
43
+ }
44
+ if (char === '\x01') {
45
+ this.cursor = 0
46
+ continue
47
+ }
48
+ if (char === '\x05') {
49
+ this.cursor = this.chars.length
50
+ continue
51
+ }
52
+ if (char === '\x02') {
53
+ this.cursor = Math.max(0, this.cursor - 1)
54
+ continue
55
+ }
56
+ if (char === '\x06') {
57
+ this.cursor = Math.min(this.chars.length, this.cursor + 1)
58
+ continue
59
+ }
60
+ if (char === '\x04') {
61
+ if (this.cursor < this.chars.length) this.chars.splice(this.cursor, 1)
62
+ continue
63
+ }
64
+ if (char === '\x0b') {
65
+ this.chars.splice(this.cursor)
66
+ continue
67
+ }
68
+ if (char === '\x15') {
69
+ this.chars = []
70
+ this.cursor = 0
71
+ continue
72
+ }
73
+ if (char === '\x17') {
74
+ this.deleteWord()
75
+ continue
76
+ }
77
+ if (char === '\x03') {
78
+ this.reset()
79
+ continue
80
+ }
81
+ if (char === '\x0c') continue
82
+ if (char < ' ' || char === '\x7f') {
83
+ this.invalidateCurrentSubmission()
84
+ continue
85
+ }
86
+ this.insert(char)
87
+ }
88
+ return { type: 'pending' }
89
+ }
90
+
91
+ reset(): void {
92
+ this.chars = []
93
+ this.cursor = 0
94
+ this.escapeBuffer = ''
95
+ this.bracketedPaste = false
96
+ this.unreliable = false
97
+ }
98
+
99
+ private value(): string {
100
+ return this.chars.join('').slice(0, MAX_PROMPT_LENGTH)
101
+ }
102
+
103
+ private insert(char: string): void {
104
+ if (this.chars.length >= MAX_PROMPT_LENGTH) return
105
+ this.chars.splice(this.cursor, 0, char)
106
+ this.cursor++
107
+ }
108
+
109
+ private backspace(): void {
110
+ if (this.cursor === 0) return
111
+ this.chars.splice(this.cursor - 1, 1)
112
+ this.cursor--
113
+ }
114
+
115
+ private deleteWord(): void {
116
+ while (this.cursor > 0 && /\s/u.test(this.chars[this.cursor - 1] ?? '')) this.backspace()
117
+ while (this.cursor > 0 && !/\s/u.test(this.chars[this.cursor - 1] ?? '')) this.backspace()
118
+ }
119
+
120
+ private feedEscape(char: string): void {
121
+ this.escapeBuffer += char
122
+ const sequence = this.escapeBuffer
123
+
124
+ if (
125
+ sequence.length === 2 &&
126
+ sequence !== '\x1b[' &&
127
+ sequence !== '\x1b]' &&
128
+ sequence !== '\x1bO'
129
+ ) {
130
+ this.escapeBuffer = ''
131
+ this.invalidateCurrentSubmission()
132
+ return
133
+ }
134
+
135
+ if (sequence.startsWith('\x1b]')) {
136
+ if (char === '\x07' || sequence.endsWith('\x1b\\')) this.escapeBuffer = ''
137
+ return
138
+ }
139
+
140
+ if (sequence.startsWith('\x1bO')) {
141
+ if (sequence.length < 3) return
142
+ this.escapeBuffer = ''
143
+ this.applyCursorSequence(sequence.at(-1) ?? '')
144
+ return
145
+ }
146
+
147
+ if (!sequence.startsWith('\x1b[')) return
148
+ if (sequence.length <= 2) return
149
+ if (!/[\x40-\x7e]$/u.test(char)) return
150
+
151
+ this.escapeBuffer = ''
152
+ switch (sequence) {
153
+ case '\x1b[200~':
154
+ this.bracketedPaste = true
155
+ return
156
+ case '\x1b[201~':
157
+ this.bracketedPaste = false
158
+ return
159
+ case '\x1b[1~':
160
+ this.cursor = 0
161
+ return
162
+ case '\x1b[4~':
163
+ this.cursor = this.chars.length
164
+ return
165
+ case '\x1b[3~':
166
+ if (this.cursor < this.chars.length) this.chars.splice(this.cursor, 1)
167
+ return
168
+ }
169
+
170
+ const final = sequence.at(-1) ?? ''
171
+ const parameters = sequence.slice(2, -1)
172
+ if ((final === 'C' || final === 'D') && parameters.includes(';')) {
173
+ this.invalidateCurrentSubmission()
174
+ return
175
+ }
176
+ const primaryParameter = parameters.split(';')[0]
177
+ const parsedCount = Number.parseInt(primaryParameter ?? '', 10)
178
+ const count = Number.isFinite(parsedCount) ? Math.max(1, parsedCount) : 1
179
+ this.applyCursorSequence(final, count)
180
+ }
181
+
182
+ private applyCursorSequence(final: string, count = 1): void {
183
+ switch (final) {
184
+ case 'A':
185
+ case 'B':
186
+ // History navigation replaces the editor buffer with content that was
187
+ // never sent through this input stream. Skip this submission rather
188
+ // than generating a title from an inaccurate reconstruction.
189
+ this.invalidateCurrentSubmission()
190
+ return
191
+ case 'D':
192
+ this.cursor = Math.max(0, this.cursor - count)
193
+ return
194
+ case 'C':
195
+ this.cursor = Math.min(this.chars.length, this.cursor + count)
196
+ return
197
+ case 'H':
198
+ this.cursor = 0
199
+ return
200
+ case 'F':
201
+ this.cursor = this.chars.length
202
+ return
203
+ }
204
+ }
205
+
206
+ private invalidateCurrentSubmission(): void {
207
+ this.chars = []
208
+ this.cursor = 0
209
+ this.unreliable = true
210
+ }
211
+ }