@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,262 +0,0 @@
|
|
|
1
|
-
import type { AppAction, AppState } from '../types'
|
|
2
|
-
|
|
3
|
-
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../ui/session-ordering'
|
|
4
|
-
import { filterSessions } from '../selectors'
|
|
5
|
-
import { restoreWorkspaceState } from '../session-persistence'
|
|
6
|
-
import { filterTabsForActiveWorktree, withActiveWorktree } from '../session-worktrees'
|
|
7
|
-
|
|
8
|
-
const CLOSED_MODAL = {
|
|
9
|
-
editBuffer: null,
|
|
10
|
-
selectedIndex: 0,
|
|
11
|
-
sessionTargetId: null,
|
|
12
|
-
type: null,
|
|
13
|
-
} as const
|
|
14
|
-
|
|
15
|
-
export function reduceSessionState(state: AppState, action: AppAction): AppState | null {
|
|
16
|
-
switch (action.type) {
|
|
17
|
-
case 'load-session': {
|
|
18
|
-
const snapshot =
|
|
19
|
-
action.workspaceSnapshot ??
|
|
20
|
-
state.sessions.find((entry) => entry.id === action.sessionId)?.workspaceSnapshot
|
|
21
|
-
// The session's activeWorktreeId may have been patched right before
|
|
22
|
-
// this load (e.g. the cross-workspace branch of handleCycleSidebarItem
|
|
23
|
-
// sets it to the worktree the user just clicked). The snapshot's
|
|
24
|
-
// activeTabId still reflects the *last* worktree they were on, so
|
|
25
|
-
// honor the patched worktree by filtering the restored tab list.
|
|
26
|
-
const loadedSession = state.sessions.find((entry) => entry.id === action.sessionId)
|
|
27
|
-
const loadedWorktrees = loadedSession?.worktrees ?? []
|
|
28
|
-
const restored = restoreWorkspaceState(state, snapshot, {
|
|
29
|
-
forceDisconnected: action.forceDisconnected ?? true,
|
|
30
|
-
// Drop tabs bound to worktrees this session no longer owns so a stale
|
|
31
|
-
// id can't be caught by a later worktree delete (closing "another
|
|
32
|
-
// worktree's" tabs) and so corrupted catalogs self-heal on load. Only
|
|
33
|
-
// prune once the session has worktrees — an empty list means it hasn't
|
|
34
|
-
// been initialized yet, and pruning then would wrongly drop bound tabs.
|
|
35
|
-
validWorktreeIds:
|
|
36
|
-
loadedWorktrees.length > 0 ? new Set(loadedWorktrees.map((w) => w.id)) : undefined,
|
|
37
|
-
})
|
|
38
|
-
const visible = filterTabsForActiveWorktree(restored.tabs, loadedSession)
|
|
39
|
-
// Prefer the snapshot's tab; if it isn't visible under the active
|
|
40
|
-
// worktree (e.g. a multi-worktree session restored onto a different
|
|
41
|
-
// worktree), fall back to the last tab we viewed in that worktree,
|
|
42
|
-
// then to the first visible tab.
|
|
43
|
-
const rememberedForWorktree =
|
|
44
|
-
loadedSession?.activeWorktreeId != null && loadedSession.activeWorktreeId !== ''
|
|
45
|
-
? state.lastActiveTabByWorktree[loadedSession.activeWorktreeId]
|
|
46
|
-
: undefined
|
|
47
|
-
const snapshotTabVisible =
|
|
48
|
-
restored.activeTabId != null &&
|
|
49
|
-
restored.activeTabId !== '' &&
|
|
50
|
-
visible.some((t) => t.id === restored.activeTabId)
|
|
51
|
-
const rememberedTabVisible =
|
|
52
|
-
rememberedForWorktree != null &&
|
|
53
|
-
rememberedForWorktree !== '' &&
|
|
54
|
-
visible.some((t) => t.id === rememberedForWorktree)
|
|
55
|
-
const fallbackTabId = rememberedTabVisible ? rememberedForWorktree : (visible[0]?.id ?? null)
|
|
56
|
-
const activeTabId = snapshotTabVisible ? restored.activeTabId : fallbackTabId
|
|
57
|
-
return {
|
|
58
|
-
...state,
|
|
59
|
-
...restored,
|
|
60
|
-
activeTabId,
|
|
61
|
-
currentSessionId: action.sessionId,
|
|
62
|
-
focusMode: 'navigation',
|
|
63
|
-
modal: CLOSED_MODAL,
|
|
64
|
-
sessions: state.sessions.map((entry) =>
|
|
65
|
-
entry.id === action.sessionId
|
|
66
|
-
? {
|
|
67
|
-
...entry,
|
|
68
|
-
lastOpenedAt: new Date().toISOString(),
|
|
69
|
-
updatedAt: new Date().toISOString(),
|
|
70
|
-
}
|
|
71
|
-
: entry
|
|
72
|
-
),
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
case 'set-sessions':
|
|
76
|
-
return { ...state, sessions: action.sessions }
|
|
77
|
-
case 'create-session-record':
|
|
78
|
-
return {
|
|
79
|
-
...state,
|
|
80
|
-
currentSessionId: action.session.id,
|
|
81
|
-
focusMode: 'navigation',
|
|
82
|
-
modal: CLOSED_MODAL,
|
|
83
|
-
sessions: [...state.sessions, action.session],
|
|
84
|
-
}
|
|
85
|
-
case 'rename-session-record':
|
|
86
|
-
return {
|
|
87
|
-
...state,
|
|
88
|
-
sessions: state.sessions.map((session) =>
|
|
89
|
-
session.id === action.sessionId
|
|
90
|
-
? { ...session, name: action.name, updatedAt: new Date().toISOString() }
|
|
91
|
-
: session
|
|
92
|
-
),
|
|
93
|
-
}
|
|
94
|
-
case 'delete-session-record': {
|
|
95
|
-
const deletingCurrent = action.sessionId === state.currentSessionId
|
|
96
|
-
const newSessions = state.sessions.filter((session) => session.id !== action.sessionId)
|
|
97
|
-
const nextStatuses = { ...state.sessionStatuses }
|
|
98
|
-
delete nextStatuses[action.sessionId]
|
|
99
|
-
if (action.openSessionPicker === true) {
|
|
100
|
-
const filteredNew = filterSessions(newSessions, state.modal.editBuffer)
|
|
101
|
-
const maxIndex = filteredNew.length
|
|
102
|
-
const clampedIndex = Math.min(state.modal.selectedIndex, maxIndex)
|
|
103
|
-
return {
|
|
104
|
-
...state,
|
|
105
|
-
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
106
|
-
currentSessionId: deletingCurrent ? null : state.currentSessionId,
|
|
107
|
-
focusMode: 'modal',
|
|
108
|
-
modal: {
|
|
109
|
-
editBuffer: null,
|
|
110
|
-
selectedIndex: clampedIndex,
|
|
111
|
-
sessionTargetId: null,
|
|
112
|
-
type: 'session-picker',
|
|
113
|
-
},
|
|
114
|
-
sessions: newSessions,
|
|
115
|
-
sessionStatuses: nextStatuses,
|
|
116
|
-
tabs: deletingCurrent ? [] : state.tabs,
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
...state,
|
|
121
|
-
activeTabId: deletingCurrent ? null : state.activeTabId,
|
|
122
|
-
currentSessionId: deletingCurrent ? null : state.currentSessionId,
|
|
123
|
-
focusMode: deletingCurrent ? 'navigation' : state.focusMode,
|
|
124
|
-
modal: deletingCurrent ? CLOSED_MODAL : state.modal,
|
|
125
|
-
sessions: newSessions,
|
|
126
|
-
sessionStatuses: nextStatuses,
|
|
127
|
-
tabs: deletingCurrent ? [] : state.tabs,
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
case 'reorder-sessions': {
|
|
131
|
-
const byId = new Map(state.sessions.map((s) => [s.id, s]))
|
|
132
|
-
const ordered: typeof state.sessions = []
|
|
133
|
-
let idx = 0
|
|
134
|
-
for (const id of action.orderedIds) {
|
|
135
|
-
const s = byId.get(id)
|
|
136
|
-
if (s) {
|
|
137
|
-
ordered.push({ ...s, order: idx })
|
|
138
|
-
byId.delete(id)
|
|
139
|
-
}
|
|
140
|
-
idx++
|
|
141
|
-
}
|
|
142
|
-
let nextOrder = ordered.length
|
|
143
|
-
for (const s of byId.values()) {
|
|
144
|
-
ordered.push({ ...s, order: nextOrder++ })
|
|
145
|
-
}
|
|
146
|
-
return { ...state, sessions: ordered }
|
|
147
|
-
}
|
|
148
|
-
case 'reorder-active-session': {
|
|
149
|
-
const currentId = state.currentSessionId
|
|
150
|
-
if (currentId == null || currentId === '') return state
|
|
151
|
-
const ids = orderSessionsForDisplay(state.sessions).map((s) => s.id)
|
|
152
|
-
const from = ids.indexOf(currentId)
|
|
153
|
-
if (from < 0) return state
|
|
154
|
-
const to = from + action.delta
|
|
155
|
-
const targetId = ids[to]
|
|
156
|
-
if (targetId == null) return state
|
|
157
|
-
const nextIds = moveIdToIdPosition(ids, currentId, targetId)
|
|
158
|
-
const byId = new Map(state.sessions.map((s) => [s.id, s]))
|
|
159
|
-
const nextSessions = nextIds.map((id, idx) => {
|
|
160
|
-
const s = byId.get(id)
|
|
161
|
-
return s ? { ...s, order: idx } : s
|
|
162
|
-
})
|
|
163
|
-
const filtered = nextSessions.filter((s): s is NonNullable<typeof s> => s != null)
|
|
164
|
-
if (filtered.length !== state.sessions.length) return state
|
|
165
|
-
return { ...state, sessions: filtered }
|
|
166
|
-
}
|
|
167
|
-
case 'set-session-status': {
|
|
168
|
-
const prev = state.sessionStatuses[action.sessionId]
|
|
169
|
-
if (
|
|
170
|
-
prev !== undefined &&
|
|
171
|
-
prev.working === action.status.working &&
|
|
172
|
-
prev.waiting === action.status.waiting
|
|
173
|
-
) {
|
|
174
|
-
return state
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
...state,
|
|
178
|
-
sessionStatuses: { ...state.sessionStatuses, [action.sessionId]: action.status },
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
case 'add-worktree-record':
|
|
182
|
-
return {
|
|
183
|
-
...state,
|
|
184
|
-
sessions: state.sessions.map((session) => {
|
|
185
|
-
if (session.id !== action.sessionId) return session
|
|
186
|
-
const next = {
|
|
187
|
-
...session,
|
|
188
|
-
activeWorktreeId:
|
|
189
|
-
action.activate === true ? action.worktree.id : session.activeWorktreeId,
|
|
190
|
-
projectPath: action.activate === true ? action.worktree.path : session.projectPath,
|
|
191
|
-
updatedAt: new Date().toISOString(),
|
|
192
|
-
worktrees: [...(session.worktrees ?? []), action.worktree],
|
|
193
|
-
}
|
|
194
|
-
return next.activeWorktreeId != null && next.activeWorktreeId !== ''
|
|
195
|
-
? next
|
|
196
|
-
: withActiveWorktree(next, action.worktree.id)
|
|
197
|
-
}),
|
|
198
|
-
}
|
|
199
|
-
case 'set-active-worktree': {
|
|
200
|
-
const sessions = state.sessions.map((session) =>
|
|
201
|
-
session.id === action.sessionId ? withActiveWorktree(session, action.worktreeId) : session
|
|
202
|
-
)
|
|
203
|
-
// Remember the tab we're leaving so coming back to this worktree later
|
|
204
|
-
// restores it instead of snapping to the first tab. Keyed by the
|
|
205
|
-
// worktree we're departing (the current session's active one).
|
|
206
|
-
const leavingSession = state.sessions.find((s) => s.id === action.sessionId)
|
|
207
|
-
const leavingWorktreeId = leavingSession?.activeWorktreeId
|
|
208
|
-
const lastActiveTabByWorktree =
|
|
209
|
-
action.sessionId === state.currentSessionId &&
|
|
210
|
-
leavingWorktreeId != null &&
|
|
211
|
-
leavingWorktreeId !== '' &&
|
|
212
|
-
state.activeTabId != null &&
|
|
213
|
-
state.activeTabId !== ''
|
|
214
|
-
? { ...state.lastActiveTabByWorktree, [leavingWorktreeId]: state.activeTabId }
|
|
215
|
-
: state.lastActiveTabByWorktree
|
|
216
|
-
// Changing the worktree of a non-current session has no effect on
|
|
217
|
-
// which tab the user is looking at — leave activeTabId alone.
|
|
218
|
-
if (action.sessionId !== state.currentSessionId) {
|
|
219
|
-
return { ...state, lastActiveTabByWorktree, sessions }
|
|
220
|
-
}
|
|
221
|
-
// For the current session: if the existing activeTabId belongs to
|
|
222
|
-
// the new worktree, keep it. Otherwise restore the last tab we viewed
|
|
223
|
-
// in that worktree, falling back to the first visible tab (or clearing
|
|
224
|
-
// it if the worktree is empty — the pane then renders the "this
|
|
225
|
-
// worktree has no tabs" placeholder).
|
|
226
|
-
const updated = sessions.find((s) => s.id === action.sessionId)
|
|
227
|
-
const visible = filterTabsForActiveWorktree(state.tabs, updated)
|
|
228
|
-
const currentStillVisible =
|
|
229
|
-
state.activeTabId != null &&
|
|
230
|
-
state.activeTabId !== '' &&
|
|
231
|
-
visible.some((t) => t.id === state.activeTabId)
|
|
232
|
-
if (currentStillVisible) {
|
|
233
|
-
return { ...state, lastActiveTabByWorktree, sessions }
|
|
234
|
-
}
|
|
235
|
-
const remembered = lastActiveTabByWorktree[action.worktreeId]
|
|
236
|
-
const nextActiveTabId =
|
|
237
|
-
remembered != null && remembered !== '' && visible.some((t) => t.id === remembered)
|
|
238
|
-
? remembered
|
|
239
|
-
: (visible[0]?.id ?? null)
|
|
240
|
-
return { ...state, activeTabId: nextActiveTabId, lastActiveTabByWorktree, sessions }
|
|
241
|
-
}
|
|
242
|
-
case 'update-worktree-record':
|
|
243
|
-
return {
|
|
244
|
-
...state,
|
|
245
|
-
sessions: state.sessions.map((session) =>
|
|
246
|
-
session.id === action.sessionId
|
|
247
|
-
? {
|
|
248
|
-
...session,
|
|
249
|
-
updatedAt: new Date().toISOString(),
|
|
250
|
-
worktrees: session.worktrees?.map((worktree) =>
|
|
251
|
-
worktree.id === action.worktreeId
|
|
252
|
-
? { ...worktree, ...action.patch, updatedAt: new Date().toISOString() }
|
|
253
|
-
: worktree
|
|
254
|
-
),
|
|
255
|
-
}
|
|
256
|
-
: session
|
|
257
|
-
),
|
|
258
|
-
}
|
|
259
|
-
default:
|
|
260
|
-
return null
|
|
261
|
-
}
|
|
262
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
|
|
4
|
-
import type { SessionRecord } from './types'
|
|
5
|
-
|
|
6
|
-
import { loadConfig, saveConfig } from '../config'
|
|
7
|
-
import { logDebug } from '../debug/input-log'
|
|
8
|
-
import { getProfileConfigDir } from '../profile-paths'
|
|
9
|
-
import { ensureSessionWorktrees } from './session-worktrees'
|
|
10
|
-
import { toast } from './toast-store'
|
|
11
|
-
import { isSessionRecord } from './validation'
|
|
12
|
-
|
|
13
|
-
interface SessionCatalogFile {
|
|
14
|
-
version: 1
|
|
15
|
-
sessions: SessionRecord[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const SESSIONS_PATH = join(getProfileConfigDir(), 'aimux-sessions.json')
|
|
19
|
-
|
|
20
|
-
function readCatalogFile(): { file: SessionCatalogFile | null; issue?: string } {
|
|
21
|
-
try {
|
|
22
|
-
if (!existsSync(SESSIONS_PATH)) {
|
|
23
|
-
return { file: null }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const parsed = JSON.parse(readFileSync(SESSIONS_PATH, 'utf8')) as {
|
|
27
|
-
version?: unknown
|
|
28
|
-
sessions?: unknown
|
|
29
|
-
}
|
|
30
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.sessions)) {
|
|
31
|
-
return { file: null, issue: 'invalid session catalog header' }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (!parsed.sessions.every(isSessionRecord)) {
|
|
35
|
-
return { file: null, issue: 'invalid session catalog entries' }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return { file: { sessions: parsed.sessions, version: 1 } }
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return {
|
|
41
|
-
file: null,
|
|
42
|
-
issue: `failed to load session catalog: ${error instanceof Error ? error.message : String(error)}`,
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function loadSessionCatalog(): SessionRecord[] {
|
|
48
|
-
const { file, issue } = readCatalogFile()
|
|
49
|
-
if (file) {
|
|
50
|
-
logDebug('sessions.catalog.load', { sessionCount: file.sessions.length })
|
|
51
|
-
const normalized = normalizeSessions(file.sessions)
|
|
52
|
-
if (JSON.stringify(normalized) !== JSON.stringify(file.sessions)) {
|
|
53
|
-
saveSessionCatalog(normalized)
|
|
54
|
-
}
|
|
55
|
-
return normalized
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (issue != null && issue !== '') {
|
|
59
|
-
logDebug('sessions.catalog.loadIssue', { issue, path: SESSIONS_PATH })
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const config = loadConfig()
|
|
63
|
-
if (!config.workspaceSnapshot) {
|
|
64
|
-
logDebug('sessions.catalog.load', { sessionCount: 0 })
|
|
65
|
-
return []
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const now = new Date().toISOString()
|
|
69
|
-
const migrated: SessionRecord = {
|
|
70
|
-
createdAt: now,
|
|
71
|
-
id: `session-${Date.now()}`,
|
|
72
|
-
lastOpenedAt: now,
|
|
73
|
-
name: 'Last workspace',
|
|
74
|
-
updatedAt: now,
|
|
75
|
-
workspaceSnapshot: config.workspaceSnapshot,
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const normalized = normalizeSessions([migrated])
|
|
79
|
-
saveSessionCatalog(normalized)
|
|
80
|
-
saveConfig({ ...config, workspaceSnapshot: undefined })
|
|
81
|
-
logDebug('sessions.catalog.migrateLegacyWorkspace', {
|
|
82
|
-
migratedSessionId: migrated.id,
|
|
83
|
-
tabCount: migrated.workspaceSnapshot?.tabs.length ?? 0,
|
|
84
|
-
})
|
|
85
|
-
return normalized
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function saveSessionCatalog(sessions: SessionRecord[]): void {
|
|
89
|
-
try {
|
|
90
|
-
mkdirSync(getProfileConfigDir(), { recursive: true })
|
|
91
|
-
writeFileSync(SESSIONS_PATH, `${JSON.stringify({ sessions, version: 1 }, null, 2)}\n`)
|
|
92
|
-
logDebug('sessions.catalog.save', { sessionCount: sessions.length })
|
|
93
|
-
} catch (error) {
|
|
94
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
95
|
-
logDebug('sessions.catalog.saveError', {
|
|
96
|
-
error: message,
|
|
97
|
-
path: SESSIONS_PATH,
|
|
98
|
-
sessionCount: sessions.length,
|
|
99
|
-
})
|
|
100
|
-
// Persisting the catalog underpins workspace/worktree state — a silent
|
|
101
|
-
// failure means the user loses sessions on restart. Surface it.
|
|
102
|
-
toast.error(`Failed to save sessions: ${message}`)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function getSessionCatalogPath(): string {
|
|
107
|
-
return SESSIONS_PATH
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function findMostRecentSession(sessions: SessionRecord[]): SessionRecord | undefined {
|
|
111
|
-
let best: SessionRecord | undefined
|
|
112
|
-
for (const candidate of sessions) {
|
|
113
|
-
if (!best || candidate.lastOpenedAt.localeCompare(best.lastOpenedAt) > 0) {
|
|
114
|
-
best = candidate
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return best
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Assign a stable `order` to every session. Records with an existing numeric
|
|
122
|
-
* `order` keep their slot (sorted ascending); the rest are appended by
|
|
123
|
-
* createdAt ascending so older sessions come first.
|
|
124
|
-
*/
|
|
125
|
-
function normalizeOrder(sessions: SessionRecord[]): SessionRecord[] {
|
|
126
|
-
const withOrder: SessionRecord[] = []
|
|
127
|
-
const withoutOrder: SessionRecord[] = []
|
|
128
|
-
for (const s of sessions) {
|
|
129
|
-
if (typeof s.order === 'number' && Number.isFinite(s.order)) {
|
|
130
|
-
withOrder.push(s)
|
|
131
|
-
} else {
|
|
132
|
-
withoutOrder.push(s)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
withOrder.sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
|
|
136
|
-
withoutOrder.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
|
|
137
|
-
const merged = [...withOrder, ...withoutOrder]
|
|
138
|
-
return merged.map((s, i) => (s.order === i ? s : { ...s, order: i }))
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function normalizeSessions(sessions: SessionRecord[]): SessionRecord[] {
|
|
142
|
-
return normalizeOrder(sessions).map((session) => ensureSessionWorktrees(session))
|
|
143
|
-
}
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { createHash } from 'node:crypto'
|
|
3
|
-
import { existsSync } from 'node:fs'
|
|
4
|
-
import { basename } from 'node:path'
|
|
5
|
-
|
|
6
|
-
import type { BranchDivergence, SessionRecord, TabSession, WorktreeRecord } from './types'
|
|
7
|
-
|
|
8
|
-
import { createPrefixedId } from '../platform/id'
|
|
9
|
-
import { isInsideAimuxWorktreeRoot } from '../platform/worktree-paths'
|
|
10
|
-
|
|
11
|
-
export function formatDivergence(divergence: BranchDivergence | undefined): string {
|
|
12
|
-
if (divergence == null) return ''
|
|
13
|
-
const parts: string[] = []
|
|
14
|
-
if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
|
|
15
|
-
if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
|
|
16
|
-
return parts.join(' ')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createPrimaryWorktree(projectPath: string, now: string): WorktreeRecord {
|
|
20
|
-
const id = createPrefixedId('worktree')
|
|
21
|
-
return {
|
|
22
|
-
createdAt: now,
|
|
23
|
-
createdByAimux: false,
|
|
24
|
-
id,
|
|
25
|
-
name: basename(projectPath) || projectPath,
|
|
26
|
-
path: projectPath,
|
|
27
|
-
repoRoot: projectPath,
|
|
28
|
-
source: 'primary',
|
|
29
|
-
updatedAt: now,
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function ensureSessionWorktrees(
|
|
34
|
-
session: SessionRecord,
|
|
35
|
-
now = new Date().toISOString()
|
|
36
|
-
): SessionRecord {
|
|
37
|
-
if (session.worktrees?.length != null && session.worktrees?.length !== 0) {
|
|
38
|
-
const worktrees = mergeExistingGitWorktrees(
|
|
39
|
-
pruneMissingAimuxTempWorktrees(session.worktrees),
|
|
40
|
-
now
|
|
41
|
-
)
|
|
42
|
-
if (worktrees.length === 0 && session.projectPath != null && session.projectPath !== '') {
|
|
43
|
-
const worktree = createPrimaryWorktree(session.projectPath, now)
|
|
44
|
-
return { ...session, activeWorktreeId: worktree.id, worktrees: [worktree] }
|
|
45
|
-
}
|
|
46
|
-
const activeWorktreeId = worktrees.some((w) => w.id === session.activeWorktreeId)
|
|
47
|
-
? session.activeWorktreeId
|
|
48
|
-
: worktrees[0]?.id
|
|
49
|
-
const active = worktrees.find((w) => w.id === activeWorktreeId)
|
|
50
|
-
return {
|
|
51
|
-
...session,
|
|
52
|
-
activeWorktreeId,
|
|
53
|
-
projectPath: active?.path ?? session.projectPath,
|
|
54
|
-
worktrees,
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!(session.projectPath != null && session.projectPath !== '')) {
|
|
59
|
-
return session
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const worktree = createPrimaryWorktree(session.projectPath, now)
|
|
63
|
-
const worktrees = mergeExistingGitWorktrees([worktree], now)
|
|
64
|
-
return {
|
|
65
|
-
...session,
|
|
66
|
-
activeWorktreeId:
|
|
67
|
-
worktrees.find((entry) => entry.path === session.projectPath)?.id ?? worktree.id,
|
|
68
|
-
worktrees,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function stableWorktreeId(path: string): string {
|
|
73
|
-
return `worktree-${createHash('sha1').update(path).digest('hex').slice(0, 10)}`
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function mergeExistingGitWorktrees(worktrees: WorktreeRecord[], now: string): WorktreeRecord[] {
|
|
77
|
-
const anchor = worktrees.find((worktree) => existsSync(worktree.path)) ?? worktrees[0]
|
|
78
|
-
if (!anchor) return worktrees
|
|
79
|
-
const discovered = listGitWorktreesSync(anchor.path)
|
|
80
|
-
if (discovered.length === 0) return worktrees
|
|
81
|
-
|
|
82
|
-
// Git keeps admin entries for worktrees whose directory is gone (e.g. a temp
|
|
83
|
-
// dir cleared on reboot). Drop that stale state so the entries stop
|
|
84
|
-
// resurfacing in `git worktree list`, and ignore them while reconciling.
|
|
85
|
-
if (discovered.some((entry) => entry.prunable === true)) {
|
|
86
|
-
pruneGitWorktreesSync(anchor.path)
|
|
87
|
-
}
|
|
88
|
-
const live = discovered.filter((entry) => entry.prunable !== true)
|
|
89
|
-
|
|
90
|
-
const byPath = new Map(worktrees.map((worktree) => [worktree.path, worktree]))
|
|
91
|
-
// `git worktree list` reports the main worktree first; prefer the primary
|
|
92
|
-
// record's repoRoot when it still exists, then git's main worktree.
|
|
93
|
-
const primaryRepoRoot = worktrees.find((worktree) => worktree.source === 'primary')?.repoRoot
|
|
94
|
-
const repoRoot =
|
|
95
|
-
primaryRepoRoot !== undefined && existsSync(primaryRepoRoot)
|
|
96
|
-
? primaryRepoRoot
|
|
97
|
-
: (live[0]?.path ?? anchor.repoRoot)
|
|
98
|
-
for (const entry of live) {
|
|
99
|
-
const existing = byPath.get(entry.path)
|
|
100
|
-
if (existing) {
|
|
101
|
-
// Patch branch / commitSha from git for entries we already track —
|
|
102
|
-
// notably the primary worktree, whose `branch` is never set at creation
|
|
103
|
-
// time (createPrimaryWorktree doesn't run `git`). Without this, the UI
|
|
104
|
-
// and the divergence poller have no branch for primary worktrees.
|
|
105
|
-
const patchedBranch = entry.branch ?? existing.branch
|
|
106
|
-
const patchedSha = entry.head ?? existing.commitSha
|
|
107
|
-
if (patchedBranch !== existing.branch || patchedSha !== existing.commitSha) {
|
|
108
|
-
byPath.set(entry.path, {
|
|
109
|
-
...existing,
|
|
110
|
-
branch: patchedBranch,
|
|
111
|
-
commitSha: patchedSha,
|
|
112
|
-
updatedAt: now,
|
|
113
|
-
})
|
|
114
|
-
}
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
if (isInsideAimuxWorktreeRoot(entry.path)) continue
|
|
118
|
-
const worktree: WorktreeRecord = {
|
|
119
|
-
branch: entry.branch,
|
|
120
|
-
commitSha: entry.head,
|
|
121
|
-
createdAt: now,
|
|
122
|
-
createdByAimux: false,
|
|
123
|
-
id: stableWorktreeId(entry.path),
|
|
124
|
-
name: basename(entry.path) || entry.path,
|
|
125
|
-
path: entry.path,
|
|
126
|
-
repoRoot,
|
|
127
|
-
source: 'external',
|
|
128
|
-
updatedAt: now,
|
|
129
|
-
}
|
|
130
|
-
byPath.set(entry.path, worktree)
|
|
131
|
-
}
|
|
132
|
-
return (
|
|
133
|
-
[...byPath.values()]
|
|
134
|
-
// Drop records whose directory no longer exists (a deleted external
|
|
135
|
-
// worktree that git already pruned) so they don't reappear on restart.
|
|
136
|
-
.filter((worktree) => worktree.source === 'primary' || existsSync(worktree.path))
|
|
137
|
-
// Heal records whose repoRoot points at a since-deleted sibling worktree
|
|
138
|
-
// so later git operations (e.g. `git -C repoRoot`) don't fail.
|
|
139
|
-
.map((worktree) =>
|
|
140
|
-
existsSync(worktree.repoRoot) ? worktree : { ...worktree, repoRoot, updatedAt: now }
|
|
141
|
-
)
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
interface DiscoveredWorktree {
|
|
146
|
-
path: string
|
|
147
|
-
head?: string
|
|
148
|
-
branch?: string
|
|
149
|
-
prunable?: boolean
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function listGitWorktreesSync(cwd: string): DiscoveredWorktree[] {
|
|
153
|
-
const result = spawnSync('git', ['-C', cwd, 'worktree', 'list', '--porcelain'], {
|
|
154
|
-
encoding: 'utf8',
|
|
155
|
-
})
|
|
156
|
-
if (result.status !== 0 || !result.stdout) return []
|
|
157
|
-
const worktrees: DiscoveredWorktree[] = []
|
|
158
|
-
let current: DiscoveredWorktree | null = null
|
|
159
|
-
for (const line of result.stdout.split('\n')) {
|
|
160
|
-
if (line.startsWith('worktree ')) {
|
|
161
|
-
if (current) worktrees.push(current)
|
|
162
|
-
current = { path: line.slice('worktree '.length) }
|
|
163
|
-
} else if (current && line.startsWith('HEAD ')) {
|
|
164
|
-
current.head = line.slice('HEAD '.length)
|
|
165
|
-
} else if (current && line.startsWith('branch ')) {
|
|
166
|
-
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '')
|
|
167
|
-
} else if (current && line.startsWith('prunable')) {
|
|
168
|
-
current.prunable = true
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (current) worktrees.push(current)
|
|
172
|
-
return worktrees
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function pruneGitWorktreesSync(cwd: string): void {
|
|
176
|
-
spawnSync('git', ['-C', cwd, 'worktree', 'prune'], { encoding: 'utf8' })
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function pruneMissingAimuxTempWorktrees(worktrees: WorktreeRecord[]): WorktreeRecord[] {
|
|
180
|
-
return worktrees.filter((worktree) => {
|
|
181
|
-
if (
|
|
182
|
-
worktree.source !== 'aimux-temp' ||
|
|
183
|
-
!worktree.createdByAimux ||
|
|
184
|
-
!isInsideAimuxWorktreeRoot(worktree.path)
|
|
185
|
-
) {
|
|
186
|
-
return true
|
|
187
|
-
}
|
|
188
|
-
return existsSync(worktree.path)
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export function getActiveWorktree(session: SessionRecord | undefined): WorktreeRecord | undefined {
|
|
193
|
-
if (!(session?.worktrees?.length != null && session?.worktrees?.length !== 0)) return undefined
|
|
194
|
-
return (
|
|
195
|
-
session.worktrees.find((worktree) => worktree.id === session.activeWorktreeId) ??
|
|
196
|
-
session.worktrees[0]
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function getSessionProjectPath(session: SessionRecord | undefined): string | undefined {
|
|
201
|
-
return getActiveWorktree(session)?.path ?? session?.projectPath
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function withActiveWorktree(session: SessionRecord, worktreeId: string): SessionRecord {
|
|
205
|
-
const worktree = session.worktrees?.find((entry) => entry.id === worktreeId)
|
|
206
|
-
if (!worktree) return session
|
|
207
|
-
return {
|
|
208
|
-
...session,
|
|
209
|
-
activeWorktreeId: worktreeId,
|
|
210
|
-
projectPath: worktree.path,
|
|
211
|
-
updatedAt: new Date().toISOString(),
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function getRenderedTabWorktreeId(
|
|
216
|
-
tab: { worktreeId?: string },
|
|
217
|
-
worktrees: { id: string }[]
|
|
218
|
-
): string {
|
|
219
|
-
return tab.worktreeId ?? worktrees[0]?.id ?? '__main__'
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function filterTabsForActiveWorktree(
|
|
223
|
-
tabs: TabSession[],
|
|
224
|
-
session: SessionRecord | undefined
|
|
225
|
-
): TabSession[] {
|
|
226
|
-
if (!session) return tabs
|
|
227
|
-
const activeWorktreeId = session.activeWorktreeId
|
|
228
|
-
if (activeWorktreeId == null || activeWorktreeId === '') return tabs
|
|
229
|
-
const worktrees = session.worktrees ?? []
|
|
230
|
-
const primaryId = worktrees[0]?.id
|
|
231
|
-
const activeIsPrimary = primaryId != null && primaryId === activeWorktreeId
|
|
232
|
-
return tabs.filter((tab) => {
|
|
233
|
-
const owned = tab.worktreeId != null && tab.worktreeId !== ''
|
|
234
|
-
if (owned) return tab.worktreeId === activeWorktreeId
|
|
235
|
-
// Unbound (legacy) tabs surface only under the primary worktree.
|
|
236
|
-
return activeIsPrimary
|
|
237
|
-
})
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function orderTabsByWorktree(
|
|
241
|
-
tabs: TabSession[],
|
|
242
|
-
session: SessionRecord | undefined
|
|
243
|
-
): TabSession[] {
|
|
244
|
-
const worktrees = session?.worktrees ?? []
|
|
245
|
-
const order = new Map<string, number>()
|
|
246
|
-
for (const [index, worktree] of worktrees.entries()) {
|
|
247
|
-
order.set(worktree.id, index)
|
|
248
|
-
}
|
|
249
|
-
order.set('__main__', order.size)
|
|
250
|
-
|
|
251
|
-
return tabs
|
|
252
|
-
.map((tab, index) => ({ index, tab }))
|
|
253
|
-
.sort((a, b) => {
|
|
254
|
-
const aw = order.get(getRenderedTabWorktreeId(a.tab, worktrees)) ?? Number.MAX_SAFE_INTEGER
|
|
255
|
-
const bw = order.get(getRenderedTabWorktreeId(b.tab, worktrees)) ?? Number.MAX_SAFE_INTEGER
|
|
256
|
-
if (aw !== bw) return aw - bw
|
|
257
|
-
return a.index - b.index
|
|
258
|
-
})
|
|
259
|
-
.map((entry) => entry.tab)
|
|
260
|
-
}
|