@brimveyn/aimux 1.12.6 → 1.13.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 +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +19 -3
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getAdjacentLeaf,
|
|
6
6
|
getGroupIdForTab,
|
|
7
7
|
getTreeForTab,
|
|
8
|
+
type LayoutNode,
|
|
8
9
|
pruneLayoutTree,
|
|
9
10
|
removeNode,
|
|
10
11
|
resizeSplit,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
splitNode,
|
|
13
14
|
} from '../layout-tree'
|
|
14
15
|
import { normalizeGroupedTabOrder } from '../session-persistence'
|
|
16
|
+
import { orderTabsByWorktree, withActiveWorktree } from '../session-worktrees'
|
|
15
17
|
import { createDefaultTerminalModes } from '../terminal-modes'
|
|
16
18
|
import {
|
|
17
19
|
type AppAction,
|
|
@@ -118,12 +120,13 @@ function moveLayoutGroup(
|
|
|
118
120
|
|
|
119
121
|
if (delta > 0 && groupEnd < tabs.length - 1) {
|
|
120
122
|
const adjacentId = getTabIdAtIndex(tabs, groupEnd + 1)
|
|
121
|
-
if (!adjacentId) {
|
|
123
|
+
if (!(adjacentId != null && adjacentId !== '')) {
|
|
122
124
|
return null
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
const adjacentGroupId = getGroupIdForTab(state.tabGroupMap, adjacentId)
|
|
126
|
-
const adjacentTree =
|
|
128
|
+
const adjacentTree =
|
|
129
|
+
adjacentGroupId != null && adjacentGroupId !== '' ? state.layoutTrees[adjacentGroupId] : null
|
|
127
130
|
if (adjacentTree && adjacentTree.type === 'split') {
|
|
128
131
|
const adjacentIds = new Set(allLeafIds(adjacentTree))
|
|
129
132
|
let otherEnd = groupEnd + 1
|
|
@@ -149,12 +152,13 @@ function moveLayoutGroup(
|
|
|
149
152
|
|
|
150
153
|
if (delta < 0 && groupStart > 0) {
|
|
151
154
|
const adjacentId = getTabIdAtIndex(tabs, groupStart - 1)
|
|
152
|
-
if (!adjacentId) {
|
|
155
|
+
if (!(adjacentId != null && adjacentId !== '')) {
|
|
153
156
|
return null
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
const adjacentGroupId = getGroupIdForTab(state.tabGroupMap, adjacentId)
|
|
157
|
-
const adjacentTree =
|
|
160
|
+
const adjacentTree =
|
|
161
|
+
adjacentGroupId != null && adjacentGroupId !== '' ? state.layoutTrees[adjacentGroupId] : null
|
|
158
162
|
if (adjacentTree && adjacentTree.type === 'split') {
|
|
159
163
|
const adjacentIds = new Set(allLeafIds(adjacentTree))
|
|
160
164
|
let otherStart = groupStart - 1
|
|
@@ -187,13 +191,37 @@ function updateTab(
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
function getActiveIndex(state: AppState): number {
|
|
190
|
-
if (!state.activeTabId) {
|
|
194
|
+
if (!(state.activeTabId != null && state.activeTabId !== '')) {
|
|
191
195
|
return -1
|
|
192
196
|
}
|
|
193
197
|
|
|
194
198
|
return state.tabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
195
199
|
}
|
|
196
200
|
|
|
201
|
+
function getCurrentSession(state: AppState) {
|
|
202
|
+
return state.currentSessionId != null && state.currentSessionId !== ''
|
|
203
|
+
? state.sessions.find((session) => session.id === state.currentSessionId)
|
|
204
|
+
: undefined
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function withActiveTabWorktree(state: AppState, tabId: string | null): AppState {
|
|
208
|
+
if (
|
|
209
|
+
tabId == null ||
|
|
210
|
+
tabId === '' ||
|
|
211
|
+
!(state.currentSessionId != null && state.currentSessionId !== '')
|
|
212
|
+
)
|
|
213
|
+
return state
|
|
214
|
+
const tab = state.tabs.find((entry) => entry.id === tabId)
|
|
215
|
+
if (!(tab?.worktreeId != null && tab?.worktreeId !== '')) return state
|
|
216
|
+
const worktreeId = tab.worktreeId
|
|
217
|
+
return {
|
|
218
|
+
...state,
|
|
219
|
+
sessions: state.sessions.map((session) =>
|
|
220
|
+
session.id === state.currentSessionId ? withActiveWorktree(session, worktreeId) : session
|
|
221
|
+
),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
197
225
|
function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
198
226
|
if (indexToClose < 0 || indexToClose >= state.tabs.length) {
|
|
199
227
|
return state
|
|
@@ -203,8 +231,11 @@ function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
|
203
231
|
const tabs = state.tabs.filter((_, index) => index !== indexToClose)
|
|
204
232
|
|
|
205
233
|
// Find the tree for the closing tab's group
|
|
206
|
-
const groupId =
|
|
207
|
-
|
|
234
|
+
const groupId =
|
|
235
|
+
closingTabId != null && closingTabId !== ''
|
|
236
|
+
? getGroupIdForTab(state.tabGroupMap, closingTabId)
|
|
237
|
+
: null
|
|
238
|
+
const groupTree = groupId != null && groupId !== '' ? (state.layoutTrees[groupId] ?? null) : null
|
|
208
239
|
|
|
209
240
|
let nextActiveTabId: string | null
|
|
210
241
|
if (state.activeTabId === closingTabId) {
|
|
@@ -223,7 +254,13 @@ function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
|
223
254
|
// Update the group's tree and clean up if needed
|
|
224
255
|
let newLayoutTrees = state.layoutTrees
|
|
225
256
|
let newTabGroupMap = state.tabGroupMap
|
|
226
|
-
if (
|
|
257
|
+
if (
|
|
258
|
+
closingTabId != null &&
|
|
259
|
+
closingTabId !== '' &&
|
|
260
|
+
groupId != null &&
|
|
261
|
+
groupId !== '' &&
|
|
262
|
+
groupTree
|
|
263
|
+
) {
|
|
227
264
|
const newTree = removeNode(groupTree, closingTabId)
|
|
228
265
|
newLayoutTrees = { ...state.layoutTrees }
|
|
229
266
|
newTabGroupMap = { ...state.tabGroupMap }
|
|
@@ -239,38 +276,46 @@ function closeTabAtIndex(state: AppState, indexToClose: number): AppState {
|
|
|
239
276
|
}
|
|
240
277
|
}
|
|
241
278
|
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
return withActiveTabWorktree(
|
|
280
|
+
{
|
|
281
|
+
...state,
|
|
282
|
+
activeTabId: nextActiveTabId,
|
|
283
|
+
focusMode: tabs.length === 0 ? 'navigation' : state.focusMode,
|
|
284
|
+
layoutTrees: newLayoutTrees,
|
|
285
|
+
tabGroupMap: newTabGroupMap,
|
|
286
|
+
tabs,
|
|
287
|
+
},
|
|
288
|
+
nextActiveTabId
|
|
289
|
+
)
|
|
250
290
|
}
|
|
251
291
|
|
|
252
292
|
export function reduceTabState(state: AppState, action: AppAction): AppState | null {
|
|
253
293
|
switch (action.type) {
|
|
254
294
|
case 'add-tab': {
|
|
255
295
|
const newTab = { ...action.tab, activity: action.tab.activity ?? 'idle' }
|
|
256
|
-
return
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
296
|
+
return withActiveTabWorktree(
|
|
297
|
+
{
|
|
298
|
+
...state,
|
|
299
|
+
activeTabId: newTab.id,
|
|
300
|
+
focusMode: 'navigation',
|
|
301
|
+
modal: { editBuffer: null, selectedIndex: 0, sessionTargetId: null, type: null },
|
|
302
|
+
tabs: [...state.tabs, newTab],
|
|
303
|
+
},
|
|
304
|
+
newTab.id
|
|
305
|
+
)
|
|
263
306
|
}
|
|
264
307
|
case 'hydrate-workspace': {
|
|
265
308
|
const hydratedActiveTabId =
|
|
266
|
-
action.activeTabId
|
|
309
|
+
action.activeTabId != null &&
|
|
310
|
+
action.activeTabId !== '' &&
|
|
311
|
+
action.tabs.some((tab) => tab.id === action.activeTabId)
|
|
267
312
|
? action.activeTabId
|
|
268
313
|
: (action.tabs[0]?.id ?? null)
|
|
269
314
|
const tabIds = new Set(action.tabs.map((t) => t.id))
|
|
270
315
|
|
|
271
316
|
// Restore from new multi-tree format or migrate from legacy single tree
|
|
272
|
-
|
|
273
|
-
|
|
317
|
+
const hydratedTrees: Record<string, LayoutNode> = {}
|
|
318
|
+
const hydratedGroupMap: Record<string, string> = {}
|
|
274
319
|
|
|
275
320
|
if (action.layoutTrees && action.tabGroupMap) {
|
|
276
321
|
// New format: prune each group tree
|
|
@@ -296,14 +341,17 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
296
341
|
}
|
|
297
342
|
}
|
|
298
343
|
|
|
299
|
-
return
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
344
|
+
return withActiveTabWorktree(
|
|
345
|
+
{
|
|
346
|
+
...state,
|
|
347
|
+
activeTabId: hydratedActiveTabId,
|
|
348
|
+
focusMode: 'navigation',
|
|
349
|
+
layoutTrees: hydratedTrees,
|
|
350
|
+
tabGroupMap: hydratedGroupMap,
|
|
351
|
+
tabs: normalizeGroupedTabOrder(action.tabs, hydratedTrees, hydratedGroupMap),
|
|
352
|
+
},
|
|
353
|
+
hydratedActiveTabId
|
|
354
|
+
)
|
|
307
355
|
}
|
|
308
356
|
case 'close-tab':
|
|
309
357
|
return closeTabAtIndex(
|
|
@@ -313,18 +361,19 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
313
361
|
case 'close-active-tab':
|
|
314
362
|
return closeTabAtIndex(state, getActiveIndex(state))
|
|
315
363
|
case 'set-active-tab':
|
|
316
|
-
return { ...state, activeTabId: action.tabId }
|
|
364
|
+
return withActiveTabWorktree({ ...state, activeTabId: action.tabId }, action.tabId)
|
|
317
365
|
case 'move-active-tab': {
|
|
318
366
|
if (state.tabs.length === 0) {
|
|
319
367
|
return state
|
|
320
368
|
}
|
|
321
|
-
const
|
|
369
|
+
const orderedTabs = orderTabsByWorktree(state.tabs, getCurrentSession(state))
|
|
370
|
+
const currentIndex = orderedTabs.findIndex((tab) => tab.id === state.activeTabId)
|
|
322
371
|
const safeIndex = currentIndex === -1 ? 0 : currentIndex
|
|
323
|
-
const nextIndex = (safeIndex + action.delta +
|
|
324
|
-
const nextTabId =
|
|
325
|
-
return
|
|
372
|
+
const nextIndex = (safeIndex + action.delta + orderedTabs.length) % orderedTabs.length
|
|
373
|
+
const nextTabId = orderedTabs[nextIndex]?.id
|
|
374
|
+
return nextTabId == null || nextTabId === '' || nextTabId === state.activeTabId
|
|
326
375
|
? state
|
|
327
|
-
: { ...state, activeTabId: nextTabId }
|
|
376
|
+
: withActiveTabWorktree({ ...state, activeTabId: nextTabId }, nextTabId)
|
|
328
377
|
}
|
|
329
378
|
case 'reorder-active-tab': {
|
|
330
379
|
const activeIndex = getActiveIndex(state)
|
|
@@ -333,12 +382,19 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
333
382
|
}
|
|
334
383
|
|
|
335
384
|
// Group-aware reordering: move entire layout group together
|
|
336
|
-
const activeGroupId =
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
385
|
+
const activeGroupId =
|
|
386
|
+
state.activeTabId != null && state.activeTabId !== ''
|
|
387
|
+
? getGroupIdForTab(state.tabGroupMap, state.activeTabId)
|
|
388
|
+
: null
|
|
389
|
+
const activeGroupTree =
|
|
390
|
+
activeGroupId != null && activeGroupId !== '' ? state.layoutTrees[activeGroupId] : null
|
|
340
391
|
const layoutIds = activeGroupTree ? allLeafIds(activeGroupTree) : []
|
|
341
|
-
if (
|
|
392
|
+
if (
|
|
393
|
+
layoutIds.length > 1 &&
|
|
394
|
+
state.activeTabId != null &&
|
|
395
|
+
state.activeTabId !== '' &&
|
|
396
|
+
layoutIds.includes(state.activeTabId)
|
|
397
|
+
) {
|
|
342
398
|
const tabs = moveLayoutGroup(state, activeIndex, layoutIds, action.delta)
|
|
343
399
|
if (!tabs) {
|
|
344
400
|
return state
|
|
@@ -354,11 +410,12 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
354
410
|
}
|
|
355
411
|
// Check if target belongs to a group → skip over the entire group
|
|
356
412
|
const targetTabId = getTabIdAtIndex(state.tabs, nextIndex)
|
|
357
|
-
if (!targetTabId) {
|
|
413
|
+
if (!(targetTabId != null && targetTabId !== '')) {
|
|
358
414
|
return state
|
|
359
415
|
}
|
|
360
416
|
const targetGroupId = getGroupIdForTab(state.tabGroupMap, targetTabId)
|
|
361
|
-
const targetTree =
|
|
417
|
+
const targetTree =
|
|
418
|
+
targetGroupId != null && targetGroupId !== '' ? state.layoutTrees[targetGroupId] : null
|
|
362
419
|
if (targetTree && targetTree.type === 'split') {
|
|
363
420
|
const targetIds = new Set(allLeafIds(targetTree))
|
|
364
421
|
const tabs = moveTabAcrossTargetGroup(
|
|
@@ -380,22 +437,25 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
380
437
|
return { ...state, tabs }
|
|
381
438
|
}
|
|
382
439
|
case 'reset-tab-session':
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
440
|
+
return withActiveTabWorktree(
|
|
441
|
+
{
|
|
442
|
+
...state,
|
|
443
|
+
activeTabId: action.tabId,
|
|
444
|
+
focusMode: 'navigation',
|
|
445
|
+
tabs: updateTab(state.tabs, action.tabId, (tab) => ({
|
|
446
|
+
...tab,
|
|
447
|
+
activity: 'idle',
|
|
448
|
+
buffer: '',
|
|
449
|
+
errorMessage: undefined,
|
|
450
|
+
exitCode: undefined,
|
|
451
|
+
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
452
|
+
status: 'starting',
|
|
453
|
+
terminalModes: createDefaultTerminalModes(),
|
|
454
|
+
viewport: undefined,
|
|
455
|
+
})),
|
|
456
|
+
},
|
|
457
|
+
action.tabId
|
|
458
|
+
)
|
|
399
459
|
case 'append-tab-buffer':
|
|
400
460
|
return {
|
|
401
461
|
...state,
|
|
@@ -449,16 +509,17 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
449
509
|
tabs: updateTab(state.tabs, action.tabId, (tab) => ({ ...tab, title: action.title })),
|
|
450
510
|
}
|
|
451
511
|
case 'split-pane': {
|
|
452
|
-
if (!state.activeTabId) {
|
|
512
|
+
if (!(state.activeTabId != null && state.activeTabId !== '')) {
|
|
453
513
|
return state
|
|
454
514
|
}
|
|
455
515
|
const newTab = { ...action.newTab, activity: action.newTab.activity ?? 'idle' }
|
|
456
516
|
|
|
457
517
|
// Find existing group for active tab, or create a new one
|
|
458
518
|
let groupId = getGroupIdForTab(state.tabGroupMap, state.activeTabId)
|
|
459
|
-
let tree:
|
|
460
|
-
const existingTree =
|
|
461
|
-
|
|
519
|
+
let tree: LayoutNode
|
|
520
|
+
const existingTree =
|
|
521
|
+
groupId != null && groupId !== '' ? state.layoutTrees[groupId] : undefined
|
|
522
|
+
if (groupId != null && groupId !== '' && existingTree) {
|
|
462
523
|
tree = existingTree
|
|
463
524
|
} else {
|
|
464
525
|
groupId = createGroupId()
|
|
@@ -472,31 +533,34 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
472
533
|
let insertIndex = state.tabs.length
|
|
473
534
|
for (let i = state.tabs.length - 1; i >= 0; i--) {
|
|
474
535
|
const tabId = getTabIdAtIndex(state.tabs, i)
|
|
475
|
-
if (tabId && layoutIdSet.has(tabId)) {
|
|
536
|
+
if (tabId != null && tabId !== '' && layoutIdSet.has(tabId)) {
|
|
476
537
|
insertIndex = i + 1
|
|
477
538
|
break
|
|
478
539
|
}
|
|
479
540
|
}
|
|
480
541
|
const tabs = [...state.tabs.slice(0, insertIndex), newTab, ...state.tabs.slice(insertIndex)]
|
|
481
542
|
|
|
482
|
-
return
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
543
|
+
return withActiveTabWorktree(
|
|
544
|
+
{
|
|
545
|
+
...state,
|
|
546
|
+
activeTabId: newTab.id,
|
|
547
|
+
layoutTrees: { ...state.layoutTrees, [groupId]: newTree },
|
|
548
|
+
tabGroupMap: {
|
|
549
|
+
...state.tabGroupMap,
|
|
550
|
+
[newTab.id]: groupId,
|
|
551
|
+
[state.activeTabId]: groupId,
|
|
552
|
+
},
|
|
553
|
+
tabs,
|
|
490
554
|
},
|
|
491
|
-
|
|
492
|
-
|
|
555
|
+
newTab.id
|
|
556
|
+
)
|
|
493
557
|
}
|
|
494
558
|
case 'close-pane': {
|
|
495
559
|
const idx = state.tabs.findIndex((tab) => tab.id === action.tabId)
|
|
496
560
|
return closeTabAtIndex(state, idx)
|
|
497
561
|
}
|
|
498
562
|
case 'focus-pane-direction': {
|
|
499
|
-
if (!state.activeTabId) {
|
|
563
|
+
if (!(state.activeTabId != null && state.activeTabId !== '')) {
|
|
500
564
|
return state
|
|
501
565
|
}
|
|
502
566
|
const focusTree = getTreeForTab(state.layoutTrees, state.tabGroupMap, state.activeTabId)
|
|
@@ -504,15 +568,16 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
504
568
|
return state
|
|
505
569
|
}
|
|
506
570
|
const neighbor = getAdjacentLeaf(focusTree, state.activeTabId, action.direction)
|
|
507
|
-
if (!neighbor) {
|
|
571
|
+
if (!(neighbor != null && neighbor !== '')) {
|
|
508
572
|
return state
|
|
509
573
|
}
|
|
510
|
-
return { ...state, activeTabId: neighbor }
|
|
574
|
+
return withActiveTabWorktree({ ...state, activeTabId: neighbor }, neighbor)
|
|
511
575
|
}
|
|
512
576
|
case 'resize-pane': {
|
|
513
577
|
const resizeGroupId = getGroupIdForTab(state.tabGroupMap, action.tabId)
|
|
514
|
-
const resizeTree =
|
|
515
|
-
|
|
578
|
+
const resizeTree =
|
|
579
|
+
resizeGroupId != null && resizeGroupId !== '' ? state.layoutTrees[resizeGroupId] : null
|
|
580
|
+
if (resizeGroupId == null || resizeGroupId === '' || !resizeTree) {
|
|
516
581
|
return state
|
|
517
582
|
}
|
|
518
583
|
const newTree = resizeSplit(resizeTree, action.tabId, action.delta, action.axis)
|
|
@@ -523,8 +588,9 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
523
588
|
}
|
|
524
589
|
case 'set-split-ratio': {
|
|
525
590
|
const ratioGroupId = getGroupIdForTab(state.tabGroupMap, action.tabId)
|
|
526
|
-
const ratioTree =
|
|
527
|
-
|
|
591
|
+
const ratioTree =
|
|
592
|
+
ratioGroupId != null && ratioGroupId !== '' ? state.layoutTrees[ratioGroupId] : null
|
|
593
|
+
if (ratioGroupId == null || ratioGroupId === '' || !ratioTree) {
|
|
528
594
|
return state
|
|
529
595
|
}
|
|
530
596
|
const newTree = setSplitRatio(ratioTree, action.tabId, action.ratio, action.axis)
|
package/src/state/selectors.ts
CHANGED
|
@@ -5,7 +5,7 @@ export function filterAssistants(
|
|
|
5
5
|
options: AssistantOption[],
|
|
6
6
|
filter: string | null
|
|
7
7
|
): AssistantOption[] {
|
|
8
|
-
if (!filter) return options
|
|
8
|
+
if (!(filter != null && filter !== '')) return options
|
|
9
9
|
const lower = filter.toLowerCase()
|
|
10
10
|
return options.filter(
|
|
11
11
|
(o) => o.label.toLowerCase().includes(lower) || o.description.toLowerCase().includes(lower)
|
|
@@ -13,7 +13,7 @@ export function filterAssistants(
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function filterSessions(sessions: SessionRecord[], filter: string | null): SessionRecord[] {
|
|
16
|
-
if (!filter) {
|
|
16
|
+
if (!(filter != null && filter !== '')) {
|
|
17
17
|
return sessions
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -21,12 +21,14 @@ export function filterSessions(sessions: SessionRecord[], filter: string | null)
|
|
|
21
21
|
return sessions.filter(
|
|
22
22
|
(session) =>
|
|
23
23
|
session.name.toLowerCase().includes(lower) ||
|
|
24
|
-
(session.projectPath &&
|
|
24
|
+
(session.projectPath != null &&
|
|
25
|
+
session.projectPath !== '' &&
|
|
26
|
+
session.projectPath.toLowerCase().includes(lower))
|
|
25
27
|
)
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function filterSnippets(snippets: SnippetRecord[], filter: string | null): SnippetRecord[] {
|
|
29
|
-
if (!filter) {
|
|
31
|
+
if (!(filter != null && filter !== '')) {
|
|
30
32
|
return snippets
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -6,6 +6,8 @@ import type { SessionRecord } from './types'
|
|
|
6
6
|
import { loadConfig, saveConfig } from '../config'
|
|
7
7
|
import { logDebug } from '../debug/input-log'
|
|
8
8
|
import { getProfileConfigDir } from '../profile-paths'
|
|
9
|
+
import { ensureSessionWorktrees } from './session-worktrees'
|
|
10
|
+
import { toast } from './toast-store'
|
|
9
11
|
import { isSessionRecord } from './validation'
|
|
10
12
|
|
|
11
13
|
interface SessionCatalogFile {
|
|
@@ -46,10 +48,14 @@ export function loadSessionCatalog(): SessionRecord[] {
|
|
|
46
48
|
const { file, issue } = readCatalogFile()
|
|
47
49
|
if (file) {
|
|
48
50
|
logDebug('sessions.catalog.load', { sessionCount: file.sessions.length })
|
|
49
|
-
|
|
51
|
+
const normalized = normalizeSessions(file.sessions)
|
|
52
|
+
if (JSON.stringify(normalized) !== JSON.stringify(file.sessions)) {
|
|
53
|
+
saveSessionCatalog(normalized)
|
|
54
|
+
}
|
|
55
|
+
return normalized
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
if (issue) {
|
|
58
|
+
if (issue != null && issue !== '') {
|
|
53
59
|
logDebug('sessions.catalog.loadIssue', { issue, path: SESSIONS_PATH })
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -69,13 +75,14 @@ export function loadSessionCatalog(): SessionRecord[] {
|
|
|
69
75
|
workspaceSnapshot: config.workspaceSnapshot,
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
const normalized = normalizeSessions([migrated])
|
|
79
|
+
saveSessionCatalog(normalized)
|
|
73
80
|
saveConfig({ ...config, workspaceSnapshot: undefined })
|
|
74
81
|
logDebug('sessions.catalog.migrateLegacyWorkspace', {
|
|
75
82
|
migratedSessionId: migrated.id,
|
|
76
83
|
tabCount: migrated.workspaceSnapshot?.tabs.length ?? 0,
|
|
77
84
|
})
|
|
78
|
-
return
|
|
85
|
+
return normalized
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
export function saveSessionCatalog(sessions: SessionRecord[]): void {
|
|
@@ -84,11 +91,15 @@ export function saveSessionCatalog(sessions: SessionRecord[]): void {
|
|
|
84
91
|
writeFileSync(SESSIONS_PATH, `${JSON.stringify({ sessions, version: 1 }, null, 2)}\n`)
|
|
85
92
|
logDebug('sessions.catalog.save', { sessionCount: sessions.length })
|
|
86
93
|
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
87
95
|
logDebug('sessions.catalog.saveError', {
|
|
88
|
-
error:
|
|
96
|
+
error: message,
|
|
89
97
|
path: SESSIONS_PATH,
|
|
90
98
|
sessionCount: sessions.length,
|
|
91
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}`)
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -126,3 +137,7 @@ function normalizeOrder(sessions: SessionRecord[]): SessionRecord[] {
|
|
|
126
137
|
const merged = [...withOrder, ...withoutOrder]
|
|
127
138
|
return merged.map((s, i) => (s.order === i ? s : { ...s, order: i }))
|
|
128
139
|
}
|
|
140
|
+
|
|
141
|
+
function normalizeSessions(sessions: SessionRecord[]): SessionRecord[] {
|
|
142
|
+
return normalizeOrder(sessions).map((session) => ensureSessionWorktrees(session))
|
|
143
|
+
}
|
|
@@ -52,6 +52,7 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
|
|
|
52
52
|
terminalModes: tab.terminalModes,
|
|
53
53
|
title: tab.title,
|
|
54
54
|
viewport: tab.viewport,
|
|
55
|
+
worktreeId: tab.worktreeId,
|
|
55
56
|
})),
|
|
56
57
|
version: 1,
|
|
57
58
|
}
|
|
@@ -80,6 +81,7 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
80
81
|
terminalModes: tab.terminalModes,
|
|
81
82
|
title: tab.title,
|
|
82
83
|
viewport: tab.viewport,
|
|
84
|
+
worktreeId: tab.worktreeId,
|
|
83
85
|
}))
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -140,7 +142,7 @@ export function normalizeGroupedTabOrder(
|
|
|
140
142
|
|
|
141
143
|
for (const tab of tabs) {
|
|
142
144
|
const groupId = tabGroupMap[tab.id]
|
|
143
|
-
if (!groupId) {
|
|
145
|
+
if (!(groupId != null && groupId !== '')) {
|
|
144
146
|
continue
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -160,9 +162,9 @@ export function normalizeGroupedTabOrder(
|
|
|
160
162
|
|
|
161
163
|
for (const tab of tabs) {
|
|
162
164
|
const groupId = tabGroupMap[tab.id]
|
|
163
|
-
const groupTree = groupId ? layoutTrees[groupId] : undefined
|
|
165
|
+
const groupTree = groupId != null && groupId !== '' ? layoutTrees[groupId] : undefined
|
|
164
166
|
|
|
165
|
-
if (
|
|
167
|
+
if (groupId == null || groupId === '' || !groupTree || groupTree.type !== 'split') {
|
|
166
168
|
orderedTabs.push(tab)
|
|
167
169
|
continue
|
|
168
170
|
}
|
|
@@ -194,7 +196,9 @@ export function restoreWorkspaceState(
|
|
|
194
196
|
> {
|
|
195
197
|
const tabs = restoreTabsFromWorkspace(workspaceSnapshot)
|
|
196
198
|
const activeTabId =
|
|
197
|
-
workspaceSnapshot?.activeTabId
|
|
199
|
+
workspaceSnapshot?.activeTabId != null &&
|
|
200
|
+
workspaceSnapshot?.activeTabId !== '' &&
|
|
201
|
+
tabs.some((tab) => tab.id === workspaceSnapshot.activeTabId)
|
|
198
202
|
? workspaceSnapshot.activeTabId
|
|
199
203
|
: (tabs[0]?.id ?? null)
|
|
200
204
|
|