@brimveyn/aimux 1.12.2 → 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/pty-write.ts +52 -0
- 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 +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- 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 +143 -8
- package/src/app.tsx +25 -9
- 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 +13 -1
- package/src/input/raw-input-handler.ts +34 -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 +18 -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 +8 -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 +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- 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 +385 -24
- 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/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- 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-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- 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 +65 -14
- 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
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type WorkspaceSnapshotV1,
|
|
18
18
|
} from '../state/types'
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface SessionRegistryEvents {
|
|
21
21
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
22
22
|
exit: [tabId: string, exitCode: number]
|
|
23
23
|
error: [tabId: string, message: string]
|
|
@@ -81,7 +81,9 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
81
81
|
this.tabs.set(tab.id, tab)
|
|
82
82
|
}
|
|
83
83
|
this.activeTabId =
|
|
84
|
-
snapshot.activeTabId
|
|
84
|
+
snapshot.activeTabId != null &&
|
|
85
|
+
snapshot.activeTabId !== '' &&
|
|
86
|
+
restoredTabs.some((tab) => tab.id === snapshot.activeTabId)
|
|
85
87
|
? snapshot.activeTabId
|
|
86
88
|
: (restoredTabs[0]?.id ?? null)
|
|
87
89
|
} else if (this.tabs.size > 0 && snapshot) {
|
|
@@ -90,9 +92,14 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
90
92
|
if (existing) {
|
|
91
93
|
existing.title = persisted.title
|
|
92
94
|
existing.scrollIntent = persisted.scrollIntent ?? DEFAULT_SCROLL_INTENT
|
|
95
|
+
existing.worktreeId = persisted.worktreeId
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
|
-
if (
|
|
98
|
+
if (
|
|
99
|
+
snapshot.activeTabId != null &&
|
|
100
|
+
snapshot.activeTabId !== '' &&
|
|
101
|
+
this.tabs.has(snapshot.activeTabId)
|
|
102
|
+
) {
|
|
96
103
|
this.activeTabId = snapshot.activeTabId
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -64,7 +64,7 @@ export function processFile(
|
|
|
64
64
|
const lines = hunk.split(SPLIT_WITH_NEWLINES)
|
|
65
65
|
const firstLine = lines.shift()
|
|
66
66
|
if (firstLine == null) {
|
|
67
|
-
if (throwOnError) throw Error('parsePatchContent: invalid hunk')
|
|
67
|
+
if (throwOnError) throw new Error('parsePatchContent: invalid hunk')
|
|
68
68
|
else console.error('parsePatchContent: invalid hunk', hunk)
|
|
69
69
|
continue
|
|
70
70
|
}
|
|
@@ -73,7 +73,7 @@ export function processFile(
|
|
|
73
73
|
let deletionLines = 0
|
|
74
74
|
if (fileHeaderMatch == null || currentFile == null) {
|
|
75
75
|
if (currentFile != null) {
|
|
76
|
-
if (throwOnError) throw Error('parsePatchContent: Invalid hunk')
|
|
76
|
+
if (throwOnError) throw new Error('parsePatchContent: Invalid hunk')
|
|
77
77
|
else console.error('parsePatchContent: Invalid hunk', hunk)
|
|
78
78
|
continue
|
|
79
79
|
}
|
|
@@ -139,8 +139,9 @@ export function processFile(
|
|
|
139
139
|
currentFile.mode = line.replace('deleted file mode', '').trim()
|
|
140
140
|
}
|
|
141
141
|
if (line.startsWith('similarity index')) {
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
currentFile.type = line.startsWith('similarity index 100%')
|
|
143
|
+
? 'rename-pure'
|
|
144
|
+
: 'rename-changed'
|
|
144
145
|
}
|
|
145
146
|
if (line.startsWith('index ')) {
|
|
146
147
|
const [, prevObjectId, newObjectId, mode] = line.trim().match(INDEX_LINE_METADATA) ?? []
|
|
@@ -162,24 +163,24 @@ export function processFile(
|
|
|
162
163
|
let lastLineType: 'addition' | 'deletion' | 'context' | undefined
|
|
163
164
|
while (
|
|
164
165
|
lines.length > 0 &&
|
|
165
|
-
(lines
|
|
166
|
-
lines
|
|
167
|
-
lines
|
|
168
|
-
lines
|
|
166
|
+
(lines.at(-1) === '\n' ||
|
|
167
|
+
lines.at(-1) === '\r' ||
|
|
168
|
+
lines.at(-1) === '\r\n' ||
|
|
169
|
+
lines.at(-1) === '')
|
|
169
170
|
) {
|
|
170
171
|
lines.pop()
|
|
171
172
|
}
|
|
172
|
-
const additionStart = parseInt(fileHeaderMatch[3] ?? '')
|
|
173
|
-
const deletionStart = parseInt(fileHeaderMatch[1] ?? '')
|
|
173
|
+
const additionStart = parseInt(fileHeaderMatch[3] ?? '', 10)
|
|
174
|
+
const deletionStart = parseInt(fileHeaderMatch[1] ?? '', 10)
|
|
174
175
|
deletionLineIndex = isPartial ? deletionLineIndex : deletionStart - 1
|
|
175
176
|
additionLineIndex = isPartial ? additionLineIndex : additionStart - 1
|
|
176
177
|
const hunkData = {
|
|
177
|
-
additionCount: parseInt(fileHeaderMatch[4] ?? '1'),
|
|
178
|
+
additionCount: parseInt(fileHeaderMatch[4] ?? '1', 10),
|
|
178
179
|
additionLineIndex,
|
|
179
180
|
additionLines,
|
|
180
181
|
additionStart,
|
|
181
182
|
collapsedBefore: 0,
|
|
182
|
-
deletionCount: parseInt(fileHeaderMatch[2] ?? '1'),
|
|
183
|
+
deletionCount: parseInt(fileHeaderMatch[2] ?? '1', 10),
|
|
183
184
|
deletionLineIndex,
|
|
184
185
|
deletionLines,
|
|
185
186
|
deletionStart,
|
|
@@ -199,7 +200,7 @@ export function processFile(
|
|
|
199
200
|
isNaN(hunkData.additionStart) ||
|
|
200
201
|
isNaN(hunkData.deletionStart)
|
|
201
202
|
) {
|
|
202
|
-
if (throwOnError) throw Error('parsePatchContent: invalid hunk metadata')
|
|
203
|
+
if (throwOnError) throw new Error('parsePatchContent: invalid hunk metadata')
|
|
203
204
|
else console.error('parsePatchContent: invalid hunk metadata', hunkData)
|
|
204
205
|
continue
|
|
205
206
|
}
|
|
@@ -285,7 +286,7 @@ export function processFile(
|
|
|
285
286
|
currentFile.unifiedLineCount += hunkData.collapsedBefore + hunkData.unifiedLineCount
|
|
286
287
|
}
|
|
287
288
|
if (currentFile == null) return
|
|
288
|
-
const lastHunk = currentFile.hunks
|
|
289
|
+
const lastHunk = currentFile.hunks.at(-1)
|
|
289
290
|
if (
|
|
290
291
|
lastHunk != null &&
|
|
291
292
|
!isPartial &&
|
|
@@ -300,8 +301,7 @@ export function processFile(
|
|
|
300
301
|
}
|
|
301
302
|
if (!isGitDiff) {
|
|
302
303
|
if (currentFile.prevName != null && currentFile.name !== currentFile.prevName) {
|
|
303
|
-
|
|
304
|
-
else currentFile.type = 'rename-pure'
|
|
304
|
+
currentFile.type = currentFile.hunks.length > 0 ? 'rename-changed' : 'rename-pure'
|
|
305
305
|
} else if (newFile != null && newFile.contents === '') currentFile.type = 'deleted'
|
|
306
306
|
else if (oldFile != null && oldFile.contents === '') currentFile.type = 'new'
|
|
307
307
|
}
|
|
@@ -323,12 +323,12 @@ export function processPatch(
|
|
|
323
323
|
for (const fileOrPatchMetadata of rawFiles) {
|
|
324
324
|
if (isGitDiff && !GIT_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
325
325
|
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
326
|
-
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
326
|
+
else if (throwOnError) throw new Error('parsePatchContent: unknown file blob')
|
|
327
327
|
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
328
328
|
continue
|
|
329
329
|
} else if (!isGitDiff && !UNIFIED_DIFF_FILE_BREAK_REGEX.test(fileOrPatchMetadata)) {
|
|
330
330
|
if (patchMetadata == null) patchMetadata = fileOrPatchMetadata
|
|
331
|
-
else if (throwOnError) throw Error('parsePatchContent: unknown file blob')
|
|
331
|
+
else if (throwOnError) throw new Error('parsePatchContent: unknown file blob')
|
|
332
332
|
else console.error('parsePatchContent: unknown file blob:', fileOrPatchMetadata)
|
|
333
333
|
continue
|
|
334
334
|
}
|
package/src/git/command-queue.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
let tail: Promise<unknown> = Promise.resolve()
|
|
2
2
|
|
|
3
|
-
export function enqueueGitOp<T>(op: () => Promise<T>): Promise<T> {
|
|
3
|
+
export async function enqueueGitOp<T>(op: () => Promise<T>): Promise<T> {
|
|
4
4
|
const next = tail.then(op, op)
|
|
5
5
|
tail = next.catch(() => {})
|
|
6
6
|
return next
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
import type { BranchDivergence } from '../state/types'
|
|
4
|
+
|
|
5
|
+
// Parses `git rev-list --left-right --count <base>...<branch>`, which prints two
|
|
6
|
+
// whitespace-separated counts: "<left> <right>". With the `<base>...<branch>`
|
|
7
|
+
// range the left count is commits in base but not branch (how far the branch is
|
|
8
|
+
// *behind* its base) and the right count is commits in branch but not base (how
|
|
9
|
+
// far it is *ahead*).
|
|
10
|
+
export function parseDivergenceCount(output: string): BranchDivergence | undefined {
|
|
11
|
+
const match = /(\d+)\s+(\d+)/.exec(output.trim())
|
|
12
|
+
if (match == null) return undefined
|
|
13
|
+
return {
|
|
14
|
+
ahead: Number.parseInt(match[2] ?? '', 10),
|
|
15
|
+
behind: Number.parseInt(match[1] ?? '', 10),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Commits a worktree branch is ahead/behind the ref it forked from. Returns
|
|
20
|
+
// undefined when the refs can't be compared (missing ref, not a repo, …) so
|
|
21
|
+
// callers can simply render nothing.
|
|
22
|
+
export async function getBranchDivergence(
|
|
23
|
+
repoPath: string,
|
|
24
|
+
baseRef: string,
|
|
25
|
+
branch: string
|
|
26
|
+
): Promise<BranchDivergence | undefined> {
|
|
27
|
+
const range = `${baseRef}...${branch}`
|
|
28
|
+
const result = await $`git -C ${repoPath} rev-list --left-right --count ${range}`
|
|
29
|
+
.quiet()
|
|
30
|
+
.nothrow()
|
|
31
|
+
if (result.exitCode !== 0) return undefined
|
|
32
|
+
return parseDivergenceCount(result.text())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Fork point: the most recent commit shared by baseRef and ref. Diffing the
|
|
36
|
+
// working tree against this shows everything the worktree changed since it
|
|
37
|
+
// branched off (commits + staged + unstaged) — i.e. what a squash-move lands.
|
|
38
|
+
export async function getMergeBase(
|
|
39
|
+
repoPath: string,
|
|
40
|
+
baseRef: string,
|
|
41
|
+
ref = 'HEAD'
|
|
42
|
+
): Promise<string | undefined> {
|
|
43
|
+
const result = await $`git -C ${repoPath} merge-base ${baseRef} ${ref}`.quiet().nothrow()
|
|
44
|
+
if (result.exitCode !== 0) return undefined
|
|
45
|
+
return result.text().trim() || undefined
|
|
46
|
+
}
|
package/src/git/git-diff.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type { DiffData, DiffFileStatus, GitFileEntry } from '../state/types'
|
|
|
5
5
|
import { imageFormatLabel, imageMimeFromPath, isImagePath } from './image-detect'
|
|
6
6
|
|
|
7
7
|
function resolveStatus(entry: GitFileEntry): { status: DiffFileStatus; oldPath?: string } {
|
|
8
|
-
if (entry.renamedFrom
|
|
8
|
+
if (entry.renamedFrom != null && entry.renamedFrom !== '')
|
|
9
|
+
return { oldPath: entry.renamedFrom, status: 'renamed' }
|
|
9
10
|
if (entry.section === 'untracked' || entry.status === '?') return { status: 'new' }
|
|
10
11
|
if (entry.status === 'D') return { status: 'deleted' }
|
|
11
12
|
if (entry.status === 'A' && (entry.section === 'staged' || entry.section === 'historical')) {
|
|
@@ -85,13 +86,19 @@ async function rawUnifiedDiff(
|
|
|
85
86
|
return result.text()
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
function resolveCompareRef(headOffset: number, compareRef: string | undefined): string {
|
|
90
|
+
if (compareRef != null && compareRef !== '') return compareRef
|
|
91
|
+
return headOffset > 0 ? `HEAD~${headOffset}` : 'HEAD'
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
export async function fetchDiff(
|
|
89
95
|
cwd: string,
|
|
90
96
|
file: GitFileEntry,
|
|
91
|
-
headOffset: number = 0
|
|
97
|
+
headOffset: number = 0,
|
|
98
|
+
compareRef?: string
|
|
92
99
|
): Promise<DiffData> {
|
|
93
100
|
const { oldPath, status } = resolveStatus(file)
|
|
94
|
-
const ref = headOffset
|
|
101
|
+
const ref = resolveCompareRef(headOffset, compareRef)
|
|
95
102
|
|
|
96
103
|
if (isImagePath(file.path)) {
|
|
97
104
|
const headPath = oldPath ?? file.path
|
|
@@ -112,7 +119,7 @@ export async function fetchDiff(
|
|
|
112
119
|
}
|
|
113
120
|
if (imageBytesBefore) data.imageBytesBefore = imageBytesBefore
|
|
114
121
|
if (imageBytesAfter) data.imageBytesAfter = imageBytesAfter
|
|
115
|
-
if (oldPath) data.oldPath = oldPath
|
|
122
|
+
if (oldPath != null && oldPath !== '') data.oldPath = oldPath
|
|
116
123
|
return data
|
|
117
124
|
}
|
|
118
125
|
|
|
@@ -137,6 +144,6 @@ export async function fetchDiff(
|
|
|
137
144
|
rawDiff,
|
|
138
145
|
status,
|
|
139
146
|
}
|
|
140
|
-
if (oldPath) data.oldPath = oldPath
|
|
147
|
+
if (oldPath != null && oldPath !== '') data.oldPath = oldPath
|
|
141
148
|
return data
|
|
142
149
|
}
|
package/src/git/git-poller.ts
CHANGED
|
@@ -13,6 +13,9 @@ interface Options {
|
|
|
13
13
|
enabled: boolean
|
|
14
14
|
projectPath: string | undefined
|
|
15
15
|
headOffset: number
|
|
16
|
+
// When set, diff the working tree against this ref (worktree fork point for
|
|
17
|
+
// review-vs-base) instead of HEAD/HEAD~N.
|
|
18
|
+
compareRef?: string
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
function tagFiles(files: GitFileEntry[], repoPath: string): GitFileEntry[] {
|
|
@@ -22,13 +25,23 @@ function tagFiles(files: GitFileEntry[], repoPath: string): GitFileEntry[] {
|
|
|
22
25
|
async function collectAggregated(
|
|
23
26
|
repos: DiscoveredRepo[],
|
|
24
27
|
fallbackCwd: string,
|
|
25
|
-
headOffset: number
|
|
28
|
+
headOffset: number,
|
|
29
|
+
compareRef: string | undefined
|
|
26
30
|
): Promise<GitCollectResult> {
|
|
27
|
-
// Historical walking (HEAD~N)
|
|
28
|
-
// the root/fallback repo
|
|
29
|
-
if (
|
|
31
|
+
// Historical walking (HEAD~N) and review-vs-base are per-repo and don't
|
|
32
|
+
// compose; fall back to the root/fallback repo to preserve existing behaviour.
|
|
33
|
+
if ((compareRef != null && compareRef !== '') || headOffset > 0) {
|
|
34
|
+
const result = await collectGitStatus(fallbackCwd, { compareRef, headOffset })
|
|
35
|
+
if (result.kind !== 'ok') return result
|
|
36
|
+
return {
|
|
37
|
+
kind: 'ok',
|
|
38
|
+
payload: { ...result.payload, files: tagFiles(result.payload.files, fallbackCwd) },
|
|
39
|
+
}
|
|
40
|
+
}
|
|
30
41
|
|
|
31
|
-
const results = await Promise.all(
|
|
42
|
+
const results = await Promise.all(
|
|
43
|
+
repos.map(async (r) => collectGitStatus(r.path, { headOffset: 0 }))
|
|
44
|
+
)
|
|
32
45
|
const files: GitFileEntry[] = []
|
|
33
46
|
let branch: string | null = null
|
|
34
47
|
let ahead = 0
|
|
@@ -58,13 +71,18 @@ async function collectAggregated(
|
|
|
58
71
|
return { kind: 'ok', payload }
|
|
59
72
|
}
|
|
60
73
|
|
|
61
|
-
export function useGitPanelPolling({
|
|
74
|
+
export function useGitPanelPolling({
|
|
75
|
+
compareRef,
|
|
76
|
+
enabled,
|
|
77
|
+
headOffset,
|
|
78
|
+
projectPath,
|
|
79
|
+
}: Options): void {
|
|
62
80
|
// Read repos from the store imperatively — changes trigger a fresh effect run
|
|
63
81
|
// because the repos identity is stable across ticks until set-repos fires.
|
|
64
82
|
const repos = useAppStore((s) => s.multiRepo.repos)
|
|
65
83
|
|
|
66
84
|
useEffect(() => {
|
|
67
|
-
if (!enabled || !projectPath) return
|
|
85
|
+
if (!enabled || !(projectPath != null && projectPath !== '')) return
|
|
68
86
|
|
|
69
87
|
dispatchGlobal({ type: 'git-panel-reset' })
|
|
70
88
|
|
|
@@ -80,11 +98,15 @@ export function useGitPanelPolling({ enabled, headOffset, projectPath }: Options
|
|
|
80
98
|
const tick = async () => {
|
|
81
99
|
const result =
|
|
82
100
|
repos.length > 0
|
|
83
|
-
? await collectAggregated(repos, projectPath, headOffset)
|
|
84
|
-
: await collectGitStatus(projectPath, { headOffset })
|
|
101
|
+
? await collectAggregated(repos, projectPath, headOffset, compareRef)
|
|
102
|
+
: await collectGitStatus(projectPath, { compareRef, headOffset })
|
|
85
103
|
if (cancelled) return
|
|
86
104
|
if (result.kind === 'ok') {
|
|
87
|
-
|
|
105
|
+
const payload =
|
|
106
|
+
repos.length === 0
|
|
107
|
+
? { ...result.payload, files: tagFiles(result.payload.files, projectPath) }
|
|
108
|
+
: result.payload
|
|
109
|
+
dispatchGlobal({ payload, type: 'git-refresh-success' })
|
|
88
110
|
delay = BASE_INTERVAL_MS
|
|
89
111
|
} else if (result.kind === 'out-of-range') {
|
|
90
112
|
dispatchGlobal({ offset: result.maxOffset, type: 'git-mode-set-head-offset' })
|
|
@@ -106,5 +128,5 @@ export function useGitPanelPolling({ enabled, headOffset, projectPath }: Options
|
|
|
106
128
|
cancelled = true
|
|
107
129
|
if (timer) clearTimeout(timer)
|
|
108
130
|
}
|
|
109
|
-
}, [enabled, projectPath, headOffset, repos])
|
|
131
|
+
}, [enabled, projectPath, headOffset, compareRef, repos])
|
|
110
132
|
}
|
package/src/git/git-status.ts
CHANGED
|
@@ -88,7 +88,7 @@ function buildEntry(
|
|
|
88
88
|
removed = 0
|
|
89
89
|
}
|
|
90
90
|
const entry: GitFileEntry = { added, path, removed, section, status }
|
|
91
|
-
if (renamedFrom) entry.renamedFrom = renamedFrom
|
|
91
|
+
if (renamedFrom != null && renamedFrom !== '') entry.renamedFrom = renamedFrom
|
|
92
92
|
return entry
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -179,7 +179,7 @@ async function countUntrackedLines(cwd: string, path: string): Promise<number |
|
|
|
179
179
|
async function annotateUntrackedCounts(cwd: string, files: GitFileEntry[]): Promise<void> {
|
|
180
180
|
const untracked = files.filter((f) => f.section === 'untracked')
|
|
181
181
|
if (untracked.length === 0) return
|
|
182
|
-
const counts = await Promise.all(untracked.map((f) => countUntrackedLines(cwd, f.path)))
|
|
182
|
+
const counts = await Promise.all(untracked.map(async (f) => countUntrackedLines(cwd, f.path)))
|
|
183
183
|
for (let i = 0; i < untracked.length; i++) {
|
|
184
184
|
const file = untracked[i]
|
|
185
185
|
const count = counts[i]
|
|
@@ -192,6 +192,9 @@ async function annotateUntrackedCounts(cwd: string, files: GitFileEntry[]): Prom
|
|
|
192
192
|
|
|
193
193
|
interface CollectOptions {
|
|
194
194
|
headOffset?: number
|
|
195
|
+
// Explicit ref to diff the working tree against (e.g. a worktree's fork
|
|
196
|
+
// point). Takes precedence over headOffset when set.
|
|
197
|
+
compareRef?: string
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
function parseNameStatus(
|
|
@@ -208,7 +211,7 @@ function parseNameStatus(
|
|
|
208
211
|
if (letter === 'R' || letter === 'C') {
|
|
209
212
|
const from = parts[1]
|
|
210
213
|
const to = parts[2]
|
|
211
|
-
if (
|
|
214
|
+
if (from == null || from === '' || !(to != null && to !== '')) continue
|
|
212
215
|
rows.push({ path: to, renamedFrom: from, status })
|
|
213
216
|
} else {
|
|
214
217
|
const path = parts.slice(1).join('\t')
|
|
@@ -252,7 +255,13 @@ async function collectAgainstHistorical(
|
|
|
252
255
|
const maxOffset = Number.isFinite(count) && count > 0 ? count - 1 : 0
|
|
253
256
|
return { kind: 'out-of-range', maxOffset }
|
|
254
257
|
}
|
|
258
|
+
return collectAgainstRef(cwd, ref)
|
|
259
|
+
}
|
|
255
260
|
|
|
261
|
+
// Diffs the working tree against an arbitrary ref (a HEAD~N commit, or a
|
|
262
|
+
// worktree's fork point for review-vs-base), surfacing changed files as a
|
|
263
|
+
// single `historical` section plus any untracked files.
|
|
264
|
+
async function collectAgainstRef(cwd: string, ref: string): Promise<GitCollectResult> {
|
|
256
265
|
const [statusResult, nameStatus, numstat, untrackedStatus] = await Promise.all([
|
|
257
266
|
$`git -C ${cwd} status --porcelain=v2 -b -z --untracked-files=all`.quiet().nothrow(),
|
|
258
267
|
$`git -C ${cwd} -c core.quotePath=false diff ${ref} --name-status`.quiet().nothrow(),
|
|
@@ -286,7 +295,9 @@ export async function collectGitStatus(
|
|
|
286
295
|
options: CollectOptions = {}
|
|
287
296
|
): Promise<GitCollectResult> {
|
|
288
297
|
const headOffset = options.headOffset ?? 0
|
|
298
|
+
const compareRef = options.compareRef
|
|
289
299
|
try {
|
|
300
|
+
if (compareRef != null && compareRef !== '') return await collectAgainstRef(cwd, compareRef)
|
|
290
301
|
return headOffset > 0
|
|
291
302
|
? await collectAgainstHistorical(cwd, headOffset)
|
|
292
303
|
: await collectAgainstHead(cwd)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
export type MoveWorktreeResult =
|
|
4
|
+
| { kind: 'ok'; filesChanged: number }
|
|
5
|
+
| { kind: 'dirty-target' }
|
|
6
|
+
| { kind: 'conflict'; files: string[] }
|
|
7
|
+
| { kind: 'error'; message: string }
|
|
8
|
+
|
|
9
|
+
async function workingTreeDirty(repoPath: string): Promise<boolean> {
|
|
10
|
+
const result = await $`git -C ${repoPath} status --porcelain`.quiet().nothrow()
|
|
11
|
+
if (result.exitCode !== 0) return false
|
|
12
|
+
return result.text().trim() !== ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Files left with conflict markers by a failed squash (unmerged index entries).
|
|
16
|
+
async function conflictedFiles(repoPath: string): Promise<string[]> {
|
|
17
|
+
const result = await $`git -C ${repoPath} diff --name-only --diff-filter=U`.quiet().nothrow()
|
|
18
|
+
if (result.exitCode !== 0) return []
|
|
19
|
+
return result
|
|
20
|
+
.text()
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((line) => line.trim())
|
|
23
|
+
.filter((line) => line !== '')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function countStaged(repoPath: string): Promise<number> {
|
|
27
|
+
const result = await $`git -C ${repoPath} diff --cached --name-only`.quiet().nothrow()
|
|
28
|
+
if (result.exitCode !== 0) return 0
|
|
29
|
+
return result
|
|
30
|
+
.text()
|
|
31
|
+
.split('\n')
|
|
32
|
+
.filter((line) => line.trim() !== '').length
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Squashes everything a source worktree changed since its fork point into the
|
|
36
|
+
// target worktree's working tree (staged, uncommitted) — committed work plus
|
|
37
|
+
// any uncommitted/untracked changes. The source is left exactly as it was; the
|
|
38
|
+
// caller decides whether to delete it. The target must be clean so two change
|
|
39
|
+
// sets are never silently fused. On conflict nothing is kept: the target is
|
|
40
|
+
// reset and the source restored.
|
|
41
|
+
export async function moveWorktree(opts: {
|
|
42
|
+
sourcePath: string
|
|
43
|
+
sourceBranch: string
|
|
44
|
+
targetPath: string
|
|
45
|
+
}): Promise<MoveWorktreeResult> {
|
|
46
|
+
const { sourceBranch, sourcePath, targetPath } = opts
|
|
47
|
+
try {
|
|
48
|
+
if (await workingTreeDirty(targetPath)) return { kind: 'dirty-target' }
|
|
49
|
+
|
|
50
|
+
const headResult = await $`git -C ${sourcePath} rev-parse HEAD`.quiet().nothrow()
|
|
51
|
+
if (headResult.exitCode !== 0) {
|
|
52
|
+
return { kind: 'error', message: 'could not resolve source HEAD' }
|
|
53
|
+
}
|
|
54
|
+
const sourceHead = headResult.text().trim()
|
|
55
|
+
|
|
56
|
+
// Capture uncommitted + untracked work in a throwaway commit so the squash
|
|
57
|
+
// includes everything; undone again before we return.
|
|
58
|
+
const tempCommitted = await workingTreeDirty(sourcePath)
|
|
59
|
+
if (tempCommitted) {
|
|
60
|
+
await $`git -C ${sourcePath} add -A`.quiet().nothrow()
|
|
61
|
+
const commit = await $`git -C ${sourcePath} commit -m ${'aimux: move WIP'} --no-verify`
|
|
62
|
+
.quiet()
|
|
63
|
+
.nothrow()
|
|
64
|
+
if (commit.exitCode !== 0) {
|
|
65
|
+
const message = commit.stderr.toString().trim()
|
|
66
|
+
return {
|
|
67
|
+
kind: 'error',
|
|
68
|
+
message: message !== '' ? message : 'failed to stage source changes',
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const restoreSource = async (): Promise<void> => {
|
|
74
|
+
if (tempCommitted) {
|
|
75
|
+
await $`git -C ${sourcePath} reset --mixed ${sourceHead}`.quiet().nothrow()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const merge = await $`git -C ${targetPath} merge --squash ${sourceBranch}`.quiet().nothrow()
|
|
80
|
+
const conflicts = await conflictedFiles(targetPath)
|
|
81
|
+
if (merge.exitCode !== 0 || conflicts.length > 0) {
|
|
82
|
+
await $`git -C ${targetPath} merge --abort`.quiet().nothrow()
|
|
83
|
+
await $`git -C ${targetPath} reset --hard HEAD`.quiet().nothrow()
|
|
84
|
+
await restoreSource()
|
|
85
|
+
if (conflicts.length > 0) return { files: conflicts, kind: 'conflict' }
|
|
86
|
+
const message = merge.stderr.toString().trim()
|
|
87
|
+
return { kind: 'error', message: message !== '' ? message : 'merge failed' }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const filesChanged = await countStaged(targetPath)
|
|
91
|
+
await restoreSource()
|
|
92
|
+
return { filesChanged, kind: 'ok' }
|
|
93
|
+
} catch (error) {
|
|
94
|
+
return { kind: 'error', message: error instanceof Error ? error.message : String(error) }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -11,10 +11,11 @@ import { discoverRepos } from './repo-discovery'
|
|
|
11
11
|
export function useRepoDiscovery(projectPath: string | undefined): void {
|
|
12
12
|
useEffect(() => {
|
|
13
13
|
const cfg = getMultiRepoConfig()
|
|
14
|
-
if (!cfg.enabled || !projectPath) {
|
|
14
|
+
if (!cfg.enabled || !(projectPath != null && projectPath !== '')) {
|
|
15
15
|
dispatchGlobal({ type: 'multi-repo-clear' })
|
|
16
16
|
return
|
|
17
17
|
}
|
|
18
|
+
dispatchGlobal({ type: 'multi-repo-clear' })
|
|
18
19
|
let cancelled = false
|
|
19
20
|
void (async () => {
|
|
20
21
|
const repos = await discoverRepos(projectPath, cfg.maxDepth)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { BranchDivergence } from '../state/types'
|
|
4
|
+
|
|
5
|
+
import { useAppStore } from '../state/app-store'
|
|
6
|
+
import { dispatchGlobal } from '../state/dispatch-ref'
|
|
7
|
+
import { getBranchDivergence } from './divergence'
|
|
8
|
+
|
|
9
|
+
const INTERVAL_MS = 4000
|
|
10
|
+
|
|
11
|
+
// Polls per-worktree base divergence for the current session while enabled and
|
|
12
|
+
// dispatches it into worktreeDivergence. Only worktrees that record a baseRef
|
|
13
|
+
// (the aimux-created ones) are measured; the primary and externally-discovered
|
|
14
|
+
// worktrees have no recorded base and are left out.
|
|
15
|
+
export function useWorktreeDivergencePolling(enabled: boolean): void {
|
|
16
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
17
|
+
const sessions = useAppStore((s) => s.sessions)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!enabled) return
|
|
21
|
+
const session =
|
|
22
|
+
currentSessionId != null && currentSessionId !== ''
|
|
23
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
24
|
+
: undefined
|
|
25
|
+
const targets = (session?.worktrees ?? []).filter(
|
|
26
|
+
(w) => w.baseRef != null && w.baseRef !== '' && w.branch != null && w.branch !== ''
|
|
27
|
+
)
|
|
28
|
+
if (targets.length === 0) return
|
|
29
|
+
|
|
30
|
+
let cancelled = false
|
|
31
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
32
|
+
|
|
33
|
+
const tick = async () => {
|
|
34
|
+
const entries = await Promise.all(
|
|
35
|
+
targets.map(async (worktree) => {
|
|
36
|
+
const base = worktree.baseRef
|
|
37
|
+
const branch = worktree.branch
|
|
38
|
+
if (base == null || branch == null) return null
|
|
39
|
+
const divergence = await getBranchDivergence(worktree.repoRoot, base, branch)
|
|
40
|
+
return divergence != null ? ([worktree.id, divergence] as const) : null
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
if (cancelled) return
|
|
44
|
+
const next: Record<string, BranchDivergence> = {}
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry != null) next[entry[0]] = entry[1]
|
|
47
|
+
}
|
|
48
|
+
dispatchGlobal({ divergence: next, type: 'set-worktree-divergence' })
|
|
49
|
+
timer = setTimeout(() => void tick(), INTERVAL_MS)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
void tick()
|
|
53
|
+
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true
|
|
56
|
+
if (timer != null) clearTimeout(timer)
|
|
57
|
+
}
|
|
58
|
+
}, [enabled, currentSessionId, sessions])
|
|
59
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
import { isInsideAimuxWorktreeRoot } from '../platform/worktree-paths'
|
|
5
|
+
|
|
6
|
+
export interface GitWorktreeInfo {
|
|
7
|
+
path: string
|
|
8
|
+
head?: string
|
|
9
|
+
branch?: string
|
|
10
|
+
// True when git still tracks the worktree but its directory is gone
|
|
11
|
+
// (e.g. a temp dir cleared on reboot). Such entries are stale admin junk.
|
|
12
|
+
prunable?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Returns the main worktree's path, i.e. the original repository checkout.
|
|
16
|
+
// `git worktree list` always reports the main worktree first, so this stays
|
|
17
|
+
// correct even when called from inside a linked worktree — unlike
|
|
18
|
+
// `rev-parse --show-toplevel`, which returns the *current* worktree's path.
|
|
19
|
+
export async function getMainWorktreeRoot(cwd: string): Promise<string | undefined> {
|
|
20
|
+
const worktrees = await listGitWorktrees(cwd)
|
|
21
|
+
return worktrees[0]?.path
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function getCurrentBranch(cwd: string): Promise<string | undefined> {
|
|
25
|
+
const result = await $`git -C ${cwd} branch --show-current`.quiet().nothrow()
|
|
26
|
+
if (result.exitCode !== 0) return undefined
|
|
27
|
+
return result.text().trim() || undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function getHeadSha(cwd: string): Promise<string | undefined> {
|
|
31
|
+
const result = await $`git -C ${cwd} rev-parse HEAD`.quiet().nothrow()
|
|
32
|
+
if (result.exitCode !== 0) return undefined
|
|
33
|
+
return result.text().trim() || undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function createGitWorktree({
|
|
37
|
+
baseRef,
|
|
38
|
+
branchName,
|
|
39
|
+
repoPath,
|
|
40
|
+
targetPath,
|
|
41
|
+
}: {
|
|
42
|
+
repoPath: string
|
|
43
|
+
targetPath: string
|
|
44
|
+
branchName: string
|
|
45
|
+
baseRef: string
|
|
46
|
+
}): Promise<void> {
|
|
47
|
+
const result = await $`git -C ${repoPath} worktree add -b ${branchName} ${targetPath} ${baseRef}`
|
|
48
|
+
.quiet()
|
|
49
|
+
.nothrow()
|
|
50
|
+
if (result.exitCode !== 0) {
|
|
51
|
+
throw new Error(result.stderr.toString().trim() || 'failed to create git worktree')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function removeGitWorktree({
|
|
56
|
+
force,
|
|
57
|
+
repoPath,
|
|
58
|
+
targetPath,
|
|
59
|
+
}: {
|
|
60
|
+
repoPath: string
|
|
61
|
+
targetPath: string
|
|
62
|
+
force: boolean
|
|
63
|
+
}): Promise<void> {
|
|
64
|
+
if (!isInsideAimuxWorktreeRoot(targetPath)) {
|
|
65
|
+
throw new Error(`refusing to delete worktree outside Aimux temp root: ${targetPath}`)
|
|
66
|
+
}
|
|
67
|
+
const result = force
|
|
68
|
+
? await $`git -C ${repoPath} worktree remove --force ${targetPath}`.quiet().nothrow()
|
|
69
|
+
: await $`git -C ${repoPath} worktree remove ${targetPath}`.quiet().nothrow()
|
|
70
|
+
if (result.exitCode === 0) return
|
|
71
|
+
// The worktree directory may already be gone, leaving only stale admin state
|
|
72
|
+
// in the main repo. Pruning clears it so deletion still reaches its goal.
|
|
73
|
+
if (!existsSync(targetPath)) {
|
|
74
|
+
await $`git -C ${repoPath} worktree prune`.quiet().nothrow()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
throw new Error(result.stderr.toString().trim() || 'failed to remove git worktree')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function listGitWorktrees(repoPath: string): Promise<GitWorktreeInfo[]> {
|
|
81
|
+
const result = await $`git -C ${repoPath} worktree list --porcelain`.quiet().nothrow()
|
|
82
|
+
if (result.exitCode !== 0) return []
|
|
83
|
+
const worktrees: GitWorktreeInfo[] = []
|
|
84
|
+
let current: GitWorktreeInfo | null = null
|
|
85
|
+
for (const line of result.text().split('\n')) {
|
|
86
|
+
if (line.startsWith('worktree ')) {
|
|
87
|
+
if (current) worktrees.push(current)
|
|
88
|
+
current = { path: line.slice('worktree '.length) }
|
|
89
|
+
} else if (current && line.startsWith('HEAD ')) {
|
|
90
|
+
current.head = line.slice('HEAD '.length)
|
|
91
|
+
} else if (current && line.startsWith('branch ')) {
|
|
92
|
+
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '')
|
|
93
|
+
} else if (current && line.startsWith('prunable')) {
|
|
94
|
+
current.prunable = true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (current) worktrees.push(current)
|
|
98
|
+
return worktrees
|
|
99
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -77,7 +77,7 @@ logDebug('index.userConfigLoaded', {
|
|
|
77
77
|
// plain code so we can re-tokenize on the snapshot. Set on process.env
|
|
78
78
|
// before backend bootstrap so the spawned daemon (and its child PTYs)
|
|
79
79
|
// inherit it.
|
|
80
|
-
if (resolvedConfig.theme?.beta?.experimentalSyntaxHighlight) {
|
|
80
|
+
if (resolvedConfig.theme?.beta?.experimentalSyntaxHighlight === true) {
|
|
81
81
|
process.env.CLAUDE_CODE_SYNTAX_HIGHLIGHT = 'false'
|
|
82
82
|
}
|
|
83
83
|
|