@brimveyn/aimux 1.12.6 → 1.13.0
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 +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +538 -83
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +1 -1
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +11 -3
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +57 -3
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { basename } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import type { SessionRecord, TabSession, WorktreeRecord } from './types'
|
|
7
|
+
|
|
8
|
+
import { createPrefixedId } from '../platform/id'
|
|
9
|
+
import { isInsideAimuxWorktreeRoot } from '../platform/worktree-paths'
|
|
10
|
+
|
|
11
|
+
const WORKTREE_COLORS = ['#7dd3fc', '#86efac', '#facc15', '#f0abfc', '#fb7185', '#c4b5fd']
|
|
12
|
+
|
|
13
|
+
export function getWorktreeColor(worktreeId: string): string {
|
|
14
|
+
let hash = 0
|
|
15
|
+
for (let i = 0; i < worktreeId.length; i++) {
|
|
16
|
+
hash = (hash * 31 + worktreeId.charCodeAt(i)) >>> 0
|
|
17
|
+
}
|
|
18
|
+
return WORKTREE_COLORS[hash % WORKTREE_COLORS.length] ?? '#7dd3fc'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createPrimaryWorktree(projectPath: string, now: string): WorktreeRecord {
|
|
22
|
+
const id = createPrefixedId('worktree')
|
|
23
|
+
return {
|
|
24
|
+
createdAt: now,
|
|
25
|
+
createdByAimux: false,
|
|
26
|
+
id,
|
|
27
|
+
name: basename(projectPath) || projectPath,
|
|
28
|
+
path: projectPath,
|
|
29
|
+
repoRoot: projectPath,
|
|
30
|
+
source: 'primary',
|
|
31
|
+
updatedAt: now,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ensureSessionWorktrees(
|
|
36
|
+
session: SessionRecord,
|
|
37
|
+
now = new Date().toISOString()
|
|
38
|
+
): SessionRecord {
|
|
39
|
+
if (session.worktrees?.length != null && session.worktrees?.length !== 0) {
|
|
40
|
+
const worktrees = mergeExistingGitWorktrees(
|
|
41
|
+
pruneMissingAimuxTempWorktrees(session.worktrees),
|
|
42
|
+
now
|
|
43
|
+
)
|
|
44
|
+
if (worktrees.length === 0 && session.projectPath != null && session.projectPath !== '') {
|
|
45
|
+
const worktree = createPrimaryWorktree(session.projectPath, now)
|
|
46
|
+
return { ...session, activeWorktreeId: worktree.id, worktrees: [worktree] }
|
|
47
|
+
}
|
|
48
|
+
const activeWorktreeId = worktrees.some((w) => w.id === session.activeWorktreeId)
|
|
49
|
+
? session.activeWorktreeId
|
|
50
|
+
: worktrees[0]?.id
|
|
51
|
+
const active = worktrees.find((w) => w.id === activeWorktreeId)
|
|
52
|
+
return {
|
|
53
|
+
...session,
|
|
54
|
+
activeWorktreeId,
|
|
55
|
+
projectPath: active?.path ?? session.projectPath,
|
|
56
|
+
worktrees,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!(session.projectPath != null && session.projectPath !== '')) {
|
|
61
|
+
return session
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const worktree = createPrimaryWorktree(session.projectPath, now)
|
|
65
|
+
const worktrees = mergeExistingGitWorktrees([worktree], now)
|
|
66
|
+
return {
|
|
67
|
+
...session,
|
|
68
|
+
activeWorktreeId:
|
|
69
|
+
worktrees.find((entry) => entry.path === session.projectPath)?.id ?? worktree.id,
|
|
70
|
+
worktrees,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stableWorktreeId(path: string): string {
|
|
75
|
+
return `worktree-${createHash('sha1').update(path).digest('hex').slice(0, 10)}`
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeExistingGitWorktrees(worktrees: WorktreeRecord[], now: string): WorktreeRecord[] {
|
|
79
|
+
const anchor = worktrees.find((worktree) => existsSync(worktree.path)) ?? worktrees[0]
|
|
80
|
+
if (!anchor) return worktrees
|
|
81
|
+
const discovered = listGitWorktreesSync(anchor.path)
|
|
82
|
+
if (discovered.length === 0) return worktrees
|
|
83
|
+
|
|
84
|
+
// Git keeps admin entries for worktrees whose directory is gone (e.g. a temp
|
|
85
|
+
// dir cleared on reboot). Drop that stale state so the entries stop
|
|
86
|
+
// resurfacing in `git worktree list`, and ignore them while reconciling.
|
|
87
|
+
if (discovered.some((entry) => entry.prunable === true)) {
|
|
88
|
+
pruneGitWorktreesSync(anchor.path)
|
|
89
|
+
}
|
|
90
|
+
const live = discovered.filter((entry) => entry.prunable !== true)
|
|
91
|
+
|
|
92
|
+
const byPath = new Map(worktrees.map((worktree) => [worktree.path, worktree]))
|
|
93
|
+
// `git worktree list` reports the main worktree first; prefer the primary
|
|
94
|
+
// record's repoRoot when it still exists, then git's main worktree.
|
|
95
|
+
const primaryRepoRoot = worktrees.find((worktree) => worktree.source === 'primary')?.repoRoot
|
|
96
|
+
const repoRoot =
|
|
97
|
+
primaryRepoRoot !== undefined && existsSync(primaryRepoRoot)
|
|
98
|
+
? primaryRepoRoot
|
|
99
|
+
: (live[0]?.path ?? anchor.repoRoot)
|
|
100
|
+
for (const entry of live) {
|
|
101
|
+
if (byPath.has(entry.path)) continue
|
|
102
|
+
if (isInsideAimuxWorktreeRoot(entry.path)) continue
|
|
103
|
+
const worktree: WorktreeRecord = {
|
|
104
|
+
branch: entry.branch,
|
|
105
|
+
commitSha: entry.head,
|
|
106
|
+
createdAt: now,
|
|
107
|
+
createdByAimux: false,
|
|
108
|
+
id: stableWorktreeId(entry.path),
|
|
109
|
+
name: basename(entry.path) || entry.path,
|
|
110
|
+
path: entry.path,
|
|
111
|
+
repoRoot,
|
|
112
|
+
source: 'external',
|
|
113
|
+
updatedAt: now,
|
|
114
|
+
}
|
|
115
|
+
byPath.set(entry.path, worktree)
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
[...byPath.values()]
|
|
119
|
+
// Drop records whose directory no longer exists (a deleted external
|
|
120
|
+
// worktree that git already pruned) so they don't reappear on restart.
|
|
121
|
+
.filter((worktree) => worktree.source === 'primary' || existsSync(worktree.path))
|
|
122
|
+
// Heal records whose repoRoot points at a since-deleted sibling worktree
|
|
123
|
+
// so later git operations (e.g. `git -C repoRoot`) don't fail.
|
|
124
|
+
.map((worktree) =>
|
|
125
|
+
existsSync(worktree.repoRoot) ? worktree : { ...worktree, repoRoot, updatedAt: now }
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface DiscoveredWorktree {
|
|
131
|
+
path: string
|
|
132
|
+
head?: string
|
|
133
|
+
branch?: string
|
|
134
|
+
prunable?: boolean
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function listGitWorktreesSync(cwd: string): DiscoveredWorktree[] {
|
|
138
|
+
const result = spawnSync('git', ['-C', cwd, 'worktree', 'list', '--porcelain'], {
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
})
|
|
141
|
+
if (result.status !== 0 || !result.stdout) return []
|
|
142
|
+
const worktrees: DiscoveredWorktree[] = []
|
|
143
|
+
let current: DiscoveredWorktree | null = null
|
|
144
|
+
for (const line of result.stdout.split('\n')) {
|
|
145
|
+
if (line.startsWith('worktree ')) {
|
|
146
|
+
if (current) worktrees.push(current)
|
|
147
|
+
current = { path: line.slice('worktree '.length) }
|
|
148
|
+
} else if (current && line.startsWith('HEAD ')) {
|
|
149
|
+
current.head = line.slice('HEAD '.length)
|
|
150
|
+
} else if (current && line.startsWith('branch ')) {
|
|
151
|
+
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '')
|
|
152
|
+
} else if (current && line.startsWith('prunable')) {
|
|
153
|
+
current.prunable = true
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (current) worktrees.push(current)
|
|
157
|
+
return worktrees
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pruneGitWorktreesSync(cwd: string): void {
|
|
161
|
+
spawnSync('git', ['-C', cwd, 'worktree', 'prune'], { encoding: 'utf8' })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pruneMissingAimuxTempWorktrees(worktrees: WorktreeRecord[]): WorktreeRecord[] {
|
|
165
|
+
return worktrees.filter((worktree) => {
|
|
166
|
+
if (
|
|
167
|
+
worktree.source !== 'aimux-temp' ||
|
|
168
|
+
!worktree.createdByAimux ||
|
|
169
|
+
!isInsideAimuxWorktreeRoot(worktree.path)
|
|
170
|
+
) {
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
return existsSync(worktree.path)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getActiveWorktree(session: SessionRecord | undefined): WorktreeRecord | undefined {
|
|
178
|
+
if (!(session?.worktrees?.length != null && session?.worktrees?.length !== 0)) return undefined
|
|
179
|
+
return (
|
|
180
|
+
session.worktrees.find((worktree) => worktree.id === session.activeWorktreeId) ??
|
|
181
|
+
session.worktrees[0]
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function getSessionProjectPath(session: SessionRecord | undefined): string | undefined {
|
|
186
|
+
return getActiveWorktree(session)?.path ?? session?.projectPath
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function withActiveWorktree(session: SessionRecord, worktreeId: string): SessionRecord {
|
|
190
|
+
const worktree = session.worktrees?.find((entry) => entry.id === worktreeId)
|
|
191
|
+
if (!worktree) return session
|
|
192
|
+
return {
|
|
193
|
+
...session,
|
|
194
|
+
activeWorktreeId: worktreeId,
|
|
195
|
+
projectPath: worktree.path,
|
|
196
|
+
updatedAt: new Date().toISOString(),
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function getRenderedTabWorktreeId(
|
|
201
|
+
tab: { worktreeId?: string },
|
|
202
|
+
worktrees: { id: string }[]
|
|
203
|
+
): string {
|
|
204
|
+
return tab.worktreeId ?? worktrees[0]?.id ?? '__main__'
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function orderTabsByWorktree(
|
|
208
|
+
tabs: TabSession[],
|
|
209
|
+
session: SessionRecord | undefined
|
|
210
|
+
): TabSession[] {
|
|
211
|
+
const worktrees = session?.worktrees ?? []
|
|
212
|
+
const order = new Map<string, number>()
|
|
213
|
+
for (const [index, worktree] of worktrees.entries()) {
|
|
214
|
+
order.set(worktree.id, index)
|
|
215
|
+
}
|
|
216
|
+
order.set('__main__', order.size)
|
|
217
|
+
|
|
218
|
+
return tabs
|
|
219
|
+
.map((tab, index) => ({ index, tab }))
|
|
220
|
+
.sort((a, b) => {
|
|
221
|
+
const aw = order.get(getRenderedTabWorktreeId(a.tab, worktrees)) ?? Number.MAX_SAFE_INTEGER
|
|
222
|
+
const bw = order.get(getRenderedTabWorktreeId(b.tab, worktrees)) ?? Number.MAX_SAFE_INTEGER
|
|
223
|
+
if (aw !== bw) return aw - bw
|
|
224
|
+
return a.index - b.index
|
|
225
|
+
})
|
|
226
|
+
.map((entry) => entry.tab)
|
|
227
|
+
}
|
package/src/state/store.ts
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useStore } from 'zustand'
|
|
4
|
+
import { createStore } from 'zustand/vanilla'
|
|
5
|
+
|
|
6
|
+
import { createPrefixedId } from '../platform/id'
|
|
7
|
+
|
|
8
|
+
export type ToastVariant = 'info' | 'success' | 'warning' | 'error'
|
|
9
|
+
export type ToastPosition = `${'top' | 'bottom'}-${'left' | 'center' | 'right'}`
|
|
10
|
+
|
|
11
|
+
export interface ToastOptions {
|
|
12
|
+
title?: string
|
|
13
|
+
message?: string
|
|
14
|
+
/** Custom body — rendered inside the toast shell instead of title/message. */
|
|
15
|
+
content?: ReactNode
|
|
16
|
+
variant?: ToastVariant
|
|
17
|
+
/** Auto-dismiss after this many ms; 0 keeps it until dismissed. */
|
|
18
|
+
durationMs?: number
|
|
19
|
+
position?: ToastPosition
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Toast {
|
|
23
|
+
id: string
|
|
24
|
+
createdAt: number
|
|
25
|
+
content?: ReactNode
|
|
26
|
+
durationMs: number
|
|
27
|
+
message?: string
|
|
28
|
+
position: ToastPosition
|
|
29
|
+
title?: string
|
|
30
|
+
variant: ToastVariant
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const TOAST_CONFIG = {
|
|
34
|
+
defaultPosition: 'top-right' as ToastPosition,
|
|
35
|
+
// Default auto-dismiss per variant when `durationMs` isn't given. Errors (and
|
|
36
|
+
// warnings) linger longer than confirmations so they're not missed.
|
|
37
|
+
durations: { error: 7000, info: 3500, success: 3500, warning: 5000 } as Record<
|
|
38
|
+
ToastVariant,
|
|
39
|
+
number
|
|
40
|
+
>,
|
|
41
|
+
gap: 1,
|
|
42
|
+
/** Most recent toasts kept visible per position; older ones are evicted. */
|
|
43
|
+
maxVisible: 4,
|
|
44
|
+
/** Instant (no slide) — set in tests or for reduced motion. */
|
|
45
|
+
reduceMotion: false,
|
|
46
|
+
width: 40,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ToastStoreState {
|
|
50
|
+
toasts: Toast[]
|
|
51
|
+
show: (opts: ToastOptions) => string
|
|
52
|
+
dismiss: (id: string) => void
|
|
53
|
+
clear: () => void
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const toastStore = createStore<ToastStoreState>((set) => ({
|
|
57
|
+
clear: () => set({ toasts: [] }),
|
|
58
|
+
dismiss: (id) => set((state) => ({ toasts: state.toasts.filter((entry) => entry.id !== id) })),
|
|
59
|
+
show: (opts) => {
|
|
60
|
+
const id = createPrefixedId('toast')
|
|
61
|
+
const variant = opts.variant ?? 'info'
|
|
62
|
+
const next: Toast = {
|
|
63
|
+
content: opts.content,
|
|
64
|
+
createdAt: Date.now(),
|
|
65
|
+
durationMs: opts.durationMs ?? TOAST_CONFIG.durations[variant],
|
|
66
|
+
id,
|
|
67
|
+
message: opts.message,
|
|
68
|
+
position: opts.position ?? TOAST_CONFIG.defaultPosition,
|
|
69
|
+
title: opts.title,
|
|
70
|
+
variant,
|
|
71
|
+
}
|
|
72
|
+
set((state) => ({ toasts: [...state.toasts, next] }))
|
|
73
|
+
return id
|
|
74
|
+
},
|
|
75
|
+
toasts: [],
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
export function useToastStore<T>(selector: (state: ToastStoreState) => T): T {
|
|
79
|
+
return useStore(toastStore, selector)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function variantShortcut(variant: ToastVariant) {
|
|
83
|
+
return (message: string, opts: Omit<ToastOptions, 'message' | 'variant'> = {}): string =>
|
|
84
|
+
toastStore.getState().show({ ...opts, message, variant })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Imperative API usable anywhere (components, side effects) without React.
|
|
88
|
+
export const toast = {
|
|
89
|
+
dismiss: (id: string) => toastStore.getState().dismiss(id),
|
|
90
|
+
error: variantShortcut('error'),
|
|
91
|
+
info: variantShortcut('info'),
|
|
92
|
+
show: (opts: ToastOptions) => toastStore.getState().show(opts),
|
|
93
|
+
success: variantShortcut('success'),
|
|
94
|
+
warning: variantShortcut('warning'),
|
|
95
|
+
}
|
package/src/state/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ModeId, SnippetVar } from '@brimveyn/aimux-config'
|
|
2
2
|
import type { ThemedToken } from 'shiki'
|
|
3
3
|
|
|
4
|
+
import type { LayoutNode, SplitDirection } from './layout-tree'
|
|
5
|
+
|
|
4
6
|
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
|
|
5
7
|
|
|
6
8
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
@@ -43,6 +45,7 @@ export type ModalType =
|
|
|
43
45
|
| 'git-commit'
|
|
44
46
|
| 'update-available'
|
|
45
47
|
| 'ai-usage'
|
|
48
|
+
| 'worktree-move'
|
|
46
49
|
| null
|
|
47
50
|
|
|
48
51
|
export interface TerminalSpan {
|
|
@@ -97,6 +100,7 @@ export interface PersistedTabSnapshot {
|
|
|
97
100
|
scrollIntent?: ScrollIntent
|
|
98
101
|
errorMessage?: string
|
|
99
102
|
exitCode?: number
|
|
103
|
+
worktreeId?: string
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
export interface WorkspaceSnapshotV1 {
|
|
@@ -108,11 +112,28 @@ export interface WorkspaceSnapshotV1 {
|
|
|
108
112
|
width: number
|
|
109
113
|
}
|
|
110
114
|
tabs: PersistedTabSnapshot[]
|
|
111
|
-
layoutTree?:
|
|
112
|
-
layoutTrees?: Record<string,
|
|
115
|
+
layoutTree?: LayoutNode
|
|
116
|
+
layoutTrees?: Record<string, LayoutNode>
|
|
113
117
|
tabGroupMap?: Record<string, string>
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
export type WorktreeSource = 'primary' | 'aimux-temp' | 'external'
|
|
121
|
+
|
|
122
|
+
export interface WorktreeRecord {
|
|
123
|
+
id: string
|
|
124
|
+
name: string
|
|
125
|
+
path: string
|
|
126
|
+
repoRoot: string
|
|
127
|
+
branch?: string
|
|
128
|
+
baseRef?: string
|
|
129
|
+
commitSha?: string
|
|
130
|
+
source: WorktreeSource
|
|
131
|
+
createdByAimux: boolean
|
|
132
|
+
color?: string
|
|
133
|
+
createdAt: string
|
|
134
|
+
updatedAt: string
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
export interface SessionRecord {
|
|
117
138
|
id: string
|
|
118
139
|
name: string
|
|
@@ -122,6 +143,8 @@ export interface SessionRecord {
|
|
|
122
143
|
lastOpenedAt: string
|
|
123
144
|
order?: number
|
|
124
145
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
146
|
+
worktrees?: WorktreeRecord[]
|
|
147
|
+
activeWorktreeId?: string
|
|
125
148
|
}
|
|
126
149
|
|
|
127
150
|
export type SessionBarPosition = 'top' | 'bottom'
|
|
@@ -144,6 +167,7 @@ export interface TabSession {
|
|
|
144
167
|
command: string
|
|
145
168
|
errorMessage?: string
|
|
146
169
|
exitCode?: number
|
|
170
|
+
worktreeId?: string
|
|
147
171
|
}
|
|
148
172
|
|
|
149
173
|
export interface SidebarState {
|
|
@@ -160,7 +184,9 @@ export type GitPanePathConfig =
|
|
|
160
184
|
| { enabled: false }
|
|
161
185
|
| { enabled: true; pathFn?: (path: string) => string }
|
|
162
186
|
|
|
163
|
-
export
|
|
187
|
+
export interface GitPaneDiffCountConfig {
|
|
188
|
+
enabled: boolean
|
|
189
|
+
}
|
|
164
190
|
|
|
165
191
|
export interface GitPaneState {
|
|
166
192
|
visible: boolean
|
|
@@ -271,6 +297,8 @@ export interface GitModeState {
|
|
|
271
297
|
folds: Record<string, Record<string, FoldState>>
|
|
272
298
|
/** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
|
|
273
299
|
headOffset: number
|
|
300
|
+
/** When true, diff the active worktree's working tree against its fork point. */
|
|
301
|
+
reviewBase: boolean
|
|
274
302
|
}
|
|
275
303
|
|
|
276
304
|
interface ModalBase {
|
|
@@ -289,6 +317,16 @@ export interface ModalClosed extends ModalBase {
|
|
|
289
317
|
export interface ModalNewTab extends ModalBase {
|
|
290
318
|
type: 'new-tab'
|
|
291
319
|
editingCommand: AssistantId | null
|
|
320
|
+
activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
|
|
321
|
+
branchError: string | null
|
|
322
|
+
branchName: string
|
|
323
|
+
createWorktree: boolean
|
|
324
|
+
selectedAssistantId: AssistantId | null
|
|
325
|
+
step: 'assistant' | 'worktree' | 'worktree-create'
|
|
326
|
+
targetWorktreeIndex: number
|
|
327
|
+
worktreeDeleteConfirmId: string | null
|
|
328
|
+
worktreeDeleteMessage: string | null
|
|
329
|
+
worktreeName: string
|
|
292
330
|
}
|
|
293
331
|
|
|
294
332
|
export interface ModalSessionPicker extends ModalBase {
|
|
@@ -326,7 +364,7 @@ export interface ModalHelp extends ModalBase {
|
|
|
326
364
|
|
|
327
365
|
export interface ModalSplitPicker extends ModalBase {
|
|
328
366
|
type: 'split-picker'
|
|
329
|
-
splitDirection:
|
|
367
|
+
splitDirection: SplitDirection
|
|
330
368
|
}
|
|
331
369
|
|
|
332
370
|
export interface ModalGitCommit extends ModalBase {
|
|
@@ -366,6 +404,13 @@ export interface ModalAIUsage extends ModalBase {
|
|
|
366
404
|
type: 'ai-usage'
|
|
367
405
|
}
|
|
368
406
|
|
|
407
|
+
export interface ModalWorktreeMove extends ModalBase {
|
|
408
|
+
type: 'worktree-move'
|
|
409
|
+
/** The worktree being moved (may differ from the active one, e.g. a tab menu). */
|
|
410
|
+
sourceWorktreeId: string
|
|
411
|
+
deleteSource: boolean
|
|
412
|
+
}
|
|
413
|
+
|
|
369
414
|
export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
|
|
370
415
|
|
|
371
416
|
export interface DirectoryResult {
|
|
@@ -388,6 +433,7 @@ export type ModalState =
|
|
|
388
433
|
| ModalGitCommit
|
|
389
434
|
| ModalUpdateAvailable
|
|
390
435
|
| ModalAIUsage
|
|
436
|
+
| ModalWorktreeMove
|
|
391
437
|
|
|
392
438
|
export interface LayoutState {
|
|
393
439
|
terminalCols: number
|
|
@@ -423,7 +469,7 @@ export const EMPTY_MULTI_REPO_STATE: MultiRepoState = { prefixes: {}, repos: []
|
|
|
423
469
|
export interface AppState {
|
|
424
470
|
tabs: TabSession[]
|
|
425
471
|
activeTabId: string | null
|
|
426
|
-
layoutTrees: Record<string,
|
|
472
|
+
layoutTrees: Record<string, LayoutNode>
|
|
427
473
|
tabGroupMap: Record<string, string>
|
|
428
474
|
sessions: SessionRecord[]
|
|
429
475
|
currentSessionId: string | null
|
|
@@ -440,6 +486,11 @@ export interface AppState {
|
|
|
440
486
|
gitMode: GitModeState
|
|
441
487
|
autoCommit: AutoCommitState
|
|
442
488
|
multiRepo: MultiRepoState
|
|
489
|
+
/**
|
|
490
|
+
* Commits each worktree's branch is ahead/behind the ref it forked from,
|
|
491
|
+
* keyed by worktree id. Ephemeral (polled); not persisted to the catalog.
|
|
492
|
+
*/
|
|
493
|
+
worktreeDivergence: Record<string, BranchDivergence>
|
|
443
494
|
/** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
|
|
444
495
|
pendingChords: string[] | null
|
|
445
496
|
}
|
|
@@ -448,9 +499,18 @@ export interface AppState {
|
|
|
448
499
|
export type ModalAction =
|
|
449
500
|
| { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
|
|
450
501
|
| { type: 'open-new-tab-modal' }
|
|
502
|
+
| { type: 'set-new-tab-branch-error'; message: string | null }
|
|
503
|
+
| {
|
|
504
|
+
type: 'set-new-tab-worktree-delete-state'
|
|
505
|
+
confirmWorktreeId?: string | null
|
|
506
|
+
message: string | null
|
|
507
|
+
}
|
|
508
|
+
| { type: 'enter-new-tab-worktree-create' }
|
|
509
|
+
| { type: 'select-new-tab-assistant'; assistantId?: AssistantId }
|
|
510
|
+
| { type: 'toggle-new-tab-worktree'; assistantId?: AssistantId }
|
|
451
511
|
| { type: 'open-edit-custom-command'; assistantId: AssistantId }
|
|
452
512
|
| { type: 'open-help-modal'; scope?: ModeId }
|
|
453
|
-
| { type: 'open-split-picker'; direction:
|
|
513
|
+
| { type: 'open-split-picker'; direction: SplitDirection }
|
|
454
514
|
| { type: 'open-session-picker' }
|
|
455
515
|
| {
|
|
456
516
|
type: 'open-session-name-modal'
|
|
@@ -475,6 +535,8 @@ export type ModalAction =
|
|
|
475
535
|
| { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
|
|
476
536
|
| { type: 'set-modal-selection-index'; index: number }
|
|
477
537
|
| { type: 'open-ai-usage-modal' }
|
|
538
|
+
| { type: 'open-worktree-move-modal'; sourceWorktreeId: string }
|
|
539
|
+
| { type: 'toggle-worktree-move-delete' }
|
|
478
540
|
|
|
479
541
|
// -- Session actions --
|
|
480
542
|
export type SessionAction =
|
|
@@ -485,6 +547,15 @@ export type SessionAction =
|
|
|
485
547
|
| { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
|
|
486
548
|
| { type: 'reorder-sessions'; orderedIds: string[] }
|
|
487
549
|
| { type: 'set-session-status'; sessionId: string; status: SessionStatus }
|
|
550
|
+
| { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
|
|
551
|
+
| { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
|
|
552
|
+
| { type: 'set-active-worktree'; sessionId: string; worktreeId: string }
|
|
553
|
+
| {
|
|
554
|
+
type: 'update-worktree-record'
|
|
555
|
+
sessionId: string
|
|
556
|
+
worktreeId: string
|
|
557
|
+
patch: Partial<WorktreeRecord>
|
|
558
|
+
}
|
|
488
559
|
|
|
489
560
|
// -- Tab actions --
|
|
490
561
|
export type TabAction =
|
|
@@ -493,8 +564,8 @@ export type TabAction =
|
|
|
493
564
|
type: 'hydrate-workspace'
|
|
494
565
|
tabs: TabSession[]
|
|
495
566
|
activeTabId: string | null
|
|
496
|
-
layoutTree?:
|
|
497
|
-
layoutTrees?: Record<string,
|
|
567
|
+
layoutTree?: LayoutNode | null
|
|
568
|
+
layoutTrees?: Record<string, LayoutNode>
|
|
498
569
|
tabGroupMap?: Record<string, string>
|
|
499
570
|
}
|
|
500
571
|
| { type: 'close-tab'; tabId: string }
|
|
@@ -520,7 +591,7 @@ export type TabAction =
|
|
|
520
591
|
export type LayoutAction =
|
|
521
592
|
| {
|
|
522
593
|
type: 'split-pane'
|
|
523
|
-
direction:
|
|
594
|
+
direction: SplitDirection
|
|
524
595
|
newTab: TabSession
|
|
525
596
|
}
|
|
526
597
|
| { type: 'close-pane'; tabId: string }
|
|
@@ -532,13 +603,13 @@ export type LayoutAction =
|
|
|
532
603
|
type: 'resize-pane'
|
|
533
604
|
tabId: string
|
|
534
605
|
delta: number
|
|
535
|
-
axis?:
|
|
606
|
+
axis?: SplitDirection
|
|
536
607
|
}
|
|
537
608
|
| {
|
|
538
609
|
type: 'set-split-ratio'
|
|
539
610
|
tabId: string
|
|
540
611
|
ratio: number
|
|
541
|
-
axis?:
|
|
612
|
+
axis?: SplitDirection
|
|
542
613
|
}
|
|
543
614
|
|
|
544
615
|
// -- UI actions --
|
|
@@ -566,10 +637,17 @@ export interface GitRefreshPayload {
|
|
|
566
637
|
files: GitFileEntry[]
|
|
567
638
|
}
|
|
568
639
|
|
|
640
|
+
/** Commits a worktree branch is ahead/behind the ref it forked from. */
|
|
641
|
+
export interface BranchDivergence {
|
|
642
|
+
ahead: number
|
|
643
|
+
behind: number
|
|
644
|
+
}
|
|
645
|
+
|
|
569
646
|
export type GitPanelAction =
|
|
570
647
|
| { type: 'git-refresh-success'; payload: GitRefreshPayload }
|
|
571
648
|
| { type: 'git-refresh-error'; kind: GitPanelError }
|
|
572
649
|
| { type: 'git-panel-reset' }
|
|
650
|
+
| { type: 'set-worktree-divergence'; divergence: Record<string, BranchDivergence> }
|
|
573
651
|
|
|
574
652
|
// -- Auto-commit state & actions --
|
|
575
653
|
export type AutoCommitSuggestion =
|
|
@@ -647,8 +725,8 @@ export type GitModeAction =
|
|
|
647
725
|
key: string
|
|
648
726
|
hash: string
|
|
649
727
|
themeId: string
|
|
650
|
-
add:
|
|
651
|
-
del:
|
|
728
|
+
add: { start: number; tokens: unknown }[]
|
|
729
|
+
del: { start: number; tokens: unknown }[]
|
|
652
730
|
}
|
|
653
731
|
| { type: 'git-mode-invalidate-diffs'; paths: string[] }
|
|
654
732
|
| { type: 'git-mode-set-loading'; key: string; loading: boolean }
|
|
@@ -657,6 +735,7 @@ export type GitModeAction =
|
|
|
657
735
|
| { type: 'git-mode-set-message'; message: string | null }
|
|
658
736
|
| { type: 'snippet-picker-set-message'; message: string | null }
|
|
659
737
|
| { type: 'git-mode-toggle-diff-view' }
|
|
738
|
+
| { type: 'git-mode-toggle-review-base' }
|
|
660
739
|
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
661
740
|
| { type: 'git-mode-set-head-offset'; offset: number }
|
|
662
741
|
| {
|
package/src/state/validation.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TerminalModeState,
|
|
6
6
|
TerminalSnapshot,
|
|
7
7
|
WorkspaceSnapshotV1,
|
|
8
|
+
WorktreeRecord,
|
|
8
9
|
} from './types'
|
|
9
10
|
|
|
10
11
|
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -105,6 +106,24 @@ function isStringRecord(value: unknown): boolean {
|
|
|
105
106
|
return Object.values(value).every(isString)
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
function isWorktreeRecord(value: unknown): value is WorktreeRecord {
|
|
110
|
+
return (
|
|
111
|
+
isObjectRecord(value) &&
|
|
112
|
+
isString(value.id) &&
|
|
113
|
+
isString(value.name) &&
|
|
114
|
+
isString(value.path) &&
|
|
115
|
+
isString(value.repoRoot) &&
|
|
116
|
+
(value.branch === undefined || isString(value.branch)) &&
|
|
117
|
+
(value.baseRef === undefined || isString(value.baseRef)) &&
|
|
118
|
+
(value.commitSha === undefined || isString(value.commitSha)) &&
|
|
119
|
+
(value.source === 'primary' || value.source === 'aimux-temp' || value.source === 'external') &&
|
|
120
|
+
isBoolean(value.createdByAimux) &&
|
|
121
|
+
(value.color === undefined || isString(value.color)) &&
|
|
122
|
+
isString(value.createdAt) &&
|
|
123
|
+
isString(value.updatedAt)
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
108
127
|
export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapshotV1 {
|
|
109
128
|
return (
|
|
110
129
|
isObjectRecord(value) &&
|
|
@@ -132,7 +151,8 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
|
|
|
132
151
|
(tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
|
|
133
152
|
(tab.scrollIntent === undefined || isScrollIntent(tab.scrollIntent)) &&
|
|
134
153
|
(tab.errorMessage === undefined || isString(tab.errorMessage)) &&
|
|
135
|
-
(tab.exitCode === undefined || isFiniteNumber(tab.exitCode))
|
|
154
|
+
(tab.exitCode === undefined || isFiniteNumber(tab.exitCode)) &&
|
|
155
|
+
(tab.worktreeId === undefined || isString(tab.worktreeId))
|
|
136
156
|
) &&
|
|
137
157
|
(value.layoutTree === undefined || isLayoutNode(value.layoutTree)) &&
|
|
138
158
|
(value.layoutTrees === undefined || isLayoutTreesMap(value.layoutTrees)) &&
|
|
@@ -151,7 +171,10 @@ export function isSessionRecord(value: unknown): value is SessionRecord {
|
|
|
151
171
|
isString(value.lastOpenedAt) &&
|
|
152
172
|
(value.order === undefined ||
|
|
153
173
|
(typeof value.order === 'number' && Number.isFinite(value.order))) &&
|
|
154
|
-
(value.workspaceSnapshot === undefined || isWorkspaceSnapshotV1(value.workspaceSnapshot))
|
|
174
|
+
(value.workspaceSnapshot === undefined || isWorkspaceSnapshotV1(value.workspaceSnapshot)) &&
|
|
175
|
+
(value.worktrees === undefined ||
|
|
176
|
+
(Array.isArray(value.worktrees) && value.worktrees.every(isWorktreeRecord))) &&
|
|
177
|
+
(value.activeWorktreeId === undefined || isString(value.activeWorktreeId))
|
|
155
178
|
)
|
|
156
179
|
}
|
|
157
180
|
|