@brimveyn/aimux 1.21.0 → 1.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import type { SessionBackend } from '../session-backend/types'
|
|
2
|
-
import type { AppAction, AppState, SessionRecord, TabSession } from '../state/types'
|
|
3
|
-
|
|
4
|
-
import { logInputDebug } from '../debug/input-log'
|
|
5
|
-
import { createPrefixedId } from '../platform/id'
|
|
6
|
-
import { saveSessionCatalog } from '../state/session-catalog'
|
|
7
|
-
import { createEmptyWorkspaceSnapshot, serializeWorkspace } from '../state/session-persistence'
|
|
8
|
-
import { createPrimaryWorktree, ensureSessionWorktrees } from '../state/session-worktrees'
|
|
9
|
-
import { toast } from '../state/toast-store'
|
|
10
|
-
import { buildSessionsWithCurrentSnapshot } from '../state/workspace-save'
|
|
11
|
-
|
|
12
|
-
export function createSessionFromCurrentState(
|
|
13
|
-
state: AppState,
|
|
14
|
-
name: string,
|
|
15
|
-
projectPath?: string
|
|
16
|
-
): { session: SessionRecord; sessions: SessionRecord[] } {
|
|
17
|
-
const now = new Date().toISOString()
|
|
18
|
-
const workspaceSnapshot =
|
|
19
|
-
(state.currentSessionId != null && state.currentSessionId !== '') || state.tabs.length === 0
|
|
20
|
-
? createEmptyWorkspaceSnapshot()
|
|
21
|
-
: serializeWorkspace(state)
|
|
22
|
-
const session: SessionRecord = {
|
|
23
|
-
activeWorktreeId: undefined,
|
|
24
|
-
createdAt: now,
|
|
25
|
-
id: createPrefixedId('session'),
|
|
26
|
-
lastOpenedAt: now,
|
|
27
|
-
name,
|
|
28
|
-
projectPath,
|
|
29
|
-
updatedAt: now,
|
|
30
|
-
workspaceSnapshot,
|
|
31
|
-
worktrees: undefined,
|
|
32
|
-
}
|
|
33
|
-
if (projectPath != null && projectPath !== '') {
|
|
34
|
-
const worktree = createPrimaryWorktree(projectPath, now)
|
|
35
|
-
session.activeWorktreeId = worktree.id
|
|
36
|
-
session.worktrees = [worktree]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
let updatedSessions = state.sessions
|
|
40
|
-
if (state.currentSessionId != null && state.currentSessionId !== '') {
|
|
41
|
-
const currentSnapshot = serializeWorkspace(state)
|
|
42
|
-
updatedSessions = state.sessions.map((entry) =>
|
|
43
|
-
entry.id === state.currentSessionId
|
|
44
|
-
? { ...entry, updatedAt: now, workspaceSnapshot: currentSnapshot }
|
|
45
|
-
: entry
|
|
46
|
-
)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
session,
|
|
51
|
-
sessions: [...updatedSessions, ensureSessionWorktrees(session)],
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function renameSessionRecords(
|
|
56
|
-
sessions: SessionRecord[],
|
|
57
|
-
sessionId: string,
|
|
58
|
-
name: string
|
|
59
|
-
): SessionRecord[] {
|
|
60
|
-
return sessions.map((session) =>
|
|
61
|
-
session.id === sessionId ? { ...session, name, updatedAt: new Date().toISOString() } : session
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function switchSessionRecords(state: AppState, session: SessionRecord): SessionRecord[] {
|
|
66
|
-
const sessionsWithSnapshot = buildSessionsWithCurrentSnapshot(
|
|
67
|
-
state.sessions,
|
|
68
|
-
state.currentSessionId,
|
|
69
|
-
state
|
|
70
|
-
)
|
|
71
|
-
return sessionsWithSnapshot.map((entry) =>
|
|
72
|
-
entry.id === session.id ? { ...entry, lastOpenedAt: new Date().toISOString() } : entry
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function deleteSessionRecords(
|
|
77
|
-
sessions: SessionRecord[],
|
|
78
|
-
sessionId: string
|
|
79
|
-
): SessionRecord[] {
|
|
80
|
-
return sessions.filter((session) => session.id !== sessionId)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function handleCreateSessionEffect(
|
|
84
|
-
state: AppState,
|
|
85
|
-
dispatch: (action: AppAction) => void,
|
|
86
|
-
name: string,
|
|
87
|
-
projectPath?: string
|
|
88
|
-
): void {
|
|
89
|
-
const { session, sessions } = createSessionFromCurrentState(state, name, projectPath)
|
|
90
|
-
logInputDebug('app.session.create', {
|
|
91
|
-
fromCurrentWorkspace:
|
|
92
|
-
(state.currentSessionId == null || state.currentSessionId === '') && state.tabs.length > 0,
|
|
93
|
-
name,
|
|
94
|
-
sessionId: session.id,
|
|
95
|
-
tabCount: session.workspaceSnapshot?.tabs.length ?? 0,
|
|
96
|
-
})
|
|
97
|
-
saveSessionCatalog(sessions)
|
|
98
|
-
dispatch({ sessions, type: 'set-sessions' })
|
|
99
|
-
dispatch({
|
|
100
|
-
sessionId: session.id,
|
|
101
|
-
type: 'load-session',
|
|
102
|
-
workspaceSnapshot: session.workspaceSnapshot,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function handleRenameSessionEffect(
|
|
107
|
-
sessions: SessionRecord[],
|
|
108
|
-
dispatch: (action: AppAction) => void,
|
|
109
|
-
sessionId: string,
|
|
110
|
-
name: string
|
|
111
|
-
): void {
|
|
112
|
-
logInputDebug('app.session.rename', { name, sessionId })
|
|
113
|
-
const renamed = renameSessionRecords(sessions, sessionId, name)
|
|
114
|
-
saveSessionCatalog(renamed)
|
|
115
|
-
dispatch({ name, sessionId, type: 'rename-session-record' })
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function handleSwitchSessionEffect(
|
|
119
|
-
state: AppState,
|
|
120
|
-
backend: SessionBackend,
|
|
121
|
-
dispatch: (action: AppAction) => void,
|
|
122
|
-
session: SessionRecord
|
|
123
|
-
): void {
|
|
124
|
-
logInputDebug('app.session.switch.start', {
|
|
125
|
-
currentTabCount: state.tabs.length,
|
|
126
|
-
fromSessionId: state.currentSessionId,
|
|
127
|
-
restoredTabCount: session.workspaceSnapshot?.tabs.length ?? 0,
|
|
128
|
-
toName: session.name,
|
|
129
|
-
toSessionId: session.id,
|
|
130
|
-
})
|
|
131
|
-
const sessions = switchSessionRecords(state, session)
|
|
132
|
-
saveSessionCatalog(sessions)
|
|
133
|
-
void backend.destroy(true)
|
|
134
|
-
dispatch({ sessions, type: 'set-sessions' })
|
|
135
|
-
dispatch({
|
|
136
|
-
sessionId: session.id,
|
|
137
|
-
type: 'load-session',
|
|
138
|
-
workspaceSnapshot: session.workspaceSnapshot,
|
|
139
|
-
})
|
|
140
|
-
logInputDebug('app.session.switch.dispatched', { toSessionId: session.id })
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function handleDeleteSessionEffect(
|
|
144
|
-
state: AppState,
|
|
145
|
-
backend: SessionBackend,
|
|
146
|
-
dispatch: (action: AppAction) => void,
|
|
147
|
-
sessionId: string,
|
|
148
|
-
options?: { openSessionPicker?: boolean }
|
|
149
|
-
): void {
|
|
150
|
-
const name = state.sessions.find((entry) => entry.id === sessionId)?.name
|
|
151
|
-
const remaining = deleteSessionRecords(state.sessions, sessionId)
|
|
152
|
-
logInputDebug('app.session.delete', {
|
|
153
|
-
remainingCount: remaining.length,
|
|
154
|
-
sessionId,
|
|
155
|
-
wasCurrent: sessionId === state.currentSessionId,
|
|
156
|
-
})
|
|
157
|
-
saveSessionCatalog(remaining)
|
|
158
|
-
if (sessionId === state.currentSessionId) {
|
|
159
|
-
void backend.destroy(true)
|
|
160
|
-
}
|
|
161
|
-
dispatch({
|
|
162
|
-
openSessionPicker: options?.openSessionPicker,
|
|
163
|
-
sessionId,
|
|
164
|
-
type: 'delete-session-record',
|
|
165
|
-
})
|
|
166
|
-
toast.success(name != null && name !== '' ? `Deleted workspace "${name}"` : 'Workspace deleted')
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function restartTabSession(
|
|
170
|
-
backend: SessionBackend,
|
|
171
|
-
dispatch: (action: AppAction) => void,
|
|
172
|
-
clearIdleTimer: (tabId: string) => void,
|
|
173
|
-
clearStartupGrace: (tabId: string) => void,
|
|
174
|
-
startTabSession: (tab: TabSession) => void,
|
|
175
|
-
tab: TabSession
|
|
176
|
-
): void {
|
|
177
|
-
logInputDebug('app.restartTab', {
|
|
178
|
-
command: tab.command,
|
|
179
|
-
status: tab.status,
|
|
180
|
-
tabId: tab.id,
|
|
181
|
-
})
|
|
182
|
-
clearIdleTimer(tab.id)
|
|
183
|
-
clearStartupGrace(tab.id)
|
|
184
|
-
backend.disposeSession(tab.id)
|
|
185
|
-
dispatch({ tabId: tab.id, type: 'reset-tab-session' })
|
|
186
|
-
startTabSession(tab)
|
|
187
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import type { SessionRecord, WorktreeRecord } from '../../state/types'
|
|
2
|
-
|
|
3
|
-
import { findMostRecentSession, loadSessionCatalog } from '../../state/session-catalog'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Where a command's target workspace came from. An orchestrator needs this to
|
|
7
|
-
* tell "I asked for pragma-once" from "aimux guessed, and the UI had moved on":
|
|
8
|
-
* `active` is the only origin that can silently follow the UI to another repo.
|
|
9
|
-
*/
|
|
10
|
-
export type WorkspaceOrigin = 'flag' | 'env' | 'active'
|
|
11
|
-
|
|
12
|
-
export interface ResolvedWorkspace {
|
|
13
|
-
origin: WorkspaceOrigin
|
|
14
|
-
record: SessionRecord
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Env pin for headless orchestrators — `--workspace` still wins over it. */
|
|
18
|
-
export const WORKSPACE_ENV_VAR = 'AIMUX_WORKSPACE'
|
|
19
|
-
|
|
20
|
-
/** The workspace's primary (root) worktree, i.e. the repository it is about. */
|
|
21
|
-
export function findPrimaryWorktree(session: SessionRecord): WorktreeRecord | undefined {
|
|
22
|
-
return session.worktrees?.find((worktree) => worktree.source === 'primary')
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The repo every fresh worktree for this workspace is cut from. Surfaced in
|
|
27
|
-
* every worker envelope so an agent can see *which project* it just acted on
|
|
28
|
-
* instead of inferring it from a worktree path hash.
|
|
29
|
-
*/
|
|
30
|
-
export function workspaceRepoRoot(session: SessionRecord): string | null {
|
|
31
|
-
return findPrimaryWorktree(session)?.repoRoot ?? session.projectPath ?? null
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Stable workspace identity block embedded in command output. */
|
|
35
|
-
export function workspaceIdentity(session: SessionRecord): {
|
|
36
|
-
id: string
|
|
37
|
-
name: string
|
|
38
|
-
repoRoot: string | null
|
|
39
|
-
} {
|
|
40
|
-
return { id: session.id, name: session.name, repoRoot: workspaceRepoRoot(session) }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Resolve `--workspace W` to a session record from the catalog, reporting where
|
|
45
|
-
* the choice came from. Precedence: the explicit flag, then `AIMUX_WORKSPACE`,
|
|
46
|
-
* then the most recently opened session. Throws when the catalog is empty (no
|
|
47
|
-
* session has ever been created) or when the explicit name/id doesn't match.
|
|
48
|
-
*
|
|
49
|
-
* Matching: exact id wins; otherwise exact name (case-sensitive); otherwise
|
|
50
|
-
* unique case-insensitive name match.
|
|
51
|
-
*/
|
|
52
|
-
export function resolveWorkspaceWithOrigin(name: string | undefined): ResolvedWorkspace {
|
|
53
|
-
const sessions = loadSessionCatalog()
|
|
54
|
-
if (sessions.length === 0) {
|
|
55
|
-
throw new Error(
|
|
56
|
-
'no sessions found — create one from the aimux UI first (or pass --workspace once created)'
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const flag = name !== undefined && name !== '' ? name : undefined
|
|
61
|
-
const env = process.env[WORKSPACE_ENV_VAR]
|
|
62
|
-
const fromEnv = env != null && env !== '' ? env : undefined
|
|
63
|
-
const selector = flag ?? fromEnv
|
|
64
|
-
if (selector === undefined) {
|
|
65
|
-
const active = findMostRecentSession(sessions)
|
|
66
|
-
if (!active) {
|
|
67
|
-
throw new Error('no active workspace and the catalog is empty')
|
|
68
|
-
}
|
|
69
|
-
return { origin: 'active', record: active }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
origin: flag !== undefined ? 'flag' : 'env',
|
|
74
|
-
record: matchWorkspace(sessions, selector),
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function resolveWorkspace(name: string | undefined): SessionRecord {
|
|
79
|
-
return resolveWorkspaceWithOrigin(name).record
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function matchWorkspace(sessions: SessionRecord[], name: string): SessionRecord {
|
|
83
|
-
const byId = sessions.find((session) => session.id === name)
|
|
84
|
-
if (byId) return byId
|
|
85
|
-
|
|
86
|
-
const exactNameMatches = sessions.filter((session) => session.name === name)
|
|
87
|
-
if (exactNameMatches.length > 1) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
`workspace "${name}" matches multiple sessions: ${exactNameMatches.map((s) => s.id).join(', ')}`
|
|
90
|
-
)
|
|
91
|
-
}
|
|
92
|
-
const exactOnly = exactNameMatches[0]
|
|
93
|
-
if (exactOnly) return exactOnly
|
|
94
|
-
|
|
95
|
-
const lower = name.toLowerCase()
|
|
96
|
-
const ciMatches = sessions.filter((session) => session.name.toLowerCase() === lower)
|
|
97
|
-
if (ciMatches.length > 1) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`workspace "${name}" matches multiple sessions: ${ciMatches.map((s) => s.id).join(', ')}`
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
const ciOnly = ciMatches[0]
|
|
103
|
-
if (ciOnly) return ciOnly
|
|
104
|
-
|
|
105
|
-
throw new Error(`workspace not found: ${name}`)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function listWorkspaces(): SessionRecord[] {
|
|
109
|
-
return loadSessionCatalog()
|
|
110
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { CliCommand } from '../../registry'
|
|
2
|
-
|
|
3
|
-
import { IPC_CAPABILITY_WORKSPACE_LIFECYCLE } from '../../../ipc/protocol'
|
|
4
|
-
import { resolveWorkspace } from '../../client/workspace-resolver'
|
|
5
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
6
|
-
import { EXIT_OK, writeJson } from '../../output'
|
|
7
|
-
|
|
8
|
-
export const workspaceClose: CliCommand = {
|
|
9
|
-
args: [{ complete: { kind: 'dynamic', source: 'workspace' }, name: 'workspace', required: true }],
|
|
10
|
-
flags: SHARED_FLAGS,
|
|
11
|
-
group: 'workspace',
|
|
12
|
-
run: async (ctx) => {
|
|
13
|
-
const target = ctx.args.positionals[0]
|
|
14
|
-
if (typeof target !== 'string' || target.length === 0) {
|
|
15
|
-
throw new Error('target workspace is required (id or name)')
|
|
16
|
-
}
|
|
17
|
-
const session = resolveWorkspace(target)
|
|
18
|
-
|
|
19
|
-
const daemon = await ctx.getDaemon()
|
|
20
|
-
if (!daemon.hasCapability(IPC_CAPABILITY_WORKSPACE_LIFECYCLE)) {
|
|
21
|
-
throw new Error(
|
|
22
|
-
'daemon predates workspaceLifecycle capability — restart aimux to pick up the new daemon'
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
await daemon.expectOk('closeWorkspace', { targetSessionId: session.id })
|
|
27
|
-
writeJson({ closedSessionId: session.id, name: session.name })
|
|
28
|
-
return EXIT_OK
|
|
29
|
-
},
|
|
30
|
-
summary: 'Close a workspace (via the UI when attached, otherwise the catalog)',
|
|
31
|
-
verb: 'close',
|
|
32
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { CliCommand } from '../../registry'
|
|
2
|
-
|
|
3
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
4
|
-
import { EXIT_OK, writeJson } from '../../output'
|
|
5
|
-
|
|
6
|
-
export const workspaceShow: CliCommand = {
|
|
7
|
-
args: [],
|
|
8
|
-
flags: SHARED_FLAGS,
|
|
9
|
-
group: 'workspace',
|
|
10
|
-
run: async (ctx) => {
|
|
11
|
-
const workspace = ctx.getWorkspace()
|
|
12
|
-
writeJson({
|
|
13
|
-
activeWorktreeId: workspace.activeWorktreeId,
|
|
14
|
-
createdAt: workspace.createdAt,
|
|
15
|
-
id: workspace.id,
|
|
16
|
-
lastOpenedAt: workspace.lastOpenedAt,
|
|
17
|
-
name: workspace.name,
|
|
18
|
-
projectPath: workspace.projectPath,
|
|
19
|
-
worktrees:
|
|
20
|
-
workspace.worktrees?.map((w) => ({
|
|
21
|
-
branch: w.branch,
|
|
22
|
-
id: w.id,
|
|
23
|
-
name: w.name,
|
|
24
|
-
path: w.path,
|
|
25
|
-
source: w.source,
|
|
26
|
-
})) ?? [],
|
|
27
|
-
})
|
|
28
|
-
return EXIT_OK
|
|
29
|
-
},
|
|
30
|
-
summary: 'Show the active workspace (or the one named via --workspace)',
|
|
31
|
-
verb: 'show',
|
|
32
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { CliCommand } from '../../registry'
|
|
2
|
-
|
|
3
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
4
|
-
import { EXIT_OK, writeJson } from '../../output'
|
|
5
|
-
import { createWorkspaceWorktree } from './create-core'
|
|
6
|
-
|
|
7
|
-
export const worktreeCreate: CliCommand = {
|
|
8
|
-
args: [],
|
|
9
|
-
flags: [
|
|
10
|
-
...SHARED_FLAGS,
|
|
11
|
-
{
|
|
12
|
-
complete: { kind: 'none' },
|
|
13
|
-
description: 'display name for the new worktree',
|
|
14
|
-
kind: 'string',
|
|
15
|
-
name: 'name',
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
complete: { kind: 'none' },
|
|
19
|
-
description: 'branch name (defaults to aimux/<name>)',
|
|
20
|
-
kind: 'string',
|
|
21
|
-
name: 'branch',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
complete: { kind: 'dynamic', source: 'git-ref' },
|
|
25
|
-
description: 'base ref for the branch (defaults to HEAD)',
|
|
26
|
-
kind: 'string',
|
|
27
|
-
name: 'base',
|
|
28
|
-
},
|
|
29
|
-
],
|
|
30
|
-
group: 'worktree',
|
|
31
|
-
run: async (ctx) => {
|
|
32
|
-
const name = ctx.args.flags.name
|
|
33
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
34
|
-
throw new Error('--name is required')
|
|
35
|
-
}
|
|
36
|
-
const branch =
|
|
37
|
-
typeof ctx.args.flags.branch === 'string' ? ctx.args.flags.branch : `aimux/${name}`
|
|
38
|
-
const base = typeof ctx.args.flags.base === 'string' ? ctx.args.flags.base : 'HEAD'
|
|
39
|
-
|
|
40
|
-
const workspace = ctx.getWorkspace()
|
|
41
|
-
const daemon = await ctx.getDaemon()
|
|
42
|
-
const record = await createWorkspaceWorktree({ base, branch, daemon, name, workspace })
|
|
43
|
-
|
|
44
|
-
writeJson({
|
|
45
|
-
branch: record.branch,
|
|
46
|
-
id: record.id,
|
|
47
|
-
name: record.name,
|
|
48
|
-
path: record.path,
|
|
49
|
-
repoRoot: record.repoRoot,
|
|
50
|
-
})
|
|
51
|
-
return EXIT_OK
|
|
52
|
-
},
|
|
53
|
-
summary: 'Create a new worktree in the active workspace',
|
|
54
|
-
verb: 'create',
|
|
55
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { CliCommand } from '../../registry'
|
|
2
|
-
|
|
3
|
-
import { listGitWorktrees } from '../../../git/worktree'
|
|
4
|
-
import { IPC_CAPABILITY_LIST_TABS } from '../../../ipc/protocol'
|
|
5
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
6
|
-
import { EXIT_OK, writeJson } from '../../output'
|
|
7
|
-
|
|
8
|
-
export const worktreeList: CliCommand = {
|
|
9
|
-
args: [],
|
|
10
|
-
flags: SHARED_FLAGS,
|
|
11
|
-
group: 'worktree',
|
|
12
|
-
run: async (ctx) => {
|
|
13
|
-
const workspace = ctx.getWorkspace()
|
|
14
|
-
const records = workspace.worktrees ?? []
|
|
15
|
-
|
|
16
|
-
// Count live tabs per worktree so an orchestrator can see co-location
|
|
17
|
-
// (several workers sharing one worktree) at a glance. Best-effort: `null`
|
|
18
|
-
// when the daemon is unreachable or predates the listTabs capability —
|
|
19
|
-
// `worktree list` otherwise reads purely from the catalog + git, so we don't
|
|
20
|
-
// want a down daemon to fail it.
|
|
21
|
-
const tabCounts = new Map<string, number>()
|
|
22
|
-
let tabCountsAvailable = false
|
|
23
|
-
try {
|
|
24
|
-
const daemon = await ctx.getDaemon()
|
|
25
|
-
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
26
|
-
const { tabs } = await daemon.listTabs(workspace.id)
|
|
27
|
-
for (const tab of tabs) {
|
|
28
|
-
if (tab.worktreeId != null) {
|
|
29
|
-
tabCounts.set(tab.worktreeId, (tabCounts.get(tab.worktreeId) ?? 0) + 1)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
tabCountsAvailable = true
|
|
33
|
-
}
|
|
34
|
-
} catch {
|
|
35
|
-
// daemon unreachable — leave tabCount null rather than failing the list.
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Cross-check against git so we can flag catalog entries that git no
|
|
39
|
-
// longer knows about (prunable / vanished) — otherwise the CLI would
|
|
40
|
-
// happily report worktrees that don't exist on disk.
|
|
41
|
-
const primary = records.find((w) => w.source === 'primary')
|
|
42
|
-
const gitPaths = new Set<string>()
|
|
43
|
-
if (primary) {
|
|
44
|
-
for (const w of await listGitWorktrees(primary.repoRoot)) {
|
|
45
|
-
if (w.prunable !== true) gitPaths.add(w.path)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
writeJson({
|
|
50
|
-
activeWorktreeId: workspace.activeWorktreeId ?? null,
|
|
51
|
-
workspaceId: workspace.id,
|
|
52
|
-
worktrees: records.map((w) => ({
|
|
53
|
-
branch: w.branch,
|
|
54
|
-
createdByAimux: w.createdByAimux,
|
|
55
|
-
gitTracked: primary ? gitPaths.has(w.path) : null,
|
|
56
|
-
id: w.id,
|
|
57
|
-
name: w.name,
|
|
58
|
-
path: w.path,
|
|
59
|
-
repoRoot: w.repoRoot,
|
|
60
|
-
source: w.source,
|
|
61
|
-
tabCount: tabCountsAvailable ? (tabCounts.get(w.id) ?? 0) : null,
|
|
62
|
-
})),
|
|
63
|
-
})
|
|
64
|
-
return EXIT_OK
|
|
65
|
-
},
|
|
66
|
-
summary: 'List worktrees for a workspace',
|
|
67
|
-
verb: 'list',
|
|
68
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { CliCommand } from '../../registry'
|
|
2
|
-
|
|
3
|
-
import { removeGitWorktree } from '../../../git/worktree'
|
|
4
|
-
import {
|
|
5
|
-
IPC_CAPABILITY_LIST_TABS,
|
|
6
|
-
IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS,
|
|
7
|
-
} from '../../../ipc/protocol'
|
|
8
|
-
import { pruneEmptyWorktreeParent } from '../../../platform/worktree-paths'
|
|
9
|
-
import { SHARED_FLAGS } from '../../flags'
|
|
10
|
-
import { EXIT_OK, writeJson } from '../../output'
|
|
11
|
-
|
|
12
|
-
export const worktreeRemove: CliCommand = {
|
|
13
|
-
args: [{ complete: { kind: 'dynamic', source: 'worktree' }, name: 'worktree', required: true }],
|
|
14
|
-
flags: [
|
|
15
|
-
...SHARED_FLAGS,
|
|
16
|
-
{ description: 'pass --force to git worktree remove', kind: 'boolean', name: 'force' },
|
|
17
|
-
],
|
|
18
|
-
group: 'worktree',
|
|
19
|
-
run: async (ctx) => {
|
|
20
|
-
const target = ctx.args.positionals[0]
|
|
21
|
-
if (typeof target !== 'string' || target.length === 0) {
|
|
22
|
-
throw new Error('worktree id or path is required')
|
|
23
|
-
}
|
|
24
|
-
const force = ctx.args.flags.force === true
|
|
25
|
-
|
|
26
|
-
const workspace = ctx.getWorkspace()
|
|
27
|
-
const worktree =
|
|
28
|
-
workspace.worktrees?.find((w) => w.id === target) ??
|
|
29
|
-
workspace.worktrees?.find((w) => w.path === target)
|
|
30
|
-
if (!worktree) {
|
|
31
|
-
const known = workspace.worktrees?.map((w) => w.id).join(', ') ?? '(none)'
|
|
32
|
-
throw new Error(`unknown worktree: ${target} (known: ${known})`)
|
|
33
|
-
}
|
|
34
|
-
if (worktree.source === 'primary') {
|
|
35
|
-
throw new Error('refusing to remove the primary worktree')
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const primary = workspace.worktrees?.find((w) => w.source === 'primary')
|
|
39
|
-
if (!primary) {
|
|
40
|
-
throw new Error('workspace has no primary worktree — cannot resolve repoRoot for git remove')
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const daemon = await ctx.getDaemon()
|
|
44
|
-
if (!daemon.hasCapability(IPC_CAPABILITY_WORKTREE_LIFECYCLE_EVENTS)) {
|
|
45
|
-
throw new Error(
|
|
46
|
-
'daemon predates worktreeLifecycleEvents capability — restart aimux to pick up the new daemon'
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
if (daemon.hasCapability(IPC_CAPABILITY_LIST_TABS)) {
|
|
50
|
-
const live = (await daemon.listTabs(workspace.id)).tabs.filter(
|
|
51
|
-
(tab) => tab.worktreeId === worktree.id
|
|
52
|
-
)
|
|
53
|
-
if (live.length > 0) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`refusing to remove worktree with live tabs: ${live.map((tab) => tab.id).join(', ')}`
|
|
56
|
-
)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// All capability and liveness checks happen before touching git. If git
|
|
61
|
-
// refuses a dirty worktree the catalog remains unchanged.
|
|
62
|
-
await removeGitWorktree({ force, repoPath: primary.repoRoot, targetPath: worktree.path })
|
|
63
|
-
await pruneEmptyWorktreeParent(worktree.path)
|
|
64
|
-
try {
|
|
65
|
-
await daemon.expectOk('removeWorktreeRecord', {
|
|
66
|
-
sessionId: workspace.id,
|
|
67
|
-
worktreeId: worktree.id,
|
|
68
|
-
})
|
|
69
|
-
} catch (error) {
|
|
70
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
71
|
-
throw new Error(
|
|
72
|
-
`worktree removed from git but catalog reconciliation failed for ${worktree.id}: ${message}`
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
writeJson({ id: worktree.id, name: worktree.name, path: worktree.path })
|
|
77
|
-
return EXIT_OK
|
|
78
|
-
},
|
|
79
|
-
summary: 'Remove a worktree from the active workspace',
|
|
80
|
-
verb: 'remove',
|
|
81
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
import { appStore } from '../state/app-store'
|
|
4
|
-
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
5
|
-
import { getCurrentBranch } from '../ui/git-branch'
|
|
6
|
-
|
|
7
|
-
const INTERVAL_MS = 4000
|
|
8
|
-
|
|
9
|
-
// Single source of truth for "what git branch is each worktree on right now".
|
|
10
|
-
// Polls every known worktree's path and dispatches update-worktree-record
|
|
11
|
-
// when the live branch differs from the stored one. Components read
|
|
12
|
-
// `worktree.branch` from state — no per-component polling, no flickers.
|
|
13
|
-
//
|
|
14
|
-
// Runs once when enabled; reads sessions from the store on each tick so
|
|
15
|
-
// session updates do NOT re-create the effect (which would otherwise feedback
|
|
16
|
-
// off our own dispatches).
|
|
17
|
-
export function useWorktreeBranchPolling(enabled: boolean): void {
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
if (!enabled) return
|
|
20
|
-
|
|
21
|
-
let cancelled = false
|
|
22
|
-
let timer: ReturnType<typeof setTimeout> | null = null
|
|
23
|
-
|
|
24
|
-
const tick = async () => {
|
|
25
|
-
const sessions = appStore.getState().sessions
|
|
26
|
-
await Promise.all(
|
|
27
|
-
sessions.flatMap((session) =>
|
|
28
|
-
(session.worktrees ?? []).map(async (worktree) => {
|
|
29
|
-
if (worktree.path == null || worktree.path === '') return
|
|
30
|
-
const branch = await getCurrentBranch(worktree.path)
|
|
31
|
-
if (cancelled) return
|
|
32
|
-
if (branch != null && branch !== worktree.branch) {
|
|
33
|
-
dispatchGlobal({
|
|
34
|
-
patch: { branch },
|
|
35
|
-
sessionId: session.id,
|
|
36
|
-
type: 'update-worktree-record',
|
|
37
|
-
worktreeId: worktree.id,
|
|
38
|
-
})
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
if (cancelled) return
|
|
44
|
-
timer = setTimeout(() => void tick(), INTERVAL_MS)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
void tick()
|
|
48
|
-
|
|
49
|
-
return () => {
|
|
50
|
-
cancelled = true
|
|
51
|
-
if (timer != null) clearTimeout(timer)
|
|
52
|
-
}
|
|
53
|
-
}, [enabled])
|
|
54
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
2
|
-
|
|
3
|
-
import type { BranchDivergence } from '../state/types'
|
|
4
|
-
|
|
5
|
-
import { useAppStore } from '../state/app-store'
|
|
6
|
-
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
7
|
-
import { getBranchDivergence } from './divergence'
|
|
8
|
-
|
|
9
|
-
const INTERVAL_MS = 4000
|
|
10
|
-
|
|
11
|
-
// Polls per-worktree base divergence for the current session while enabled and
|
|
12
|
-
// dispatches it into worktreeDivergence. Only worktrees that record a baseRef
|
|
13
|
-
// (the aimux-created ones) are measured; the primary and externally-discovered
|
|
14
|
-
// worktrees have no recorded base and are left out.
|
|
15
|
-
export function useWorktreeDivergencePolling(enabled: boolean): void {
|
|
16
|
-
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
17
|
-
const sessions = useAppStore((s) => s.sessions)
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!enabled) return
|
|
21
|
-
const session =
|
|
22
|
-
currentSessionId != null && currentSessionId !== ''
|
|
23
|
-
? sessions.find((s) => s.id === currentSessionId)
|
|
24
|
-
: undefined
|
|
25
|
-
const targets = (session?.worktrees ?? []).filter(
|
|
26
|
-
(w) => w.baseRef != null && w.baseRef !== '' && w.branch != null && w.branch !== ''
|
|
27
|
-
)
|
|
28
|
-
if (targets.length === 0) return
|
|
29
|
-
|
|
30
|
-
let cancelled = false
|
|
31
|
-
let timer: ReturnType<typeof setTimeout> | null = null
|
|
32
|
-
|
|
33
|
-
const tick = async () => {
|
|
34
|
-
const entries = await Promise.all(
|
|
35
|
-
targets.map(async (worktree) => {
|
|
36
|
-
const base = worktree.baseRef
|
|
37
|
-
const branch = worktree.branch
|
|
38
|
-
if (base == null || branch == null) return null
|
|
39
|
-
const divergence = await getBranchDivergence(worktree.repoRoot, base, branch)
|
|
40
|
-
return divergence != null ? ([worktree.id, divergence] as const) : null
|
|
41
|
-
})
|
|
42
|
-
)
|
|
43
|
-
if (cancelled) return
|
|
44
|
-
const next: Record<string, BranchDivergence> = {}
|
|
45
|
-
for (const entry of entries) {
|
|
46
|
-
if (entry != null) next[entry[0]] = entry[1]
|
|
47
|
-
}
|
|
48
|
-
dispatchGlobal({ divergence: next, type: 'set-worktree-divergence' })
|
|
49
|
-
timer = setTimeout(() => void tick(), INTERVAL_MS)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
void tick()
|
|
53
|
-
|
|
54
|
-
return () => {
|
|
55
|
-
cancelled = true
|
|
56
|
-
if (timer != null) clearTimeout(timer)
|
|
57
|
-
}
|
|
58
|
-
}, [enabled, currentSessionId, sessions])
|
|
59
|
-
}
|