@brimveyn/aimux 1.16.2 → 1.18.1
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 +4 -2
- package/src/app-runtime/backend-runtime-events.ts +179 -0
- package/src/app-runtime/side-effects.ts +3 -1
- package/src/app.tsx +26 -0
- package/src/cli/chord.ts +77 -0
- package/src/cli/client/bootstrap.ts +76 -0
- package/src/cli/client/daemon-client.ts +228 -0
- package/src/cli/client/workspace-resolver.ts +57 -0
- package/src/cli/commands/tab/close.ts +33 -0
- package/src/cli/commands/tab/create.ts +109 -0
- package/src/cli/commands/tab/focus.ts +33 -0
- package/src/cli/commands/tab/list.ts +52 -0
- package/src/cli/commands/tab/send.ts +83 -0
- package/src/cli/commands/tab/snapshot.ts +127 -0
- package/src/cli/commands/tab/tail.ts +171 -0
- package/src/cli/commands/tab/wait.ts +86 -0
- package/src/cli/commands/workspace/close.ts +32 -0
- package/src/cli/commands/workspace/create.ts +92 -0
- package/src/cli/commands/workspace/list.ts +25 -0
- package/src/cli/commands/workspace/show.ts +32 -0
- package/src/cli/commands/workspace/switch.ts +75 -0
- package/src/cli/commands/worktree/create.ts +113 -0
- package/src/cli/commands/worktree/list.ts +44 -0
- package/src/cli/commands/worktree/remove.ts +59 -0
- package/src/cli/context.ts +16 -0
- package/src/cli/flags.ts +101 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/output.ts +30 -0
- package/src/cli/registry.ts +54 -0
- package/src/cli/snapshot-render.ts +54 -0
- package/src/daemon/catalog-writer.ts +123 -0
- package/src/daemon/daemon.ts +566 -11
- package/src/daemon/reexec-client.ts +147 -0
- package/src/daemon/runtime-paths.ts +161 -1
- package/src/daemon/session-registry.ts +17 -0
- package/src/index.tsx +9 -0
- package/src/input/modes/bridge.ts +6 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +1 -0
- package/src/ipc/README.md +112 -0
- package/src/ipc/manager-protocol.ts +54 -4
- package/src/ipc/protocol.ts +466 -25
- package/src/platform/daemon-control.ts +14 -0
- package/src/restart-daemon.ts +36 -4
- package/src/session-backend/bootstrap.ts +110 -0
- package/src/session-backend/local-session-backend.ts +6 -0
- package/src/session-backend/remote-session-backend.ts +63 -0
- package/src/session-backend/types.ts +34 -0
- package/src/state/reducers/modal-state.ts +66 -1
- package/src/state/types.ts +40 -0
- package/src/state/validation.ts +1 -1
- package/src/terminal-manager/manager-client.ts +24 -7
- package/src/ui/components/flash/flash-label-badge.tsx +38 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +2 -0
- package/src/ui/components/layout/sidebar/workspace-list.tsx +4 -0
- package/src/ui/components/layout/sidebar/worktree-row.tsx +2 -0
- package/src/ui/components/layout/top-tab-bar.tsx +2 -0
- package/src/ui/flash/assign-labels.ts +126 -0
- package/src/ui/flash/build-labels.ts +78 -0
- package/src/ui/hooks/use-flash-label.ts +40 -0
- package/src/ui/root.tsx +3 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { DaemonClient } from './client/daemon-client'
|
|
2
|
+
import type { CliContext } from './context'
|
|
3
|
+
|
|
4
|
+
import { connectToDaemon, DaemonUnreachableError } from './client/bootstrap'
|
|
5
|
+
import { resolveWorkspace } from './client/workspace-resolver'
|
|
6
|
+
import { CliUsageError, parseArgs } from './flags'
|
|
7
|
+
import {
|
|
8
|
+
EXIT_DAEMON_UNREACHABLE,
|
|
9
|
+
EXIT_OK,
|
|
10
|
+
EXIT_RUNTIME,
|
|
11
|
+
EXIT_USAGE,
|
|
12
|
+
writeError,
|
|
13
|
+
writeJson,
|
|
14
|
+
} from './output'
|
|
15
|
+
import { COMMANDS, resolveCommand } from './registry'
|
|
16
|
+
|
|
17
|
+
function printHelp(): void {
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
'aimux CLI control plane\n\nUsage:\n aimux <group> <verb> [flags] [args]\n\n'
|
|
20
|
+
)
|
|
21
|
+
const byGroup = new Map<string, typeof COMMANDS>()
|
|
22
|
+
for (const command of COMMANDS) {
|
|
23
|
+
const existing = byGroup.get(command.group) ?? []
|
|
24
|
+
byGroup.set(command.group, [...existing, command] as typeof COMMANDS)
|
|
25
|
+
}
|
|
26
|
+
for (const [group, commands] of byGroup) {
|
|
27
|
+
process.stdout.write(` ${group}\n`)
|
|
28
|
+
for (const command of commands) {
|
|
29
|
+
process.stdout.write(` aimux ${group} ${command.verb.padEnd(10)} ${command.summary}\n`)
|
|
30
|
+
}
|
|
31
|
+
process.stdout.write('\n')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runCli(argv: readonly string[]): Promise<number> {
|
|
36
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
37
|
+
printHelp()
|
|
38
|
+
return EXIT_OK
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const group = argv[0] ?? ''
|
|
42
|
+
const verb = argv[1] ?? ''
|
|
43
|
+
const command = resolveCommand(group, verb)
|
|
44
|
+
if (!command) {
|
|
45
|
+
writeError(`unknown command: ${group} ${verb}`)
|
|
46
|
+
printHelp()
|
|
47
|
+
return EXIT_USAGE
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let parsed
|
|
51
|
+
try {
|
|
52
|
+
parsed = parseArgs(argv.slice(2), command.flags, command.args)
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (error instanceof CliUsageError) {
|
|
55
|
+
writeError(error.message)
|
|
56
|
+
writeError(`usage: aimux ${command.group} ${command.verb}`)
|
|
57
|
+
return EXIT_USAGE
|
|
58
|
+
}
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --profile overrides the env var BEFORE anyone touches a runtime path —
|
|
63
|
+
// the daemon socket path, catalog path, and TM socket path all derive from
|
|
64
|
+
// it. After this point all `getIpcDaemonSocketPath()` reads pick it up.
|
|
65
|
+
if (typeof parsed.flags.profile === 'string' && parsed.flags.profile !== '') {
|
|
66
|
+
process.env.AIMUX_PROFILE = parsed.flags.profile
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const state: {
|
|
70
|
+
daemon: DaemonClient | null
|
|
71
|
+
workspace: ReturnType<typeof resolveWorkspace> | null
|
|
72
|
+
} = {
|
|
73
|
+
daemon: null,
|
|
74
|
+
workspace: null,
|
|
75
|
+
}
|
|
76
|
+
const ctx: CliContext = {
|
|
77
|
+
args: parsed,
|
|
78
|
+
getDaemon: async () => {
|
|
79
|
+
if (state.daemon) return state.daemon
|
|
80
|
+
state.daemon = await connectToDaemon()
|
|
81
|
+
return state.daemon
|
|
82
|
+
},
|
|
83
|
+
getWorkspace: () => {
|
|
84
|
+
if (state.workspace) return state.workspace
|
|
85
|
+
const workspaceFlag =
|
|
86
|
+
typeof parsed.flags.workspace === 'string' ? parsed.flags.workspace : undefined
|
|
87
|
+
state.workspace = resolveWorkspace(workspaceFlag)
|
|
88
|
+
return state.workspace
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const code = await command.run(ctx)
|
|
94
|
+
return code
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
97
|
+
// Classify by error type, not by string-sniffing the message: a runtime
|
|
98
|
+
// error whose message happens to include "socket" (e.g. daemon reply
|
|
99
|
+
// "socket write failed for tab X") must not masquerade as
|
|
100
|
+
// daemon-unreachable, or CI that treats exit 4 as a "restart the daemon"
|
|
101
|
+
// signal will retry spuriously.
|
|
102
|
+
if (error instanceof DaemonUnreachableError) {
|
|
103
|
+
writeError(message)
|
|
104
|
+
writeJson({ error: message, kind: 'daemon-unreachable' })
|
|
105
|
+
return EXIT_DAEMON_UNREACHABLE
|
|
106
|
+
}
|
|
107
|
+
writeError(message)
|
|
108
|
+
writeJson({ error: message, kind: 'runtime-error' })
|
|
109
|
+
return EXIT_RUNTIME
|
|
110
|
+
} finally {
|
|
111
|
+
state.daemon?.close()
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-pure stdio for CLI commands. Every command writes one JSON object to
|
|
3
|
+
* stdout (or an NDJSON stream for `wait`/`tail`) and one human-readable line
|
|
4
|
+
* to stderr on failure. No ANSI, no prose interleaved with JSON — agents
|
|
5
|
+
* consume stdout, humans read stderr.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function writeJson(value: unknown): void {
|
|
9
|
+
process.stdout.write(`${JSON.stringify(value)}\n`)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function writeNdjson(value: unknown): void {
|
|
13
|
+
process.stdout.write(`${JSON.stringify(value)}\n`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function writeError(message: string): void {
|
|
17
|
+
process.stderr.write(`aimux: ${message}\n`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Exit code contract (docs/reference/cli.md):
|
|
21
|
+
// 0 success
|
|
22
|
+
// 2 usage error (bad flags, unknown command, missing argument)
|
|
23
|
+
// 3 runtime error (server replied with `error`, command failed)
|
|
24
|
+
// 4 daemon unreachable (socket missing and autostart failed)
|
|
25
|
+
// 124 timeout (`tab wait`, `tab tail --timeout`, `workspace switch --wait`)
|
|
26
|
+
export const EXIT_OK = 0
|
|
27
|
+
export const EXIT_USAGE = 2
|
|
28
|
+
export const EXIT_RUNTIME = 3
|
|
29
|
+
export const EXIT_DAEMON_UNREACHABLE = 4
|
|
30
|
+
export const EXIT_TIMEOUT = 124
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CliContext } from './context'
|
|
2
|
+
import type { ArgSpec, FlagSpec } from './flags'
|
|
3
|
+
|
|
4
|
+
import { tabClose } from './commands/tab/close'
|
|
5
|
+
import { tabCreate } from './commands/tab/create'
|
|
6
|
+
import { tabFocus } from './commands/tab/focus'
|
|
7
|
+
import { tabList } from './commands/tab/list'
|
|
8
|
+
import { tabSend } from './commands/tab/send'
|
|
9
|
+
import { tabSnapshot } from './commands/tab/snapshot'
|
|
10
|
+
import { tabTail } from './commands/tab/tail'
|
|
11
|
+
import { tabWait } from './commands/tab/wait'
|
|
12
|
+
import { workspaceClose } from './commands/workspace/close'
|
|
13
|
+
import { workspaceCreate } from './commands/workspace/create'
|
|
14
|
+
import { workspaceList } from './commands/workspace/list'
|
|
15
|
+
import { workspaceShow } from './commands/workspace/show'
|
|
16
|
+
import { workspaceSwitch } from './commands/workspace/switch'
|
|
17
|
+
import { worktreeCreate } from './commands/worktree/create'
|
|
18
|
+
import { worktreeList } from './commands/worktree/list'
|
|
19
|
+
import { worktreeRemove } from './commands/worktree/remove'
|
|
20
|
+
|
|
21
|
+
export interface CliCommand {
|
|
22
|
+
group: string
|
|
23
|
+
verb: string
|
|
24
|
+
summary: string
|
|
25
|
+
flags: readonly FlagSpec[]
|
|
26
|
+
args: readonly ArgSpec[]
|
|
27
|
+
run: (ctx: CliContext) => Promise<number>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const COMMANDS: readonly CliCommand[] = [
|
|
31
|
+
tabList,
|
|
32
|
+
tabCreate,
|
|
33
|
+
tabSend,
|
|
34
|
+
tabFocus,
|
|
35
|
+
tabClose,
|
|
36
|
+
tabSnapshot,
|
|
37
|
+
tabTail,
|
|
38
|
+
tabWait,
|
|
39
|
+
workspaceList,
|
|
40
|
+
workspaceShow,
|
|
41
|
+
workspaceCreate,
|
|
42
|
+
workspaceSwitch,
|
|
43
|
+
workspaceClose,
|
|
44
|
+
worktreeList,
|
|
45
|
+
worktreeCreate,
|
|
46
|
+
worktreeRemove,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
export function resolveCommand(group: string, verb: string): CliCommand | null {
|
|
50
|
+
for (const command of COMMANDS) {
|
|
51
|
+
if (command.group === group && command.verb === verb) return command
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { TerminalSnapshot } from '../state/types'
|
|
2
|
+
|
|
3
|
+
export interface SnapshotLineOptions {
|
|
4
|
+
/** When true (default), strip trailing whitespace from each line so the
|
|
5
|
+
* serialised output isn't a sea of padding cells the LLM has to chew
|
|
6
|
+
* through. Pass false when you need the exact alignment of the terminal
|
|
7
|
+
* (e.g. ASCII art / fixed-column TUIs). */
|
|
8
|
+
trim?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Flatten a TerminalSnapshot into plain text — concatenate every span's
|
|
13
|
+
* `text` per line. No ANSI, no palette resolution. Agents consume strings,
|
|
14
|
+
* not pixels.
|
|
15
|
+
*/
|
|
16
|
+
export function snapshotToLines(
|
|
17
|
+
snapshot: TerminalSnapshot,
|
|
18
|
+
options: SnapshotLineOptions = {}
|
|
19
|
+
): string[] {
|
|
20
|
+
const trim = options.trim !== false
|
|
21
|
+
return snapshot.lines.map((line) => {
|
|
22
|
+
const joined = line.spans.map((span) => span.text).join('')
|
|
23
|
+
return trim ? joined.replace(/\s+$/u, '') : joined
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Like `snapshotToLines` but trims trailing blank lines and slices to the
|
|
29
|
+
* last `n` non-blank lines. Mirrors the `--tail N` flag.
|
|
30
|
+
*/
|
|
31
|
+
export function snapshotTailLines(
|
|
32
|
+
snapshot: TerminalSnapshot,
|
|
33
|
+
n: number,
|
|
34
|
+
options: SnapshotLineOptions = {}
|
|
35
|
+
): string[] {
|
|
36
|
+
const all = snapshotToLines(snapshot, options)
|
|
37
|
+
let end = all.length
|
|
38
|
+
while (end > 0 && (all[end - 1] ?? '').trim() === '') end--
|
|
39
|
+
const trimmed = all.slice(0, end)
|
|
40
|
+
if (n <= 0 || trimmed.length <= n) return trimmed
|
|
41
|
+
return trimmed.slice(trimmed.length - n)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Plain-text dump: lines joined by `\n`, single trailing newline. Best shape
|
|
46
|
+
* for piping into an LLM since it preserves the screen's visual layout
|
|
47
|
+
* without JSON-escape noise.
|
|
48
|
+
*/
|
|
49
|
+
export function snapshotToText(
|
|
50
|
+
snapshot: TerminalSnapshot,
|
|
51
|
+
options: SnapshotLineOptions = {}
|
|
52
|
+
): string {
|
|
53
|
+
return `${snapshotToLines(snapshot, options).join('\n')}\n`
|
|
54
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { SessionRecord, WorktreeRecord } from '../state/types'
|
|
2
|
+
|
|
3
|
+
import { logDebug } from '../debug/input-log'
|
|
4
|
+
import { createPrefixedId } from '../platform/id'
|
|
5
|
+
import { loadSessionCatalog, saveSessionCatalog } from '../state/session-catalog'
|
|
6
|
+
import { createEmptyWorkspaceSnapshot } from '../state/session-persistence'
|
|
7
|
+
import { createPrimaryWorktree, ensureSessionWorktrees } from '../state/session-worktrees'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Catalog mutations invoked by the daemon when NO UI is attached. When a UI
|
|
11
|
+
* is attached the daemon relays the request as an event and the UI's reducer
|
|
12
|
+
* owns the write (so the live workspace snapshot is preserved).
|
|
13
|
+
*
|
|
14
|
+
* Each helper is a pure read-modify-write against `aimux-sessions.json` —
|
|
15
|
+
* safe to call from the daemon process, no React / dispatcher involved.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function createWorkspaceInCatalog(name: string, projectPath?: string): SessionRecord {
|
|
19
|
+
const sessions = loadSessionCatalog()
|
|
20
|
+
const now = new Date().toISOString()
|
|
21
|
+
const session: SessionRecord = {
|
|
22
|
+
activeWorktreeId: undefined,
|
|
23
|
+
createdAt: now,
|
|
24
|
+
id: createPrefixedId('session'),
|
|
25
|
+
lastOpenedAt: now,
|
|
26
|
+
name,
|
|
27
|
+
projectPath,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
workspaceSnapshot: createEmptyWorkspaceSnapshot(),
|
|
30
|
+
worktrees: undefined,
|
|
31
|
+
}
|
|
32
|
+
if (projectPath != null && projectPath !== '') {
|
|
33
|
+
const worktree = createPrimaryWorktree(projectPath, now)
|
|
34
|
+
session.activeWorktreeId = worktree.id
|
|
35
|
+
session.worktrees = [worktree]
|
|
36
|
+
}
|
|
37
|
+
saveSessionCatalog([...sessions, ensureSessionWorktrees(session)])
|
|
38
|
+
logDebug('daemon.catalog.createWorkspace', { name, projectPath, sessionId: session.id })
|
|
39
|
+
return session
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Throws when the target session isn't in the catalog. Used by the daemon
|
|
44
|
+
* before broadcasting a workspace-lifecycle request so the CLI's `expectOk`
|
|
45
|
+
* fails fast (with a meaningful message) instead of the `--wait` path
|
|
46
|
+
* hanging out for its timeout while the UI silently ignores an unknown id.
|
|
47
|
+
*/
|
|
48
|
+
export function assertSessionInCatalog(sessionId: string): void {
|
|
49
|
+
const sessions = loadSessionCatalog()
|
|
50
|
+
if (!sessions.some((s) => s.id === sessionId)) {
|
|
51
|
+
throw new Error(`workspace not found: ${sessionId}`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function bumpLastOpenedInCatalog(sessionId: string): void {
|
|
56
|
+
const sessions = loadSessionCatalog()
|
|
57
|
+
const now = new Date().toISOString()
|
|
58
|
+
const target = sessions.find((s) => s.id === sessionId)
|
|
59
|
+
if (!target) {
|
|
60
|
+
throw new Error(`workspace not found: ${sessionId}`)
|
|
61
|
+
}
|
|
62
|
+
const updated = sessions.map((s) => (s.id === sessionId ? { ...s, lastOpenedAt: now } : s))
|
|
63
|
+
saveSessionCatalog(updated)
|
|
64
|
+
logDebug('daemon.catalog.switchWorkspace', { sessionId })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function deleteFromCatalog(sessionId: string): void {
|
|
68
|
+
const sessions = loadSessionCatalog()
|
|
69
|
+
const remaining = sessions.filter((s) => s.id !== sessionId)
|
|
70
|
+
if (remaining.length === sessions.length) {
|
|
71
|
+
throw new Error(`workspace not found: ${sessionId}`)
|
|
72
|
+
}
|
|
73
|
+
saveSessionCatalog(remaining)
|
|
74
|
+
logDebug('daemon.catalog.closeWorkspace', { sessionId })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function addWorktreeToCatalog(sessionId: string, worktree: WorktreeRecord): void {
|
|
78
|
+
const sessions = loadSessionCatalog()
|
|
79
|
+
const target = sessions.find((s) => s.id === sessionId)
|
|
80
|
+
if (!target) {
|
|
81
|
+
throw new Error(`workspace not found: ${sessionId}`)
|
|
82
|
+
}
|
|
83
|
+
const existing = target.worktrees ?? []
|
|
84
|
+
if (existing.some((w) => w.id === worktree.id)) {
|
|
85
|
+
throw new Error(`worktree already exists: ${worktree.id}`)
|
|
86
|
+
}
|
|
87
|
+
const updated = sessions.map((s) =>
|
|
88
|
+
s.id === sessionId
|
|
89
|
+
? { ...s, updatedAt: new Date().toISOString(), worktrees: [...existing, worktree] }
|
|
90
|
+
: s
|
|
91
|
+
)
|
|
92
|
+
saveSessionCatalog(updated)
|
|
93
|
+
logDebug('daemon.catalog.addWorktree', {
|
|
94
|
+
sessionId,
|
|
95
|
+
worktreeId: worktree.id,
|
|
96
|
+
worktreeName: worktree.name,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function removeWorktreeFromCatalog(sessionId: string, worktreeId: string): void {
|
|
101
|
+
const sessions = loadSessionCatalog()
|
|
102
|
+
const target = sessions.find((s) => s.id === sessionId)
|
|
103
|
+
if (!target) {
|
|
104
|
+
throw new Error(`workspace not found: ${sessionId}`)
|
|
105
|
+
}
|
|
106
|
+
const existing = target.worktrees ?? []
|
|
107
|
+
const nextWorktrees = existing.filter((w) => w.id !== worktreeId)
|
|
108
|
+
if (nextWorktrees.length === existing.length) {
|
|
109
|
+
throw new Error(`worktree not found: ${worktreeId}`)
|
|
110
|
+
}
|
|
111
|
+
const updated = sessions.map((s) =>
|
|
112
|
+
s.id === sessionId
|
|
113
|
+
? {
|
|
114
|
+
...s,
|
|
115
|
+
activeWorktreeId: s.activeWorktreeId === worktreeId ? undefined : s.activeWorktreeId,
|
|
116
|
+
updatedAt: new Date().toISOString(),
|
|
117
|
+
worktrees: nextWorktrees,
|
|
118
|
+
}
|
|
119
|
+
: s
|
|
120
|
+
)
|
|
121
|
+
saveSessionCatalog(updated)
|
|
122
|
+
logDebug('daemon.catalog.removeWorktree', { sessionId, worktreeId })
|
|
123
|
+
}
|