@brimveyn/aimux 1.23.6 → 1.23.7
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
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getAssistantOption,
|
|
11
11
|
isCommandAvailable,
|
|
12
12
|
parseCommand,
|
|
13
|
+
stripInjectedSessionArgs,
|
|
13
14
|
} from '../pty/command-registry'
|
|
14
15
|
import { createTerminalBounds } from '../state/layout-resize'
|
|
15
16
|
import {
|
|
@@ -116,6 +117,11 @@ export function startTabSession(
|
|
|
116
117
|
ctx.startStartupGrace(tab.id, STARTUP_GRACE_MS)
|
|
117
118
|
|
|
118
119
|
const { args, executable } = parseCommand(tab.command)
|
|
120
|
+
// `tab.command` is not always the string this client wrote: a hydrate from the
|
|
121
|
+
// daemon (or a snapshot taken after one) hands back the whole argv of the last
|
|
122
|
+
// spawn, session flags included. Strip ours back out before adding this
|
|
123
|
+
// spawn's, or claude exits on the duplicate and takes the tab with it.
|
|
124
|
+
const baseArgs = stripInjectedSessionArgs(tab.assistant, ctx.state.customCommands, args)
|
|
119
125
|
// Ahead of `extraArgs` because that is where an initial prompt goes, and the
|
|
120
126
|
// prompt is positional — a flag after it would be read as part of it.
|
|
121
127
|
const sessionArgs =
|
|
@@ -134,7 +140,7 @@ export function startTabSession(
|
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
backend.createSession({
|
|
137
|
-
args: [...
|
|
143
|
+
args: [...baseArgs, ...sessionArgs, ...(extraArgs ?? [])],
|
|
138
144
|
assistant: tab.assistant,
|
|
139
145
|
autoRenameCandidate,
|
|
140
146
|
cols,
|
|
@@ -251,6 +251,41 @@ export function buildAssistantSessionArgs(
|
|
|
251
251
|
: option.session.buildSessionArgs(sessionId)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
const SESSION_FLAG = /^(?:-r|--resume|--session-id)$/
|
|
255
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Drop the session flags an earlier spawn left inside `tab.command`.
|
|
259
|
+
*
|
|
260
|
+
* The daemon stores its tabs as `[command, ...args].join(' ')`, so a client that
|
|
261
|
+
* hydrated from it — or a snapshot written by one — carries this tab's own
|
|
262
|
+
* `--session-id <uuid>` in the string the next spawn parses. Re-spawning that
|
|
263
|
+
* verbatim is `Error: Session ID … is already in use`, and an exiting PTY takes
|
|
264
|
+
* the tab with it.
|
|
265
|
+
*
|
|
266
|
+
* Only a uuid-shaped value is ours, and only when the user's own custom command
|
|
267
|
+
* does not declare that flag — a `--resume` they wrote themselves is theirs to
|
|
268
|
+
* keep, and `buildAssistantSessionArgs` already stands aside for it.
|
|
269
|
+
*/
|
|
270
|
+
export function stripInjectedSessionArgs(
|
|
271
|
+
assistant: AssistantId,
|
|
272
|
+
customCommands: Record<string, string>,
|
|
273
|
+
args: string[]
|
|
274
|
+
): string[] {
|
|
275
|
+
const custom = customCommands[assistant] ?? ''
|
|
276
|
+
if (/(^|\s)(-r|--resume|--session-id|--continue|-c)(\s|$)/.test(custom)) return args
|
|
277
|
+
const kept: string[] = []
|
|
278
|
+
for (let i = 0; i < args.length; i++) {
|
|
279
|
+
const arg = args[i] as string
|
|
280
|
+
if (SESSION_FLAG.test(arg) && UUID.test(args[i + 1] ?? '')) {
|
|
281
|
+
i++
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
kept.push(arg)
|
|
285
|
+
}
|
|
286
|
+
return kept
|
|
287
|
+
}
|
|
288
|
+
|
|
254
289
|
export function isCommandAvailable(command: string): boolean {
|
|
255
290
|
return Bun.which(command) !== null
|
|
256
291
|
}
|
|
@@ -106,6 +106,21 @@ function pruneOrphanedTabs(
|
|
|
106
106
|
)
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Recover a session id a snapshot lost. Until the client stopped adopting the
|
|
111
|
+
* daemon's tabs wholesale, `sessionId` was wiped on every attach while the
|
|
112
|
+
* daemon's argv echo left the uuid sitting in `command` — so the conversation
|
|
113
|
+
* these tabs own is still recoverable from the very string that broke them.
|
|
114
|
+
*
|
|
115
|
+
* ponytail: a migration, not a mechanism. Delete it once no snapshot in the
|
|
116
|
+
* wild predates the fix.
|
|
117
|
+
*/
|
|
118
|
+
function recoverSessionId(command: string): string | undefined {
|
|
119
|
+
return /(?:^|\s)(?:-r|--resume|--session-id)\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\s|$)/i.exec(
|
|
120
|
+
command
|
|
121
|
+
)?.[1]
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
export function restoreTabsFromProject(
|
|
110
125
|
snapshot: ProjectSnapshotV1 | undefined,
|
|
111
126
|
options: RestoreOptions = {}
|
|
@@ -130,7 +145,7 @@ export function restoreTabsFromProject(
|
|
|
130
145
|
errorMessage: tab.errorMessage,
|
|
131
146
|
exitCode: tab.exitCode,
|
|
132
147
|
id: tab.id,
|
|
133
|
-
sessionId: tab.sessionId,
|
|
148
|
+
sessionId: tab.sessionId ?? recoverSessionId(tab.command),
|
|
134
149
|
status: forceDisconnected ? getDisconnectedStatus(tab.status) : tab.status,
|
|
135
150
|
terminalModes: tab.terminalModes,
|
|
136
151
|
title: tab.title,
|
|
@@ -357,7 +357,18 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
357
357
|
state.currentProjectId != null && state.currentProjectId !== ''
|
|
358
358
|
? state.projects.find((s) => s.id === state.currentProjectId)
|
|
359
359
|
: undefined
|
|
360
|
-
|
|
360
|
+
// The daemon rebuilds `command` as `[command, ...args].join(' ')` and has
|
|
361
|
+
// no `sessionId` on the wire at all. Adopting its tabs wholesale bakes this
|
|
362
|
+
// spawn's `--session-id <uuid>` into the string the *next* spawn parses —
|
|
363
|
+
// claude exits on "Session ID … is already in use" and takes the tab with
|
|
364
|
+
// it — and drops the id the resume depends on. Both fields belong to the
|
|
365
|
+
// client, so keep what it already holds for the tabs it knows.
|
|
366
|
+
const ownedById = new Map(state.tabs.map((entry) => [entry.id, entry]))
|
|
367
|
+
const hydratedTabs = action.tabs.map((entry) => {
|
|
368
|
+
const owned = ownedById.get(entry.id)
|
|
369
|
+
return owned ? { ...entry, command: owned.command, sessionId: owned.sessionId } : entry
|
|
370
|
+
})
|
|
371
|
+
const visibleForWorkspace = filterTabsForActiveWorkspace(hydratedTabs, currentProject)
|
|
361
372
|
const visibleIds = new Set(visibleForWorkspace.map((t) => t.id))
|
|
362
373
|
const hydratedActiveTabId =
|
|
363
374
|
action.activeTabId != null &&
|
|
@@ -365,7 +376,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
365
376
|
visibleIds.has(action.activeTabId)
|
|
366
377
|
? action.activeTabId
|
|
367
378
|
: (visibleForWorkspace[0]?.id ?? null)
|
|
368
|
-
const tabIds = new Set(
|
|
379
|
+
const tabIds = new Set(hydratedTabs.map((t) => t.id))
|
|
369
380
|
|
|
370
381
|
// Restore from new multi-tree format or migrate from legacy single tree
|
|
371
382
|
const hydratedTrees: Record<string, LayoutNode> = {}
|
|
@@ -402,7 +413,7 @@ export function reduceTabState(state: AppState, action: AppAction): AppState | n
|
|
|
402
413
|
focusMode: 'navigation',
|
|
403
414
|
layoutTrees: hydratedTrees,
|
|
404
415
|
tabGroupMap: hydratedGroupMap,
|
|
405
|
-
tabs: normalizeGroupedTabOrder(
|
|
416
|
+
tabs: normalizeGroupedTabOrder(hydratedTabs, hydratedTrees, hydratedGroupMap),
|
|
406
417
|
},
|
|
407
418
|
hydratedActiveTabId,
|
|
408
419
|
{ onlyIfMissing: true }
|
|
@@ -291,9 +291,14 @@ export const PrChecksPanel = memo(function PrChecksPanel({
|
|
|
291
291
|
No checks
|
|
292
292
|
</text>
|
|
293
293
|
) : (
|
|
294
|
-
|
|
294
|
+
// GitHub can list the same workflow/name twice (a job that ran on
|
|
295
|
+
// both `push` and `pull_request`), so the pair is not a unique key
|
|
296
|
+
// and the duplicate crashes the reconciler. The rows hold no state
|
|
297
|
+
// and the list is the API's own order, so the index is the key.
|
|
298
|
+
checks.map((check, i) => (
|
|
295
299
|
<CheckRow
|
|
296
|
-
key
|
|
300
|
+
// oxlint-disable-next-line no-array-index-key
|
|
301
|
+
key={`${i}:${check.workflow}/${check.name}`}
|
|
297
302
|
bg={bg}
|
|
298
303
|
check={check}
|
|
299
304
|
showWorkflow={innerWidth >= WORKFLOW_MIN_WIDTH}
|