@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
package/src/daemon/daemon.ts
CHANGED
|
@@ -29,12 +29,12 @@ import { lastNonBlankLine } from '../pty/last-line'
|
|
|
29
29
|
import { createDefaultTerminalModes } from '../state/terminal-modes'
|
|
30
30
|
import { TerminalManagerClient } from '../terminal-manager/manager-client'
|
|
31
31
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
addWorkspaceToCatalog,
|
|
33
|
+
assertProjectInCatalog,
|
|
34
34
|
bumpLastOpenedInCatalog,
|
|
35
|
-
|
|
35
|
+
createProjectInCatalog,
|
|
36
36
|
deleteFromCatalog,
|
|
37
|
-
|
|
37
|
+
removeWorkspaceFromCatalog,
|
|
38
38
|
} from './catalog-writer'
|
|
39
39
|
import {
|
|
40
40
|
consumeDaemonHandoff,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
} from './runtime-paths'
|
|
55
55
|
|
|
56
56
|
export interface DaemonTabEntry {
|
|
57
|
-
|
|
57
|
+
projectId: string
|
|
58
58
|
assistant: AssistantId
|
|
59
59
|
command: string
|
|
60
60
|
viewport: TerminalSnapshot | undefined
|
|
@@ -73,7 +73,7 @@ export interface DaemonTabEntry {
|
|
|
73
73
|
*/
|
|
74
74
|
title?: string
|
|
75
75
|
status?: TabStatus
|
|
76
|
-
|
|
76
|
+
workspaceId?: string
|
|
77
77
|
workerName?: string
|
|
78
78
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
79
79
|
}
|
|
@@ -91,7 +91,7 @@ export interface DaemonTabEntry {
|
|
|
91
91
|
*/
|
|
92
92
|
export function mergeTabRegistryEntry(
|
|
93
93
|
registry: Map<string, DaemonTabEntry>,
|
|
94
|
-
|
|
94
|
+
projectId: string,
|
|
95
95
|
tabId: string,
|
|
96
96
|
assistant: AssistantId,
|
|
97
97
|
command: string,
|
|
@@ -100,7 +100,7 @@ export function mergeTabRegistryEntry(
|
|
|
100
100
|
metadata?: {
|
|
101
101
|
title?: string
|
|
102
102
|
status?: TabStatus
|
|
103
|
-
|
|
103
|
+
workspaceId?: string
|
|
104
104
|
workerName?: string
|
|
105
105
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
106
106
|
}
|
|
@@ -114,13 +114,13 @@ export function mergeTabRegistryEntry(
|
|
|
114
114
|
? 'attempted'
|
|
115
115
|
: (metadata?.autoRenameStatus ?? existing?.autoRenameStatus),
|
|
116
116
|
command,
|
|
117
|
-
|
|
117
|
+
projectId,
|
|
118
118
|
status: metadata?.status ?? existing?.status,
|
|
119
119
|
title: preserveAttemptedMetadata ? existing.title : (metadata?.title ?? existing?.title),
|
|
120
120
|
viewport,
|
|
121
121
|
viewportSeq: existing?.viewportSeq ?? (viewport ? allocateSeq() : 0),
|
|
122
122
|
workerName: metadata?.workerName ?? existing?.workerName,
|
|
123
|
-
|
|
123
|
+
workspaceId: metadata?.workspaceId ?? existing?.workspaceId,
|
|
124
124
|
}
|
|
125
125
|
registry.set(tabId, entry)
|
|
126
126
|
return entry
|
|
@@ -128,11 +128,11 @@ export function mergeTabRegistryEntry(
|
|
|
128
128
|
|
|
129
129
|
export function findWorkerNameConflict(
|
|
130
130
|
registry: ReadonlyMap<string, DaemonTabEntry>,
|
|
131
|
-
|
|
131
|
+
projectId: string,
|
|
132
132
|
workerName: string
|
|
133
133
|
): string | undefined {
|
|
134
134
|
for (const [tabId, entry] of registry) {
|
|
135
|
-
if (entry.
|
|
135
|
+
if (entry.projectId === projectId && entry.workerName === workerName) return tabId
|
|
136
136
|
}
|
|
137
137
|
return undefined
|
|
138
138
|
}
|
|
@@ -157,12 +157,12 @@ function sendOk(socket: Socket, id: string): void {
|
|
|
157
157
|
send(socket, { id, payload: {}, type: 'ok' })
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
function
|
|
161
|
-
const
|
|
162
|
-
if (!(
|
|
163
|
-
throw new Error('No
|
|
160
|
+
function requireProject(socket: Socket, attachedProjects: Map<Socket, string>): string {
|
|
161
|
+
const projectId = attachedProjects.get(socket)
|
|
162
|
+
if (!(projectId != null && projectId !== '')) {
|
|
163
|
+
throw new Error('No project attached')
|
|
164
164
|
}
|
|
165
|
-
return
|
|
165
|
+
return projectId
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function requireNegotiatedVersion(socket: Socket, versions: Map<Socket, number>): number {
|
|
@@ -221,7 +221,7 @@ async function ensureTerminalManagerReady(manager: TerminalManagerClient): Promi
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
export async function runDaemon(): Promise<void> {
|
|
224
|
-
const resolvedConfig = await loadUserConfig()
|
|
224
|
+
const { resolved: resolvedConfig } = await loadUserConfig()
|
|
225
225
|
const socketPath = getIpcDaemonSocketPath()
|
|
226
226
|
// A handoff file means we were spawned to take over from a predecessor
|
|
227
227
|
// daemon that already drained and renamed its socket away. Consume the
|
|
@@ -248,30 +248,30 @@ export async function runDaemon(): Promise<void> {
|
|
|
248
248
|
await ensureTerminalManagerReady(manager)
|
|
249
249
|
|
|
250
250
|
const sockets = new Set<Socket>()
|
|
251
|
-
const
|
|
251
|
+
const attachedProjects = new Map<Socket, string>()
|
|
252
252
|
const negotiatedVersions = new Map<Socket, number>()
|
|
253
253
|
// Sockets that attached with `thin: true` (headless CLIs). Used to
|
|
254
254
|
// distinguish "a UI is attached" from "only CLIs are talking to me" when
|
|
255
|
-
// deciding whether workspace
|
|
255
|
+
// deciding whether project/workspace mutations go via broadcast (UI does
|
|
256
256
|
// the write) or via catalog-writer (daemon does the write).
|
|
257
257
|
const thinAttachers = new Set<Socket>()
|
|
258
258
|
|
|
259
259
|
// Per-tab registry so the status-detection loop can poll every terminal
|
|
260
260
|
// continuously, not just the one the UI is currently attached to.
|
|
261
261
|
const tabRegistry = new Map<string, DaemonTabEntry>()
|
|
262
|
-
// Active tab id per
|
|
262
|
+
// Active tab id per project, populated from attachResult and updated by
|
|
263
263
|
// setActiveTab. Surfaced by `listTabs` so a headless CLI doesn't need to
|
|
264
264
|
// attach just to know which tab the UI is focused on.
|
|
265
|
-
const
|
|
266
|
-
// Last (cols, rows) the
|
|
265
|
+
const projectActiveTabIds = new Map<string, string | null>()
|
|
266
|
+
// Last (cols, rows) the project was attached with. `createTab` with
|
|
267
267
|
// cols/rows = 0 falls back to this when the `createTabSizeFallback`
|
|
268
268
|
// capability is in play.
|
|
269
|
-
const
|
|
269
|
+
const projectDimensions = new Map<string, { cols: number; rows: number }>()
|
|
270
270
|
let nextViewportSeq = 1
|
|
271
271
|
|
|
272
272
|
const allocateSeq = (): number => nextViewportSeq++
|
|
273
273
|
const rememberTab = (
|
|
274
|
-
|
|
274
|
+
projectId: string,
|
|
275
275
|
tabId: string,
|
|
276
276
|
assistant: AssistantId,
|
|
277
277
|
command: string,
|
|
@@ -279,7 +279,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
279
279
|
metadata?: {
|
|
280
280
|
title?: string
|
|
281
281
|
status?: TabStatus
|
|
282
|
-
|
|
282
|
+
workspaceId?: string
|
|
283
283
|
workerName?: string
|
|
284
284
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
285
285
|
}
|
|
@@ -287,7 +287,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
287
287
|
const before = tabRegistry.get(tabId)
|
|
288
288
|
const entry = mergeTabRegistryEntry(
|
|
289
289
|
tabRegistry,
|
|
290
|
-
|
|
290
|
+
projectId,
|
|
291
291
|
tabId,
|
|
292
292
|
assistant,
|
|
293
293
|
command,
|
|
@@ -301,8 +301,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
301
301
|
initialViewportProvided: initialViewport !== undefined,
|
|
302
302
|
preservedExistingViewport:
|
|
303
303
|
before?.viewport !== undefined && entry.viewport === before.viewport,
|
|
304
|
+
projectId,
|
|
304
305
|
resultSeq: entry.viewportSeq,
|
|
305
|
-
sessionId,
|
|
306
306
|
tabId,
|
|
307
307
|
})
|
|
308
308
|
return entry
|
|
@@ -314,9 +314,9 @@ export async function runDaemon(): Promise<void> {
|
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
const
|
|
317
|
+
const broadcastForProject = (projectId: string, event: ServerEvent): void => {
|
|
318
318
|
for (const socket of sockets) {
|
|
319
|
-
if (
|
|
319
|
+
if (attachedProjects.get(socket) === projectId) {
|
|
320
320
|
send(socket, event)
|
|
321
321
|
}
|
|
322
322
|
}
|
|
@@ -326,23 +326,23 @@ export async function runDaemon(): Promise<void> {
|
|
|
326
326
|
// it. Older peers whose `parseServerMessage` doesn't recognise the type
|
|
327
327
|
// would throw on receipt and drop the connection — MIN_VERSION stays at 10
|
|
328
328
|
// for compat, so we can't rely on every attached socket being v11.
|
|
329
|
-
const
|
|
330
|
-
|
|
329
|
+
const broadcastForProjectVersioned = (
|
|
330
|
+
projectId: string,
|
|
331
331
|
minVersion: number,
|
|
332
332
|
event: ServerEvent
|
|
333
333
|
): void => {
|
|
334
334
|
for (const socket of sockets) {
|
|
335
|
-
if (
|
|
335
|
+
if (attachedProjects.get(socket) !== projectId) continue
|
|
336
336
|
const version = negotiatedVersions.get(socket)
|
|
337
337
|
if (version === undefined || version < minVersion) continue
|
|
338
338
|
send(socket, event)
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
//
|
|
343
|
-
// `minVersion` regardless of which
|
|
344
|
-
// lifecycle events (create/switch/close) target a
|
|
345
|
-
// not currently be attached to, so `
|
|
342
|
+
// Project-scope broadcast: reaches every socket that negotiated at least
|
|
343
|
+
// `minVersion` regardless of which project it's attached to. Project
|
|
344
|
+
// lifecycle events (create/switch/close) target a project that the UI may
|
|
345
|
+
// not currently be attached to, so `broadcastForProjectVersioned` won't do.
|
|
346
346
|
const broadcastAllVersioned = (minVersion: number, event: ServerEvent): void => {
|
|
347
347
|
for (const socket of sockets) {
|
|
348
348
|
const version = negotiatedVersions.get(socket)
|
|
@@ -361,7 +361,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
361
361
|
if (patch.autoRenameStatus !== undefined) entry.autoRenameStatus = patch.autoRenameStatus
|
|
362
362
|
void (async () => {
|
|
363
363
|
try {
|
|
364
|
-
await manager.updateTabMetadata(entry.
|
|
364
|
+
await manager.updateTabMetadata(entry.projectId, tabId, patch)
|
|
365
365
|
} catch (error) {
|
|
366
366
|
logDebug('daemon.tabMetadata.managerFailed', {
|
|
367
367
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -369,8 +369,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
369
369
|
})
|
|
370
370
|
}
|
|
371
371
|
})()
|
|
372
|
-
|
|
373
|
-
payload: { ...patch,
|
|
372
|
+
broadcastForProjectVersioned(entry.projectId, 14, {
|
|
373
|
+
payload: { ...patch, projectId: entry.projectId, tabId },
|
|
374
374
|
type: 'tabMetadataUpdated',
|
|
375
375
|
})
|
|
376
376
|
}
|
|
@@ -390,110 +390,114 @@ export async function runDaemon(): Promise<void> {
|
|
|
390
390
|
updateTab: applyTabMetadata,
|
|
391
391
|
})
|
|
392
392
|
|
|
393
|
-
// Count UI attachers so workspace
|
|
393
|
+
// Count UI attachers so project/workspace handlers can decide whether to
|
|
394
394
|
// relay via broadcast (UI attached → its reducer owns the write) or mutate
|
|
395
395
|
// the catalog directly. A "UI attacher" here is any non-thin attach — thin
|
|
396
396
|
// attachers are CLIs which don't run reducers.
|
|
397
397
|
const countUiAttachers = (): number => {
|
|
398
398
|
let count = 0
|
|
399
399
|
for (const socket of sockets) {
|
|
400
|
-
if (
|
|
400
|
+
if (attachedProjects.get(socket) === undefined) continue
|
|
401
401
|
if (thinAttachers.has(socket)) continue
|
|
402
402
|
count++
|
|
403
403
|
}
|
|
404
404
|
return count
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
-
manager.on('render', (
|
|
407
|
+
manager.on('render', (projectId, tabId, viewport, terminalModes) => {
|
|
408
408
|
const existing = tabRegistry.get(tabId)
|
|
409
409
|
let newSeq: number | null = null
|
|
410
410
|
if (existing) {
|
|
411
411
|
existing.viewport = viewport
|
|
412
412
|
existing.viewportSeq = nextViewportSeq++
|
|
413
|
-
existing.
|
|
413
|
+
existing.projectId = projectId
|
|
414
414
|
newSeq = existing.viewportSeq
|
|
415
415
|
}
|
|
416
416
|
logDebug('daemon.manager.render', {
|
|
417
417
|
attachedSocketCount: sockets.size,
|
|
418
418
|
hadRegistryEntry: existing !== undefined,
|
|
419
419
|
newSeq,
|
|
420
|
-
|
|
420
|
+
projectId,
|
|
421
421
|
tabId,
|
|
422
422
|
viewportLines: viewport.lines.length,
|
|
423
423
|
})
|
|
424
424
|
const event: ServerEvent = { payload: { tabId, terminalModes, viewport }, type: 'tabRender' }
|
|
425
|
-
|
|
425
|
+
broadcastForProject(projectId, event)
|
|
426
426
|
})
|
|
427
|
-
manager.on('exit', (
|
|
428
|
-
logDebug('daemon.manager.exit', { exitCode,
|
|
427
|
+
manager.on('exit', (projectId, tabId, exitCode) => {
|
|
428
|
+
logDebug('daemon.manager.exit', { exitCode, projectId, tabId })
|
|
429
429
|
tabRegistry.delete(tabId)
|
|
430
430
|
autoRename.unregister(tabId)
|
|
431
|
-
if (
|
|
432
|
-
|
|
431
|
+
if (projectActiveTabIds.get(projectId) === tabId) {
|
|
432
|
+
projectActiveTabIds.set(projectId, null)
|
|
433
433
|
}
|
|
434
434
|
const event: ServerEvent = { payload: { exitCode, tabId }, type: 'tabExit' }
|
|
435
|
-
|
|
435
|
+
broadcastForProject(projectId, event)
|
|
436
436
|
})
|
|
437
|
-
manager.on('error', (
|
|
438
|
-
logDebug('daemon.manager.error', { message,
|
|
437
|
+
manager.on('error', (projectId, tabId, message) => {
|
|
438
|
+
logDebug('daemon.manager.error', { message, projectId, tabId })
|
|
439
439
|
const event: ServerEvent = { payload: { message, tabId }, type: 'tabError' }
|
|
440
|
-
|
|
440
|
+
broadcastForProject(projectId, event)
|
|
441
441
|
})
|
|
442
442
|
|
|
443
|
-
const
|
|
443
|
+
const listTabsForProject = (projectId: string): LoopTabView[] => {
|
|
444
444
|
const result: LoopTabView[] = []
|
|
445
445
|
for (const [tabId, entry] of tabRegistry) {
|
|
446
|
-
if (entry.
|
|
446
|
+
if (entry.projectId !== projectId) continue
|
|
447
447
|
result.push({
|
|
448
448
|
assistant: entry.assistant,
|
|
449
449
|
command: entry.command,
|
|
450
450
|
id: tabId,
|
|
451
451
|
viewport: entry.viewport,
|
|
452
|
+
workspaceId: entry.workspaceId,
|
|
452
453
|
})
|
|
453
454
|
}
|
|
454
455
|
return result
|
|
455
456
|
}
|
|
456
457
|
|
|
457
458
|
const statusLoop = runStatusDetectionLoop({
|
|
458
|
-
|
|
459
|
+
listProjects: () => {
|
|
459
460
|
const seen = new Set<string>()
|
|
460
|
-
for (const entry of tabRegistry.values()) seen.add(entry.
|
|
461
|
+
for (const entry of tabRegistry.values()) seen.add(entry.projectId)
|
|
461
462
|
return [...seen]
|
|
462
463
|
},
|
|
463
|
-
listTabs:
|
|
464
|
-
|
|
465
|
-
logDebug('daemon.status.
|
|
466
|
-
broadcastAll({ payload: {
|
|
464
|
+
listTabs: listTabsForProject,
|
|
465
|
+
onProjectStatus: (projectId, status) => {
|
|
466
|
+
logDebug('daemon.status.project', { projectId, status })
|
|
467
|
+
broadcastAll({ payload: { projectId, status }, type: 'projectStatus' })
|
|
467
468
|
},
|
|
468
|
-
onTabQuestion: (tabId,
|
|
469
|
-
logDebug('daemon.status.question', { kind: detail.kind,
|
|
469
|
+
onTabQuestion: (tabId, projectId, detail) => {
|
|
470
|
+
logDebug('daemon.status.question', { kind: detail.kind, projectId, tabId })
|
|
470
471
|
// v13 / capability `questionEvents`. Same v13 send-time gate as below.
|
|
471
472
|
broadcastAllVersioned(13, {
|
|
472
473
|
payload: {
|
|
473
474
|
kind: detail.kind,
|
|
474
475
|
options: detail.options,
|
|
476
|
+
projectId,
|
|
475
477
|
prompt: detail.prompt,
|
|
476
|
-
sessionId,
|
|
477
478
|
tabId,
|
|
478
479
|
},
|
|
479
480
|
type: 'tabQuestion',
|
|
480
481
|
})
|
|
481
482
|
},
|
|
482
|
-
onTabStatus: (tabId, status,
|
|
483
|
-
logDebug('daemon.status.tab', {
|
|
483
|
+
onTabStatus: (tabId, status, projectId, workspaceId) => {
|
|
484
|
+
logDebug('daemon.status.tab', { projectId, status, tabId })
|
|
484
485
|
// Broadcast to every client. Clients silently ignore events for tabIds
|
|
485
486
|
// they don't know about, so there's no UI impact — this costs one
|
|
486
487
|
// extra socket write per tab per change, which is trivial, and
|
|
487
488
|
// removes a race where in-flight tab events could be dropped when a
|
|
488
|
-
// client tears down its socket to switch
|
|
489
|
-
broadcastAll({ payload: {
|
|
489
|
+
// client tears down its socket to switch projects.
|
|
490
|
+
broadcastAll({ payload: { projectId, status, tabId, workspaceId }, type: 'tabStatus' })
|
|
490
491
|
},
|
|
491
|
-
onTurnComplete: (tabId,
|
|
492
|
-
logDebug('daemon.status.turnComplete', { idleMs,
|
|
492
|
+
onTurnComplete: (tabId, projectId, idleMs, workspaceId) => {
|
|
493
|
+
logDebug('daemon.status.turnComplete', { idleMs, projectId, tabId })
|
|
493
494
|
// v13 / capability `turnLifecycle`. Gate at send time — pre-v13 parsers
|
|
494
495
|
// throw on unknown event types and would drop the connection. MIN stays
|
|
495
496
|
// at 10, so we fan this only to peers that negotiated at least v13.
|
|
496
|
-
broadcastAllVersioned(13, {
|
|
497
|
+
broadcastAllVersioned(13, {
|
|
498
|
+
payload: { idleMs, projectId, tabId, workspaceId },
|
|
499
|
+
type: 'tabTurnComplete',
|
|
500
|
+
})
|
|
497
501
|
},
|
|
498
502
|
turnSettleMs: turnCompleteSettleMs(),
|
|
499
503
|
})
|
|
@@ -546,7 +550,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
546
550
|
* Tell the TM whether to bother snapshotting + broadcasting. Toggled on
|
|
547
551
|
* 0↔1 transitions of the client socket count: when no UI is watching, the
|
|
548
552
|
* TM can skip per-chunk viewport diff/projection work entirely. The TM
|
|
549
|
-
* flushes a fresh snapshot per
|
|
553
|
+
* flushes a fresh snapshot per project on re-enable, so reattaching gives
|
|
550
554
|
* the client a current viewport.
|
|
551
555
|
*
|
|
552
556
|
* Fire-and-forget: failure to send isn't fatal (TM will just keep its
|
|
@@ -633,9 +637,9 @@ export async function runDaemon(): Promise<void> {
|
|
|
633
637
|
case 'attach': {
|
|
634
638
|
logDebug('daemon.request.attach.start', {
|
|
635
639
|
cols: message.payload.cols,
|
|
640
|
+
projectId: message.payload.projectId,
|
|
636
641
|
rows: message.payload.rows,
|
|
637
|
-
|
|
638
|
-
snapshotTabs: message.payload.workspaceSnapshot?.tabs.length ?? 0,
|
|
642
|
+
snapshotTabs: message.payload.projectSnapshot?.tabs.length ?? 0,
|
|
639
643
|
thin: message.payload.thin === true,
|
|
640
644
|
})
|
|
641
645
|
const negotiatedVersion = requireNegotiatedVersion(socket, negotiatedVersions)
|
|
@@ -645,27 +649,27 @@ export async function runDaemon(): Promise<void> {
|
|
|
645
649
|
)
|
|
646
650
|
}
|
|
647
651
|
|
|
648
|
-
|
|
652
|
+
attachedProjects.set(socket, message.payload.projectId)
|
|
649
653
|
if (message.payload.thin === true) {
|
|
650
654
|
thinAttachers.add(socket)
|
|
651
655
|
} else {
|
|
652
656
|
thinAttachers.delete(socket)
|
|
653
657
|
}
|
|
654
658
|
// A thin attacher (headless CLI) does not own a viewport, so
|
|
655
|
-
// it must not resize PTYs on a
|
|
659
|
+
// it must not resize PTYs on a project a UI is driving. TM's
|
|
656
660
|
// attachSession always calls `sessionManager.resize` on the
|
|
657
661
|
// incoming dimensions, so we substitute prior dims (or a
|
|
658
662
|
// safe default when none exist) before calling it. That
|
|
659
663
|
// makes the resize a no-op against the current PTY size
|
|
660
|
-
// instead of clobbering it. We also seed
|
|
664
|
+
// instead of clobbering it. We also seed projectDimensions
|
|
661
665
|
// on first-ever thin attach so `createTab`'s 0×0 fallback
|
|
662
666
|
// works for headless bootstrap flows (no UI has ever
|
|
663
|
-
// attached to this
|
|
667
|
+
// attached to this project).
|
|
664
668
|
let attachCols = message.payload.cols
|
|
665
669
|
let attachRows = message.payload.rows
|
|
666
670
|
const isThin = message.payload.thin === true
|
|
667
671
|
if (isThin) {
|
|
668
|
-
const prior =
|
|
672
|
+
const prior = projectDimensions.get(message.payload.projectId)
|
|
669
673
|
if (prior) {
|
|
670
674
|
attachCols = prior.cols
|
|
671
675
|
attachRows = prior.rows
|
|
@@ -676,32 +680,32 @@ export async function runDaemon(): Promise<void> {
|
|
|
676
680
|
// real UI attach afterwards overwrites this.
|
|
677
681
|
attachCols = 80
|
|
678
682
|
attachRows = 24
|
|
679
|
-
|
|
683
|
+
projectDimensions.set(message.payload.projectId, {
|
|
680
684
|
cols: attachCols,
|
|
681
685
|
rows: attachRows,
|
|
682
686
|
})
|
|
683
687
|
logDebug('daemon.attach.thin.seedDefaultDimensions', {
|
|
684
688
|
cols: attachCols,
|
|
689
|
+
projectId: message.payload.projectId,
|
|
685
690
|
rows: attachRows,
|
|
686
|
-
sessionId: message.payload.sessionId,
|
|
687
691
|
})
|
|
688
692
|
}
|
|
689
693
|
} else {
|
|
690
|
-
|
|
694
|
+
projectDimensions.set(message.payload.projectId, {
|
|
691
695
|
cols: message.payload.cols,
|
|
692
696
|
rows: message.payload.rows,
|
|
693
697
|
})
|
|
694
698
|
}
|
|
695
699
|
const attachResult = await manager.attachSession({
|
|
696
700
|
cols: attachCols,
|
|
701
|
+
projectId: message.payload.projectId,
|
|
702
|
+
projectSnapshot: message.payload.projectSnapshot,
|
|
697
703
|
rows: attachRows,
|
|
698
|
-
sessionId: message.payload.sessionId,
|
|
699
|
-
workspaceSnapshot: message.payload.workspaceSnapshot,
|
|
700
704
|
})
|
|
701
|
-
|
|
705
|
+
projectActiveTabIds.set(message.payload.projectId, attachResult.activeTabId)
|
|
702
706
|
for (const tab of attachResult.tabs) {
|
|
703
707
|
const remembered = rememberTab(
|
|
704
|
-
message.payload.
|
|
708
|
+
message.payload.projectId,
|
|
705
709
|
tab.id,
|
|
706
710
|
tab.assistant,
|
|
707
711
|
tab.command,
|
|
@@ -711,7 +715,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
711
715
|
status: tab.status,
|
|
712
716
|
title: tab.title,
|
|
713
717
|
workerName: tab.workerName,
|
|
714
|
-
|
|
718
|
+
workspaceId: tab.workspaceId,
|
|
715
719
|
}
|
|
716
720
|
)
|
|
717
721
|
autoRename.register({
|
|
@@ -722,14 +726,14 @@ export async function runDaemon(): Promise<void> {
|
|
|
722
726
|
})
|
|
723
727
|
}
|
|
724
728
|
// Classify synchronously BEFORE sending attachResult so
|
|
725
|
-
// each tab's activity and the full
|
|
729
|
+
// each tab's activity and the full project-status snapshot
|
|
726
730
|
// are available to embed in the reply. This makes the
|
|
727
|
-
// client apply them atomically with `hydrate-
|
|
731
|
+
// client apply them atomically with `hydrate-project`
|
|
728
732
|
// and closes the race where separate `tabStatus` events
|
|
729
733
|
// landed before the tab existed in client state.
|
|
730
|
-
const tabsForLoop =
|
|
734
|
+
const tabsForLoop = listTabsForProject(message.payload.projectId)
|
|
731
735
|
logDebug('daemon.attach.preClassify', {
|
|
732
|
-
|
|
736
|
+
projectId: message.payload.projectId,
|
|
733
737
|
tabs: tabsForLoop.map((t) => ({
|
|
734
738
|
assistant: t.assistant,
|
|
735
739
|
hasViewport: t.viewport !== undefined,
|
|
@@ -737,7 +741,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
737
741
|
seq: tabRegistry.get(t.id)?.viewportSeq ?? null,
|
|
738
742
|
})),
|
|
739
743
|
})
|
|
740
|
-
statusLoop.classifyNow(message.payload.
|
|
744
|
+
statusLoop.classifyNow(message.payload.projectId, tabsForLoop)
|
|
741
745
|
const tabsWithActivity = attachResult.tabs.map((tab) => {
|
|
742
746
|
const metadata = tabRegistry.get(tab.id)
|
|
743
747
|
return {
|
|
@@ -748,17 +752,17 @@ export async function runDaemon(): Promise<void> {
|
|
|
748
752
|
workerName: metadata?.workerName ?? tab.workerName,
|
|
749
753
|
}
|
|
750
754
|
})
|
|
751
|
-
const
|
|
755
|
+
const initialProjectStatuses = statusLoop.snapshotProjects()
|
|
752
756
|
logDebug('daemon.attach.replay', {
|
|
753
|
-
|
|
754
|
-
|
|
757
|
+
projectId: message.payload.projectId,
|
|
758
|
+
projects: initialProjectStatuses,
|
|
755
759
|
tabs: tabsWithActivity.map((t) => ({ activity: t.activity, id: t.id })),
|
|
756
760
|
})
|
|
757
761
|
send(socket, {
|
|
758
762
|
id: message.id,
|
|
759
763
|
payload: {
|
|
760
764
|
activeTabId: attachResult.activeTabId,
|
|
761
|
-
|
|
765
|
+
initialProjectStatuses,
|
|
762
766
|
protocolVersion: negotiatedVersion,
|
|
763
767
|
tabs: tabsWithActivity,
|
|
764
768
|
},
|
|
@@ -766,13 +770,13 @@ export async function runDaemon(): Promise<void> {
|
|
|
766
770
|
})
|
|
767
771
|
logDebug('daemon.request.attach.success', {
|
|
768
772
|
activeTabId: attachResult.activeTabId,
|
|
769
|
-
|
|
773
|
+
projectId: message.payload.projectId,
|
|
770
774
|
tabs: attachResult.tabs.length,
|
|
771
775
|
})
|
|
772
776
|
break
|
|
773
777
|
}
|
|
774
778
|
case 'createTab': {
|
|
775
|
-
const
|
|
779
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
776
780
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
777
781
|
if (message.payload.workerName !== undefined) {
|
|
778
782
|
if (!manager.hasCapability(MANAGER_CAPABILITY_WORKER_METADATA)) {
|
|
@@ -782,29 +786,29 @@ export async function runDaemon(): Promise<void> {
|
|
|
782
786
|
}
|
|
783
787
|
const conflictTabId = findWorkerNameConflict(
|
|
784
788
|
tabRegistry,
|
|
785
|
-
|
|
789
|
+
projectId,
|
|
786
790
|
message.payload.workerName
|
|
787
791
|
)
|
|
788
792
|
if (conflictTabId !== undefined) {
|
|
789
793
|
throw new Error(
|
|
790
|
-
`worker name already exists in this
|
|
794
|
+
`worker name already exists in this project: ${message.payload.workerName} (${conflictTabId})`
|
|
791
795
|
)
|
|
792
796
|
}
|
|
793
797
|
}
|
|
794
798
|
// Capability `createTabSizeFallback`: cols/rows = 0 means
|
|
795
|
-
// "use the
|
|
799
|
+
// "use the project's last attached dimensions". Headless
|
|
796
800
|
// CLIs don't have a viewport of their own, so this lets
|
|
797
801
|
// them spawn a PTY that lines up with the UI on the same
|
|
798
|
-
//
|
|
802
|
+
// project. If we have no remembered size yet, the TM
|
|
799
803
|
// would receive 0 and spawn a zero-sized PTY — surface a
|
|
800
804
|
// clear error instead.
|
|
801
805
|
let cols = message.payload.cols
|
|
802
806
|
let rows = message.payload.rows
|
|
803
807
|
if (cols === 0 || rows === 0) {
|
|
804
|
-
const prior =
|
|
808
|
+
const prior = projectDimensions.get(projectId)
|
|
805
809
|
if (!prior) {
|
|
806
810
|
throw new Error(
|
|
807
|
-
'createTab requested size fallback (cols=0 or rows=0) but no UI has attached to this
|
|
811
|
+
'createTab requested size fallback (cols=0 or rows=0) but no UI has attached to this project yet'
|
|
808
812
|
)
|
|
809
813
|
}
|
|
810
814
|
if (cols === 0) cols = prior.cols
|
|
@@ -813,8 +817,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
813
817
|
logDebug('daemon.request.createTab.start', {
|
|
814
818
|
cols,
|
|
815
819
|
command: message.payload.command,
|
|
820
|
+
projectId,
|
|
816
821
|
rows,
|
|
817
|
-
sessionId,
|
|
818
822
|
sizeFallback: message.payload.cols === 0 || message.payload.rows === 0,
|
|
819
823
|
tabId: message.payload.tabId,
|
|
820
824
|
title: message.payload.title,
|
|
@@ -829,7 +833,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
829
833
|
message.payload.autoRenameCandidate === true
|
|
830
834
|
)
|
|
831
835
|
rememberTab(
|
|
832
|
-
|
|
836
|
+
projectId,
|
|
833
837
|
message.payload.tabId,
|
|
834
838
|
message.payload.assistant,
|
|
835
839
|
fullCommand,
|
|
@@ -839,7 +843,7 @@ export async function runDaemon(): Promise<void> {
|
|
|
839
843
|
status: 'starting',
|
|
840
844
|
title: message.payload.title,
|
|
841
845
|
workerName: message.payload.workerName,
|
|
842
|
-
|
|
846
|
+
workspaceId: message.payload.workspaceId,
|
|
843
847
|
}
|
|
844
848
|
)
|
|
845
849
|
// Inject the hook bridge env so Claude Code's hooks can
|
|
@@ -858,8 +862,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
858
862
|
autoRenameStatus,
|
|
859
863
|
cols,
|
|
860
864
|
env,
|
|
865
|
+
projectId,
|
|
861
866
|
rows,
|
|
862
|
-
sessionId,
|
|
863
867
|
})
|
|
864
868
|
} catch (error) {
|
|
865
869
|
// `rememberTab` must happen before the manager call so
|
|
@@ -885,32 +889,32 @@ export async function runDaemon(): Promise<void> {
|
|
|
885
889
|
terminalModes: createDefaultTerminalModes(),
|
|
886
890
|
title: message.payload.title,
|
|
887
891
|
workerName: message.payload.workerName,
|
|
888
|
-
|
|
892
|
+
workspaceId: message.payload.workspaceId,
|
|
889
893
|
}
|
|
890
|
-
|
|
891
|
-
payload: {
|
|
894
|
+
broadcastForProjectVersioned(projectId, 11, {
|
|
895
|
+
payload: { projectId, tab: synthesizedTab },
|
|
892
896
|
type: 'tabAdded',
|
|
893
897
|
})
|
|
894
898
|
autoRename.register(synthesizedTab)
|
|
895
899
|
logDebug('daemon.request.createTab.success', {
|
|
896
|
-
|
|
900
|
+
projectId,
|
|
897
901
|
tabId: message.payload.tabId,
|
|
898
902
|
})
|
|
899
903
|
break
|
|
900
904
|
}
|
|
901
905
|
case 'write': {
|
|
902
|
-
const
|
|
906
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
903
907
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
904
|
-
await manager.write(
|
|
908
|
+
await manager.write(projectId, message.payload.tabId, message.payload.data)
|
|
905
909
|
autoRename.observeWrite(message.payload.tabId, message.payload.data)
|
|
906
910
|
sendOk(socket, message.id)
|
|
907
911
|
break
|
|
908
912
|
}
|
|
909
913
|
case 'renameTab': {
|
|
910
|
-
const
|
|
914
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
911
915
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
912
|
-
if (tabRegistry.get(message.payload.tabId)?.
|
|
913
|
-
throw new Error(`Tab not found in attached
|
|
916
|
+
if (tabRegistry.get(message.payload.tabId)?.projectId !== projectId) {
|
|
917
|
+
throw new Error(`Tab not found in attached project: ${message.payload.tabId}`)
|
|
914
918
|
}
|
|
915
919
|
autoRename.manualRename(message.payload.tabId)
|
|
916
920
|
applyTabMetadata(message.payload.tabId, {
|
|
@@ -921,21 +925,21 @@ export async function runDaemon(): Promise<void> {
|
|
|
921
925
|
break
|
|
922
926
|
}
|
|
923
927
|
case 'resizeClient': {
|
|
924
|
-
const
|
|
928
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
925
929
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
926
|
-
|
|
930
|
+
projectDimensions.set(projectId, {
|
|
927
931
|
cols: message.payload.cols,
|
|
928
932
|
rows: message.payload.rows,
|
|
929
933
|
})
|
|
930
|
-
await manager.resize(
|
|
934
|
+
await manager.resize(projectId, message.payload.cols, message.payload.rows)
|
|
931
935
|
sendOk(socket, message.id)
|
|
932
936
|
break
|
|
933
937
|
}
|
|
934
938
|
case 'resizeTab': {
|
|
935
|
-
const
|
|
939
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
936
940
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
937
941
|
await manager.resizeTab(
|
|
938
|
-
|
|
942
|
+
projectId,
|
|
939
943
|
message.payload.tabId,
|
|
940
944
|
message.payload.cols,
|
|
941
945
|
message.payload.rows
|
|
@@ -944,49 +948,49 @@ export async function runDaemon(): Promise<void> {
|
|
|
944
948
|
break
|
|
945
949
|
}
|
|
946
950
|
case 'scroll': {
|
|
947
|
-
const
|
|
951
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
948
952
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
949
|
-
await manager.scroll(
|
|
953
|
+
await manager.scroll(projectId, message.payload.tabId, message.payload.deltaLines)
|
|
950
954
|
sendOk(socket, message.id)
|
|
951
955
|
break
|
|
952
956
|
}
|
|
953
957
|
case 'scrollToBottom': {
|
|
954
|
-
const
|
|
958
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
955
959
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
956
|
-
await manager.scrollToBottom(
|
|
960
|
+
await manager.scrollToBottom(projectId, message.payload.tabId)
|
|
957
961
|
sendOk(socket, message.id)
|
|
958
962
|
break
|
|
959
963
|
}
|
|
960
964
|
case 'setActiveTab': {
|
|
961
|
-
const
|
|
965
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
962
966
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
963
967
|
// Update the cache AFTER the TM confirms the change — if
|
|
964
968
|
// manager.setActiveTab throws (tab doesn't exist, TM
|
|
965
969
|
// rejects), the cache would otherwise retain the bogus tabId
|
|
966
970
|
// and a subsequent listTabs would return a stale activeTabId.
|
|
967
|
-
await manager.setActiveTab(
|
|
968
|
-
|
|
971
|
+
await manager.setActiveTab(projectId, message.payload.tabId)
|
|
972
|
+
projectActiveTabIds.set(projectId, message.payload.tabId)
|
|
969
973
|
sendOk(socket, message.id)
|
|
970
974
|
break
|
|
971
975
|
}
|
|
972
976
|
case 'closeTab': {
|
|
973
|
-
const
|
|
977
|
+
const projectId = requireProject(socket, attachedProjects)
|
|
974
978
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
975
979
|
tabRegistry.delete(message.payload.tabId)
|
|
976
980
|
autoRename.unregister(message.payload.tabId)
|
|
977
|
-
if (
|
|
978
|
-
|
|
981
|
+
if (projectActiveTabIds.get(projectId) === message.payload.tabId) {
|
|
982
|
+
projectActiveTabIds.set(projectId, null)
|
|
979
983
|
}
|
|
980
|
-
await manager.closeTab(
|
|
984
|
+
await manager.closeTab(projectId, message.payload.tabId)
|
|
981
985
|
sendOk(socket, message.id)
|
|
982
986
|
break
|
|
983
987
|
}
|
|
984
988
|
case 'listTabs': {
|
|
985
989
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
986
|
-
const
|
|
990
|
+
const projectId = message.payload.projectId
|
|
987
991
|
const tabs: TabSessionSummary[] = []
|
|
988
992
|
for (const [tabId, entry] of tabRegistry) {
|
|
989
|
-
if (entry.
|
|
993
|
+
if (entry.projectId !== projectId) continue
|
|
990
994
|
tabs.push({
|
|
991
995
|
activity: statusLoop.getTabStatus(tabId) ?? undefined,
|
|
992
996
|
assistant: entry.assistant,
|
|
@@ -1001,33 +1005,33 @@ export async function runDaemon(): Promise<void> {
|
|
|
1001
1005
|
status: entry.status ?? 'running',
|
|
1002
1006
|
title: entry.title ?? '',
|
|
1003
1007
|
workerName: entry.workerName,
|
|
1004
|
-
|
|
1008
|
+
workspaceId: entry.workspaceId,
|
|
1005
1009
|
})
|
|
1006
1010
|
}
|
|
1007
1011
|
send(socket, {
|
|
1008
1012
|
id: message.id,
|
|
1009
1013
|
payload: {
|
|
1010
|
-
activeTabId:
|
|
1014
|
+
activeTabId: projectActiveTabIds.get(projectId) ?? null,
|
|
1011
1015
|
tabs,
|
|
1012
1016
|
},
|
|
1013
1017
|
type: 'listTabsResult',
|
|
1014
1018
|
})
|
|
1015
|
-
logDebug('daemon.request.listTabs', {
|
|
1019
|
+
logDebug('daemon.request.listTabs', { projectId, tabs: tabs.length })
|
|
1016
1020
|
break
|
|
1017
1021
|
}
|
|
1018
1022
|
case 'disposeAll': {
|
|
1019
|
-
const
|
|
1023
|
+
const projectId = attachedProjects.get(socket)
|
|
1020
1024
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1021
|
-
if (
|
|
1025
|
+
if (projectId != null && projectId !== '') {
|
|
1022
1026
|
for (const [tabId, entry] of tabRegistry) {
|
|
1023
|
-
if (entry.
|
|
1027
|
+
if (entry.projectId === projectId) {
|
|
1024
1028
|
autoRename.unregister(tabId)
|
|
1025
1029
|
tabRegistry.delete(tabId)
|
|
1026
1030
|
}
|
|
1027
1031
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
await manager.disposeSession(
|
|
1032
|
+
projectActiveTabIds.delete(projectId)
|
|
1033
|
+
projectDimensions.delete(projectId)
|
|
1034
|
+
await manager.disposeSession(projectId)
|
|
1031
1035
|
}
|
|
1032
1036
|
sendOk(socket, message.id)
|
|
1033
1037
|
break
|
|
@@ -1043,133 +1047,133 @@ export async function runDaemon(): Promise<void> {
|
|
|
1043
1047
|
await handleReexecRequest(socket, message.id, message.payload.reason)
|
|
1044
1048
|
break
|
|
1045
1049
|
}
|
|
1046
|
-
case '
|
|
1050
|
+
case 'createProject': {
|
|
1047
1051
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1048
1052
|
const { name, projectPath, switch: doSwitch } = message.payload
|
|
1049
1053
|
if (countUiAttachers() > 0) {
|
|
1050
1054
|
// Relay to the UI so it can preserve the live snapshot
|
|
1051
|
-
// of the currently-open
|
|
1055
|
+
// of the currently-open project before appending the
|
|
1052
1056
|
// new one. Fire-and-forget from the daemon's side.
|
|
1053
1057
|
broadcastAllVersioned(12, {
|
|
1054
1058
|
payload: { name, projectPath, switch: doSwitch },
|
|
1055
|
-
type: '
|
|
1059
|
+
type: 'projectCreateRequested',
|
|
1056
1060
|
})
|
|
1057
|
-
logDebug('daemon.request.
|
|
1061
|
+
logDebug('daemon.request.createProject.relay', { name, projectPath })
|
|
1058
1062
|
} else {
|
|
1059
|
-
const created =
|
|
1063
|
+
const created = createProjectInCatalog(name, projectPath)
|
|
1060
1064
|
if (doSwitch === true) {
|
|
1061
1065
|
bumpLastOpenedInCatalog(created.id)
|
|
1062
1066
|
// Mirror the UI-attached path: an ack event so a
|
|
1063
1067
|
// `--wait` CLI can exit even in the headless flow.
|
|
1064
1068
|
broadcastAllVersioned(12, {
|
|
1065
|
-
payload: {
|
|
1066
|
-
type: '
|
|
1069
|
+
payload: { projectId: created.id },
|
|
1070
|
+
type: 'projectSwitched',
|
|
1067
1071
|
})
|
|
1068
1072
|
}
|
|
1069
|
-
logDebug('daemon.request.
|
|
1073
|
+
logDebug('daemon.request.createProject.direct', {
|
|
1070
1074
|
name,
|
|
1071
|
-
|
|
1075
|
+
projectId: created.id,
|
|
1072
1076
|
})
|
|
1073
1077
|
}
|
|
1074
1078
|
sendOk(socket, message.id)
|
|
1075
1079
|
break
|
|
1076
1080
|
}
|
|
1077
|
-
case '
|
|
1081
|
+
case 'switchProject': {
|
|
1078
1082
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1079
|
-
const {
|
|
1083
|
+
const { targetProjectId } = message.payload
|
|
1080
1084
|
// Fail fast when the target is unknown — otherwise a UI-
|
|
1081
1085
|
// attached `--wait` CLI would sit until timeout while the
|
|
1082
1086
|
// UI silently drops the broadcast.
|
|
1083
|
-
|
|
1087
|
+
assertProjectInCatalog(targetProjectId)
|
|
1084
1088
|
if (countUiAttachers() > 0) {
|
|
1085
1089
|
broadcastAllVersioned(12, {
|
|
1086
|
-
payload: {
|
|
1087
|
-
type: '
|
|
1090
|
+
payload: { targetProjectId },
|
|
1091
|
+
type: 'projectSwitchRequested',
|
|
1088
1092
|
})
|
|
1089
|
-
logDebug('daemon.request.
|
|
1093
|
+
logDebug('daemon.request.switchProject.relay', { targetProjectId })
|
|
1090
1094
|
} else {
|
|
1091
|
-
bumpLastOpenedInCatalog(
|
|
1095
|
+
bumpLastOpenedInCatalog(targetProjectId)
|
|
1092
1096
|
// No UI to run the switch handler, so the switch is
|
|
1093
1097
|
// effectively complete once the catalog reflects it.
|
|
1094
1098
|
// Emit the switched event so a --wait CLI can exit.
|
|
1095
1099
|
broadcastAllVersioned(12, {
|
|
1096
|
-
payload: {
|
|
1097
|
-
type: '
|
|
1100
|
+
payload: { projectId: targetProjectId },
|
|
1101
|
+
type: 'projectSwitched',
|
|
1098
1102
|
})
|
|
1099
|
-
logDebug('daemon.request.
|
|
1103
|
+
logDebug('daemon.request.switchProject.direct', { targetProjectId })
|
|
1100
1104
|
}
|
|
1101
1105
|
sendOk(socket, message.id)
|
|
1102
1106
|
break
|
|
1103
1107
|
}
|
|
1104
|
-
case '
|
|
1108
|
+
case 'closeProject': {
|
|
1105
1109
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1106
|
-
const {
|
|
1107
|
-
|
|
1110
|
+
const { targetProjectId } = message.payload
|
|
1111
|
+
assertProjectInCatalog(targetProjectId)
|
|
1108
1112
|
if (countUiAttachers() > 0) {
|
|
1109
1113
|
broadcastAllVersioned(12, {
|
|
1110
|
-
payload: {
|
|
1111
|
-
type: '
|
|
1114
|
+
payload: { targetProjectId },
|
|
1115
|
+
type: 'projectCloseRequested',
|
|
1112
1116
|
})
|
|
1113
|
-
logDebug('daemon.request.
|
|
1117
|
+
logDebug('daemon.request.closeProject.relay', { targetProjectId })
|
|
1114
1118
|
} else {
|
|
1115
|
-
deleteFromCatalog(
|
|
1116
|
-
logDebug('daemon.request.
|
|
1119
|
+
deleteFromCatalog(targetProjectId)
|
|
1120
|
+
logDebug('daemon.request.closeProject.direct', { targetProjectId })
|
|
1117
1121
|
}
|
|
1118
1122
|
sendOk(socket, message.id)
|
|
1119
1123
|
break
|
|
1120
1124
|
}
|
|
1121
|
-
case '
|
|
1125
|
+
case 'announceProjectSwitched': {
|
|
1122
1126
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1123
1127
|
// UI-emitted acknowledgement. Relay so any --wait CLI can
|
|
1124
1128
|
// exit. We don't validate that the announcement matches
|
|
1125
1129
|
// an outstanding request — a UI can announce a switch that
|
|
1126
1130
|
// happened via any means (menu, keybind, CLI).
|
|
1127
1131
|
broadcastAllVersioned(12, {
|
|
1128
|
-
payload: {
|
|
1129
|
-
type: '
|
|
1132
|
+
payload: { projectId: message.payload.projectId },
|
|
1133
|
+
type: 'projectSwitched',
|
|
1130
1134
|
})
|
|
1131
1135
|
sendOk(socket, message.id)
|
|
1132
1136
|
break
|
|
1133
1137
|
}
|
|
1134
|
-
case '
|
|
1138
|
+
case 'addWorkspaceRecord': {
|
|
1135
1139
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1136
|
-
const {
|
|
1140
|
+
const { projectId: targetProjectId, workspace } = message.payload
|
|
1137
1141
|
if (countUiAttachers() > 0) {
|
|
1138
1142
|
broadcastAllVersioned(12, {
|
|
1139
|
-
payload: {
|
|
1140
|
-
type: '
|
|
1143
|
+
payload: { projectId: targetProjectId, workspace },
|
|
1144
|
+
type: 'workspaceAdded',
|
|
1141
1145
|
})
|
|
1142
|
-
logDebug('daemon.request.
|
|
1143
|
-
|
|
1144
|
-
|
|
1146
|
+
logDebug('daemon.request.addWorkspaceRecord.relay', {
|
|
1147
|
+
projectId: targetProjectId,
|
|
1148
|
+
workspaceId: workspace.id,
|
|
1145
1149
|
})
|
|
1146
1150
|
} else {
|
|
1147
|
-
|
|
1148
|
-
logDebug('daemon.request.
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
+
addWorkspaceToCatalog(targetProjectId, workspace)
|
|
1152
|
+
logDebug('daemon.request.addWorkspaceRecord.direct', {
|
|
1153
|
+
projectId: targetProjectId,
|
|
1154
|
+
workspaceId: workspace.id,
|
|
1151
1155
|
})
|
|
1152
1156
|
}
|
|
1153
1157
|
sendOk(socket, message.id)
|
|
1154
1158
|
break
|
|
1155
1159
|
}
|
|
1156
|
-
case '
|
|
1160
|
+
case 'removeWorkspaceRecord': {
|
|
1157
1161
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
1158
|
-
const {
|
|
1162
|
+
const { projectId: targetProjectId, workspaceId } = message.payload
|
|
1159
1163
|
if (countUiAttachers() > 0) {
|
|
1160
1164
|
broadcastAllVersioned(12, {
|
|
1161
|
-
payload: {
|
|
1162
|
-
type: '
|
|
1165
|
+
payload: { projectId: targetProjectId, workspaceId },
|
|
1166
|
+
type: 'workspaceRemoved',
|
|
1163
1167
|
})
|
|
1164
|
-
logDebug('daemon.request.
|
|
1165
|
-
|
|
1166
|
-
|
|
1168
|
+
logDebug('daemon.request.removeWorkspaceRecord.relay', {
|
|
1169
|
+
projectId: targetProjectId,
|
|
1170
|
+
workspaceId,
|
|
1167
1171
|
})
|
|
1168
1172
|
} else {
|
|
1169
|
-
|
|
1170
|
-
logDebug('daemon.request.
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
+
removeWorkspaceFromCatalog(targetProjectId, workspaceId)
|
|
1174
|
+
logDebug('daemon.request.removeWorkspaceRecord.direct', {
|
|
1175
|
+
projectId: targetProjectId,
|
|
1176
|
+
workspaceId,
|
|
1173
1177
|
})
|
|
1174
1178
|
}
|
|
1175
1179
|
sendOk(socket, message.id)
|
|
@@ -1196,17 +1200,17 @@ export async function runDaemon(): Promise<void> {
|
|
|
1196
1200
|
})
|
|
1197
1201
|
|
|
1198
1202
|
socket.on('close', () => {
|
|
1199
|
-
logDebug('daemon.client.close', {
|
|
1203
|
+
logDebug('daemon.client.close', { projectId: attachedProjects.get(socket) ?? null })
|
|
1200
1204
|
sockets.delete(socket)
|
|
1201
|
-
|
|
1205
|
+
attachedProjects.delete(socket)
|
|
1202
1206
|
negotiatedVersions.delete(socket)
|
|
1203
1207
|
thinAttachers.delete(socket)
|
|
1204
1208
|
if (sockets.size === 0) updateTmBroadcastForClientCount(0)
|
|
1205
1209
|
})
|
|
1206
1210
|
socket.on('error', () => {
|
|
1207
|
-
logDebug('daemon.client.error', {
|
|
1211
|
+
logDebug('daemon.client.error', { projectId: attachedProjects.get(socket) ?? null })
|
|
1208
1212
|
sockets.delete(socket)
|
|
1209
|
-
|
|
1213
|
+
attachedProjects.delete(socket)
|
|
1210
1214
|
negotiatedVersions.delete(socket)
|
|
1211
1215
|
thinAttachers.delete(socket)
|
|
1212
1216
|
if (sockets.size === 0) updateTmBroadcastForClientCount(0)
|