@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.
- package/README.md +6 -0
- package/package.json +3 -2
- package/skills/aimux-orchestrator/SKILL.md +93 -0
- package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
- package/skills/aimux-orchestrator/references/prompts.md +57 -0
- package/skills/aimux-orchestrator/references/review.md +18 -0
- package/src/app-runtime/backend-runtime-events.ts +16 -0
- package/src/app-runtime/side-effects.ts +15 -2
- package/src/auto-rename/coordinator.ts +97 -0
- package/src/auto-rename/prompt-capture.ts +211 -0
- package/src/auto-rename/title-runner.ts +96 -0
- package/src/cli/client/daemon-client.ts +16 -0
- package/src/cli/commands/tab/create.ts +236 -124
- package/src/cli/commands/tab/prompt-io.ts +2 -1
- package/src/cli/commands/worker/await.ts +33 -0
- package/src/cli/commands/worker/doctor.ts +129 -0
- package/src/cli/commands/worker/list.ts +25 -0
- package/src/cli/commands/worker/prompt.ts +49 -0
- package/src/cli/commands/worker/run.ts +97 -0
- package/src/cli/commands/worker/shared.ts +255 -0
- package/src/cli/commands/worker/stop.ts +84 -0
- package/src/cli/commands/worktree/remove.ts +29 -9
- package/src/cli/index.ts +21 -9
- package/src/cli/registry.ts +12 -0
- package/src/daemon/daemon.ts +173 -12
- package/src/daemon/session-manager.ts +8 -0
- package/src/daemon/session-registry.ts +22 -1
- package/src/git/worktree.ts +8 -0
- package/src/index.tsx +21 -82
- package/src/input/modes/types.ts +1 -0
- package/src/ipc/manager-protocol.ts +52 -2
- package/src/ipc/protocol.ts +74 -3
- package/src/session-backend/bootstrap.ts +3 -1
- package/src/session-backend/local-session-backend.ts +63 -2
- package/src/session-backend/remote-session-backend.ts +17 -0
- package/src/session-backend/types.ts +7 -0
- package/src/state/reducers/tab-state.ts +14 -1
- package/src/state/session-persistence.ts +4 -0
- package/src/state/types.ts +18 -1
- package/src/state/validation.ts +5 -1
- package/src/terminal-manager/manager-client.ts +33 -1
- 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.
|
|
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.
|
|
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
|
+
}
|