@brimveyn/aimux 1.21.0 → 1.22.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 +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { getProfileConfigDir } from '../profile-paths'
|
|
5
|
+
|
|
6
|
+
const SETUP_HEADER = `#!/usr/bin/env bash
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
`
|
|
10
|
+
|
|
11
|
+
const SETUP_STUB = `#!/usr/bin/env bash
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
|
|
14
|
+
# aimux setup script.
|
|
15
|
+
#
|
|
16
|
+
# Runs once per newly created workspace, with the working directory set to that
|
|
17
|
+
# workspace's root. A workspace is a fresh \`git worktree\`, so it contains only
|
|
18
|
+
# files tracked by git — no .env, no node_modules, no .venv, no build cache.
|
|
19
|
+
#
|
|
20
|
+
# Keep it idempotent and non-interactive. A non-zero exit is surfaced in the
|
|
21
|
+
# setup widget and recorded on the workspace.
|
|
22
|
+
|
|
23
|
+
echo "nothing to do yet — edit this script"
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
// Resolved per call, never module-cached: --profile is applied after module
|
|
27
|
+
// import, so a cached path would point at the wrong profile.
|
|
28
|
+
export function getProjectDataDir(projectId: string): string {
|
|
29
|
+
return join(getProfileConfigDir(), 'projects', projectId)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getSetupScriptPath(projectId: string): string {
|
|
33
|
+
return join(getProjectDataDir(projectId), 'setup.sh')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function hasSetupScript(projectId: string): boolean {
|
|
37
|
+
return existsSync(getSetupScriptPath(projectId))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Returns the script path, creating an executable stub if there is none. */
|
|
41
|
+
export function ensureSetupScriptStub(projectId: string): string {
|
|
42
|
+
const path = getSetupScriptPath(projectId)
|
|
43
|
+
if (existsSync(path)) return path
|
|
44
|
+
mkdirSync(getProjectDataDir(projectId), { recursive: true })
|
|
45
|
+
writeFileSync(path, SETUP_STUB)
|
|
46
|
+
chmodSync(path, 0o755)
|
|
47
|
+
return path
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const EMPTY_LINES: readonly string[] = []
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Keyed by path, not project id: the path depends on the active profile, which is
|
|
54
|
+
* resolved per call. Holds one small array per project that has a setup script.
|
|
55
|
+
*/
|
|
56
|
+
const setupLinesCache = new Map<string, { mtimeMs: number; lines: string[] }>()
|
|
57
|
+
|
|
58
|
+
/** A line that does something, as opposed to a shebang, an option or a comment. */
|
|
59
|
+
function isWorkLine(line: string): boolean {
|
|
60
|
+
const trimmed = line.trim()
|
|
61
|
+
return (
|
|
62
|
+
trimmed !== '' &&
|
|
63
|
+
!trimmed.startsWith('#') &&
|
|
64
|
+
!/^set\s+-[a-zA-Z]/.test(trimmed) &&
|
|
65
|
+
!/^set\s+-o\s/.test(trimmed)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseSetupScript(source: string): string[] {
|
|
70
|
+
return source
|
|
71
|
+
.split('\n')
|
|
72
|
+
.filter((line) => isWorkLine(line))
|
|
73
|
+
.map((line) => line.trim())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The script's actual work, with the boilerplate stripped: the shebang, the
|
|
78
|
+
* `set -e…` line, comments and blank lines. One line left means the script is a
|
|
79
|
+
* command, and a single text field can hold all of it; more than one means it is
|
|
80
|
+
* a script, and a text field would silently truncate it.
|
|
81
|
+
*
|
|
82
|
+
* Cached against the file's mtime, because the settings screen builds its Setup
|
|
83
|
+
* rows from this: the search filter runs it for every project on every keystroke,
|
|
84
|
+
* and `getModalOptionCount` runs it from inside the reducer. A `statSync` per
|
|
85
|
+
* project is a syscall; a read-and-parse per project per keystroke is a habit
|
|
86
|
+
* that gets worse with every project the user adds. An edit made outside aimux
|
|
87
|
+
* still lands — that is what the mtime is for.
|
|
88
|
+
*
|
|
89
|
+
* `readonly` because the array it hands back is the cached one, not a copy: a
|
|
90
|
+
* caller that sorted or spliced it would be editing every later caller's answer.
|
|
91
|
+
*/
|
|
92
|
+
export function readSetupScriptLines(projectId: string): readonly string[] {
|
|
93
|
+
const path = getSetupScriptPath(projectId)
|
|
94
|
+
try {
|
|
95
|
+
const { mtimeMs } = statSync(path)
|
|
96
|
+
const cached = setupLinesCache.get(path)
|
|
97
|
+
if (cached?.mtimeMs === mtimeMs) return cached.lines
|
|
98
|
+
const lines = parseSetupScript(readFileSync(path, 'utf8'))
|
|
99
|
+
setupLinesCache.set(path, { lines, mtimeMs })
|
|
100
|
+
return lines
|
|
101
|
+
} catch {
|
|
102
|
+
// Missing or unreadable. Not the same thing, but the caller has nothing
|
|
103
|
+
// better to show for either, and the editor path still works.
|
|
104
|
+
setupLinesCache.delete(path)
|
|
105
|
+
return EMPTY_LINES
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Put this one command in the script, replacing the work line that was there.
|
|
111
|
+
* Round-trips with `readSetupScriptLines`.
|
|
112
|
+
*
|
|
113
|
+
* Everything that is not that line survives: the shebang, the `set -e…`, and any
|
|
114
|
+
* comments. Rewriting the file from a fixed header was simpler and quietly threw
|
|
115
|
+
* away whatever the user had written around their one command — a field that
|
|
116
|
+
* deletes the notes next to what it edits is not an editor.
|
|
117
|
+
*/
|
|
118
|
+
export function writeSetupCommand(projectId: string, command: string): void {
|
|
119
|
+
mkdirSync(getProjectDataDir(projectId), { recursive: true })
|
|
120
|
+
const path = getSetupScriptPath(projectId)
|
|
121
|
+
writeFileSync(path, nextSetupSource(path, command.trim()))
|
|
122
|
+
chmodSync(path, 0o755)
|
|
123
|
+
// Not left to the mtime: a write and the read that follows it can land in the
|
|
124
|
+
// same millisecond, and the cache would hand back what was there before.
|
|
125
|
+
setupLinesCache.delete(path)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function nextSetupSource(path: string, command: string): string {
|
|
129
|
+
let existing: string
|
|
130
|
+
try {
|
|
131
|
+
existing = readFileSync(path, 'utf8')
|
|
132
|
+
} catch {
|
|
133
|
+
// No script yet (or none we can read): the template is all there is to keep.
|
|
134
|
+
return `${SETUP_HEADER}${command}\n`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const lines = existing.split('\n')
|
|
138
|
+
const first = lines.findIndex((line) => isWorkLine(line))
|
|
139
|
+
// Nothing to replace — the stub, or comments only. Append rather than drop them.
|
|
140
|
+
if (first === -1) return `${existing.replace(/\n*$/, '')}\n\n${command}\n`
|
|
141
|
+
lines[first] = command
|
|
142
|
+
return lines.join('\n')
|
|
143
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { AppState, TabSession, TabStatus
|
|
1
|
+
import type { AppState, ProjectSnapshotV1, TabSession, TabStatus } from './types'
|
|
2
2
|
|
|
3
3
|
import { allLeafIds, createGroupId, type LayoutNode, pruneLayoutTree } from './layout-tree'
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function createEmptyProjectSnapshot(): ProjectSnapshotV1 {
|
|
6
6
|
return {
|
|
7
7
|
activeTabId: null,
|
|
8
8
|
savedAt: new Date().toISOString(),
|
|
@@ -16,13 +16,13 @@ export function createEmptyWorkspaceSnapshot(): WorkspaceSnapshotV1 {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* The per-
|
|
20
|
-
* data accrues across
|
|
19
|
+
* The per-workspace last-tab memory is serialized into every snapshot now so the
|
|
20
|
+
* data accrues across projects, but restoring it at startup stays dormant until
|
|
21
21
|
* a future (non-breaking) change is ready to consume it. Flipping this to true
|
|
22
|
-
* makes restart restore the last-viewed tab per
|
|
22
|
+
* makes restart restore the last-viewed tab per workspace; until then restart
|
|
23
23
|
* behavior is unchanged and the live in-memory map is never clobbered on switch.
|
|
24
24
|
*/
|
|
25
|
-
const
|
|
25
|
+
const RESTORE_LAST_ACTIVE_TAB_BY_WORKSPACE: boolean = false
|
|
26
26
|
|
|
27
27
|
function getDisconnectedStatus(status: TabStatus): TabStatus {
|
|
28
28
|
if (status === 'running' || status === 'starting') {
|
|
@@ -32,75 +32,81 @@ function getDisconnectedStatus(status: TabStatus): TabStatus {
|
|
|
32
32
|
return status
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function
|
|
35
|
+
export function serializeProject(state: AppState): ProjectSnapshotV1 {
|
|
36
36
|
return {
|
|
37
37
|
activeTabId: state.activeTabId,
|
|
38
|
-
|
|
39
|
-
Object.keys(state.
|
|
40
|
-
? state.
|
|
38
|
+
lastActiveTabByWorkspace:
|
|
39
|
+
Object.keys(state.lastActiveTabByWorkspace).length > 0
|
|
40
|
+
? state.lastActiveTabByWorkspace
|
|
41
41
|
: undefined,
|
|
42
42
|
layoutTree: Object.values(state.layoutTrees)[0] ?? undefined,
|
|
43
43
|
layoutTrees: Object.keys(state.layoutTrees).length > 0 ? state.layoutTrees : undefined,
|
|
44
44
|
savedAt: new Date().toISOString(),
|
|
45
|
-
// Mirror of the left bar. `
|
|
46
|
-
// dropping it invalidates every entry in the
|
|
45
|
+
// Mirror of the left bar. `isProjectSnapshotV1` requires this key —
|
|
46
|
+
// dropping it invalidates every entry in the project catalog.
|
|
47
47
|
sidebar: {
|
|
48
48
|
visible: state.bars.left.visible,
|
|
49
49
|
width: state.bars.left.width,
|
|
50
50
|
},
|
|
51
51
|
tabGroupMap: Object.keys(state.tabGroupMap).length > 0 ? state.tabGroupMap : undefined,
|
|
52
|
-
tabs
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
52
|
+
// Hidden tabs (the setup runner's PTY) are session-scoped on purpose: the
|
|
53
|
+
// durable record of a setup run is `WorkspaceRecord.setupRanAt`, and keeping
|
|
54
|
+
// them out here is what lets `hidden` stay off the ipc/daemon wire.
|
|
55
|
+
tabs: state.tabs
|
|
56
|
+
.filter((tab) => tab.hidden !== true)
|
|
57
|
+
.map((tab) => ({
|
|
58
|
+
assistant: tab.assistant,
|
|
59
|
+
autoRenameStatus: tab.autoRenameStatus,
|
|
60
|
+
buffer: tab.buffer,
|
|
61
|
+
command: tab.command,
|
|
62
|
+
errorMessage: tab.errorMessage,
|
|
63
|
+
exitCode: tab.exitCode,
|
|
64
|
+
id: tab.id,
|
|
65
|
+
status: tab.status === 'disconnected' ? 'running' : tab.status,
|
|
66
|
+
terminalModes: tab.terminalModes,
|
|
67
|
+
title: tab.title,
|
|
68
|
+
viewport: tab.viewport,
|
|
69
|
+
workerName: tab.workerName,
|
|
70
|
+
workspaceId: tab.workspaceId,
|
|
71
|
+
})),
|
|
67
72
|
version: 1,
|
|
68
73
|
}
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export interface RestoreOptions {
|
|
72
77
|
// Force any running/starting tab to 'disconnected' on restore. True for
|
|
73
|
-
// cold-start (daemon may not own the
|
|
74
|
-
// catalog hydration. False for live in-app
|
|
75
|
-
// attach() is guaranteed to follow within a frame and hydrate-
|
|
78
|
+
// cold-start (daemon may not own the projects yet) and the daemon's own
|
|
79
|
+
// catalog hydration. False for live in-app project switches where an
|
|
80
|
+
// attach() is guaranteed to follow within a frame and hydrate-project
|
|
76
81
|
// will overwrite the status with daemon truth — leaving the flag on would
|
|
77
82
|
// briefly flash the "Restored snapshot" hint on every j/k cycle.
|
|
78
83
|
forceDisconnected?: boolean
|
|
79
|
-
// When provided, drop tabs pinned to a
|
|
80
|
-
// longer owns. Some delete paths (notably the sidebar's "Remove
|
|
81
|
-
// historically removed the
|
|
84
|
+
// When provided, drop tabs pinned to a workspace id that the project no
|
|
85
|
+
// longer owns. Some delete paths (notably the sidebar's "Remove workspace")
|
|
86
|
+
// historically removed the workspace record without closing its tabs, leaving
|
|
82
87
|
// orphans bound to a vanished id. Those orphans are invisible (filtered out
|
|
83
|
-
// by the active-
|
|
84
|
-
// delete can collide with — exactly what closes "another
|
|
88
|
+
// by the active-workspace filter) yet keep a workspace id that a *future*
|
|
89
|
+
// delete can collide with — exactly what closes "another workspace's" tabs.
|
|
85
90
|
// Pruning them on restore both repairs corrupted catalogs and prevents the
|
|
86
|
-
// collision. Tabs with no
|
|
87
|
-
|
|
91
|
+
// collision. Tabs with no workspace id (legacy/unbound) are always kept.
|
|
92
|
+
validWorkspaceIds?: ReadonlySet<string>
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
// Drop tabs bound to a
|
|
91
|
-
// (no
|
|
95
|
+
// Drop tabs bound to a workspace id the project no longer owns. Unbound tabs
|
|
96
|
+
// (no workspaceId) are kept — they surface under the primary workspace.
|
|
92
97
|
function pruneOrphanedTabs(
|
|
93
98
|
tabs: TabSession[],
|
|
94
|
-
|
|
99
|
+
validWorkspaceIds: ReadonlySet<string> | undefined
|
|
95
100
|
): TabSession[] {
|
|
96
|
-
if (!
|
|
101
|
+
if (!validWorkspaceIds) return tabs
|
|
97
102
|
return tabs.filter(
|
|
98
|
-
(tab) =>
|
|
103
|
+
(tab) =>
|
|
104
|
+
tab.workspaceId == null || tab.workspaceId === '' || validWorkspaceIds.has(tab.workspaceId)
|
|
99
105
|
)
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
export function
|
|
103
|
-
snapshot:
|
|
108
|
+
export function restoreTabsFromProject(
|
|
109
|
+
snapshot: ProjectSnapshotV1 | undefined,
|
|
104
110
|
options: RestoreOptions = {}
|
|
105
111
|
): TabSession[] {
|
|
106
112
|
if (!snapshot || snapshot.version !== 1) {
|
|
@@ -128,13 +134,13 @@ export function restoreTabsFromWorkspace(
|
|
|
128
134
|
title: tab.title,
|
|
129
135
|
viewport: tab.viewport,
|
|
130
136
|
workerName: tab.workerName,
|
|
131
|
-
|
|
137
|
+
workspaceId: tab.workspaceId,
|
|
132
138
|
}))
|
|
133
|
-
return pruneOrphanedTabs(restored, options.
|
|
139
|
+
return pruneOrphanedTabs(restored, options.validWorkspaceIds)
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
export function restoreLayoutTrees(
|
|
137
|
-
snapshot:
|
|
143
|
+
snapshot: ProjectSnapshotV1 | undefined,
|
|
138
144
|
tabs: TabSession[]
|
|
139
145
|
): { layoutTrees: Record<string, LayoutNode>; tabGroupMap: Record<string, string> } {
|
|
140
146
|
const validTabIds = new Set(tabs.map((t) => t.id))
|
|
@@ -167,16 +173,16 @@ export function restoreLayoutTrees(
|
|
|
167
173
|
return { layoutTrees, tabGroupMap }
|
|
168
174
|
}
|
|
169
175
|
|
|
170
|
-
// When a
|
|
171
|
-
// — but the
|
|
172
|
-
//
|
|
176
|
+
// When a workspace is removed, its tabs are killed live via disposeWorkspaceTabs
|
|
177
|
+
// — but the project's persisted projectSnapshot still references them by
|
|
178
|
+
// workspaceId. Without this pruning, a subsequent load-project (project switch
|
|
173
179
|
// or restart) resurrects the dead tabs and they reappear in h-l navigation.
|
|
174
|
-
export function
|
|
175
|
-
snapshot:
|
|
176
|
-
|
|
177
|
-
):
|
|
180
|
+
export function pruneSnapshotOfWorkspace(
|
|
181
|
+
snapshot: ProjectSnapshotV1 | undefined,
|
|
182
|
+
workspaceId: string
|
|
183
|
+
): ProjectSnapshotV1 | undefined {
|
|
178
184
|
if (!snapshot) return snapshot
|
|
179
|
-
const keptTabs = snapshot.tabs.filter((tab) => tab.
|
|
185
|
+
const keptTabs = snapshot.tabs.filter((tab) => tab.workspaceId !== workspaceId)
|
|
180
186
|
if (keptTabs.length === snapshot.tabs.length) return snapshot
|
|
181
187
|
const validTabIds = new Set(keptTabs.map((tab) => tab.id))
|
|
182
188
|
|
|
@@ -275,29 +281,29 @@ export function normalizeGroupedTabOrder(
|
|
|
275
281
|
return orderedTabs
|
|
276
282
|
}
|
|
277
283
|
|
|
278
|
-
export function
|
|
284
|
+
export function restoreProjectState(
|
|
279
285
|
state: AppState,
|
|
280
|
-
|
|
286
|
+
projectSnapshot: ProjectSnapshotV1 | undefined,
|
|
281
287
|
options: RestoreOptions = {}
|
|
282
288
|
): Pick<AppState, 'tabs' | 'activeTabId' | 'focusMode' | 'layoutTrees' | 'tabGroupMap'> &
|
|
283
|
-
Partial<Pick<AppState, '
|
|
284
|
-
const tabs =
|
|
289
|
+
Partial<Pick<AppState, 'lastActiveTabByWorkspace'>> {
|
|
290
|
+
const tabs = restoreTabsFromProject(projectSnapshot, options)
|
|
285
291
|
const activeTabId =
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
tabs.some((tab) => tab.id ===
|
|
289
|
-
?
|
|
292
|
+
projectSnapshot?.activeTabId != null &&
|
|
293
|
+
projectSnapshot?.activeTabId !== '' &&
|
|
294
|
+
tabs.some((tab) => tab.id === projectSnapshot.activeTabId)
|
|
295
|
+
? projectSnapshot.activeTabId
|
|
290
296
|
: (tabs[0]?.id ?? null)
|
|
291
297
|
|
|
292
|
-
const { layoutTrees, tabGroupMap } = restoreLayoutTrees(
|
|
298
|
+
const { layoutTrees, tabGroupMap } = restoreLayoutTrees(projectSnapshot, tabs)
|
|
293
299
|
const orderedTabs = normalizeGroupedTabOrder(tabs, layoutTrees, tabGroupMap)
|
|
294
300
|
|
|
295
|
-
// Dormant until the gate flips: merge the persisted per-
|
|
301
|
+
// Dormant until the gate flips: merge the persisted per-workspace memory over
|
|
296
302
|
// the live in-memory map. While off, the key is omitted entirely so callers
|
|
297
303
|
// that spread the result never overwrite their existing map on a switch.
|
|
298
304
|
const persistedLastActiveTab =
|
|
299
|
-
|
|
300
|
-
? { ...state.
|
|
305
|
+
RESTORE_LAST_ACTIVE_TAB_BY_WORKSPACE && projectSnapshot?.lastActiveTabByWorkspace
|
|
306
|
+
? { ...state.lastActiveTabByWorkspace, ...projectSnapshot.lastActiveTabByWorkspace }
|
|
301
307
|
: undefined
|
|
302
308
|
|
|
303
309
|
return {
|
|
@@ -306,6 +312,6 @@ export function restoreWorkspaceState(
|
|
|
306
312
|
layoutTrees,
|
|
307
313
|
tabGroupMap,
|
|
308
314
|
tabs: orderedTabs,
|
|
309
|
-
...(persistedLastActiveTab ? {
|
|
315
|
+
...(persistedLastActiveTab ? { lastActiveTabByWorkspace: persistedLastActiveTab } : {}),
|
|
310
316
|
}
|
|
311
317
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AppState, ProjectRecord } from './types'
|
|
2
|
+
|
|
3
|
+
import { loadConfig, saveConfig } from '../config'
|
|
4
|
+
import { saveProjectCatalog } from './project-catalog'
|
|
5
|
+
import { serializeProject } from './project-persistence'
|
|
6
|
+
|
|
7
|
+
export function buildProjectsWithCurrentSnapshot(
|
|
8
|
+
projects: ProjectRecord[],
|
|
9
|
+
currentProjectId: string | null,
|
|
10
|
+
state: AppState
|
|
11
|
+
): ProjectRecord[] {
|
|
12
|
+
return projects.map((project) =>
|
|
13
|
+
project.id === currentProjectId
|
|
14
|
+
? {
|
|
15
|
+
...project,
|
|
16
|
+
projectSnapshot: serializeProject(state),
|
|
17
|
+
updatedAt: new Date().toISOString(),
|
|
18
|
+
}
|
|
19
|
+
: project
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveCurrentProject(state: AppState): void {
|
|
24
|
+
saveConfig({
|
|
25
|
+
...loadConfig(),
|
|
26
|
+
bars: state.bars,
|
|
27
|
+
customCommands: state.customCommands,
|
|
28
|
+
gitPane: {
|
|
29
|
+
diffModeRatio: state.gitPane.diffModeRatio,
|
|
30
|
+
fileListMode: state.gitPane.fileListMode,
|
|
31
|
+
// Was missing, so every autosave dropped it and `t`/`T` in git mode never
|
|
32
|
+
// survived a restart — this write replaces the whole `gitPane` block.
|
|
33
|
+
treeCompaction: state.gitPane.treeCompaction,
|
|
34
|
+
},
|
|
35
|
+
projectBarVisible: state.projectBar.visible,
|
|
36
|
+
// Legacy mirror of the left bar: lets an older build (and the project
|
|
37
|
+
// snapshot schema) still find a sidebar after a downgrade.
|
|
38
|
+
sidebar: {
|
|
39
|
+
visible: state.bars.left.visible,
|
|
40
|
+
width: state.bars.left.width,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
saveProjectCatalog(
|
|
44
|
+
buildProjectsWithCurrentSnapshot(state.projects, state.currentProjectId, state)
|
|
45
|
+
)
|
|
46
|
+
}
|