@brimveyn/aimux 1.20.4 → 1.22.0
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 +305 -1589
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/split-drag-controller.ts +4 -8
- 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 +28 -42
- 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 +28 -33
- 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 +102 -85
- 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 +149 -101
- 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-merge.ts +64 -0
- package/src/git/pr-status-poller.ts +57 -0
- package/src/git/pr-status.ts +227 -0
- 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 +11 -6
- 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/open-url.ts +39 -0
- 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 +3 -1
- 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 +75 -0
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/layout-resize.ts +2 -2
- package/src/state/pr-status-store.ts +39 -0
- 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} +77 -73
- 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 -53
- 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 +86 -15
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +83 -57
- package/src/state/types.ts +261 -417
- 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/diff-renderer/pierre-diff.tsx +2 -1
- 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-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
- package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
- package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
- package/src/ui/components/layout/bar.tsx +191 -0
- 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 +48 -37
- 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 +164 -230
- 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 +21 -0
- package/src/ui/widgets/widget-context-menu.ts +85 -0
- 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 -45
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
- 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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* (https://github.com/ogulcancelik/herdr, MIT). Rule tables trace back to
|
|
6
6
|
* `src/detect.rs` in that repo.
|
|
7
7
|
*
|
|
8
|
-
* The detector classifies a terminal
|
|
8
|
+
* The detector classifies a terminal project as `working`, `waiting-input`,
|
|
9
9
|
* or `idle`. Built-in CLIs (claude, codex, opencode, grok, kimi) have dedicated
|
|
10
10
|
* classify* functions. Custom CLIs fall back to a generic heuristic that
|
|
11
11
|
* (a) recognises common shells as always-idle and (b) uses pane-tail change
|
|
@@ -51,6 +51,13 @@ export class AssistantStatusDetector {
|
|
|
51
51
|
if (!viewport) return this.remember(tabId, '', now, 'idle')
|
|
52
52
|
|
|
53
53
|
const tail = extractTailText(viewport, TAIL_LINE_COUNT)
|
|
54
|
+
// Whole visible screen. Claude's Ctrl+T todo overlay pushes the spinner
|
|
55
|
+
// status line above the 10-line tail, so "is it working" has to look at
|
|
56
|
+
// the full frame. "Is it waiting on me" stays tail-scoped.
|
|
57
|
+
// ponytail: a scrolled-away pane falls back to `viewport.tailLines`, capped
|
|
58
|
+
// at SNAPSHOT_TAIL_LINE_COUNT (10) — the overlay still hides the spinner
|
|
59
|
+
// there. Raise that constant if it bites; it grows every render payload.
|
|
60
|
+
const screen = extractTailText(viewport, viewport.lines.length)
|
|
54
61
|
const prev = this.entries.get(tabId)
|
|
55
62
|
const changedAt = prev && prev.tail === tail ? prev.changedAt : now
|
|
56
63
|
const haystack = tail.toLowerCase()
|
|
@@ -59,7 +66,7 @@ export class AssistantStatusDetector {
|
|
|
59
66
|
return this.remember(tabId, tail, changedAt, 'idle')
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
const perCli = classifyBuiltin(assistant, haystack, tail)
|
|
69
|
+
const perCli = classifyBuiltin(assistant, haystack, tail, screen)
|
|
63
70
|
if (perCli) return this.remember(tabId, tail, changedAt, perCli)
|
|
64
71
|
|
|
65
72
|
const generic = classifyGeneric(haystack, changedAt, now)
|
|
@@ -129,11 +136,12 @@ function extractTailText(viewport: TerminalSnapshot, lineCount: number): string
|
|
|
129
136
|
function classifyBuiltin(
|
|
130
137
|
assistant: AssistantId,
|
|
131
138
|
haystack: string,
|
|
132
|
-
rawTail: string
|
|
139
|
+
rawTail: string,
|
|
140
|
+
rawScreen: string
|
|
133
141
|
): TabActivity | null {
|
|
134
142
|
switch (assistant) {
|
|
135
143
|
case 'claude':
|
|
136
|
-
return classifyClaude(haystack,
|
|
144
|
+
return classifyClaude(haystack, rawScreen)
|
|
137
145
|
case 'codex':
|
|
138
146
|
return classifyCodex(haystack)
|
|
139
147
|
case 'opencode':
|
|
@@ -147,7 +155,15 @@ function classifyBuiltin(
|
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
|
|
158
|
+
/**
|
|
159
|
+
* `haystack` is the lowercased 10-line tail, `rawScreen` the whole visible
|
|
160
|
+
* frame. Prompts always land at the bottom, so waiting-input stays on the tail
|
|
161
|
+
* — widening it would let a "Do you want…" from the transcript above fake a
|
|
162
|
+
* pending question. Working markers get the full frame: with the Ctrl+T todo
|
|
163
|
+
* overlay open, Claude drops the `esc to interrupt` hint entirely and the
|
|
164
|
+
* spinner line sits above however many todos are listed.
|
|
165
|
+
*/
|
|
166
|
+
function classifyClaude(haystack: string, rawScreen: string): TabActivity {
|
|
151
167
|
if (
|
|
152
168
|
haystack.includes('do you want') ||
|
|
153
169
|
haystack.includes('would you like') ||
|
|
@@ -157,15 +173,16 @@ function classifyClaude(haystack: string, rawTail: string): TabActivity {
|
|
|
157
173
|
) {
|
|
158
174
|
return 'waiting-input'
|
|
159
175
|
}
|
|
176
|
+
const screen = rawScreen.toLowerCase()
|
|
160
177
|
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
178
|
+
screen.includes('esc/ctrl+c to interrupt') ||
|
|
179
|
+
screen.includes('esc to interrupt') ||
|
|
180
|
+
screen.includes('ctrl+c to interrupt') ||
|
|
181
|
+
screen.includes('esc interrupt')
|
|
165
182
|
) {
|
|
166
183
|
return 'working'
|
|
167
184
|
}
|
|
168
|
-
if (hasClaudeSpinner(
|
|
185
|
+
if (hasClaudeSpinner(rawScreen)) return 'working'
|
|
169
186
|
return 'idle'
|
|
170
187
|
}
|
|
171
188
|
|
|
@@ -174,8 +191,16 @@ function classifyClaude(haystack: string, rawTail: string): TabActivity {
|
|
|
174
191
|
// separators Claude scatters mid-line through its header/footer (e.g.
|
|
175
192
|
// `Opus 4.7 … · Claude Max`, `… · ← for agents`), which would otherwise pair
|
|
176
193
|
// with a stray `...` placeholder and mark an idle home screen as working.
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
//
|
|
195
|
+
// The ellipsis is also what separates a live status line (`✱ Flowing… (3s · …)`)
|
|
196
|
+
// from a finished one left in the transcript (`✱ Sautéed for 10s`) — that's why
|
|
197
|
+
// scanning the whole screen instead of the tail doesn't resurrect past turns.
|
|
198
|
+
// ponytail: a spinner line frozen mid-`…` (interrupted turn) would pin the pane
|
|
199
|
+
// to `working` until it scrolls off. Chosen failure direction — a false
|
|
200
|
+
// `working` notifies nobody, a false `idle` fires a turn-complete. Cross-check
|
|
201
|
+
// against screen-text velocity (the `(3s` timer ticks) if it ever shows up.
|
|
202
|
+
function hasClaudeSpinner(rawText: string): boolean {
|
|
203
|
+
for (const line of rawText.split('\n')) {
|
|
179
204
|
const trimmed = line.trimStart()
|
|
180
205
|
const first = trimmed[0]
|
|
181
206
|
if (first == null || !CLAUDE_SPINNER_GLYPH_SET.has(first)) continue
|
|
@@ -275,8 +300,8 @@ function classifyKimi(haystack: string, rawTail: string): TabActivity {
|
|
|
275
300
|
haystack.includes('apply these edits?') ||
|
|
276
301
|
haystack.includes('stop this task?') ||
|
|
277
302
|
haystack.includes('ready to build with this plan?') ||
|
|
278
|
-
haystack.includes('approve for
|
|
279
|
-
haystack.includes('approve for this
|
|
303
|
+
haystack.includes('approve for project') ||
|
|
304
|
+
haystack.includes('approve for this project') ||
|
|
280
305
|
haystack.includes('type feedback') ||
|
|
281
306
|
haystack.includes('waiting for authorization') ||
|
|
282
307
|
haystack.includes('sign in to kimi') ||
|
|
@@ -297,7 +322,7 @@ function classifyKimi(haystack: string, rawTail: string): TabActivity {
|
|
|
297
322
|
|
|
298
323
|
// Agent actively streaming / calling tools.
|
|
299
324
|
// Anchor thinking on ellipsis so the footer model suffix "thinking" alone
|
|
300
|
-
// (e.g. "kimi-k2 thinking") does not mark an idle
|
|
325
|
+
// (e.g. "kimi-k2 thinking") does not mark an idle project as working.
|
|
301
326
|
if (
|
|
302
327
|
haystack.includes('working...') ||
|
|
303
328
|
haystack.includes('working…') ||
|
|
@@ -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
|
|
@@ -10,9 +10,11 @@ const DEFAULT_TIMEOUT_MS = 15_000
|
|
|
10
10
|
export async function runCli(
|
|
11
11
|
command: string,
|
|
12
12
|
args: string[],
|
|
13
|
-
timeoutMs: number = DEFAULT_TIMEOUT_MS
|
|
13
|
+
timeoutMs: number = DEFAULT_TIMEOUT_MS,
|
|
14
|
+
cwd?: string
|
|
14
15
|
): Promise<CliResult> {
|
|
15
16
|
const proc = Bun.spawn([command, ...args], {
|
|
17
|
+
cwd,
|
|
16
18
|
stderr: 'pipe',
|
|
17
19
|
stdin: 'ignore',
|
|
18
20
|
stdout: 'pipe',
|
|
@@ -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
|
}
|