@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/src/index.tsx
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { createCliRenderer } from '@opentui/core'
|
|
3
|
-
import { createRoot } from '@opentui/react'
|
|
4
|
-
|
|
5
|
-
import { App } from './app'
|
|
6
|
-
import { loadUserConfig } from './config/loader'
|
|
7
2
|
import { runDaemon } from './daemon/daemon'
|
|
8
3
|
import { getRuntimeProfile } from './daemon/runtime-paths'
|
|
9
4
|
import { logDebug } from './debug/input-log'
|
|
@@ -12,8 +7,6 @@ import { runRestartDaemon } from './restart-daemon'
|
|
|
12
7
|
import { runRestartTerminalManager } from './restart-terminal-manager'
|
|
13
8
|
import { createSessionBackend } from './session-backend/bootstrap'
|
|
14
9
|
import { runTerminalManager } from './terminal-manager/terminal-manager'
|
|
15
|
-
import { BreakingUpdateScreen } from './ui/breaking-update-screen'
|
|
16
|
-
import { setHostPalette } from './ui/host-palette'
|
|
17
10
|
import { runUpdate } from './update'
|
|
18
11
|
|
|
19
12
|
const command = process.argv[2]
|
|
@@ -22,7 +15,7 @@ const runtimeProfile = getRuntimeProfile()
|
|
|
22
15
|
// CLI control plane (docs/reference/cli.md). Branch BEFORE the UI bootstrap so
|
|
23
16
|
// `aimux tab list` from a non-TTY shell never spins up the React renderer.
|
|
24
17
|
// Dynamic import keeps the CLI code out of the UI's cold-start cost.
|
|
25
|
-
const CLI_GROUPS = new Set(['tab', 'workspace', 'worktree'])
|
|
18
|
+
const CLI_GROUPS = new Set(['tab', 'workspace', 'worktree', 'worker'])
|
|
26
19
|
if (typeof command === 'string' && CLI_GROUPS.has(command)) {
|
|
27
20
|
const { runCli } = await import('./cli')
|
|
28
21
|
process.exit(await runCli(process.argv.slice(2)))
|
|
@@ -61,81 +54,27 @@ if (command === 'terminal-manager') {
|
|
|
61
54
|
}
|
|
62
55
|
|
|
63
56
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'aimux — terminal multiplexer for AI CLIs',
|
|
67
|
-
'',
|
|
68
|
-
'Two surfaces:',
|
|
69
|
-
' • Interactive TUI (no args): drive assistants side-by-side in one window.',
|
|
70
|
-
' • CLI control plane (`aimux <group> <verb>`): script-friendly, JSON output,',
|
|
71
|
-
' designed to be driven by another agent or a shell pipeline.',
|
|
72
|
-
'',
|
|
73
|
-
'Interactive',
|
|
74
|
-
' aimux Start the TUI in the current profile',
|
|
75
|
-
'',
|
|
76
|
-
'CLI control plane (JSON on stdout, human-readable errors on stderr)',
|
|
77
|
-
' aimux tab list | create | send | focus | close | snapshot | tail | wait',
|
|
78
|
-
' aimux workspace list | show | create | switch | close',
|
|
79
|
-
' aimux worktree list | create | remove',
|
|
80
|
-
'',
|
|
81
|
-
' aimux <group> --help List verbs in that group',
|
|
82
|
-
' aimux <group> <verb> --help Show flags, args, and exit codes',
|
|
83
|
-
'',
|
|
84
|
-
'Common CLI verbs at a glance',
|
|
85
|
-
' aimux tab list Enumerate tabs (+ activeTabId)',
|
|
86
|
-
' aimux tab create --assistant <id> [--title …] Spawn claude / codex / opencode / grok / kimi / terminal / …',
|
|
87
|
-
' aimux tab send <tabId> [text] [--enter|--keys|--stdin] Type, chord, or paste into a tab',
|
|
88
|
-
' aimux tab focus <tabId> Bring a tab to the foreground',
|
|
89
|
-
' aimux tab close <tabId> Terminate a tab',
|
|
90
|
-
' aimux tab snapshot <tabId> [--tail N] [--format …] Capture the screen (json | text)',
|
|
91
|
-
' aimux tab tail <tabId> [--follow-status] […] NDJSON stream of renders / status',
|
|
92
|
-
' aimux tab wait <tabId> --status <idle|working|waiting-input> Block on an activity state',
|
|
93
|
-
' aimux workspace show Dump the active workspace + worktrees',
|
|
94
|
-
' aimux workspace create <name> [--project P] [--switch [--wait]]',
|
|
95
|
-
' aimux workspace switch <ws> [--wait --timeout N] Move the running UI to another workspace',
|
|
96
|
-
' aimux worktree create --name <n> [--branch B --base R]',
|
|
97
|
-
' aimux worktree remove <id|path> [--force]',
|
|
98
|
-
'',
|
|
99
|
-
'Shared flags (accepted by every CLI verb)',
|
|
100
|
-
' --workspace <id|name> Target a specific workspace (default: active)',
|
|
101
|
-
' --profile <name> Runtime profile (also settable via AIMUX_PROFILE)',
|
|
102
|
-
' --json Reserved; JSON is always on',
|
|
103
|
-
'',
|
|
104
|
-
'Maintenance',
|
|
105
|
-
' aimux update Update aimux to the latest published version',
|
|
106
|
-
' aimux doctor Diagnose setup (paths, sockets, PTY, integrations)',
|
|
107
|
-
' aimux restart-daemon Restart the IPC daemon (keeps live workspaces)',
|
|
108
|
-
' aimux restart-terminal-manager Restart terminal-manager (kills live workspaces)',
|
|
109
|
-
' aimux --version Print the installed version',
|
|
110
|
-
'',
|
|
111
|
-
'Exit codes',
|
|
112
|
-
' 0 success',
|
|
113
|
-
' 2 usage error (bad flags, unknown command, missing argument)',
|
|
114
|
-
' 3 runtime error (server replied with error, command failed)',
|
|
115
|
-
' 4 daemon unreachable (socket missing and autostart failed)',
|
|
116
|
-
' 124 timeout (tab wait, tab tail --timeout, workspace switch --wait)',
|
|
117
|
-
'',
|
|
118
|
-
'Env',
|
|
119
|
-
' AIMUX_PROFILE Runtime profile (state dir, socket paths)',
|
|
120
|
-
'',
|
|
121
|
-
'Recipes for agents',
|
|
122
|
-
' # spawn Claude, wait for idle, dump the last 40 non-blank lines',
|
|
123
|
-
' TAB=$(aimux tab create --assistant claude --title fixup | jq -r .tabId)',
|
|
124
|
-
' aimux tab send "$TAB" "explain this repo" --enter',
|
|
125
|
-
' aimux tab wait "$TAB" --status idle --timeout 60000',
|
|
126
|
-
' aimux tab snapshot "$TAB" --tail 40 --format text',
|
|
127
|
-
'',
|
|
128
|
-
' # send a control chord (Ctrl-C then Esc) using vim-style notation',
|
|
129
|
-
' aimux tab send "$TAB" "<C-c><Esc>" --keys',
|
|
130
|
-
'',
|
|
131
|
-
' # follow a tab as NDJSON, one render (or tabStatus) per line',
|
|
132
|
-
' aimux tab tail "$TAB" --rate-limit-ms 100 --follow-status',
|
|
133
|
-
'',
|
|
134
|
-
].join('\n')
|
|
135
|
-
)
|
|
136
|
-
process.exit(0)
|
|
57
|
+
const { runCli } = await import('./cli')
|
|
58
|
+
process.exit(await runCli([]))
|
|
137
59
|
}
|
|
138
60
|
|
|
61
|
+
const [
|
|
62
|
+
{ createCliRenderer },
|
|
63
|
+
{ createRoot },
|
|
64
|
+
{ App },
|
|
65
|
+
{ loadUserConfig },
|
|
66
|
+
{ BreakingUpdateScreen },
|
|
67
|
+
{ setHostPalette },
|
|
68
|
+
] = await Promise.all([
|
|
69
|
+
import('@opentui/core'),
|
|
70
|
+
import('@opentui/react'),
|
|
71
|
+
import('./app'),
|
|
72
|
+
import('./config/loader'),
|
|
73
|
+
import('./ui/breaking-update-screen'),
|
|
74
|
+
import('./ui/host-palette'),
|
|
75
|
+
])
|
|
76
|
+
const resolvedConfig = await loadUserConfig()
|
|
77
|
+
|
|
139
78
|
const renderer = await createCliRenderer({
|
|
140
79
|
autoFocus: true,
|
|
141
80
|
// Transparent clear color so cells untouched by BoxRenderable paints (e.g.
|
|
@@ -164,7 +103,6 @@ try {
|
|
|
164
103
|
|
|
165
104
|
const root = createRoot(renderer)
|
|
166
105
|
|
|
167
|
-
const resolvedConfig = await loadUserConfig()
|
|
168
106
|
logDebug('index.userConfigLoaded', {
|
|
169
107
|
leader: resolvedConfig.keymaps.leader,
|
|
170
108
|
modeCount: resolvedConfig.keymaps.modes.size,
|
|
@@ -179,6 +117,7 @@ if (resolvedConfig.theme?.beta?.experimentalSyntaxHighlight === true) {
|
|
|
179
117
|
}
|
|
180
118
|
|
|
181
119
|
const backend = await createSessionBackend({
|
|
120
|
+
autoRenameConfig: resolvedConfig.autoRename,
|
|
182
121
|
onBreakingUpdateRequired: () =>
|
|
183
122
|
new Promise<void>((resolve) => {
|
|
184
123
|
root.render(<BreakingUpdateScreen onConfirm={resolve} />)
|
package/src/input/modes/types.ts
CHANGED
|
@@ -52,6 +52,7 @@ export type SideEffect =
|
|
|
52
52
|
| { type: 'apply-theme'; action: 'confirm' }
|
|
53
53
|
| { type: 'apply-theme'; action: 'preview'; delta: 1 | -1 }
|
|
54
54
|
| { type: 'rename-session'; sessionId: string; name: string }
|
|
55
|
+
| { type: 'rename-tab'; tabId: string; title: string }
|
|
55
56
|
| {
|
|
56
57
|
type: 'split-pane'
|
|
57
58
|
direction: SplitDirection
|
|
@@ -32,8 +32,17 @@ import {
|
|
|
32
32
|
// either way (pre-v7 TM → new client never sees the indices; new TM →
|
|
33
33
|
// pre-v7 client ignores them and falls back to the theme default), so Min
|
|
34
34
|
// is raised in lockstep to force matching binaries.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
//
|
|
36
|
+
// v9: additive — `tabMetadata` updates titles and auto-rename state without
|
|
37
|
+
// restarting PTYs. Capability-gated; MIN remains at 8.
|
|
38
|
+
//
|
|
39
|
+
// v10: additive — `workerName` persists a workspace-scoped orchestration
|
|
40
|
+
// handle on tabs created through the headless worker facade.
|
|
41
|
+
//
|
|
42
|
+
// v11: breaking release boundary — force a fresh terminal-manager so named
|
|
43
|
+
// worker metadata is guaranteed to survive daemon reattach and process swaps.
|
|
44
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 11
|
|
45
|
+
export const MANAGER_PROTOCOL_VERSION = 11
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Capability strings advertised by *this* process in its `helloResult`. New
|
|
@@ -44,8 +53,13 @@ export const MANAGER_PROTOCOL_VERSION = 8
|
|
|
44
53
|
export const MANAGER_PROTOCOL_CAPABILITIES: readonly string[] = [
|
|
45
54
|
'setBroadcastEnabled',
|
|
46
55
|
'createTabWorktreeId',
|
|
56
|
+
'tabMetadata',
|
|
57
|
+
'workerMetadata',
|
|
47
58
|
]
|
|
48
59
|
|
|
60
|
+
export const MANAGER_CAPABILITY_TAB_METADATA = 'tabMetadata'
|
|
61
|
+
export const MANAGER_CAPABILITY_WORKER_METADATA = 'workerMetadata'
|
|
62
|
+
|
|
49
63
|
/**
|
|
50
64
|
* Capability name a daemon must observe on the TM's helloResult before
|
|
51
65
|
* sending `setBroadcastEnabled`.
|
|
@@ -119,9 +133,21 @@ export type ManagerRequest =
|
|
|
119
133
|
* `worktreeId = undefined`.
|
|
120
134
|
*/
|
|
121
135
|
worktreeId?: string
|
|
136
|
+
workerName?: string
|
|
137
|
+
autoRenameStatus?: 'eligible' | 'attempted'
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
| { id: string; type: 'write'; payload: { sessionId: string; tabId: string; data: string } }
|
|
141
|
+
| {
|
|
142
|
+
id: string
|
|
143
|
+
type: 'updateTabMetadata'
|
|
144
|
+
payload: {
|
|
145
|
+
sessionId: string
|
|
146
|
+
tabId: string
|
|
147
|
+
title?: string
|
|
148
|
+
autoRenameStatus?: 'eligible' | 'attempted'
|
|
149
|
+
}
|
|
150
|
+
}
|
|
125
151
|
| {
|
|
126
152
|
id: string
|
|
127
153
|
type: 'resizeClient'
|
|
@@ -364,12 +390,36 @@ export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
|
364
390
|
value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
|
|
365
391
|
'createTab.worktreeId must be a string when present'
|
|
366
392
|
)
|
|
393
|
+
assert(
|
|
394
|
+
value.payload.workerName === undefined || isString(value.payload.workerName),
|
|
395
|
+
'createTab.workerName must be a string when present'
|
|
396
|
+
)
|
|
397
|
+
assert(
|
|
398
|
+
value.payload.autoRenameStatus === undefined ||
|
|
399
|
+
value.payload.autoRenameStatus === 'eligible' ||
|
|
400
|
+
value.payload.autoRenameStatus === 'attempted',
|
|
401
|
+
'createTab.autoRenameStatus is invalid'
|
|
402
|
+
)
|
|
367
403
|
return value as ManagerRequest
|
|
368
404
|
case 'write':
|
|
369
405
|
assert(isString(value.payload.sessionId), 'write.sessionId must be a string')
|
|
370
406
|
assert(isString(value.payload.tabId), 'write.tabId must be a string')
|
|
371
407
|
assert(isString(value.payload.data), 'write.data must be a string')
|
|
372
408
|
return value as ManagerRequest
|
|
409
|
+
case 'updateTabMetadata':
|
|
410
|
+
assert(isString(value.payload.sessionId), 'updateTabMetadata.sessionId must be a string')
|
|
411
|
+
assert(isString(value.payload.tabId), 'updateTabMetadata.tabId must be a string')
|
|
412
|
+
assert(
|
|
413
|
+
value.payload.title === undefined || isString(value.payload.title),
|
|
414
|
+
'updateTabMetadata.title must be a string when present'
|
|
415
|
+
)
|
|
416
|
+
assert(
|
|
417
|
+
value.payload.autoRenameStatus === undefined ||
|
|
418
|
+
value.payload.autoRenameStatus === 'eligible' ||
|
|
419
|
+
value.payload.autoRenameStatus === 'attempted',
|
|
420
|
+
'updateTabMetadata.autoRenameStatus is invalid'
|
|
421
|
+
)
|
|
422
|
+
return value as ManagerRequest
|
|
373
423
|
case 'resizeClient':
|
|
374
424
|
assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
|
|
375
425
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -37,8 +37,17 @@ import { isWorkspaceSnapshotV1, isWorktreeRecord } from '../state/validation'
|
|
|
37
37
|
// plus best-effort parsed options), and an additive `lastLine` field on
|
|
38
38
|
// `TabSessionSummary`. Gated behind `turnLifecycle`, `questionEvents`, and
|
|
39
39
|
// `listTabsLastLine` respectively; MIN stays at 10.
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
//
|
|
41
|
+
// v14: additive — tab metadata synchronization for manual and automatic
|
|
42
|
+
// renames. The new request/event are capability-gated; MIN stays at 10.
|
|
43
|
+
//
|
|
44
|
+
// v15: additive — workspace-scoped `workerName` metadata on create, attach,
|
|
45
|
+
// and list results. This powers stable name-or-id orchestration selectors.
|
|
46
|
+
//
|
|
47
|
+
// v16: breaking release boundary — the agent-first worker control plane and
|
|
48
|
+
// its metadata guarantees are now required. Old app/daemon pairs must not mix.
|
|
49
|
+
export const IPC_PROTOCOL_MIN_VERSION = 16
|
|
50
|
+
export const IPC_PROTOCOL_VERSION = 16
|
|
42
51
|
|
|
43
52
|
/**
|
|
44
53
|
* Capability advertised by a daemon that knows how to drain + handoff its
|
|
@@ -103,6 +112,7 @@ export const IPC_CAPABILITY_WORKSPACE_LIFECYCLE = 'workspaceLifecycle'
|
|
|
103
112
|
* fanout as workspaceLifecycle.
|
|
104
113
|
*/
|
|
105
114
|
export const IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS = 'worktreeLifecycleEvents'
|
|
115
|
+
export const IPC_CAPABILITY_WORKER_METADATA = 'workerMetadata'
|
|
106
116
|
|
|
107
117
|
/**
|
|
108
118
|
* v12 — capability marker for `aimux tab tail`. Functionally it's just a
|
|
@@ -137,6 +147,9 @@ export const IPC_CAPABILITY_QUESTION_EVENTS = 'questionEvents'
|
|
|
137
147
|
*/
|
|
138
148
|
export const IPC_CAPABILITY_LIST_TABS_LAST_LINE = 'listTabsLastLine'
|
|
139
149
|
|
|
150
|
+
/** Additive tab-title and auto-rename metadata synchronization. */
|
|
151
|
+
export const IPC_CAPABILITY_TAB_METADATA = 'tabMetadata'
|
|
152
|
+
|
|
140
153
|
/**
|
|
141
154
|
* Capabilities advertised by *this* process in its `helloResult`. Additive
|
|
142
155
|
* features should be introduced as new capability strings here rather than
|
|
@@ -160,6 +173,8 @@ export const IPC_PROTOCOL_CAPABILITIES: readonly string[] = [
|
|
|
160
173
|
IPC_CAPABILITY_TURN_LIFECYCLE,
|
|
161
174
|
IPC_CAPABILITY_QUESTION_EVENTS,
|
|
162
175
|
IPC_CAPABILITY_LIST_TABS_LAST_LINE,
|
|
176
|
+
IPC_CAPABILITY_TAB_METADATA,
|
|
177
|
+
IPC_CAPABILITY_WORKER_METADATA,
|
|
163
178
|
]
|
|
164
179
|
|
|
165
180
|
export interface ProtocolHelloRequest {
|
|
@@ -168,6 +183,8 @@ export interface ProtocolHelloRequest {
|
|
|
168
183
|
}
|
|
169
184
|
|
|
170
185
|
export interface ProtocolHelloResult {
|
|
186
|
+
/** aimux package version; absent on older daemons. */
|
|
187
|
+
appVersion?: string
|
|
171
188
|
minVersion: number
|
|
172
189
|
maxVersion: number
|
|
173
190
|
processVersion: string
|
|
@@ -177,6 +194,8 @@ export interface ProtocolHelloResult {
|
|
|
177
194
|
* not yet advertise capabilities are normalised to `[]` at parse time.
|
|
178
195
|
*/
|
|
179
196
|
capabilities: string[]
|
|
197
|
+
/** Capabilities negotiated with the terminal-manager; absent on older daemons. */
|
|
198
|
+
managerCapabilities?: string[]
|
|
180
199
|
/**
|
|
181
200
|
* Manager-protocol version currently negotiated between the daemon and
|
|
182
201
|
* its terminal-manager. `undefined` when the daemon has not yet connected
|
|
@@ -220,6 +239,7 @@ export interface TabSessionSummary {
|
|
|
220
239
|
activity?: TabActivity
|
|
221
240
|
command: string
|
|
222
241
|
worktreeId?: string
|
|
242
|
+
workerName?: string
|
|
223
243
|
/**
|
|
224
244
|
* v13 / capability `listTabsLastLine`. The tab's last non-blank rendered
|
|
225
245
|
* line, trimmed. Present only when the daemon advertises the capability;
|
|
@@ -269,9 +289,14 @@ export type ClientRequest =
|
|
|
269
289
|
* only forwards it to the TM when its own capability is in play.
|
|
270
290
|
*/
|
|
271
291
|
worktreeId?: string
|
|
292
|
+
/** v15 / capability `workerMetadata`: stable workspace-scoped handle. */
|
|
293
|
+
workerName?: string
|
|
294
|
+
/** True only when the creator did not provide an explicit title. */
|
|
295
|
+
autoRenameCandidate?: boolean
|
|
272
296
|
}
|
|
273
297
|
}
|
|
274
298
|
| { id: string; type: 'write'; payload: { tabId: string; data: string } }
|
|
299
|
+
| { id: string; type: 'renameTab'; payload: { tabId: string; title: string } }
|
|
275
300
|
| {
|
|
276
301
|
id: string
|
|
277
302
|
type: 'resizeClient'
|
|
@@ -395,6 +420,15 @@ export type ServerEvent =
|
|
|
395
420
|
type: 'tabAdded'
|
|
396
421
|
payload: { sessionId: string; tab: TabSession }
|
|
397
422
|
}
|
|
423
|
+
| {
|
|
424
|
+
type: 'tabMetadataUpdated'
|
|
425
|
+
payload: {
|
|
426
|
+
sessionId: string
|
|
427
|
+
tabId: string
|
|
428
|
+
title?: string
|
|
429
|
+
autoRenameStatus?: 'eligible' | 'attempted'
|
|
430
|
+
}
|
|
431
|
+
}
|
|
398
432
|
// v12 / capability `workspaceLifecycle`. Broadcast to every socket when a
|
|
399
433
|
// CLI issues `createWorkspace` while a UI is attached — the UI runs its
|
|
400
434
|
// create-session handler so the live workspace snapshot is preserved.
|
|
@@ -547,12 +581,14 @@ function isProtocolHelloResult(value: unknown): value is ProtocolHelloResult {
|
|
|
547
581
|
isFiniteNumber(value.maxVersion) &&
|
|
548
582
|
isFiniteNumber(value.selectedVersion) &&
|
|
549
583
|
isString(value.processVersion) &&
|
|
584
|
+
(value.appVersion === undefined || isString(value.appVersion)) &&
|
|
550
585
|
// Wire-back-compat: peers that predate the capabilities field omit it
|
|
551
586
|
// entirely. parseServerMessage normalises that to `[]` before the cast
|
|
552
587
|
// so the typed shape stays non-optional.
|
|
553
588
|
(value.capabilities === undefined || isStringArray(value.capabilities)) &&
|
|
554
589
|
// Additive: peers that predate managerSelectedVersion omit it.
|
|
555
|
-
(value.managerSelectedVersion === undefined || isFiniteNumber(value.managerSelectedVersion))
|
|
590
|
+
(value.managerSelectedVersion === undefined || isFiniteNumber(value.managerSelectedVersion)) &&
|
|
591
|
+
(value.managerCapabilities === undefined || isStringArray(value.managerCapabilities))
|
|
556
592
|
)
|
|
557
593
|
}
|
|
558
594
|
|
|
@@ -573,6 +609,7 @@ function isTabSessionSummary(value: unknown): value is TabSessionSummary {
|
|
|
573
609
|
value.activity === 'idle') &&
|
|
574
610
|
isString(value.command) &&
|
|
575
611
|
(value.worktreeId === undefined || isString(value.worktreeId)) &&
|
|
612
|
+
(value.workerName === undefined || isString(value.workerName)) &&
|
|
576
613
|
(value.lastLine === undefined || isString(value.lastLine))
|
|
577
614
|
)
|
|
578
615
|
}
|
|
@@ -608,6 +645,10 @@ function isTabSession(value: unknown): value is TabSession {
|
|
|
608
645
|
isString(value.buffer) &&
|
|
609
646
|
isTerminalModeState(value.terminalModes) &&
|
|
610
647
|
isString(value.command) &&
|
|
648
|
+
(value.workerName === undefined || isString(value.workerName)) &&
|
|
649
|
+
(value.autoRenameStatus === undefined ||
|
|
650
|
+
value.autoRenameStatus === 'eligible' ||
|
|
651
|
+
value.autoRenameStatus === 'attempted') &&
|
|
611
652
|
(value.viewport === undefined || isTerminalSnapshot(value.viewport)) &&
|
|
612
653
|
(value.errorMessage === undefined || isString(value.errorMessage)) &&
|
|
613
654
|
(value.exitCode === undefined || isFiniteNumber(value.exitCode)) &&
|
|
@@ -714,11 +755,27 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
714
755
|
value.payload.worktreeId === undefined || isString(value.payload.worktreeId),
|
|
715
756
|
'createTab.worktreeId must be a string when present'
|
|
716
757
|
)
|
|
758
|
+
assert(
|
|
759
|
+
value.payload.workerName === undefined || isString(value.payload.workerName),
|
|
760
|
+
'createTab.workerName must be a string when present'
|
|
761
|
+
)
|
|
762
|
+
assert(
|
|
763
|
+
value.payload.autoRenameCandidate === undefined ||
|
|
764
|
+
typeof value.payload.autoRenameCandidate === 'boolean',
|
|
765
|
+
'createTab.autoRenameCandidate must be a boolean when present'
|
|
766
|
+
)
|
|
717
767
|
return value as ClientRequest
|
|
718
768
|
case 'write':
|
|
719
769
|
assert(isString(value.payload.tabId), 'write.tabId must be a string')
|
|
720
770
|
assert(isString(value.payload.data), 'write.data must be a string')
|
|
721
771
|
return value as ClientRequest
|
|
772
|
+
case 'renameTab':
|
|
773
|
+
assert(isString(value.payload.tabId), 'renameTab.tabId must be a string')
|
|
774
|
+
assert(
|
|
775
|
+
isString(value.payload.title) && value.payload.title.trim().length > 0,
|
|
776
|
+
'renameTab.title must be a non-empty string'
|
|
777
|
+
)
|
|
778
|
+
return value as ClientRequest
|
|
722
779
|
case 'resizeClient':
|
|
723
780
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
724
781
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
@@ -872,6 +929,20 @@ export function parseServerMessage(value: unknown): ServerResponse | ServerEvent
|
|
|
872
929
|
assert(isString(value.payload.sessionId), 'tabAdded.sessionId must be a string')
|
|
873
930
|
assert(isTabSession(value.payload.tab), 'tabAdded.tab is invalid')
|
|
874
931
|
return value as ServerEvent
|
|
932
|
+
case 'tabMetadataUpdated':
|
|
933
|
+
assert(isString(value.payload.sessionId), 'tabMetadataUpdated.sessionId must be a string')
|
|
934
|
+
assert(isString(value.payload.tabId), 'tabMetadataUpdated.tabId must be a string')
|
|
935
|
+
assert(
|
|
936
|
+
value.payload.title === undefined || isString(value.payload.title),
|
|
937
|
+
'tabMetadataUpdated.title must be a string when present'
|
|
938
|
+
)
|
|
939
|
+
assert(
|
|
940
|
+
value.payload.autoRenameStatus === undefined ||
|
|
941
|
+
value.payload.autoRenameStatus === 'eligible' ||
|
|
942
|
+
value.payload.autoRenameStatus === 'attempted',
|
|
943
|
+
'tabMetadataUpdated.autoRenameStatus is invalid'
|
|
944
|
+
)
|
|
945
|
+
return value as ServerEvent
|
|
875
946
|
case 'sessionStatus':
|
|
876
947
|
assert(isString(value.payload.sessionId), 'sessionStatus.sessionId must be a string')
|
|
877
948
|
assert(isSessionStatus(value.payload.status), 'sessionStatus.status is invalid')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connect } from 'node:net'
|
|
2
2
|
|
|
3
|
+
import type { AutoRenameConfigSnapshot } from '../auto-rename/coordinator'
|
|
3
4
|
import type { SessionBackend } from './types'
|
|
4
5
|
|
|
5
6
|
import { negotiateDaemonReexec, waitForSocketRemoval } from '../daemon/reexec-client'
|
|
@@ -273,10 +274,11 @@ async function stopTerminalManager(): Promise<void> {
|
|
|
273
274
|
|
|
274
275
|
export async function createSessionBackend(opts?: {
|
|
275
276
|
onBreakingUpdateRequired?: () => Promise<void>
|
|
277
|
+
autoRenameConfig?: AutoRenameConfigSnapshot
|
|
276
278
|
}): Promise<SessionBackend> {
|
|
277
279
|
if (process.env.AIMUX_LOCAL_BACKEND === '1') {
|
|
278
280
|
logDebug('backend.create.localExplicit')
|
|
279
|
-
return new LocalSessionBackend()
|
|
281
|
+
return new LocalSessionBackend(opts?.autoRenameConfig)
|
|
280
282
|
}
|
|
281
283
|
|
|
282
284
|
const socketPath = getIpcDaemonSocketPath()
|
|
@@ -8,6 +8,11 @@ import type {
|
|
|
8
8
|
} from '../state/types'
|
|
9
9
|
import type { ResizeOptions, SessionBackend, SessionBackendEvents } from './types'
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
type AutoRenameConfigSnapshot,
|
|
13
|
+
AutoRenameCoordinator,
|
|
14
|
+
initialAutoRenameStatus,
|
|
15
|
+
} from '../auto-rename/coordinator'
|
|
11
16
|
import { SessionManager } from '../daemon/session-manager'
|
|
12
17
|
import { logDebug } from '../debug/input-log'
|
|
13
18
|
import { runStatusDetectionLoop } from '../pty/assistant-status-detection-loop'
|
|
@@ -40,9 +45,26 @@ export class LocalSessionBackend
|
|
|
40
45
|
string,
|
|
41
46
|
{ viewport: TerminalSnapshot; terminalModes: TerminalModeState }
|
|
42
47
|
>()
|
|
48
|
+
private readonly autoRename: AutoRenameCoordinator
|
|
49
|
+
private readonly autoRenameConfig: AutoRenameConfigSnapshot
|
|
43
50
|
|
|
44
|
-
constructor(
|
|
51
|
+
constructor(
|
|
52
|
+
autoRenameConfig: AutoRenameConfigSnapshot = { enabled: false, models: {}, timeoutMs: 15_000 }
|
|
53
|
+
) {
|
|
45
54
|
super()
|
|
55
|
+
this.autoRenameConfig = autoRenameConfig
|
|
56
|
+
this.autoRename = new AutoRenameCoordinator({
|
|
57
|
+
config: autoRenameConfig,
|
|
58
|
+
getTab: (tabId) => this.findTab(tabId)?.tab,
|
|
59
|
+
updateTab: (tabId, patch) => {
|
|
60
|
+
const found = this.findTab(tabId)
|
|
61
|
+
if (!found) return
|
|
62
|
+
this.sessionManager.updateTabMetadata(found.sessionId, tabId, patch)
|
|
63
|
+
if (found.sessionId === this.currentSessionId) {
|
|
64
|
+
this.emit('tabMetadataUpdated', found.sessionId, tabId, patch)
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
})
|
|
46
68
|
this.sessionManager.on('render', (sessionId, tabId, viewport, terminalModes) => {
|
|
47
69
|
if (sessionId !== this.currentSessionId) return
|
|
48
70
|
if (this.paneReady.get(tabId) === false) {
|
|
@@ -106,6 +128,7 @@ export class LocalSessionBackend
|
|
|
106
128
|
)
|
|
107
129
|
for (const tab of attachResult.tabs) {
|
|
108
130
|
this.gatePaneRender(tab.id)
|
|
131
|
+
this.autoRename.register(tab)
|
|
109
132
|
}
|
|
110
133
|
// Run a synchronous classification pass so every tab's activity and the
|
|
111
134
|
// session-status snapshot are available to embed in the reply — mirrors
|
|
@@ -133,6 +156,7 @@ export class LocalSessionBackend
|
|
|
133
156
|
rows: number
|
|
134
157
|
cwd?: string
|
|
135
158
|
worktreeId?: string
|
|
159
|
+
autoRenameCandidate?: boolean
|
|
136
160
|
}): void {
|
|
137
161
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
|
|
138
162
|
logDebug('backend.local.skipCreateWithoutSession', { tabId: options.tabId })
|
|
@@ -145,7 +169,14 @@ export class LocalSessionBackend
|
|
|
145
169
|
worktreeId: options.worktreeId ?? null,
|
|
146
170
|
})
|
|
147
171
|
this.gatePaneRender(options.tabId)
|
|
148
|
-
|
|
172
|
+
const autoRenameStatus = initialAutoRenameStatus(
|
|
173
|
+
this.getAutoRenameConfig(),
|
|
174
|
+
options.assistant,
|
|
175
|
+
options.autoRenameCandidate === true
|
|
176
|
+
)
|
|
177
|
+
this.sessionManager.createTab(this.currentSessionId, { ...options, autoRenameStatus })
|
|
178
|
+
const tab = this.findTab(options.tabId)?.tab
|
|
179
|
+
if (tab) this.autoRename.register(tab)
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
/** Suppress render emission for this tab until the frontend acknowledges its
|
|
@@ -175,9 +206,21 @@ export class LocalSessionBackend
|
|
|
175
206
|
sessionId: this.currentSessionId,
|
|
176
207
|
tabId,
|
|
177
208
|
})
|
|
209
|
+
this.autoRename.observeWrite(tabId, input)
|
|
178
210
|
this.sessionManager.write(this.currentSessionId, tabId, input)
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
renameTab(tabId: string, title: string): void {
|
|
214
|
+
const found = this.findTab(tabId)
|
|
215
|
+
if (!found) return
|
|
216
|
+
this.autoRename.manualRename(tabId)
|
|
217
|
+
const patch = { autoRenameStatus: 'attempted' as const, title }
|
|
218
|
+
this.sessionManager.updateTabMetadata(found.sessionId, tabId, patch)
|
|
219
|
+
if (found.sessionId === this.currentSessionId) {
|
|
220
|
+
this.emit('tabMetadataUpdated', found.sessionId, tabId, patch)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
181
224
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
182
225
|
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
183
226
|
this.sessionManager.scroll(this.currentSessionId, tabId, deltaLines)
|
|
@@ -212,6 +255,7 @@ export class LocalSessionBackend
|
|
|
212
255
|
logDebug('backend.local.disposeSession', { sessionId: this.currentSessionId, tabId })
|
|
213
256
|
this.paneReady.delete(tabId)
|
|
214
257
|
this.pendingRender.delete(tabId)
|
|
258
|
+
this.autoRename.unregister(tabId)
|
|
215
259
|
this.sessionManager.closeTab(this.currentSessionId, tabId)
|
|
216
260
|
}
|
|
217
261
|
|
|
@@ -220,6 +264,9 @@ export class LocalSessionBackend
|
|
|
220
264
|
logDebug('backend.local.disposeAll', { sessionId: this.currentSessionId })
|
|
221
265
|
this.paneReady.clear()
|
|
222
266
|
this.pendingRender.clear()
|
|
267
|
+
for (const tab of this.sessionManager.listTabs(this.currentSessionId)) {
|
|
268
|
+
this.autoRename.unregister(tab.id)
|
|
269
|
+
}
|
|
223
270
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
224
271
|
}
|
|
225
272
|
|
|
@@ -235,4 +282,18 @@ export class LocalSessionBackend
|
|
|
235
282
|
announceWorkspaceSwitched(_sessionId: string): void {
|
|
236
283
|
// No daemon on the local backend, so there's no CLI to notify.
|
|
237
284
|
}
|
|
285
|
+
|
|
286
|
+
private findTab(
|
|
287
|
+
tabId: string
|
|
288
|
+
): { sessionId: string; tab: ReturnType<SessionManager['listTabs']>[number] } | null {
|
|
289
|
+
for (const sessionId of this.sessionManager.listSessionIds()) {
|
|
290
|
+
const tab = this.sessionManager.listTabs(sessionId).find((entry) => entry.id === tabId)
|
|
291
|
+
if (tab) return { sessionId, tab }
|
|
292
|
+
}
|
|
293
|
+
return null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private getAutoRenameConfig(): AutoRenameConfigSnapshot {
|
|
297
|
+
return this.autoRenameConfig
|
|
298
|
+
}
|
|
238
299
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type AttachResult,
|
|
11
11
|
type ClientRequest,
|
|
12
12
|
encodeMessage,
|
|
13
|
+
IPC_CAPABILITY_TAB_METADATA,
|
|
13
14
|
IPC_PROTOCOL_MIN_VERSION,
|
|
14
15
|
IPC_PROTOCOL_VERSION,
|
|
15
16
|
MessageDecoder,
|
|
@@ -211,6 +212,12 @@ export class RemoteSessionBackend
|
|
|
211
212
|
})
|
|
212
213
|
this.emit('tabAdded', message.payload.sessionId, message.payload.tab)
|
|
213
214
|
break
|
|
215
|
+
case 'tabMetadataUpdated':
|
|
216
|
+
this.emit('tabMetadataUpdated', message.payload.sessionId, message.payload.tabId, {
|
|
217
|
+
autoRenameStatus: message.payload.autoRenameStatus,
|
|
218
|
+
title: message.payload.title,
|
|
219
|
+
})
|
|
220
|
+
break
|
|
214
221
|
case 'workspaceCreateRequested':
|
|
215
222
|
this.emit(
|
|
216
223
|
'workspaceCreateRequested',
|
|
@@ -411,6 +418,7 @@ export class RemoteSessionBackend
|
|
|
411
418
|
rows: number
|
|
412
419
|
cwd?: string
|
|
413
420
|
worktreeId?: string
|
|
421
|
+
autoRenameCandidate?: boolean
|
|
414
422
|
}): void {
|
|
415
423
|
if (!this.attached) {
|
|
416
424
|
logDebug('backend.remote.skipCreateBeforeAttach', { tabId: options.tabId })
|
|
@@ -436,6 +444,15 @@ export class RemoteSessionBackend
|
|
|
436
444
|
)
|
|
437
445
|
}
|
|
438
446
|
|
|
447
|
+
renameTab(tabId: string, title: string): void {
|
|
448
|
+
if (!this.attached || !this.daemonAdvertises(IPC_CAPABILITY_TAB_METADATA)) return
|
|
449
|
+
this.dispatchCommand(
|
|
450
|
+
{ id: crypto.randomUUID(), payload: { tabId, title }, type: 'renameTab' },
|
|
451
|
+
'renameTab',
|
|
452
|
+
tabId
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
|
|
439
456
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
440
457
|
if (!this.attached) {
|
|
441
458
|
return
|
|
@@ -23,6 +23,11 @@ export interface SessionBackendEvents {
|
|
|
23
23
|
* `tabRender` event lands.
|
|
24
24
|
*/
|
|
25
25
|
tabAdded: [sessionId: string, tab: TabSession]
|
|
26
|
+
tabMetadataUpdated: [
|
|
27
|
+
sessionId: string,
|
|
28
|
+
tabId: string,
|
|
29
|
+
patch: { title?: string; autoRenameStatus?: 'eligible' | 'attempted' },
|
|
30
|
+
]
|
|
26
31
|
/**
|
|
27
32
|
* v12 workspace-lifecycle events. Fired when a CLI issued
|
|
28
33
|
* `createWorkspace` / `switchWorkspace` / `closeWorkspace` and the daemon
|
|
@@ -86,8 +91,10 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
|
|
86
91
|
* registry surfaces the right grouping in `listTabs` for headless
|
|
87
92
|
* consumers (CLI control plane). */
|
|
88
93
|
worktreeId?: string
|
|
94
|
+
autoRenameCandidate?: boolean
|
|
89
95
|
}): void
|
|
90
96
|
write(tabId: string, input: string): void
|
|
97
|
+
renameTab(tabId: string, title: string): void
|
|
91
98
|
scrollViewport(tabId: string, deltaLines: number): void
|
|
92
99
|
scrollViewportToBottom(tabId: string): void
|
|
93
100
|
setActiveTab(tabId: string | null): void
|
|
@@ -543,7 +543,20 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
543
543
|
case 'rename-tab':
|
|
544
544
|
return {
|
|
545
545
|
...state,
|
|
546
|
-
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
546
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
547
|
+
...tab,
|
|
548
|
+
autoRenameStatus: action.autoRenameStatus ?? tab.autoRenameStatus,
|
|
549
|
+
title: action.title,
|
|
550
|
+
})),
|
|
551
|
+
}
|
|
552
|
+
case 'update-tab-metadata':
|
|
553
|
+
return {
|
|
554
|
+
...state,
|
|
555
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
556
|
+
...tab,
|
|
557
|
+
autoRenameStatus: action.autoRenameStatus ?? tab.autoRenameStatus,
|
|
558
|
+
title: action.title ?? tab.title,
|
|
559
|
+
})),
|
|
547
560
|
}
|
|
548
561
|
case 'split-pane': {
|
|
549
562
|
if (!(state.activeTabId != null && state.activeTabId !== '')) {
|