@brimveyn/aimux 1.21.0 → 1.22.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 +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename } from 'node:path'
|
|
2
|
+
|
|
1
3
|
import type { AssistantId } from '../state/types'
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -22,6 +24,21 @@ export interface AssistantOption {
|
|
|
22
24
|
command: string
|
|
23
25
|
description: string
|
|
24
26
|
model?: AssistantModelSpec
|
|
27
|
+
/**
|
|
28
|
+
* The CLI starts an interactive session with a positional prompt argument
|
|
29
|
+
* (`claude "…"`, `codex "…"`). When it does, handing the prompt over at spawn
|
|
30
|
+
* beats pasting it into the running TUI afterwards: no readiness poll, no
|
|
31
|
+
* screen probe, no retries.
|
|
32
|
+
*
|
|
33
|
+
* Absent means "unknown or not supported" and the caller falls back to
|
|
34
|
+
* `injectPromptWhenReady`. `opencode`'s positional is a project path and its
|
|
35
|
+
* `run` subcommand is non-interactive, so it stays on the fallback.
|
|
36
|
+
*
|
|
37
|
+
* ponytail: a boolean, because both CLIs that support it are positional.
|
|
38
|
+
* Promote to a builder — as `AssistantModelSpec` already does for flags — when
|
|
39
|
+
* a `--prompt <x>`-shaped one shows up.
|
|
40
|
+
*/
|
|
41
|
+
acceptsPromptArg?: boolean
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
const DEFAULT_SHELL =
|
|
@@ -30,6 +47,7 @@ const SHELL_NAME = DEFAULT_SHELL.split('/').pop() ?? 'shell'
|
|
|
30
47
|
|
|
31
48
|
export const ASSISTANT_OPTIONS: AssistantOption[] = [
|
|
32
49
|
{
|
|
50
|
+
acceptsPromptArg: true,
|
|
33
51
|
command: 'claude',
|
|
34
52
|
description: 'Anthropic Claude CLI',
|
|
35
53
|
id: 'claude',
|
|
@@ -40,6 +58,7 @@ export const ASSISTANT_OPTIONS: AssistantOption[] = [
|
|
|
40
58
|
},
|
|
41
59
|
},
|
|
42
60
|
{
|
|
61
|
+
acceptsPromptArg: true,
|
|
43
62
|
command: 'codex',
|
|
44
63
|
description: 'OpenAI Codex CLI',
|
|
45
64
|
id: 'codex',
|
|
@@ -116,12 +135,73 @@ export function getAllAssistantOptions(customCommands: Record<string, string>):
|
|
|
116
135
|
return [...ASSISTANT_OPTIONS, ...customOptions]
|
|
117
136
|
}
|
|
118
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Whether this assistant can take the initial prompt as a spawn argument.
|
|
140
|
+
*
|
|
141
|
+
* A custom command counts only while it still runs the same program — extra
|
|
142
|
+
* flags are fine, a wrapper is not. A wrapper that forgets `"$@"` would swallow
|
|
143
|
+
* the prompt with no error anywhere, and pasting works for any command, so the
|
|
144
|
+
* unrecognised executable falls back rather than gambling.
|
|
145
|
+
*/
|
|
146
|
+
export function assistantAcceptsPromptArg(
|
|
147
|
+
assistant: AssistantId,
|
|
148
|
+
customCommands: Record<string, string>
|
|
149
|
+
): boolean {
|
|
150
|
+
const option = getAllAssistantOptions(customCommands).find((entry) => entry.id === assistant)
|
|
151
|
+
if (option?.acceptsPromptArg !== true) return false
|
|
152
|
+
const custom = customCommands[assistant]
|
|
153
|
+
if (custom == null || custom === '') return true
|
|
154
|
+
return basename(parseCommand(custom).executable) === option.command
|
|
155
|
+
}
|
|
156
|
+
|
|
119
157
|
export function isCommandAvailable(command: string): boolean {
|
|
120
158
|
return Bun.which(command) !== null
|
|
121
159
|
}
|
|
122
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Minimal POSIX shell-word splitter — respects single/double quotes and
|
|
163
|
+
* backslash escapes so values like `code --user-data-dir "/tmp/foo bar"` or a
|
|
164
|
+
* script path under a `$HOME` containing a space tokenize correctly.
|
|
165
|
+
* Does not expand variables or globs.
|
|
166
|
+
*/
|
|
167
|
+
export function shellSplit(input: string): string[] {
|
|
168
|
+
const out: string[] = []
|
|
169
|
+
let current = ''
|
|
170
|
+
let inSingle = false
|
|
171
|
+
let inDouble = false
|
|
172
|
+
let hasToken = false
|
|
173
|
+
for (let i = 0; i < input.length; i++) {
|
|
174
|
+
const c = input[i] ?? ''
|
|
175
|
+
if (!inSingle && !inDouble && /\s/.test(c)) {
|
|
176
|
+
if (hasToken) {
|
|
177
|
+
out.push(current)
|
|
178
|
+
current = ''
|
|
179
|
+
hasToken = false
|
|
180
|
+
}
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
hasToken = true
|
|
184
|
+
if (c === "'" && !inDouble) {
|
|
185
|
+
inSingle = !inSingle
|
|
186
|
+
} else if (c === '"' && !inSingle) {
|
|
187
|
+
inDouble = !inDouble
|
|
188
|
+
} else if (c === '\\' && !inSingle && i + 1 < input.length) {
|
|
189
|
+
current += input[++i]
|
|
190
|
+
} else {
|
|
191
|
+
current += c
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (hasToken) out.push(current)
|
|
195
|
+
return out
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Wrap a value so `shellSplit` (and a real shell) return it as one word. */
|
|
199
|
+
export function shellQuote(value: string): string {
|
|
200
|
+
return `'${value.replaceAll("'", `'\\''`)}'`
|
|
201
|
+
}
|
|
202
|
+
|
|
123
203
|
export function parseCommand(commandString: string): { executable: string; args: string[] } {
|
|
124
|
-
const parts = commandString.trim()
|
|
204
|
+
const parts = shellSplit(commandString.trim())
|
|
125
205
|
return { args: parts.slice(1), executable: parts[0] ?? '' }
|
|
126
206
|
}
|
|
127
207
|
|
|
@@ -21,7 +21,7 @@ export async function runRestartTerminalManager(): Promise<number> {
|
|
|
21
21
|
const managerPid = await findTerminalManagerPid()
|
|
22
22
|
if (managerPid !== null) {
|
|
23
23
|
process.stdout.write(
|
|
24
|
-
`WARNING: killing terminal-manager (pid ${managerPid}) will kill live
|
|
24
|
+
`WARNING: killing terminal-manager (pid ${managerPid}) will kill live projects.\n`
|
|
25
25
|
)
|
|
26
26
|
await killProcess(managerPid)
|
|
27
27
|
process.stdout.write('Terminal-manager stopped.\n')
|
|
@@ -6,7 +6,14 @@ import { fetchClaudeUsage } from './adapters/claude'
|
|
|
6
6
|
import { fetchCodexUsage } from './adapters/codex'
|
|
7
7
|
import { loadCachedSnapshot, saveCachedSnapshot } from './cache'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Claude's OAuth usage endpoint drops a caller polling faster than this into a
|
|
11
|
+
* punitive rate-limit bucket, which shows up as an indicator that stops updating
|
|
12
|
+
* rather than as an error. So it is the floor as well as the default, and it is
|
|
13
|
+
* not per-tool: one interval drives every tool in the list, and a Codex-only
|
|
14
|
+
* setup loses nothing measurable by refreshing a quota bar every three minutes.
|
|
15
|
+
*/
|
|
16
|
+
const MIN_POLL_SECONDS = 180
|
|
10
17
|
const DEFAULT_TOOLS: AIUsageTool[] = ['claude', 'codex']
|
|
11
18
|
|
|
12
19
|
export interface AIUsageServiceHandle {
|
|
@@ -28,7 +35,7 @@ export function startAIUsageService(
|
|
|
28
35
|
onUpdate: (snap: UsageSnapshot) => void
|
|
29
36
|
): AIUsageServiceHandle {
|
|
30
37
|
const tools = config.tools && config.tools.length > 0 ? config.tools : DEFAULT_TOOLS
|
|
31
|
-
const pollMs = Math.max(
|
|
38
|
+
const pollMs = Math.max(MIN_POLL_SECONDS, config.pollSeconds ?? MIN_POLL_SECONDS) * 1000
|
|
32
39
|
|
|
33
40
|
let stopped = false
|
|
34
41
|
let timer: ReturnType<typeof setTimeout> | null = null
|
|
@@ -13,12 +13,25 @@ export async function runCli(
|
|
|
13
13
|
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
14
14
|
cwd?: string
|
|
15
15
|
): Promise<CliResult> {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
let proc: Bun.Subprocess<'ignore', 'pipe', 'pipe'>
|
|
17
|
+
try {
|
|
18
|
+
proc = Bun.spawn([command, ...args], {
|
|
19
|
+
cwd,
|
|
20
|
+
stderr: 'pipe',
|
|
21
|
+
stdin: 'ignore',
|
|
22
|
+
stdout: 'pipe',
|
|
23
|
+
})
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// posix_spawn throws — synchronously — for a missing binary AND for a cwd
|
|
26
|
+
// that no longer exists (a deleted project dir or a pruned worktree), and
|
|
27
|
+
// it names the binary in both. Uncaught, that took the whole app down.
|
|
28
|
+
return {
|
|
29
|
+
error: `${command}: ${String(error)}`.slice(0, 200),
|
|
30
|
+
ok: false,
|
|
31
|
+
stderr: '',
|
|
32
|
+
stdout: '',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
22
35
|
|
|
23
36
|
const timeout = setTimeout(() => {
|
|
24
37
|
try {
|
|
@@ -106,7 +106,7 @@ export async function probeDaemonProtocolCompatibility(
|
|
|
106
106
|
const helloRequestId = crypto.randomUUID()
|
|
107
107
|
const attachRequestId = crypto.randomUUID()
|
|
108
108
|
const disposeRequestId = crypto.randomUUID()
|
|
109
|
-
const
|
|
109
|
+
const probeProjectId = `probe-${crypto.randomUUID()}`
|
|
110
110
|
let daemonProcessVersion: string | undefined
|
|
111
111
|
let daemonCapabilities: readonly string[] = []
|
|
112
112
|
let daemonManagerSelectedVersion: number | undefined
|
|
@@ -167,9 +167,9 @@ export async function probeDaemonProtocolCompatibility(
|
|
|
167
167
|
id: attachRequestId,
|
|
168
168
|
payload: {
|
|
169
169
|
cols: 80,
|
|
170
|
+
projectId: probeProjectId,
|
|
170
171
|
protocolVersion: message.payload.selectedVersion,
|
|
171
172
|
rows: 24,
|
|
172
|
-
sessionId: probeSessionId,
|
|
173
173
|
},
|
|
174
174
|
type: 'attach',
|
|
175
175
|
})
|
|
@@ -311,7 +311,7 @@ export async function createSessionBackend(opts?: {
|
|
|
311
311
|
// Ring 3: prefer hot-reexec over the legacy stopTM+restart path when
|
|
312
312
|
// the running daemon advertises `hotReexec` and the operator has opted
|
|
313
313
|
// in via AIMUX_HOT_REEXEC=1. The terminal-manager and every PTY stay
|
|
314
|
-
// alive; the daemon binary swaps under them — no
|
|
314
|
+
// alive; the daemon binary swaps under them — no project-loss prompt.
|
|
315
315
|
const reexecEnabled = process.env.AIMUX_HOT_REEXEC === '1'
|
|
316
316
|
const daemonSupportsReexec =
|
|
317
317
|
handshake.capabilities?.includes(IPC_CAPABILITY_HOT_REEXEC) ?? false
|
|
@@ -363,7 +363,7 @@ export async function createSessionBackend(opts?: {
|
|
|
363
363
|
|
|
364
364
|
// We're about to kill the terminal-manager and every PTY — warn the UI
|
|
365
365
|
// now so the user isn't surprised. The hot-reexec branch above never
|
|
366
|
-
// reaches this point, so the callback fires only when
|
|
366
|
+
// reaches this point, so the callback fires only when projects really
|
|
367
367
|
// do die.
|
|
368
368
|
await opts?.onBreakingUpdateRequired?.()
|
|
369
369
|
|
|
@@ -2,11 +2,11 @@ import { EventEmitter } from 'node:events'
|
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
AssistantId,
|
|
5
|
+
ProjectSnapshotV1,
|
|
5
6
|
TerminalModeState,
|
|
6
7
|
TerminalSnapshot,
|
|
7
|
-
WorkspaceSnapshotV1,
|
|
8
8
|
} from '../state/types'
|
|
9
|
-
import type { ResizeOptions, SessionBackend
|
|
9
|
+
import type { ProjectBackendEvents, ResizeOptions, SessionBackend } from './types'
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
type AutoRenameConfigSnapshot,
|
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
} from '../state/layout-resize'
|
|
25
25
|
|
|
26
26
|
export class LocalSessionBackend
|
|
27
|
-
extends EventEmitter<
|
|
27
|
+
extends EventEmitter<ProjectBackendEvents>
|
|
28
28
|
implements SessionBackend
|
|
29
29
|
{
|
|
30
30
|
private readonly sessionManager = new SessionManager()
|
|
31
|
-
private
|
|
31
|
+
private currentProjectId: string | null = null
|
|
32
32
|
private readonly statusLoop: ReturnType<typeof runStatusDetectionLoop>
|
|
33
33
|
/**
|
|
34
34
|
* Per-tab snapshot gate. While `false`, render events from the underlying
|
|
@@ -36,8 +36,8 @@ export class LocalSessionBackend
|
|
|
36
36
|
* the UI. The frontend lifts the gate once `usePaneSizeReport` has reported
|
|
37
37
|
* the *actual* rendered pane size via `resizeTab({ confirmedFromMeasurement })`,
|
|
38
38
|
* so the first snapshot the UI ever paints is sized against the real box —
|
|
39
|
-
* never against the open-loop chrome estimate used at
|
|
40
|
-
* A tab without an entry here is treated as ready (legacy paths,
|
|
39
|
+
* never against the open-loop chrome estimate used at project create / attach.
|
|
40
|
+
* A tab without an entry here is treated as ready (legacy paths, projects
|
|
41
41
|
* not subject to the gate). See plan: il-y-a-eu-stateful-boot.md.
|
|
42
42
|
*/
|
|
43
43
|
private readonly paneReady = new Map<string, boolean>()
|
|
@@ -59,89 +59,92 @@ export class LocalSessionBackend
|
|
|
59
59
|
updateTab: (tabId, patch) => {
|
|
60
60
|
const found = this.findTab(tabId)
|
|
61
61
|
if (!found) return
|
|
62
|
-
this.sessionManager.updateTabMetadata(found.
|
|
63
|
-
if (found.
|
|
64
|
-
this.emit('tabMetadataUpdated', found.
|
|
62
|
+
this.sessionManager.updateTabMetadata(found.projectId, tabId, patch)
|
|
63
|
+
if (found.projectId === this.currentProjectId) {
|
|
64
|
+
this.emit('tabMetadataUpdated', found.projectId, tabId, patch)
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
})
|
|
68
|
-
this.sessionManager.on('render', (
|
|
69
|
-
if (
|
|
68
|
+
this.sessionManager.on('render', (projectId, tabId, viewport, terminalModes) => {
|
|
69
|
+
if (projectId !== this.currentProjectId) return
|
|
70
70
|
if (this.paneReady.get(tabId) === false) {
|
|
71
71
|
this.pendingRender.set(tabId, { terminalModes, viewport })
|
|
72
72
|
return
|
|
73
73
|
}
|
|
74
74
|
this.emit('render', tabId, viewport, terminalModes)
|
|
75
75
|
})
|
|
76
|
-
this.sessionManager.on('exit', (
|
|
77
|
-
if (
|
|
76
|
+
this.sessionManager.on('exit', (projectId, tabId, exitCode) => {
|
|
77
|
+
if (projectId === this.currentProjectId) {
|
|
78
78
|
this.emit('exit', tabId, exitCode)
|
|
79
79
|
}
|
|
80
80
|
})
|
|
81
|
-
this.sessionManager.on('error', (
|
|
82
|
-
if (
|
|
81
|
+
this.sessionManager.on('error', (projectId, tabId, message) => {
|
|
82
|
+
if (projectId === this.currentProjectId) {
|
|
83
83
|
this.emit('error', tabId, message)
|
|
84
84
|
}
|
|
85
85
|
})
|
|
86
86
|
this.statusLoop = runStatusDetectionLoop({
|
|
87
|
-
|
|
88
|
-
listTabs: (
|
|
89
|
-
|
|
90
|
-
this.emit('
|
|
87
|
+
listProjects: () => this.sessionManager.listSessionIds(),
|
|
88
|
+
listTabs: (projectId) => this.sessionManager.listTabs(projectId),
|
|
89
|
+
onProjectStatus: (projectId, status) => {
|
|
90
|
+
this.emit('projectActivity', projectId, status)
|
|
91
91
|
},
|
|
92
|
-
onTabStatus: (tabId, status,
|
|
93
|
-
if (
|
|
94
|
-
this.emit('tabActivity', tabId, status)
|
|
92
|
+
onTabStatus: (tabId, status, projectId, workspaceId) => {
|
|
93
|
+
if (projectId === this.currentProjectId) {
|
|
94
|
+
this.emit('tabActivity', tabId, status, workspaceId)
|
|
95
95
|
}
|
|
96
96
|
},
|
|
97
|
+
onTurnComplete: (tabId, projectId, _idleMs, workspaceId) => {
|
|
98
|
+
this.emit('tabTurnComplete', tabId, projectId, workspaceId)
|
|
99
|
+
},
|
|
97
100
|
})
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
async attach(options: {
|
|
101
|
-
|
|
104
|
+
projectId: string
|
|
102
105
|
cols: number
|
|
103
106
|
rows: number
|
|
104
|
-
|
|
107
|
+
projectSnapshot?: ProjectSnapshotV1
|
|
105
108
|
}) {
|
|
106
109
|
logDebug('backend.local.attach', {
|
|
107
110
|
cols: options.cols,
|
|
111
|
+
projectId: options.projectId,
|
|
108
112
|
rows: options.rows,
|
|
109
|
-
|
|
110
|
-
snapshotTabs: options.workspaceSnapshot?.tabs.length ?? 0,
|
|
113
|
+
snapshotTabs: options.projectSnapshot?.tabs.length ?? 0,
|
|
111
114
|
})
|
|
112
|
-
this.
|
|
113
|
-
const trees = getSnapshotTrees(options.
|
|
115
|
+
this.currentProjectId = options.projectId
|
|
116
|
+
const trees = getSnapshotTrees(options.projectSnapshot)
|
|
114
117
|
const splitTrees = trees.filter((t) => t.type === 'split')
|
|
115
118
|
if (splitTrees.length > 0) {
|
|
116
119
|
const bounds = createTerminalBounds(options.cols, options.rows)
|
|
117
120
|
forEachSplitPaneRect(splitTrees, bounds, (tabId, rect) => {
|
|
118
121
|
const size = toTerminalContentSize(rect)
|
|
119
122
|
this.gatePaneRender(tabId)
|
|
120
|
-
this.sessionManager.resizeTab(options.
|
|
123
|
+
this.sessionManager.resizeTab(options.projectId, tabId, size.cols, size.rows)
|
|
121
124
|
})
|
|
122
125
|
} else {
|
|
123
|
-
this.sessionManager.resize(options.
|
|
126
|
+
this.sessionManager.resize(options.projectId, options.cols, options.rows)
|
|
124
127
|
}
|
|
125
128
|
const attachResult = this.sessionManager.attachSession(
|
|
126
|
-
options.
|
|
127
|
-
options.
|
|
129
|
+
options.projectId,
|
|
130
|
+
options.projectSnapshot
|
|
128
131
|
)
|
|
129
132
|
for (const tab of attachResult.tabs) {
|
|
130
133
|
this.gatePaneRender(tab.id)
|
|
131
134
|
this.autoRename.register(tab)
|
|
132
135
|
}
|
|
133
136
|
// Run a synchronous classification pass so every tab's activity and the
|
|
134
|
-
//
|
|
137
|
+
// project-status snapshot are available to embed in the reply — mirrors
|
|
135
138
|
// the remote backend's behavior and keeps hydrate dispatches atomic on
|
|
136
139
|
// the client side.
|
|
137
|
-
this.statusLoop.classifyNow(options.
|
|
140
|
+
this.statusLoop.classifyNow(options.projectId, this.sessionManager.listTabs(options.projectId))
|
|
138
141
|
const tabsWithActivity = attachResult.tabs.map((tab) => ({
|
|
139
142
|
...tab,
|
|
140
143
|
activity: this.statusLoop.getTabStatus(tab.id) ?? tab.activity,
|
|
141
144
|
}))
|
|
142
145
|
return {
|
|
143
146
|
activeTabId: attachResult.activeTabId,
|
|
144
|
-
|
|
147
|
+
initialProjectStatuses: this.statusLoop.snapshotProjects(),
|
|
145
148
|
tabs: tabsWithActivity,
|
|
146
149
|
}
|
|
147
150
|
}
|
|
@@ -155,18 +158,18 @@ export class LocalSessionBackend
|
|
|
155
158
|
cols: number
|
|
156
159
|
rows: number
|
|
157
160
|
cwd?: string
|
|
158
|
-
|
|
161
|
+
workspaceId?: string
|
|
159
162
|
autoRenameCandidate?: boolean
|
|
160
163
|
}): void {
|
|
161
|
-
if (!(this.
|
|
162
|
-
logDebug('backend.local.
|
|
164
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) {
|
|
165
|
+
logDebug('backend.local.skipCreateWithoutProject', { tabId: options.tabId })
|
|
163
166
|
return
|
|
164
167
|
}
|
|
165
168
|
logDebug('backend.local.createSession', {
|
|
166
|
-
|
|
169
|
+
projectId: this.currentProjectId,
|
|
167
170
|
tabId: options.tabId,
|
|
168
171
|
title: options.title,
|
|
169
|
-
|
|
172
|
+
workspaceId: options.workspaceId ?? null,
|
|
170
173
|
})
|
|
171
174
|
this.gatePaneRender(options.tabId)
|
|
172
175
|
const autoRenameStatus = initialAutoRenameStatus(
|
|
@@ -174,7 +177,7 @@ export class LocalSessionBackend
|
|
|
174
177
|
options.assistant,
|
|
175
178
|
options.autoRenameCandidate === true
|
|
176
179
|
)
|
|
177
|
-
this.sessionManager.createTab(this.
|
|
180
|
+
this.sessionManager.createTab(this.currentProjectId, { ...options, autoRenameStatus })
|
|
178
181
|
const tab = this.findTab(options.tabId)?.tab
|
|
179
182
|
if (tab) this.autoRename.register(tab)
|
|
180
183
|
}
|
|
@@ -197,17 +200,17 @@ export class LocalSessionBackend
|
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
write(tabId: string, input: string): void {
|
|
200
|
-
if (!(this.
|
|
201
|
-
logDebug('backend.local.
|
|
203
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) {
|
|
204
|
+
logDebug('backend.local.skipWriteWithoutProject', { inputLength: input.length, tabId })
|
|
202
205
|
return
|
|
203
206
|
}
|
|
204
207
|
logDebug('backend.local.write', {
|
|
205
208
|
inputLength: input.length,
|
|
206
|
-
|
|
209
|
+
projectId: this.currentProjectId,
|
|
207
210
|
tabId,
|
|
208
211
|
})
|
|
209
212
|
this.autoRename.observeWrite(tabId, input)
|
|
210
|
-
this.sessionManager.write(this.
|
|
213
|
+
this.sessionManager.write(this.currentProjectId, tabId, input)
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
renameTab(tabId: string, title: string): void {
|
|
@@ -215,80 +218,80 @@ export class LocalSessionBackend
|
|
|
215
218
|
if (!found) return
|
|
216
219
|
this.autoRename.manualRename(tabId)
|
|
217
220
|
const patch = { autoRenameStatus: 'attempted' as const, title }
|
|
218
|
-
this.sessionManager.updateTabMetadata(found.
|
|
219
|
-
if (found.
|
|
220
|
-
this.emit('tabMetadataUpdated', found.
|
|
221
|
+
this.sessionManager.updateTabMetadata(found.projectId, tabId, patch)
|
|
222
|
+
if (found.projectId === this.currentProjectId) {
|
|
223
|
+
this.emit('tabMetadataUpdated', found.projectId, tabId, patch)
|
|
221
224
|
}
|
|
222
225
|
}
|
|
223
226
|
|
|
224
227
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
225
|
-
if (!(this.
|
|
226
|
-
this.sessionManager.scroll(this.
|
|
228
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
229
|
+
this.sessionManager.scroll(this.currentProjectId, tabId, deltaLines)
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
scrollViewportToBottom(tabId: string): void {
|
|
230
|
-
if (!(this.
|
|
231
|
-
this.sessionManager.scrollToBottom(this.
|
|
233
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
234
|
+
this.sessionManager.scrollToBottom(this.currentProjectId, tabId)
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
setActiveTab(tabId: string | null): void {
|
|
235
|
-
if (!(this.
|
|
236
|
-
logDebug('backend.local.setActiveTab', {
|
|
237
|
-
this.sessionManager.setActiveTab(this.
|
|
238
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
239
|
+
logDebug('backend.local.setActiveTab', { projectId: this.currentProjectId, tabId })
|
|
240
|
+
this.sessionManager.setActiveTab(this.currentProjectId, tabId)
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
resizeAll(cols: number, rows: number, options?: ResizeOptions): void {
|
|
241
|
-
if (!(this.
|
|
242
|
-
this.sessionManager.resize(this.
|
|
244
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
245
|
+
this.sessionManager.resize(this.currentProjectId, cols, rows, options)
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
resizeTab(tabId: string, cols: number, rows: number, options?: ResizeOptions): void {
|
|
246
|
-
if (!(this.
|
|
247
|
-
this.sessionManager.resizeTab(this.
|
|
249
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
250
|
+
this.sessionManager.resizeTab(this.currentProjectId, tabId, cols, rows, options)
|
|
248
251
|
if (options?.confirmedFromMeasurement === true) {
|
|
249
252
|
this.releasePaneRender(tabId)
|
|
250
253
|
}
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
disposeSession(tabId: string): void {
|
|
254
|
-
if (!(this.
|
|
255
|
-
logDebug('backend.local.disposeSession', {
|
|
257
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
258
|
+
logDebug('backend.local.disposeSession', { projectId: this.currentProjectId, tabId })
|
|
256
259
|
this.paneReady.delete(tabId)
|
|
257
260
|
this.pendingRender.delete(tabId)
|
|
258
261
|
this.autoRename.unregister(tabId)
|
|
259
|
-
this.sessionManager.closeTab(this.
|
|
262
|
+
this.sessionManager.closeTab(this.currentProjectId, tabId)
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
disposeAll(): void {
|
|
263
|
-
if (!(this.
|
|
264
|
-
logDebug('backend.local.disposeAll', {
|
|
266
|
+
if (!(this.currentProjectId != null && this.currentProjectId !== '')) return
|
|
267
|
+
logDebug('backend.local.disposeAll', { projectId: this.currentProjectId })
|
|
265
268
|
this.paneReady.clear()
|
|
266
269
|
this.pendingRender.clear()
|
|
267
|
-
for (const tab of this.sessionManager.listTabs(this.
|
|
270
|
+
for (const tab of this.sessionManager.listTabs(this.currentProjectId)) {
|
|
268
271
|
this.autoRename.unregister(tab.id)
|
|
269
272
|
}
|
|
270
|
-
this.sessionManager.disposeSession(this.
|
|
273
|
+
this.sessionManager.disposeSession(this.currentProjectId)
|
|
271
274
|
}
|
|
272
275
|
|
|
273
276
|
destroy(keepSessions = true): void {
|
|
274
|
-
logDebug('backend.local.destroy', { keepSessions,
|
|
275
|
-
if (!keepSessions && this.
|
|
276
|
-
this.sessionManager.disposeSession(this.
|
|
277
|
+
logDebug('backend.local.destroy', { keepSessions, projectId: this.currentProjectId })
|
|
278
|
+
if (!keepSessions && this.currentProjectId != null && this.currentProjectId !== '') {
|
|
279
|
+
this.sessionManager.disposeSession(this.currentProjectId)
|
|
277
280
|
}
|
|
278
281
|
this.statusLoop.stop()
|
|
279
|
-
this.
|
|
282
|
+
this.currentProjectId = null
|
|
280
283
|
}
|
|
281
284
|
|
|
282
|
-
|
|
285
|
+
announceProjectSwitched(_projectId: string): void {
|
|
283
286
|
// No daemon on the local backend, so there's no CLI to notify.
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
private findTab(
|
|
287
290
|
tabId: string
|
|
288
|
-
): {
|
|
289
|
-
for (const
|
|
290
|
-
const tab = this.sessionManager.listTabs(
|
|
291
|
-
if (tab) return {
|
|
291
|
+
): { projectId: string; tab: ReturnType<SessionManager['listTabs']>[number] } | null {
|
|
292
|
+
for (const projectId of this.sessionManager.listSessionIds()) {
|
|
293
|
+
const tab = this.sessionManager.listTabs(projectId).find((entry) => entry.id === tabId)
|
|
294
|
+
if (tab) return { projectId, tab }
|
|
292
295
|
}
|
|
293
296
|
return null
|
|
294
297
|
}
|