@brimveyn/aimux 1.14.13 → 1.14.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.14.13",
3
+ "version": "1.14.14",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -28,12 +28,24 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
28
28
  // honor the patched worktree by filtering the restored tab list.
29
29
  const loadedSession = state.sessions.find((entry) => entry.id === action.sessionId)
30
30
  const visible = filterTabsForActiveWorktree(restored.tabs, loadedSession)
31
- const activeTabId =
31
+ // Prefer the snapshot's tab; if it isn't visible under the active
32
+ // worktree (e.g. a multi-worktree session restored onto a different
33
+ // worktree), fall back to the last tab we viewed in that worktree,
34
+ // then to the first visible tab.
35
+ const rememberedForWorktree =
36
+ loadedSession?.activeWorktreeId != null && loadedSession.activeWorktreeId !== ''
37
+ ? state.lastActiveTabByWorktree[loadedSession.activeWorktreeId]
38
+ : undefined
39
+ const snapshotTabVisible =
32
40
  restored.activeTabId != null &&
33
41
  restored.activeTabId !== '' &&
34
42
  visible.some((t) => t.id === restored.activeTabId)
35
- ? restored.activeTabId
36
- : (visible[0]?.id ?? null)
43
+ const rememberedTabVisible =
44
+ rememberedForWorktree != null &&
45
+ rememberedForWorktree !== '' &&
46
+ visible.some((t) => t.id === rememberedForWorktree)
47
+ const fallbackTabId = rememberedTabVisible ? rememberedForWorktree : (visible[0]?.id ?? null)
48
+ const activeTabId = snapshotTabVisible ? restored.activeTabId : fallbackTabId
37
49
  return {
38
50
  ...state,
39
51
  ...restored,
@@ -196,15 +208,29 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
196
208
  const sessions = state.sessions.map((session) =>
197
209
  session.id === action.sessionId ? withActiveWorktree(session, action.worktreeId) : session
198
210
  )
211
+ // Remember the tab we're leaving so coming back to this worktree later
212
+ // restores it instead of snapping to the first tab. Keyed by the
213
+ // worktree we're departing (the current session's active one).
214
+ const leavingSession = state.sessions.find((s) => s.id === action.sessionId)
215
+ const leavingWorktreeId = leavingSession?.activeWorktreeId
216
+ const lastActiveTabByWorktree =
217
+ action.sessionId === state.currentSessionId &&
218
+ leavingWorktreeId != null &&
219
+ leavingWorktreeId !== '' &&
220
+ state.activeTabId != null &&
221
+ state.activeTabId !== ''
222
+ ? { ...state.lastActiveTabByWorktree, [leavingWorktreeId]: state.activeTabId }
223
+ : state.lastActiveTabByWorktree
199
224
  // Changing the worktree of a non-current session has no effect on
200
225
  // which tab the user is looking at — leave activeTabId alone.
201
226
  if (action.sessionId !== state.currentSessionId) {
202
- return { ...state, sessions }
227
+ return { ...state, lastActiveTabByWorktree, sessions }
203
228
  }
204
229
  // For the current session: if the existing activeTabId belongs to
205
- // the new worktree, keep it. Otherwise hop to the first tab visible
206
- // under that worktree (or clear it if the worktree is empty the
207
- // pane then renders the "this worktree has no tabs" placeholder).
230
+ // the new worktree, keep it. Otherwise restore the last tab we viewed
231
+ // in that worktree, falling back to the first visible tab (or clearing
232
+ // it if the worktree is empty — the pane then renders the "this
233
+ // worktree has no tabs" placeholder).
208
234
  const updated = sessions.find((s) => s.id === action.sessionId)
209
235
  const visible = filterTabsForActiveWorktree(state.tabs, updated)
210
236
  const currentStillVisible =
@@ -212,9 +238,14 @@ export function reduceSessionState(state: AppState, action: AppAction): AppState
212
238
  state.activeTabId !== '' &&
213
239
  visible.some((t) => t.id === state.activeTabId)
214
240
  if (currentStillVisible) {
215
- return { ...state, sessions }
241
+ return { ...state, lastActiveTabByWorktree, sessions }
216
242
  }
217
- return { ...state, activeTabId: visible[0]?.id ?? null, sessions }
243
+ const remembered = lastActiveTabByWorktree[action.worktreeId]
244
+ const nextActiveTabId =
245
+ remembered != null && remembered !== '' && visible.some((t) => t.id === remembered)
246
+ ? remembered
247
+ : (visible[0]?.id ?? null)
248
+ return { ...state, activeTabId: nextActiveTabId, lastActiveTabByWorktree, sessions }
218
249
  }
219
250
  case 'update-worktree-record':
220
251
  return {
@@ -15,6 +15,15 @@ export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
15
15
  }
16
16
  }
17
17
 
18
+ /**
19
+ * The per-worktree last-tab memory is serialized into every snapshot now so the
20
+ * data accrues across sessions, but restoring it at startup stays dormant until
21
+ * a future (non-breaking) change is ready to consume it. Flipping this to true
22
+ * makes restart restore the last-viewed tab per worktree; until then restart
23
+ * behavior is unchanged and the live in-memory map is never clobbered on switch.
24
+ */
25
+ const RESTORE_LAST_ACTIVE_TAB_BY_WORKTREE: boolean = false
26
+
18
27
  function getDisconnectedStatus(status: TabStatus): TabStatus {
19
28
  if (status === 'running' || status === 'starting') {
20
29
  return 'disconnected'
@@ -26,6 +35,10 @@ function getDisconnectedStatus(status: TabStatus): TabStatus {
26
35
  export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
27
36
  return {
28
37
  activeTabId: state.activeTabId,
38
+ lastActiveTabByWorktree:
39
+ Object.keys(state.lastActiveTabByWorktree).length > 0
40
+ ? state.lastActiveTabByWorktree
41
+ : undefined,
29
42
  layoutTree: Object.values(state.layoutTrees)[0] ?? undefined,
30
43
  layoutTrees: Object.keys(state.layoutTrees).length > 0 ? state.layoutTrees : undefined,
31
44
  savedAt: new Date().toISOString(),
@@ -241,7 +254,8 @@ export function restoreWorkspaceState(
241
254
  ): Pick<
242
255
  AppState,
243
256
  'tabs' | 'activeTabId' | 'focusMode' | 'sidebar' | 'layoutTrees' | 'tabGroupMap'
244
- > {
257
+ > &
258
+ Partial<Pick<AppState, 'lastActiveTabByWorktree'>> {
245
259
  const tabs = restoreTabsFromWorkspace(workspaceSnapshot, options)
246
260
  const activeTabId =
247
261
  workspaceSnapshot?.activeTabId != null &&
@@ -253,6 +267,14 @@ export function restoreWorkspaceState(
253
267
  const { layoutTrees, tabGroupMap } = restoreLayoutTrees(workspaceSnapshot, tabs)
254
268
  const orderedTabs = normalizeGroupedTabOrder(tabs, layoutTrees, tabGroupMap)
255
269
 
270
+ // Dormant until the gate flips: merge the persisted per-worktree memory over
271
+ // the live in-memory map. While off, the key is omitted entirely so callers
272
+ // that spread the result never overwrite their existing map on a switch.
273
+ const persistedLastActiveTab =
274
+ RESTORE_LAST_ACTIVE_TAB_BY_WORKTREE && workspaceSnapshot?.lastActiveTabByWorktree
275
+ ? { ...state.lastActiveTabByWorktree, ...workspaceSnapshot.lastActiveTabByWorktree }
276
+ : undefined
277
+
256
278
  return {
257
279
  activeTabId,
258
280
  focusMode: 'navigation',
@@ -260,5 +282,6 @@ export function restoreWorkspaceState(
260
282
  sidebar: state.sidebar,
261
283
  tabGroupMap,
262
284
  tabs: orderedTabs,
285
+ ...(persistedLastActiveTab ? { lastActiveTabByWorktree: persistedLastActiveTab } : {}),
263
286
  }
264
287
  }
@@ -84,6 +84,7 @@ export function createInitialState(
84
84
  position: gitPanePosition,
85
85
  },
86
86
  gitPanel: emptyGitPanel(),
87
+ lastActiveTabByWorktree: {},
87
88
  layout: {
88
89
  terminalCols: DEFAULT_TERMINAL_COLS,
89
90
  terminalRows: DEFAULT_TERMINAL_ROWS,
@@ -117,6 +117,13 @@ export interface WorkspaceSnapshotV1 {
117
117
  layoutTree?: LayoutNode
118
118
  layoutTrees?: Record<string, LayoutNode>
119
119
  tabGroupMap?: Record<string, string>
120
+ /**
121
+ * Last viewed tab per worktree, keyed by worktree id. Optional and additive
122
+ * (no version bump): older builds ignore it, newer builds tolerate its
123
+ * absence. Written now so the data accrues, but restoring it at startup is
124
+ * gated off until a future change flips RESTORE_LAST_ACTIVE_TAB_BY_WORKTREE.
125
+ */
126
+ lastActiveTabByWorktree?: Record<string, string>
120
127
  }
121
128
 
122
129
  export type WorktreeSource = 'primary' | 'aimux-temp' | 'external'
@@ -489,6 +496,12 @@ export interface AppState {
489
496
  * keyed by worktree id. Ephemeral (polled); not persisted to the catalog.
490
497
  */
491
498
  worktreeDivergence: Record<string, BranchDivergence>
499
+ /**
500
+ * Last active tab a user viewed within each worktree, keyed by worktree id.
501
+ * Lets switching back to a worktree restore its last-viewed tab instead of
502
+ * snapping to the first one. Ephemeral (in-memory); not persisted to the catalog.
503
+ */
504
+ lastActiveTabByWorktree: Record<string, string>
492
505
  /** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
493
506
  pendingChords: string[] | null
494
507
  }