@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { buildHeadlessInvocation, type HeadlessInvocation } from '../auto-commit/headless-commands'
|
|
2
|
+
|
|
3
|
+
export type TitleSpawnFn = (
|
|
4
|
+
invocation: HeadlessInvocation,
|
|
5
|
+
signal: AbortSignal
|
|
6
|
+
) => Promise<{ stdout: string; exitCode: number } | null>
|
|
7
|
+
|
|
8
|
+
export function buildTitlePrompt(firstPrompt: string): string {
|
|
9
|
+
return [
|
|
10
|
+
'Create a concise tab title for the user request below.',
|
|
11
|
+
'Return only the title: 2 to 6 words, at most 48 characters, in the same language as the request.',
|
|
12
|
+
'Do not use quotes, a label, markdown, or ending punctuation.',
|
|
13
|
+
'',
|
|
14
|
+
firstPrompt.slice(0, 8_000),
|
|
15
|
+
].join('\n')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sanitizeGeneratedTitle(raw: string): string | null {
|
|
19
|
+
const first = raw
|
|
20
|
+
.split(/\r?\n/u)
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.find(Boolean)
|
|
23
|
+
if (first == null || first === '') return null
|
|
24
|
+
|
|
25
|
+
const unlabelled = first.replace(/^TITLE\s*:\s*/iu, '').replaceAll(/^["'“”‘’]+|["'“”‘’]+$/gu, '')
|
|
26
|
+
const clean = unlabelled
|
|
27
|
+
.replaceAll(/\s+/gu, ' ')
|
|
28
|
+
.replace(/[.!?,;:…]+$/u, '')
|
|
29
|
+
.trim()
|
|
30
|
+
const words = clean.split(' ').filter(Boolean)
|
|
31
|
+
const usesUnspacedScript = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(clean)
|
|
32
|
+
if (words.length < 2 && !usesUnspacedScript) return null
|
|
33
|
+
|
|
34
|
+
let title = words.slice(0, 6).join(' ')
|
|
35
|
+
if (title.length > 48) {
|
|
36
|
+
title = title
|
|
37
|
+
.slice(0, 48)
|
|
38
|
+
.replace(/\s+\S*$/u, '')
|
|
39
|
+
.trim()
|
|
40
|
+
}
|
|
41
|
+
return title === '' || (title.split(' ').filter(Boolean).length < 2 && !usesUnspacedScript)
|
|
42
|
+
? null
|
|
43
|
+
: title
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function generateTabTitle(options: {
|
|
47
|
+
provider: string
|
|
48
|
+
model?: string
|
|
49
|
+
firstPrompt: string
|
|
50
|
+
timeoutMs: number
|
|
51
|
+
signal: AbortSignal
|
|
52
|
+
spawn?: TitleSpawnFn
|
|
53
|
+
}): Promise<string | null> {
|
|
54
|
+
const invocation = buildHeadlessInvocation(
|
|
55
|
+
options.provider,
|
|
56
|
+
buildTitlePrompt(options.firstPrompt),
|
|
57
|
+
options.model
|
|
58
|
+
)
|
|
59
|
+
if (!invocation) return null
|
|
60
|
+
|
|
61
|
+
const signal = AbortSignal.any([options.signal, AbortSignal.timeout(options.timeoutMs)])
|
|
62
|
+
try {
|
|
63
|
+
const result = await (options.spawn ?? defaultSpawn)(invocation, signal)
|
|
64
|
+
if (!result || result.exitCode !== 0 || signal.aborted) return null
|
|
65
|
+
return sanitizeGeneratedTitle(result.stdout)
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function defaultSpawn(
|
|
72
|
+
invocation: HeadlessInvocation,
|
|
73
|
+
signal: AbortSignal
|
|
74
|
+
): Promise<{ stdout: string; exitCode: number } | null> {
|
|
75
|
+
try {
|
|
76
|
+
const proc = Bun.spawn([invocation.executable, ...invocation.args], {
|
|
77
|
+
stderr: 'ignore',
|
|
78
|
+
stdin: 'ignore',
|
|
79
|
+
stdout: 'pipe',
|
|
80
|
+
})
|
|
81
|
+
const abort = () => {
|
|
82
|
+
try {
|
|
83
|
+
proc.kill()
|
|
84
|
+
} catch {
|
|
85
|
+
// Best effort: the process may already have exited.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
89
|
+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited])
|
|
90
|
+
signal.removeEventListener('abort', abort)
|
|
91
|
+
if (signal.aborted) return null
|
|
92
|
+
return { exitCode: exitCode ?? 1, stdout }
|
|
93
|
+
} catch {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -135,10 +135,26 @@ export class DaemonClient {
|
|
|
135
135
|
return this.hello.capabilities
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
getAppVersion(): string | null {
|
|
139
|
+
return this.hello.appVersion ?? null
|
|
140
|
+
}
|
|
141
|
+
|
|
138
142
|
getSelectedVersion(): number {
|
|
139
143
|
return this.hello.selectedVersion
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
getProcessVersion(): string {
|
|
147
|
+
return this.hello.processVersion
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getManagerCapabilities(): readonly string[] {
|
|
151
|
+
return this.hello.managerCapabilities ?? []
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getManagerSelectedVersion(): number | null {
|
|
155
|
+
return this.hello.managerSelectedVersion ?? null
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
private async send(request: ClientRequest): Promise<ServerResponse> {
|
|
143
159
|
return new Promise((resolve, reject) => {
|
|
144
160
|
const timer = setTimeout(() => {
|
|
@@ -1,11 +1,16 @@
|
|
|
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'
|
|
11
16
|
import {
|
|
@@ -14,13 +19,42 @@ import {
|
|
|
14
19
|
getAllAssistantOptions,
|
|
15
20
|
parseCommand,
|
|
16
21
|
} from '../../../pty/command-registry'
|
|
17
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
22
|
+
import { CliUsageError, SHARED_FLAGS } from '../../flags'
|
|
18
23
|
import { EXIT_OK, writeJson } from '../../output'
|
|
19
24
|
import { createWorkspaceWorktree } from '../worktree/create-core'
|
|
20
25
|
|
|
21
26
|
const FALLBACK_COLS = 200
|
|
22
27
|
const FALLBACK_ROWS = 60
|
|
23
28
|
|
|
29
|
+
export interface CreateCliTabOptions {
|
|
30
|
+
assistantId: string
|
|
31
|
+
base?: string
|
|
32
|
+
branch?: string
|
|
33
|
+
commandOverride?: string
|
|
34
|
+
cwd?: string
|
|
35
|
+
effort?: string
|
|
36
|
+
model?: string
|
|
37
|
+
newWorktree?: boolean | string
|
|
38
|
+
title?: string
|
|
39
|
+
workerName?: string
|
|
40
|
+
worktreeId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CreateCliTabResult {
|
|
44
|
+
assistant: string
|
|
45
|
+
branch: string | null
|
|
46
|
+
command: string
|
|
47
|
+
cwd: string | null
|
|
48
|
+
effort: string | null
|
|
49
|
+
model: string | null
|
|
50
|
+
name: string | null
|
|
51
|
+
path: string | null
|
|
52
|
+
tabId: string
|
|
53
|
+
title: string
|
|
54
|
+
workerName: string | null
|
|
55
|
+
worktreeId: string | null
|
|
56
|
+
}
|
|
57
|
+
|
|
24
58
|
/**
|
|
25
59
|
* Resolve the cwd a spawned tab should run in. Precedence: explicit `--cwd`
|
|
26
60
|
* (resolved to absolute) > the resolved worktree's path > undefined (the
|
|
@@ -50,6 +84,189 @@ export function resolveAssistantCommand(
|
|
|
50
84
|
return commandOverride ?? customCommands[option.id] ?? option.command
|
|
51
85
|
}
|
|
52
86
|
|
|
87
|
+
async function rollbackCreatedWorktree(
|
|
88
|
+
record: WorktreeRecord,
|
|
89
|
+
ctx: CliContext,
|
|
90
|
+
originalError: unknown
|
|
91
|
+
): Promise<never> {
|
|
92
|
+
const daemon = await ctx.getDaemon()
|
|
93
|
+
const workspace = ctx.getWorkspace()
|
|
94
|
+
let rollbackError: unknown
|
|
95
|
+
try {
|
|
96
|
+
await removeGitWorktree({
|
|
97
|
+
force: true,
|
|
98
|
+
repoPath: record.repoRoot,
|
|
99
|
+
targetPath: record.path,
|
|
100
|
+
})
|
|
101
|
+
await daemon.expectOk('removeWorktreeRecord', {
|
|
102
|
+
sessionId: workspace.id,
|
|
103
|
+
worktreeId: record.id,
|
|
104
|
+
})
|
|
105
|
+
} catch (error) {
|
|
106
|
+
rollbackError = error
|
|
107
|
+
}
|
|
108
|
+
const original = originalError instanceof Error ? originalError.message : String(originalError)
|
|
109
|
+
if (rollbackError === undefined) throw originalError
|
|
110
|
+
const rollback =
|
|
111
|
+
rollbackError instanceof Error ? rollbackError.message : JSON.stringify(rollbackError)
|
|
112
|
+
throw new Error(`${original}; rollback failed: ${rollback}`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Create a tab without writing to stdout. Worker commands compose this helper
|
|
117
|
+
* with prompt dispatch/await while `tab create` remains a thin compatibility
|
|
118
|
+
* adapter. All validation and daemon attachment happen before a worktree is
|
|
119
|
+
* created; any later failure rolls the fresh worktree back.
|
|
120
|
+
*/
|
|
121
|
+
export async function createCliTab(
|
|
122
|
+
ctx: CliContext,
|
|
123
|
+
options: CreateCliTabOptions
|
|
124
|
+
): Promise<CreateCliTabResult> {
|
|
125
|
+
const {
|
|
126
|
+
assistantId,
|
|
127
|
+
base,
|
|
128
|
+
branch,
|
|
129
|
+
commandOverride,
|
|
130
|
+
cwd: cwdRaw,
|
|
131
|
+
effort,
|
|
132
|
+
model,
|
|
133
|
+
newWorktree,
|
|
134
|
+
title: requestedTitle,
|
|
135
|
+
workerName,
|
|
136
|
+
worktreeId: requestedWorktreeId,
|
|
137
|
+
} = options
|
|
138
|
+
if (assistantId.length === 0) throw new CliUsageError('--assistant is required')
|
|
139
|
+
if (workerName !== undefined && workerName.trim().length === 0) {
|
|
140
|
+
throw new CliUsageError('--name must be a non-empty string')
|
|
141
|
+
}
|
|
142
|
+
if (commandOverride !== undefined && (model !== undefined || effort !== undefined)) {
|
|
143
|
+
throw new CliUsageError(
|
|
144
|
+
'--model / --effort cannot be combined with --command (bake them into --command)'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const createFreshWorktree = newWorktree !== undefined && newWorktree !== false
|
|
149
|
+
if (createFreshWorktree && requestedWorktreeId !== undefined) {
|
|
150
|
+
throw new CliUsageError(
|
|
151
|
+
'--new-worktree creates its own; use --worktree <id> to co-locate instead'
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
if (createFreshWorktree && cwdRaw !== undefined) {
|
|
155
|
+
throw new CliUsageError('--new-worktree sets the cwd to the new worktree; drop --cwd')
|
|
156
|
+
}
|
|
157
|
+
if (!createFreshWorktree && (base !== undefined || branch !== undefined)) {
|
|
158
|
+
throw new CliUsageError(
|
|
159
|
+
'--base / --branch require --new-worktree (use `worktree create` otherwise)'
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Resolve and validate the complete assistant invocation before touching git.
|
|
164
|
+
const { customCommands } = loadConfig()
|
|
165
|
+
const option = getAllAssistantOptions(customCommands).find((entry) => entry.id === assistantId)
|
|
166
|
+
if (!option) {
|
|
167
|
+
const known = getAllAssistantOptions(customCommands)
|
|
168
|
+
.map((entry) => entry.id)
|
|
169
|
+
.join(', ')
|
|
170
|
+
throw new CliUsageError(`unknown assistant: ${assistantId} (known: ${known})`)
|
|
171
|
+
}
|
|
172
|
+
const command = resolveAssistantCommand(commandOverride, customCommands, option)
|
|
173
|
+
const { args: baseArgs, executable } = parseCommand(command)
|
|
174
|
+
const args = [...baseArgs, ...buildAssistantModelArgs(option, { effort, model })]
|
|
175
|
+
const title = requestedTitle ?? option.label
|
|
176
|
+
const tabId = createPrefixedId('tab')
|
|
177
|
+
|
|
178
|
+
const workspace = ctx.getWorkspace()
|
|
179
|
+
const daemon = await ctx.getDaemon()
|
|
180
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_THIN_ATTACH)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
'daemon predates thinAttach capability — restart aimux to pick up the new daemon'
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
if (workerName !== undefined && !daemon.hasCapability(IPC_CAPABILITY_WORKER_METADATA)) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
'daemon predates workerMetadata capability — restart aimux to use worker commands'
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
if (workerName !== undefined) {
|
|
191
|
+
if (!daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
192
|
+
throw new Error('daemon cannot validate worker-name uniqueness — restart aimux')
|
|
193
|
+
}
|
|
194
|
+
const existing = await daemon.listTabs(workspace.id)
|
|
195
|
+
if (existing.tabs.some((tab) => tab.workerName === workerName)) {
|
|
196
|
+
throw new Error(`worker name already exists in workspace "${workspace.name}": ${workerName}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Attach before creating a worktree so stale daemon/session failures have no
|
|
201
|
+
// git-side effect.
|
|
202
|
+
await daemon.attach({ cols: 0, rows: 0, sessionId: workspace.id, thin: true })
|
|
203
|
+
|
|
204
|
+
let worktreeId = requestedWorktreeId ?? workspace.activeWorktreeId
|
|
205
|
+
let worktreeRecord =
|
|
206
|
+
worktreeId !== undefined
|
|
207
|
+
? workspace.worktrees?.find((entry) => entry.id === worktreeId)
|
|
208
|
+
: undefined
|
|
209
|
+
if (requestedWorktreeId !== undefined && worktreeRecord === undefined) {
|
|
210
|
+
const ids = workspace.worktrees?.map((entry) => entry.id).join(', ') ?? '(none)'
|
|
211
|
+
throw new Error(`unknown worktree id: ${requestedWorktreeId} (known: ${ids})`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let createdWorktree: WorktreeRecord | undefined
|
|
215
|
+
if (createFreshWorktree) {
|
|
216
|
+
const worktreeName =
|
|
217
|
+
typeof newWorktree === 'string' && newWorktree !== ''
|
|
218
|
+
? newWorktree
|
|
219
|
+
: `${assistantId}-${tabId.slice(-6)}`
|
|
220
|
+
createdWorktree = await createWorkspaceWorktree({
|
|
221
|
+
base: base ?? 'HEAD',
|
|
222
|
+
branch: branch ?? `aimux/${worktreeName}`,
|
|
223
|
+
daemon,
|
|
224
|
+
name: worktreeName,
|
|
225
|
+
workspace,
|
|
226
|
+
})
|
|
227
|
+
worktreeId = createdWorktree.id
|
|
228
|
+
worktreeRecord = createdWorktree
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const cwd = resolveTabCwd(cwdRaw, worktreeRecord)
|
|
232
|
+
const useFallback = daemon.hasCapability(IPC_CAPABILITY_CREATE_TAB_SIZE_FALLBACK)
|
|
233
|
+
try {
|
|
234
|
+
await daemon.expectOk('createTab', {
|
|
235
|
+
args,
|
|
236
|
+
assistant: assistantId,
|
|
237
|
+
autoRenameCandidate: requestedTitle === undefined,
|
|
238
|
+
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
239
|
+
command: executable,
|
|
240
|
+
cwd,
|
|
241
|
+
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
242
|
+
tabId,
|
|
243
|
+
title,
|
|
244
|
+
workerName,
|
|
245
|
+
worktreeId,
|
|
246
|
+
})
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (createdWorktree !== undefined) {
|
|
249
|
+
return rollbackCreatedWorktree(createdWorktree, ctx, error)
|
|
250
|
+
}
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
assistant: assistantId,
|
|
256
|
+
branch: worktreeRecord?.branch ?? null,
|
|
257
|
+
command: [executable, ...args].join(' '),
|
|
258
|
+
cwd: cwd ?? null,
|
|
259
|
+
effort: effort ?? null,
|
|
260
|
+
model: model ?? null,
|
|
261
|
+
name: worktreeRecord?.name ?? null,
|
|
262
|
+
path: worktreeRecord?.path ?? null,
|
|
263
|
+
tabId,
|
|
264
|
+
title,
|
|
265
|
+
workerName: workerName ?? null,
|
|
266
|
+
worktreeId: worktreeId ?? null,
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
53
270
|
export const tabCreate: CliCommand = {
|
|
54
271
|
args: [],
|
|
55
272
|
flags: [
|
|
@@ -97,142 +314,37 @@ export const tabCreate: CliCommand = {
|
|
|
97
314
|
run: async (ctx) => {
|
|
98
315
|
const assistantId = ctx.args.flags.assistant
|
|
99
316
|
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
|
-
)
|
|
317
|
+
throw new CliUsageError('--assistant is required')
|
|
113
318
|
}
|
|
114
319
|
const commandOverride =
|
|
115
320
|
typeof ctx.args.flags.command === 'string' ? ctx.args.flags.command : undefined
|
|
116
321
|
const model = typeof ctx.args.flags.model === 'string' ? ctx.args.flags.model : undefined
|
|
117
322
|
const effort = typeof ctx.args.flags.effort === 'string' ? ctx.args.flags.effort : undefined
|
|
118
323
|
|
|
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
|
|
324
|
+
const title = typeof ctx.args.flags.title === 'string' ? ctx.args.flags.title : undefined
|
|
130
325
|
const cwdRaw = typeof ctx.args.flags.cwd === 'string' ? ctx.args.flags.cwd : undefined
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const newWorktree = newWorktreeFlag !== undefined
|
|
326
|
+
const newWorktreeRaw = ctx.args.flags['new-worktree']
|
|
327
|
+
const newWorktreeFlag =
|
|
328
|
+
typeof newWorktreeRaw === 'string' || typeof newWorktreeRaw === 'boolean'
|
|
329
|
+
? newWorktreeRaw
|
|
330
|
+
: undefined
|
|
137
331
|
const worktreeFlag =
|
|
138
332
|
typeof ctx.args.flags.worktree === 'string' ? ctx.args.flags.worktree : undefined
|
|
139
333
|
const baseFlag = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : undefined
|
|
140
334
|
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
|
-
cols: useFallback ? 0 : FALLBACK_COLS,
|
|
214
|
-
command: executable,
|
|
215
|
-
cwd,
|
|
216
|
-
rows: useFallback ? 0 : FALLBACK_ROWS,
|
|
217
|
-
tabId,
|
|
218
|
-
title,
|
|
219
|
-
worktreeId,
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
const resolvedCommand = [executable, ...args].join(' ')
|
|
223
|
-
writeJson({
|
|
224
|
-
assistant: assistantId,
|
|
225
|
-
branch: worktreeRecord?.branch ?? null,
|
|
226
|
-
command: resolvedCommand,
|
|
227
|
-
cwd: cwd ?? null,
|
|
228
|
-
effort: effort ?? null,
|
|
229
|
-
model: model ?? null,
|
|
230
|
-
name: worktreeRecord?.name ?? null,
|
|
231
|
-
path: worktreeRecord?.path ?? null,
|
|
232
|
-
tabId,
|
|
335
|
+
const result = await createCliTab(ctx, {
|
|
336
|
+
assistantId,
|
|
337
|
+
base: baseFlag,
|
|
338
|
+
branch: branchFlag,
|
|
339
|
+
commandOverride,
|
|
340
|
+
cwd: cwdRaw,
|
|
341
|
+
effort,
|
|
342
|
+
model,
|
|
343
|
+
newWorktree: newWorktreeFlag,
|
|
233
344
|
title,
|
|
234
|
-
worktreeId:
|
|
345
|
+
worktreeId: worktreeFlag,
|
|
235
346
|
})
|
|
347
|
+
writeJson(result)
|
|
236
348
|
return EXIT_OK
|
|
237
349
|
},
|
|
238
350
|
summary: 'Create a new tab in the active workspace',
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
resolveWorkerTab,
|
|
9
|
+
workerEnvelope,
|
|
10
|
+
workerOutcomeExitCode,
|
|
11
|
+
workerView,
|
|
12
|
+
} from './shared'
|
|
13
|
+
|
|
14
|
+
export const workerAwait: CliCommand = {
|
|
15
|
+
args: [{ 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 = await resolveWorkerTab(ctx, ctx.args.positionals[0] ?? '')
|
|
23
|
+
const outcome = await awaitExistingWorker(
|
|
24
|
+
ctx,
|
|
25
|
+
tab.id,
|
|
26
|
+
typeof ctx.args.flags.timeout === 'number' ? ctx.args.flags.timeout : DEFAULT_TIMEOUT_MS
|
|
27
|
+
)
|
|
28
|
+
writeJson(workerEnvelope(workerView(ctx, tab), outcome))
|
|
29
|
+
return workerOutcomeExitCode(outcome)
|
|
30
|
+
},
|
|
31
|
+
summary: "Await an existing worker's in-flight turn",
|
|
32
|
+
verb: 'await',
|
|
33
|
+
}
|