@brimveyn/aimux 1.19.7 → 1.20.2
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 +14 -0
- package/package.json +3 -2
- package/skills/aimux-orchestrator/SKILL.md +127 -0
- package/skills/aimux-orchestrator/assets/ledger.template.md +18 -0
- package/skills/aimux-orchestrator/references/prompts.md +68 -0
- package/skills/aimux-orchestrator/references/review.md +18 -0
- package/src/cli/client/daemon-client.ts +16 -0
- package/src/cli/client/workspace-resolver.ts +61 -8
- package/src/cli/commands/tab/await.ts +1 -1
- package/src/cli/commands/tab/close.ts +1 -1
- package/src/cli/commands/tab/create.ts +263 -128
- package/src/cli/commands/tab/focus.ts +1 -1
- package/src/cli/commands/tab/prompt-io.ts +8 -2
- package/src/cli/commands/tab/run.ts +10 -2
- package/src/cli/commands/tab/send.ts +12 -7
- package/src/cli/commands/tab/snapshot.ts +2 -1
- package/src/cli/commands/tab/tail.ts +1 -1
- package/src/cli/commands/tab/wait.ts +2 -1
- package/src/cli/commands/worker/await.ts +34 -0
- package/src/cli/commands/worker/doctor.ts +153 -0
- package/src/cli/commands/worker/list.ts +67 -0
- package/src/cli/commands/worker/prompt.ts +74 -0
- package/src/cli/commands/worker/run.ts +143 -0
- package/src/cli/commands/worker/shared.ts +515 -0
- package/src/cli/commands/worker/stop.ts +113 -0
- package/src/cli/commands/worker/submit.ts +40 -0
- package/src/cli/commands/workspace/close.ts +1 -1
- package/src/cli/commands/workspace/create.ts +2 -1
- package/src/cli/commands/workspace/switch.ts +1 -1
- package/src/cli/commands/worktree/create-core.ts +27 -7
- package/src/cli/commands/worktree/create.ts +18 -3
- package/src/cli/commands/worktree/remove.ts +32 -10
- package/src/cli/completion/entry.ts +181 -0
- package/src/cli/completion/install.ts +222 -0
- package/src/cli/completion/plan.ts +216 -0
- package/src/cli/completion/scripts.ts +147 -0
- package/src/cli/completion/sources.ts +74 -0
- package/src/cli/context.ts +15 -0
- package/src/cli/flags.ts +45 -2
- package/src/cli/index.ts +40 -18
- package/src/cli/output.ts +3 -0
- package/src/cli/registry.ts +14 -0
- package/src/daemon/daemon.ts +58 -8
- package/src/daemon/session-registry.ts +5 -0
- package/src/doctor.ts +4 -0
- package/src/git/worktree.ts +23 -1
- package/src/index.tsx +53 -92
- package/src/ipc/manager-protocol.ts +15 -2
- package/src/ipc/protocol.ts +26 -3
- package/src/platform/worktree-paths.ts +21 -1
- package/src/state/session-persistence.ts +2 -0
- package/src/state/types.ts +4 -0
- package/src/state/validation.ts +1 -0
- package/src/terminal-manager/manager-client.ts +18 -3
|
@@ -1,26 +1,61 @@
|
|
|
1
1
|
import { resolve as resolvePath } from 'node:path'
|
|
2
2
|
|
|
3
|
+
import type { WorktreeRecord } from '../../../state/types'
|
|
4
|
+
import type { CliContext } from '../../context'
|
|
3
5
|
import type { CliCommand } from '../../registry'
|
|
4
6
|
|
|
5
7
|
import { loadConfig } from '../../../config'
|
|
8
|
+
import { removeGitWorktree } from '../../../git/worktree'
|
|
6
9
|
import {
|
|
7
10
|
IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK,
|
|
11
|
+
IPC_CAPABILITY_LIST_TABS,
|
|
8
12
|
IPC_CAPABILITY_THIN_ATTACH,
|
|
13
|
+
IPC_CAPABILITY_WORKER_METADATA,
|
|
9
14
|
} from '../../../ipc/protocol'
|
|
10
15
|
import { createPrefixedId } from '../../../platform/id'
|
|
16
|
+
import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
|
|
11
17
|
import {
|
|
12
18
|
type AssistantOption,
|
|
13
19
|
buildAssistantModelArgs,
|
|
14
20
|
getAllAssistantOptions,
|
|
15
21
|
parseCommand,
|
|
16
22
|
} from '../../../pty/command-registry'
|
|
17
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
23
|
+
import { CliUsageError, SHARED_FLAGS } from '../../flags'
|
|
18
24
|
import { EXIT_OK, writeJson } from '../../output'
|
|
19
25
|
import { createWorkspaceWorktree } from '../worktree/create-core'
|
|
20
26
|
|
|
21
27
|
const FALLBACK_COLS = 200
|
|
22
28
|
const FALLBACK_ROWS = 60
|
|
23
29
|
|
|
30
|
+
export interface CreateCliTabOptions {
|
|
31
|
+
assistantId: string
|
|
32
|
+
base?: string
|
|
33
|
+
branch?: string
|
|
34
|
+
commandOverride?: string
|
|
35
|
+
cwd?: string
|
|
36
|
+
effort?: string
|
|
37
|
+
model?: string
|
|
38
|
+
newWorktree?: boolean | string
|
|
39
|
+
title?: string
|
|
40
|
+
workerName?: string
|
|
41
|
+
worktreeId?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CreateCliTabResult {
|
|
45
|
+
assistant: string
|
|
46
|
+
branch: string | null
|
|
47
|
+
command: string
|
|
48
|
+
cwd: string | null
|
|
49
|
+
effort: string | null
|
|
50
|
+
model: string | null
|
|
51
|
+
name: string | null
|
|
52
|
+
path: string | null
|
|
53
|
+
tabId: string
|
|
54
|
+
title: string
|
|
55
|
+
workerName: string | null
|
|
56
|
+
worktreeId: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
24
59
|
/**
|
|
25
60
|
* Resolve the cwd a spawned tab should run in. Precedence: explicit `--cwd`
|
|
26
61
|
* (resolved to absolute) > the resolved worktree's path > undefined (the
|
|
@@ -50,44 +85,250 @@ export function resolveAssistantCommand(
|
|
|
50
85
|
return commandOverride ?? customCommands[option.id] ?? option.command
|
|
51
86
|
}
|
|
52
87
|
|
|
88
|
+
async function rollbackCreatedWorktree(
|
|
89
|
+
record: WorktreeRecord,
|
|
90
|
+
ctx: CliContext,
|
|
91
|
+
originalError: unknown
|
|
92
|
+
): Promise<never> {
|
|
93
|
+
const daemon = await ctx.getDaemon()
|
|
94
|
+
const workspace = ctx.getWorkspace()
|
|
95
|
+
let rollbackError: unknown
|
|
96
|
+
try {
|
|
97
|
+
await removeGitWorktree({
|
|
98
|
+
force: true,
|
|
99
|
+
repoPath: record.repoRoot,
|
|
100
|
+
targetPath: record.path,
|
|
101
|
+
})
|
|
102
|
+
await pruneEmptyWorktreeParent(record.path)
|
|
103
|
+
await daemon.expectOk('removeWorktreeRecord', {
|
|
104
|
+
sessionId: workspace.id,
|
|
105
|
+
worktreeId: record.id,
|
|
106
|
+
})
|
|
107
|
+
} catch (error) {
|
|
108
|
+
rollbackError = error
|
|
109
|
+
}
|
|
110
|
+
const original = originalError instanceof Error ? originalError.message : String(originalError)
|
|
111
|
+
if (rollbackError === undefined) throw originalError
|
|
112
|
+
const rollback =
|
|
113
|
+
rollbackError instanceof Error ? rollbackError.message : JSON.stringify(rollbackError)
|
|
114
|
+
throw new Error(`${original}; rollback failed: ${rollback}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a tab without writing to stdout. Worker commands compose this helper
|
|
119
|
+
* with prompt dispatch/await while `tab create` remains a thin compatibility
|
|
120
|
+
* adapter. All validation and daemon attachment happen before a worktree is
|
|
121
|
+
* created; any later failure rolls the fresh worktree back.
|
|
122
|
+
*/
|
|
123
|
+
export async function createCliTab(
|
|
124
|
+
ctx: CliContext,
|
|
125
|
+
options: CreateCliTabOptions
|
|
126
|
+
): Promise<CreateCliTabResult> {
|
|
127
|
+
const {
|
|
128
|
+
assistantId,
|
|
129
|
+
base,
|
|
130
|
+
branch,
|
|
131
|
+
commandOverride,
|
|
132
|
+
cwd: cwdRaw,
|
|
133
|
+
effort,
|
|
134
|
+
model,
|
|
135
|
+
newWorktree,
|
|
136
|
+
title: requestedTitle,
|
|
137
|
+
workerName,
|
|
138
|
+
worktreeId: requestedWorktreeId,
|
|
139
|
+
} = options
|
|
140
|
+
if (assistantId.length === 0) throw new CliUsageError('--assistant is required')
|
|
141
|
+
if (workerName !== undefined && workerName.trim().length === 0) {
|
|
142
|
+
throw new CliUsageError('--name must be a non-empty string')
|
|
143
|
+
}
|
|
144
|
+
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
145
|
+
throw new CliUsageError(
|
|
146
|
+
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const createFreshWorktree = newWorktree !== undefined && newWorktree !== false
|
|
151
|
+
if (createFreshWorktree && requestedWorktreeId !== undefined) {
|
|
152
|
+
throw new CliUsageError(
|
|
153
|
+
'--new-worktree creates its own; use --worktree <id> to co-locate instead'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
if (createFreshWorktree && cwdRaw !== undefined) {
|
|
157
|
+
throw new CliUsageError('--new-worktree sets the cwd to the new worktree; drop --cwd')
|
|
158
|
+
}
|
|
159
|
+
if (!createFreshWorktree && (base !== undefined || branch !== undefined)) {
|
|
160
|
+
throw new CliUsageError(
|
|
161
|
+
'--base / --branch require --new-worktree (use `worktree create` otherwise)'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Resolve and validate the complete assistant invocation before touching git.
|
|
166
|
+
const { customCommands } = loadConfig()
|
|
167
|
+
const option = getAllAssistantOptions(customCommands).find((entry) => entry.id === assistantId)
|
|
168
|
+
if (!option) {
|
|
169
|
+
const known = getAllAssistantOptions(customCommands)
|
|
170
|
+
.map((entry) => entry.id)
|
|
171
|
+
.join(', ')
|
|
172
|
+
throw new CliUsageError(`unknown assistant: ${assistantId} (known: ${known})`)
|
|
173
|
+
}
|
|
174
|
+
const command = resolveAssistantCommand(commandOverride, customCommands, option)
|
|
175
|
+
const { args: baseArgs, executable } = parseCommand(command)
|
|
176
|
+
const args = [...baseArgs, ...buildAssistantModelArgs(option, { effort, model })]
|
|
177
|
+
const title = requestedTitle ?? option.label
|
|
178
|
+
const tabId = createPrefixedId('tab')
|
|
179
|
+
|
|
180
|
+
const workspace = ctx.getWorkspace()
|
|
181
|
+
const daemon = await ctx.getDaemon()
|
|
182
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
if (workerName !== undefined && !daemon.hasCapability(IPC_CAPABILITY_WORKER_METADATA)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
'daemon predates workerMetadata capability — restart aimux to use worker commands'
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
if (workerName !== undefined) {
|
|
193
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
194
|
+
throw new Error('daemon cannot validate worker-name uniqueness — restart aimux')
|
|
195
|
+
}
|
|
196
|
+
const existing = await daemon.listTabs(workspace.id)
|
|
197
|
+
if (existing.tabs.some((tab) => tab.workerName === workerName)) {
|
|
198
|
+
throw new Error(`worker name already exists in workspace "${workspace.name}": ${workerName}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Attach before creating a worktree so stale daemon/session failures have no
|
|
203
|
+
// git-side effect.
|
|
204
|
+
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
205
|
+
|
|
206
|
+
let worktreeId = requestedWorktreeId ?? workspace.activeWorktreeId
|
|
207
|
+
let worktreeRecord =
|
|
208
|
+
worktreeId !== undefined
|
|
209
|
+
? workspace.worktrees?.find((entry) => entry.id === worktreeId)
|
|
210
|
+
: undefined
|
|
211
|
+
if (requestedWorktreeId !== undefined && worktreeRecord === undefined) {
|
|
212
|
+
const ids = workspace.worktrees?.map((entry) => entry.id).join(', ') ?? '(none)'
|
|
213
|
+
throw new Error(`unknown worktree id: ${requestedWorktreeId} (known: ${ids})`)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let createdWorktree: WorktreeRecord | undefined
|
|
217
|
+
if (createFreshWorktree) {
|
|
218
|
+
const worktreeName =
|
|
219
|
+
typeof newWorktree === 'string' && newWorktree !== ''
|
|
220
|
+
? newWorktree
|
|
221
|
+
: `${assistantId}-${tabId.slice(-6)}`
|
|
222
|
+
createdWorktree = await createWorkspaceWorktree({
|
|
223
|
+
base: base ?? 'HEAD',
|
|
224
|
+
branch: branch ?? `aimux/${worktreeName}`,
|
|
225
|
+
daemon,
|
|
226
|
+
name: worktreeName,
|
|
227
|
+
workspace,
|
|
228
|
+
})
|
|
229
|
+
worktreeId = createdWorktree.id
|
|
230
|
+
worktreeRecord = createdWorktree
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const cwd = resolveTabCwd(cwdRaw, worktreeRecord)
|
|
234
|
+
const useFallback = daemon.hasCapability(IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK)
|
|
235
|
+
try {
|
|
236
|
+
await daemon.expectOk('createTab', {
|
|
237
|
+
args,
|
|
238
|
+
assistant: assistantId,
|
|
239
|
+
autoRenameCandidate: requestedTitle === undefined,
|
|
240
|
+
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
241
|
+
command: executable,
|
|
242
|
+
cwd,
|
|
243
|
+
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
244
|
+
tabId,
|
|
245
|
+
title,
|
|
246
|
+
workerName,
|
|
247
|
+
worktreeId,
|
|
248
|
+
})
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (createdWorktree !== undefined) {
|
|
251
|
+
return rollbackCreatedWorktree(createdWorktree, ctx, error)
|
|
252
|
+
}
|
|
253
|
+
throw error
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
assistant: assistantId,
|
|
258
|
+
branch: worktreeRecord?.branch ?? null,
|
|
259
|
+
command: [executable, ...args].join(' '),
|
|
260
|
+
cwd: cwd ?? null,
|
|
261
|
+
effort: effort ?? null,
|
|
262
|
+
model: model ?? null,
|
|
263
|
+
name: worktreeRecord?.name ?? null,
|
|
264
|
+
path: worktreeRecord?.path ?? null,
|
|
265
|
+
tabId,
|
|
266
|
+
title,
|
|
267
|
+
workerName: workerName ?? null,
|
|
268
|
+
worktreeId: worktreeId ?? null,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
53
272
|
export const tabCreate: CliCommand = {
|
|
54
273
|
args: [],
|
|
55
274
|
flags: [
|
|
56
275
|
...SHARED_FLAGS,
|
|
57
276
|
{
|
|
277
|
+
complete: { kind: 'dynamic', source: 'assistant' },
|
|
58
278
|
description: 'assistant id (claude, codex, opencode, grok, kimi, terminal, ...)',
|
|
59
279
|
kind: 'string',
|
|
60
280
|
name: 'assistant',
|
|
61
281
|
},
|
|
62
|
-
{ description: 'tab title (defaults to assistant label)', kind: 'string', name: 'title' },
|
|
63
|
-
{ description: 'cwd for the spawned PTY', kind: 'string', name: 'cwd' },
|
|
64
282
|
{
|
|
283
|
+
complete: { kind: 'none' },
|
|
284
|
+
description: 'tab title (defaults to assistant label)',
|
|
285
|
+
kind: 'string',
|
|
286
|
+
name: 'title',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
complete: { kind: 'file' },
|
|
290
|
+
description: 'cwd for the spawned PTY',
|
|
291
|
+
kind: 'string',
|
|
292
|
+
name: 'cwd',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
complete: { kind: 'none' },
|
|
65
296
|
description: 'explicit command (overrides the assistant default)',
|
|
66
297
|
kind: 'string',
|
|
67
298
|
name: 'command',
|
|
68
299
|
},
|
|
69
300
|
{
|
|
301
|
+
complete: { kind: 'none' },
|
|
70
302
|
description: 'model for the worker (maps to the assistant’s model flag)',
|
|
71
303
|
kind: 'string',
|
|
72
304
|
name: 'model',
|
|
73
305
|
},
|
|
74
306
|
{
|
|
307
|
+
complete: { kind: 'none' },
|
|
75
308
|
description: 'reasoning-effort level (maps to the assistant’s effort flag)',
|
|
76
309
|
kind: 'string',
|
|
77
310
|
name: 'effort',
|
|
78
311
|
},
|
|
79
312
|
{
|
|
313
|
+
complete: { kind: 'dynamic', source: 'worktree' },
|
|
80
314
|
description: 'worktree id the tab belongs to (defaults to the workspace’s active worktree)',
|
|
81
315
|
kind: 'string',
|
|
82
316
|
name: 'worktree',
|
|
83
317
|
},
|
|
84
318
|
{
|
|
319
|
+
complete: { kind: 'none' },
|
|
85
320
|
description: 'create a fresh worktree for this tab (optionally named: --new-worktree=<name>)',
|
|
86
321
|
kind: 'optional-string',
|
|
87
322
|
name: 'new-worktree',
|
|
88
323
|
},
|
|
89
|
-
{ description: 'base ref for --new-worktree (default HEAD)', kind: 'string', name: 'base' },
|
|
90
324
|
{
|
|
325
|
+
complete: { kind: 'dynamic', source: 'git-ref' },
|
|
326
|
+
description: 'base ref for --new-worktree (default HEAD)',
|
|
327
|
+
kind: 'string',
|
|
328
|
+
name: 'base',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
complete: { kind: 'none' },
|
|
91
332
|
description: 'branch for --new-worktree (default aimux/<name>)',
|
|
92
333
|
kind: 'string',
|
|
93
334
|
name: 'branch',
|
|
@@ -97,143 +338,37 @@ export const tabCreate: CliCommand = {
|
|
|
97
338
|
run: async (ctx) => {
|
|
98
339
|
const assistantId = ctx.args.flags.assistant
|
|
99
340
|
if (typeof assistantId !== 'string' || assistantId.length === 0) {
|
|
100
|
-
throw new
|
|
101
|
-
}
|
|
102
|
-
// Load the workspace's persisted customCommands so CLI-spawned tabs honor
|
|
103
|
-
// the same assistant commands the UI uses (e.g. skip-permissions flags), and
|
|
104
|
-
// so purely-custom assistant ids resolve. loadConfig degrades to {} on a
|
|
105
|
-
// missing/invalid config — no new failure mode.
|
|
106
|
-
const { customCommands } = loadConfig()
|
|
107
|
-
const options = getAllAssistantOptions(customCommands)
|
|
108
|
-
const option = options.find((o) => o.id === assistantId)
|
|
109
|
-
if (!option) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`unknown assistant: ${assistantId} (known: ${options.map((o) => o.id).join(', ')})`
|
|
112
|
-
)
|
|
341
|
+
throw new CliUsageError('--assistant is required')
|
|
113
342
|
}
|
|
114
343
|
const commandOverride =
|
|
115
344
|
typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : undefined
|
|
116
345
|
const model = typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined
|
|
117
346
|
const effort = typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined
|
|
118
347
|
|
|
119
|
-
|
|
120
|
-
// `--effort` (which only make sense as additions to the assistant default)
|
|
121
|
-
// would be ambiguous alongside it — reject rather than silently drop them.
|
|
122
|
-
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const command = resolveAssistantCommand(commandOverride, customCommands, option)
|
|
129
|
-
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : option.label
|
|
348
|
+
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : undefined
|
|
130
349
|
const cwdRaw = typeof ctx.args.flags.cwd === 'string' ? ctx.args.flags.cwd : undefined
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const newWorktree = newWorktreeFlag !== undefined
|
|
350
|
+
const newWorktreeRaw = ctx.args.flags['new-worktree']
|
|
351
|
+
const newWorktreeFlag =
|
|
352
|
+
typeof newWorktreeRaw === 'string' || typeof newWorktreeRaw === 'boolean'
|
|
353
|
+
? newWorktreeRaw
|
|
354
|
+
: undefined
|
|
137
355
|
const worktreeFlag =
|
|
138
356
|
typeof ctx.args.flags.worktree === 'string' ? ctx.args.flags.worktree : undefined
|
|
139
357
|
const baseFlag = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : undefined
|
|
140
358
|
const branchFlag = typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : undefined
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const workspace = ctx.getWorkspace()
|
|
152
|
-
const daemon = await ctx.getDaemon()
|
|
153
|
-
|
|
154
|
-
if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Resolve the worktree the tab belongs to + its record (for the cwd default
|
|
161
|
-
// and the output). Three modes: create a fresh one, use an explicit id, or
|
|
162
|
-
// fall back to the workspace's active worktree.
|
|
163
|
-
let worktreeId: string | undefined
|
|
164
|
-
let worktreeRecord: { branch?: string; name?: string; path: string } | undefined
|
|
165
|
-
if (newWorktree) {
|
|
166
|
-
const worktreeName =
|
|
167
|
-
typeof newWorktreeFlag === 'string' && newWorktreeFlag !== ''
|
|
168
|
-
? newWorktreeFlag
|
|
169
|
-
: `${assistantId}-${tabId.slice(-6)}`
|
|
170
|
-
const record = await createWorkspaceWorktree({
|
|
171
|
-
base: baseFlag ?? 'HEAD',
|
|
172
|
-
branch: branchFlag ?? `aimux/${worktreeName}`,
|
|
173
|
-
daemon,
|
|
174
|
-
name: worktreeName,
|
|
175
|
-
workspace,
|
|
176
|
-
})
|
|
177
|
-
worktreeId = record.id
|
|
178
|
-
worktreeRecord = record
|
|
179
|
-
} else {
|
|
180
|
-
worktreeId = worktreeFlag ?? workspace.activeWorktreeId
|
|
181
|
-
if (worktreeFlag !== undefined) {
|
|
182
|
-
const known = workspace.worktrees?.some((w) => w.id === worktreeFlag) ?? false
|
|
183
|
-
if (!known) {
|
|
184
|
-
const ids = workspace.worktrees?.map((w) => w.id).join(', ') ?? '(none)'
|
|
185
|
-
throw new Error(`unknown worktree id: ${worktreeFlag} (known: ${ids})`)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
worktreeRecord =
|
|
189
|
-
worktreeId !== undefined ? workspace.worktrees?.find((w) => w.id === worktreeId) : undefined
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Default the PTY cwd to the resolved worktree's path so a worker spawned
|
|
193
|
-
// into a worktree actually runs inside it. An explicit --cwd always wins.
|
|
194
|
-
const cwd = resolveTabCwd(cwdRaw, worktreeRecord)
|
|
195
|
-
|
|
196
|
-
// Thin-attach so we don't clobber the UI's dimensions on the same session.
|
|
197
|
-
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
198
|
-
|
|
199
|
-
const { args: baseArgs, executable } = parseCommand(command)
|
|
200
|
-
// Append the model/effort flags to the assistant default. Throws if the
|
|
201
|
-
// assistant has no control for a requested dimension.
|
|
202
|
-
const modelArgs = buildAssistantModelArgs(option, { effort, model })
|
|
203
|
-
const args = [...baseArgs, ...modelArgs]
|
|
204
|
-
|
|
205
|
-
// cols/rows = 0 means "fall back to the session's last attached size" on
|
|
206
|
-
// v11 daemons. Without that capability we have nothing reasonable to put
|
|
207
|
-
// here (the CLI has no terminal of its own), so use a roomy fallback —
|
|
208
|
-
// PTYs are reflowable, so 200×60 won't break anything that adapts.
|
|
209
|
-
const useFallback = daemon.hasCapability(IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK)
|
|
210
|
-
await daemon.expectOk('createTab', {
|
|
211
|
-
args,
|
|
212
|
-
assistant: assistantId,
|
|
213
|
-
autoRenameCandidate: ctx.args.flags.title === undefined,
|
|
214
|
-
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
215
|
-
command: executable,
|
|
216
|
-
cwd,
|
|
217
|
-
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
218
|
-
tabId,
|
|
219
|
-
title,
|
|
220
|
-
worktreeId,
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
const resolvedCommand = [executable, ...args].join(' ')
|
|
224
|
-
writeJson({
|
|
225
|
-
assistant: assistantId,
|
|
226
|
-
branch: worktreeRecord?.branch ?? null,
|
|
227
|
-
command: resolvedCommand,
|
|
228
|
-
cwd: cwd ?? null,
|
|
229
|
-
effort: effort ?? null,
|
|
230
|
-
model: model ?? null,
|
|
231
|
-
name: worktreeRecord?.name ?? null,
|
|
232
|
-
path: worktreeRecord?.path ?? null,
|
|
233
|
-
tabId,
|
|
359
|
+
const result = await createCliTab(ctx, {
|
|
360
|
+
assistantId,
|
|
361
|
+
base: baseFlag,
|
|
362
|
+
branch: branchFlag,
|
|
363
|
+
commandOverride,
|
|
364
|
+
cwd: cwdRaw,
|
|
365
|
+
effort,
|
|
366
|
+
model,
|
|
367
|
+
newWorktree: newWorktreeFlag,
|
|
234
368
|
title,
|
|
235
|
-
worktreeId:
|
|
369
|
+
worktreeId: worktreeFlag,
|
|
236
370
|
})
|
|
371
|
+
writeJson(result)
|
|
237
372
|
return EXIT_OK
|
|
238
373
|
},
|
|
239
374
|
summary: 'Create a new tab in the active workspace',
|
|
@@ -5,7 +5,7 @@ import { SHARED_FLAGS } from '../../flags'
|
|
|
5
5
|
import { EXIT_OK, writeJson } from '../../output'
|
|
6
6
|
|
|
7
7
|
export const tabFocus: CliCommand = {
|
|
8
|
-
args: [{ name: 'tabId', required: true }],
|
|
8
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
9
9
|
flags: SHARED_FLAGS,
|
|
10
10
|
group: 'tab',
|
|
11
11
|
run: async (ctx) => {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { DaemonClient } from '../../client/daemon-client'
|
|
7
7
|
|
|
8
8
|
import { bracketedPaste, notationToBytes } from '../../chord'
|
|
9
|
+
import { CliUsageError } from '../../flags'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Gap between the bracketed-paste write and the trailing carriage return when
|
|
@@ -33,7 +34,7 @@ export async function resolvePromptText(
|
|
|
33
34
|
(present) => present
|
|
34
35
|
).length
|
|
35
36
|
if (sources !== 1) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new CliUsageError(
|
|
37
38
|
'provide exactly one prompt source: --prompt-file <f>, --stdin, or a [text] positional'
|
|
38
39
|
)
|
|
39
40
|
}
|
|
@@ -91,7 +92,12 @@ export async function writePromptPayload(
|
|
|
91
92
|
payload: PromptPayload,
|
|
92
93
|
appendEnter: boolean
|
|
93
94
|
): Promise<number> {
|
|
94
|
-
|
|
95
|
+
// Skip a zero-length write: "submit only" (empty payload + --enter) is a
|
|
96
|
+
// legitimate operation, but a 0-byte write reaches the pty as an empty
|
|
97
|
+
// ArrayBufferView and surfaces a raw Bun ERR_INVALID_ARG_TYPE to the user.
|
|
98
|
+
if (payload.data !== '') {
|
|
99
|
+
await daemon.expectOk('write', { data: payload.data, tabId })
|
|
100
|
+
}
|
|
95
101
|
let bytesWritten = Buffer.byteLength(payload.data, 'utf8')
|
|
96
102
|
if (appendEnter) {
|
|
97
103
|
// A bracketed paste swallows a same-burst `\r`, so settle first, then
|
|
@@ -20,10 +20,18 @@ import { awaitTurn, DEFAULT_TIMEOUT_MS, turnOutcomeExitCode } from './await-turn
|
|
|
20
20
|
import { buildPromptPayload, resolvePromptText, writePromptPayload } from './prompt-io'
|
|
21
21
|
|
|
22
22
|
export const tabRun: CliCommand = {
|
|
23
|
-
args: [
|
|
23
|
+
args: [
|
|
24
|
+
{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true },
|
|
25
|
+
{ complete: { kind: 'none' }, name: 'text' },
|
|
26
|
+
],
|
|
24
27
|
flags: [
|
|
25
28
|
...SHARED_FLAGS,
|
|
26
|
-
{
|
|
29
|
+
{
|
|
30
|
+
complete: { kind: 'file' },
|
|
31
|
+
description: 'read the prompt from this file',
|
|
32
|
+
kind: 'string',
|
|
33
|
+
name: 'prompt-file',
|
|
34
|
+
},
|
|
27
35
|
{ description: 'read the prompt from stdin', kind: 'boolean', name: 'stdin' },
|
|
28
36
|
{
|
|
29
37
|
description: 'overall turn cap in milliseconds (default 900000 = 15 min)',
|
|
@@ -9,7 +9,10 @@ import { buildPromptPayload, writePromptPayload } from './prompt-io'
|
|
|
9
9
|
const DEFAULT_AWAIT_TIMEOUT_MS = 15_000
|
|
10
10
|
|
|
11
11
|
export const tabSend: CliCommand = {
|
|
12
|
-
args: [
|
|
12
|
+
args: [
|
|
13
|
+
{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true },
|
|
14
|
+
{ complete: { kind: 'none' }, name: 'text' },
|
|
15
|
+
],
|
|
13
16
|
flags: [
|
|
14
17
|
...SHARED_FLAGS,
|
|
15
18
|
{ description: 'append \\r so the receiving CLI submits', kind: 'boolean', name: 'enter' },
|
|
@@ -24,6 +27,7 @@ export const tabSend: CliCommand = {
|
|
|
24
27
|
name: 'stdin',
|
|
25
28
|
},
|
|
26
29
|
{
|
|
30
|
+
complete: { kind: 'file' },
|
|
27
31
|
description: 'read the payload from this file instead of <text>',
|
|
28
32
|
kind: 'string',
|
|
29
33
|
name: 'prompt-file',
|
|
@@ -59,12 +63,13 @@ export const tabSend: CliCommand = {
|
|
|
59
63
|
? ctx.args.flags['await-timeout']
|
|
60
64
|
: DEFAULT_AWAIT_TIMEOUT_MS
|
|
61
65
|
|
|
62
|
-
// Uptake only means something once
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
// Uptake only means something once something submits: either the appended
|
|
67
|
+
// `\r` (--enter) or a chord that carries its own submit (--keys "<CR>",
|
|
68
|
+
// which is the recovery path for a prompt already sitting in a composer).
|
|
69
|
+
// Without one of those there is nothing to confirm, so fail loudly rather
|
|
70
|
+
// than block forever on a transition that can't come.
|
|
71
|
+
if (awaitSubmit && !appendEnter && !asKeys) {
|
|
72
|
+
throw new Error('--await-submit requires --enter or --keys (the chord carries the submit)')
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
// At most one payload source. Unlike `tab run`, zero sources is valid here
|
|
@@ -9,11 +9,12 @@ import { snapshotTailLines, snapshotToLines } from '../../snapshot-render'
|
|
|
9
9
|
const RENDER_WAIT_MS = 500
|
|
10
10
|
|
|
11
11
|
export const tabSnapshot: CliCommand = {
|
|
12
|
-
args: [{ name: 'tabId', required: true }],
|
|
12
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
13
13
|
flags: [
|
|
14
14
|
...SHARED_FLAGS,
|
|
15
15
|
{ description: 'return only the last N non-blank lines', kind: 'number', name: 'tail' },
|
|
16
16
|
{
|
|
17
|
+
complete: { kind: 'values', values: ['json', 'text'] },
|
|
17
18
|
description: 'output format: json (default) or text (raw screen dump)',
|
|
18
19
|
kind: 'string',
|
|
19
20
|
name: 'format',
|
|
@@ -21,7 +21,7 @@ function toCursor(snapshot: TerminalSnapshot): Cursor {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export const tabTail: CliCommand = {
|
|
24
|
-
args: [{ name: 'tabId', required: true }],
|
|
24
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
25
25
|
flags: [
|
|
26
26
|
...SHARED_FLAGS,
|
|
27
27
|
{
|
|
@@ -12,10 +12,11 @@ function isTabActivity(value: string): value is TabActivity {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const tabWait: CliCommand = {
|
|
15
|
-
args: [{ name: 'tabId', required: true }],
|
|
15
|
+
args: [{ complete: { kind: 'dynamic', source: 'tab' }, name: 'tabId', required: true }],
|
|
16
16
|
flags: [
|
|
17
17
|
...SHARED_FLAGS,
|
|
18
18
|
{
|
|
19
|
+
complete: { kind: 'values', values: ['idle', 'waiting-input', 'working'] },
|
|
19
20
|
description: 'target activity (idle | working | waiting-input)',
|
|
20
21
|
kind: 'string',
|
|
21
22
|
name: 'status',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CliCommand } from '../../registry'
|
|
2
|
+
|
|
3
|
+
import { SHARED_FLAGS } from '../../flags'
|
|
4
|
+
import { writeJson } from '../../output'
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS } from '../tab/await-turn'
|
|
6
|
+
import {
|
|
7
|
+
awaitExistingWorker,
|
|
8
|
+
resolveWorkerTarget,
|
|
9
|
+
workerEnvelope,
|
|
10
|
+
workerOutcomeExitCode,
|
|
11
|
+
workerView,
|
|
12
|
+
} from './shared'
|
|
13
|
+
|
|
14
|
+
export const workerAwait: CliCommand = {
|
|
15
|
+
args: [{ complete: { kind: 'dynamic', source: 'worker' }, name: 'worker', required: true }],
|
|
16
|
+
flags: [
|
|
17
|
+
...SHARED_FLAGS,
|
|
18
|
+
{ description: 'overall turn cap in milliseconds', kind: 'number', name: 'timeout' },
|
|
19
|
+
],
|
|
20
|
+
group: 'worker',
|
|
21
|
+
run: async (ctx) => {
|
|
22
|
+
const { tab, workspace } = await resolveWorkerTarget(ctx, ctx.args.positionals[0] ?? '')
|
|
23
|
+
const outcome = await awaitExistingWorker(
|
|
24
|
+
ctx,
|
|
25
|
+
workspace,
|
|
26
|
+
tab.id,
|
|
27
|
+
typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS
|
|
28
|
+
)
|
|
29
|
+
writeJson(workerEnvelope(workspace, workerView(workspace, tab), outcome))
|
|
30
|
+
return workerOutcomeExitCode(outcome)
|
|
31
|
+
},
|
|
32
|
+
summary: "Await an existing worker's in-flight turn",
|
|
33
|
+
verb: 'await',
|
|
34
|
+
}
|