@brimveyn/aimux 1.14.11 → 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.
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bump": "bun run scripts/bump.ts"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@brimveyn/aimux-config": "0.6.
|
|
63
|
+
"@brimveyn/aimux-config": "0.6.8",
|
|
64
64
|
"@opentui/core": "^0.1.90",
|
|
65
65
|
"@opentui/react": "^0.1.90",
|
|
66
66
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
removeGitWorktree,
|
|
30
30
|
} from '../git/worktree'
|
|
31
31
|
import { createPrefixedId } from '../platform/id'
|
|
32
|
-
import { linkNodeModules } from '../platform/worktree-deps'
|
|
33
32
|
import {
|
|
34
33
|
assertSafeAimuxWorktreePath,
|
|
35
34
|
isInsideAimuxWorktreeRoot,
|
|
@@ -1337,9 +1336,6 @@ async function createAimuxTempWorktree(
|
|
|
1337
1336
|
await mkdir(dirname(targetPath), { recursive: true })
|
|
1338
1337
|
await assertSafeAimuxWorktreePath(targetPath)
|
|
1339
1338
|
await createGitWorktree({ baseRef, branchName, repoPath: repoRoot, targetPath })
|
|
1340
|
-
// For JS projects, reuse the source checkout's installed dependencies instead
|
|
1341
|
-
// of forcing a fresh install in the new worktree.
|
|
1342
|
-
const linkedModules = await linkNodeModules(sourcePath, targetPath).catch(() => false)
|
|
1343
1339
|
const now = new Date().toISOString()
|
|
1344
1340
|
|
|
1345
1341
|
const worktree: WorktreeRecord = {
|
|
@@ -1364,11 +1360,7 @@ async function createAimuxTempWorktree(
|
|
|
1364
1360
|
}))
|
|
1365
1361
|
saveSessionCatalog(sessions)
|
|
1366
1362
|
ctx.dispatch({ sessions, type: 'set-sessions' })
|
|
1367
|
-
toast.success(
|
|
1368
|
-
linkedModules
|
|
1369
|
-
? `Created worktree ${branchName} (linked node_modules)`
|
|
1370
|
-
: `Created worktree ${branchName}`
|
|
1371
|
-
)
|
|
1363
|
+
toast.success(`Created worktree ${branchName}`)
|
|
1372
1364
|
return worktree
|
|
1373
1365
|
}
|
|
1374
1366
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
206
|
-
//
|
|
207
|
-
// pane then renders the "this
|
|
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
|
-
|
|
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
|
}
|
package/src/state/store.ts
CHANGED
package/src/state/types.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs'
|
|
2
|
-
import { realpath, symlink } from 'node:fs/promises'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Symlink the source checkout's `node_modules` into a freshly created worktree
|
|
7
|
-
* so a JS project's dependencies don't need reinstalling. No-op when the source
|
|
8
|
-
* isn't a JS project, has no installed `node_modules`, or the worktree already
|
|
9
|
-
* has one. Returns true when a symlink was created.
|
|
10
|
-
*/
|
|
11
|
-
export async function linkNodeModules(sourceDir: string, worktreePath: string): Promise<boolean> {
|
|
12
|
-
if (!existsSync(join(sourceDir, 'package.json'))) return false
|
|
13
|
-
const sourceModules = join(sourceDir, 'node_modules')
|
|
14
|
-
if (!existsSync(sourceModules)) return false
|
|
15
|
-
const targetModules = join(worktreePath, 'node_modules')
|
|
16
|
-
if (existsSync(targetModules)) return false
|
|
17
|
-
// Resolve through any existing symlink so the new link points at the real
|
|
18
|
-
// dependency store rather than chaining through another worktree.
|
|
19
|
-
const resolved = await realpath(sourceModules)
|
|
20
|
-
await symlink(resolved, targetModules, 'dir')
|
|
21
|
-
return true
|
|
22
|
-
}
|