@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
|
-
import { connect, Socket } from 'node:net'
|
|
2
|
+
import { connect, type Socket } from 'node:net'
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
ScrollIntent,
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
parseManagerMessage,
|
|
24
24
|
} from '../ipc/manager-protocol'
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface ManagerClientEvents {
|
|
27
27
|
render: [
|
|
28
28
|
sessionId: string,
|
|
29
29
|
tabId: string,
|
|
@@ -84,7 +84,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
84
84
|
return this.socket
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
private send(request: ManagerRequest): Promise<ManagerResponse> {
|
|
87
|
+
private async send(request: ManagerRequest): Promise<ManagerResponse> {
|
|
88
88
|
const socket = this.getConnectedSocket()
|
|
89
89
|
logDebug('managerClient.send', { id: request.id, type: request.type })
|
|
90
90
|
return new Promise((resolve, reject) => {
|
|
@@ -286,7 +286,9 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
286
286
|
return response.payload
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
createTab(
|
|
289
|
+
async createTab(
|
|
290
|
+
options: Extract<ManagerRequest, { type: 'createTab' }>['payload']
|
|
291
|
+
): Promise<void> {
|
|
290
292
|
logDebug('managerClient.createTab', {
|
|
291
293
|
command: options.command,
|
|
292
294
|
sessionId: options.sessionId,
|
|
@@ -296,7 +298,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
296
298
|
return this.sendExpectOk({ id: crypto.randomUUID(), payload: options, type: 'createTab' })
|
|
297
299
|
}
|
|
298
300
|
|
|
299
|
-
write(sessionId: string, tabId: string, data: string): Promise<void> {
|
|
301
|
+
async write(sessionId: string, tabId: string, data: string): Promise<void> {
|
|
300
302
|
return this.sendExpectOk({
|
|
301
303
|
id: crypto.randomUUID(),
|
|
302
304
|
payload: { data, sessionId, tabId },
|
|
@@ -304,7 +306,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
304
306
|
})
|
|
305
307
|
}
|
|
306
308
|
|
|
307
|
-
resize(
|
|
309
|
+
async resize(
|
|
308
310
|
sessionId: string,
|
|
309
311
|
cols: number,
|
|
310
312
|
rows: number,
|
|
@@ -317,7 +319,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
317
319
|
})
|
|
318
320
|
}
|
|
319
321
|
|
|
320
|
-
resizeTab(
|
|
322
|
+
async resizeTab(
|
|
321
323
|
sessionId: string,
|
|
322
324
|
tabId: string,
|
|
323
325
|
cols: number,
|
|
@@ -331,7 +333,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
331
333
|
})
|
|
332
334
|
}
|
|
333
335
|
|
|
334
|
-
scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
|
|
336
|
+
async scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
|
|
335
337
|
return this.sendExpectOk({
|
|
336
338
|
id: crypto.randomUUID(),
|
|
337
339
|
payload: { deltaLines, sessionId, tabId },
|
|
@@ -339,7 +341,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
339
341
|
})
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
scrollToBottom(sessionId: string, tabId: string): Promise<void> {
|
|
344
|
+
async scrollToBottom(sessionId: string, tabId: string): Promise<void> {
|
|
343
345
|
return this.sendExpectOk({
|
|
344
346
|
id: crypto.randomUUID(),
|
|
345
347
|
payload: { sessionId, tabId },
|
|
@@ -347,7 +349,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
347
349
|
})
|
|
348
350
|
}
|
|
349
351
|
|
|
350
|
-
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
|
|
352
|
+
async reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
|
|
351
353
|
return this.sendExpectOk({
|
|
352
354
|
id: crypto.randomUUID(),
|
|
353
355
|
payload: { intent, sessionId, tabId },
|
|
@@ -355,7 +357,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
355
357
|
})
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
|
|
360
|
+
async setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
|
|
359
361
|
return this.sendExpectOk({
|
|
360
362
|
id: crypto.randomUUID(),
|
|
361
363
|
payload: { sessionId, tabId },
|
|
@@ -363,7 +365,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
363
365
|
})
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
closeTab(sessionId: string, tabId: string): Promise<void> {
|
|
368
|
+
async closeTab(sessionId: string, tabId: string): Promise<void> {
|
|
367
369
|
return this.sendExpectOk({
|
|
368
370
|
id: crypto.randomUUID(),
|
|
369
371
|
payload: { sessionId, tabId },
|
|
@@ -371,7 +373,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
371
373
|
})
|
|
372
374
|
}
|
|
373
375
|
|
|
374
|
-
disposeSession(sessionId: string): Promise<void> {
|
|
376
|
+
async disposeSession(sessionId: string): Promise<void> {
|
|
375
377
|
return this.sendExpectOk({
|
|
376
378
|
id: crypto.randomUUID(),
|
|
377
379
|
payload: { sessionId },
|
|
@@ -3,11 +3,11 @@ import type { ThemedToken } from 'shiki'
|
|
|
3
3
|
|
|
4
4
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
|
5
5
|
|
|
6
|
+
import type { FoldState } from '../../../../state/types'
|
|
6
7
|
import type { ThemeId } from '../../../themes'
|
|
7
8
|
|
|
8
9
|
import { useAppStore } from '../../../../state/app-store'
|
|
9
10
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
10
|
-
import { type FoldState } from '../../../../state/types'
|
|
11
11
|
import { useTheme } from '../../../theme'
|
|
12
12
|
import { buildDiffSegments, firstChangeSegmentOffset, gutterWidth } from './build-rows'
|
|
13
13
|
import { SplitView, type SplitViewHandle } from './split-view'
|
|
@@ -22,14 +22,14 @@ interface PrepareOptions {
|
|
|
22
22
|
signal?: AbortSignal
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function yieldToEventLoop(): Promise<void> {
|
|
25
|
+
async function yieldToEventLoop(): Promise<void> {
|
|
26
26
|
return new Promise((resolve) => {
|
|
27
27
|
setImmediate(resolve)
|
|
28
28
|
})
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function throwIfAborted(signal?: AbortSignal): void {
|
|
32
|
-
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
32
|
+
if (signal?.aborted === true) throw new DOMException('Aborted', 'AbortError')
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Lifts diff parsing off the synchronous render tree and returns a segmented
|
|
@@ -230,7 +230,9 @@ function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-h
|
|
|
230
230
|
return (
|
|
231
231
|
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
232
232
|
<text fg={t.text}>{row.spec}</text>
|
|
233
|
-
{row.context
|
|
233
|
+
{row.context != null && row.context !== '' ? (
|
|
234
|
+
<text fg={t.textMuted}> {row.context}</text>
|
|
235
|
+
) : null}
|
|
234
236
|
</box>
|
|
235
237
|
)
|
|
236
238
|
}
|
|
@@ -285,9 +287,9 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
285
287
|
{tokens.map((tok, i) => {
|
|
286
288
|
const s = tokenToSpan(tok)
|
|
287
289
|
let attributes = 0
|
|
288
|
-
if (s.bold) attributes |= TextAttributes.BOLD
|
|
289
|
-
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
290
|
-
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
290
|
+
if (s.bold === true) attributes |= TextAttributes.BOLD
|
|
291
|
+
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
292
|
+
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
291
293
|
return (
|
|
292
294
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
293
295
|
{s.text}
|
|
@@ -186,7 +186,9 @@ function UnifiedRowRender({
|
|
|
186
186
|
return (
|
|
187
187
|
<box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
|
|
188
188
|
<text fg={t.text}>{row.spec}</text>
|
|
189
|
-
{row.context
|
|
189
|
+
{row.context != null && row.context !== '' ? (
|
|
190
|
+
<text fg={t.textMuted}> {row.context}</text>
|
|
191
|
+
) : null}
|
|
190
192
|
</box>
|
|
191
193
|
)
|
|
192
194
|
}
|
|
@@ -230,9 +232,9 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
230
232
|
{tokens.map((tok, i) => {
|
|
231
233
|
const s = tokenToSpan(tok)
|
|
232
234
|
let attributes = 0
|
|
233
|
-
if (s.bold) attributes |= TextAttributes.BOLD
|
|
234
|
-
if (s.italic) attributes |= TextAttributes.ITALIC
|
|
235
|
-
if (s.underline) attributes |= TextAttributes.UNDERLINE
|
|
235
|
+
if (s.bold === true) attributes |= TextAttributes.BOLD
|
|
236
|
+
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
237
|
+
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
236
238
|
return (
|
|
237
239
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
238
240
|
{s.text}
|
|
@@ -13,6 +13,7 @@ interface PrefetchOptions {
|
|
|
13
13
|
projectPath: string | undefined
|
|
14
14
|
themeId: string
|
|
15
15
|
headOffset: number
|
|
16
|
+
compareRef?: string
|
|
16
17
|
enabled: boolean
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -94,15 +95,20 @@ export function useDiffPrefetch(
|
|
|
94
95
|
|
|
95
96
|
const queueRef = useRef<PrefetchQueue | null>(null)
|
|
96
97
|
|
|
97
|
-
const { enabled, headOffset, projectPath } = opts
|
|
98
|
+
const { compareRef, enabled, headOffset, projectPath } = opts
|
|
98
99
|
const runTaskRef = useRef<(task: Task) => Promise<void>>(async () => {})
|
|
99
100
|
|
|
100
101
|
// Keep a ref to the execute function so the queue keeps the latest closure
|
|
101
102
|
// without needing to recreate the queue itself.
|
|
102
103
|
runTaskRef.current = async (task: Task) => {
|
|
103
|
-
if (!projectPath) return
|
|
104
|
+
if (!(projectPath != null && projectPath !== '')) return
|
|
104
105
|
try {
|
|
105
|
-
const diff = await fetchDiff(
|
|
106
|
+
const diff = await fetchDiff(
|
|
107
|
+
task.file.repoPath ?? projectPath,
|
|
108
|
+
task.file,
|
|
109
|
+
headOffset,
|
|
110
|
+
compareRef
|
|
111
|
+
)
|
|
106
112
|
if (task.controller.signal.aborted) return
|
|
107
113
|
const hash = diffHash(diff.rawDiff)
|
|
108
114
|
dispatchGlobal({ diff, hash, key: task.key, type: 'git-mode-set-diff' })
|
|
@@ -122,13 +128,19 @@ export function useDiffPrefetch(
|
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
if (!queueRef.current) {
|
|
125
|
-
queueRef.current = new PrefetchQueue((task) => runTaskRef.current(task), MAX_CONCURRENCY)
|
|
131
|
+
queueRef.current = new PrefetchQueue(async (task) => runTaskRef.current(task), MAX_CONCURRENCY)
|
|
126
132
|
}
|
|
127
133
|
|
|
128
134
|
useEffect(() => {
|
|
129
135
|
const queue = queueRef.current
|
|
130
136
|
if (!queue) return
|
|
131
|
-
if (
|
|
137
|
+
if (
|
|
138
|
+
!enabled ||
|
|
139
|
+
radius <= 0 ||
|
|
140
|
+
projectPath == null ||
|
|
141
|
+
projectPath === '' ||
|
|
142
|
+
!(selectedEntryKey != null && selectedEntryKey !== '')
|
|
143
|
+
) {
|
|
132
144
|
queue.cancelAll()
|
|
133
145
|
return
|
|
134
146
|
}
|
|
@@ -149,7 +161,7 @@ export function useDiffPrefetch(
|
|
|
149
161
|
const key = row.key
|
|
150
162
|
if (diffs[key]) continue
|
|
151
163
|
if (parsed[key]) continue
|
|
152
|
-
if (loading[key]) continue
|
|
164
|
+
if (loading[key] === true) continue
|
|
153
165
|
tasks.push({
|
|
154
166
|
controller: new AbortController(),
|
|
155
167
|
distance: Math.abs(i - selectedIdx),
|
|
@@ -171,6 +183,7 @@ export function useDiffPrefetch(
|
|
|
171
183
|
parsed,
|
|
172
184
|
loading,
|
|
173
185
|
headOffset,
|
|
186
|
+
compareRef,
|
|
174
187
|
])
|
|
175
188
|
|
|
176
189
|
useEffect(() => {
|
|
@@ -163,11 +163,11 @@ export function useDiffPreparation(
|
|
|
163
163
|
(segments: readonly DiffSegment[]) => {
|
|
164
164
|
const file = parsed.file
|
|
165
165
|
const filetype = parsed.filetype
|
|
166
|
-
if (!file || !filetype) return
|
|
166
|
+
if (!file || !(filetype != null && filetype !== '')) return
|
|
167
167
|
const owner = requestOwner
|
|
168
168
|
const version = requestVersionRef.current
|
|
169
169
|
const activeHash = cachedParsed?.hash ?? (localOwnerMatches ? localHash : null)
|
|
170
|
-
if (!activeHash) return
|
|
170
|
+
if (!(activeHash != null && activeHash !== '')) return
|
|
171
171
|
const next = segments.filter(
|
|
172
172
|
(segment) =>
|
|
173
173
|
(segment.kind === 'context' || segment.kind === 'change') &&
|
|
@@ -29,13 +29,16 @@ interface GitPanelProps {
|
|
|
29
29
|
pathConfig?: GitPanePathConfig
|
|
30
30
|
diffCountConfig?: GitPaneDiffCountConfig
|
|
31
31
|
headOffset?: number
|
|
32
|
+
// When set, the single changed-files section is a worktree fork-point review
|
|
33
|
+
// ("vs <base>") rather than a HEAD~N history walk.
|
|
34
|
+
baseLabel?: string
|
|
32
35
|
compact?: boolean
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
function sectionTitle(section: GitFileSection, headOffset: number): string {
|
|
38
|
+
function sectionTitle(section: GitFileSection, headOffset: number, baseLabel?: string): string {
|
|
36
39
|
switch (section) {
|
|
37
40
|
case 'historical':
|
|
38
|
-
return `HEAD~${headOffset}`
|
|
41
|
+
return baseLabel != null && baseLabel !== '' ? baseLabel : `HEAD~${headOffset}`
|
|
39
42
|
case 'staged':
|
|
40
43
|
return 'Staged Changes'
|
|
41
44
|
case 'unstaged':
|
|
@@ -105,8 +108,8 @@ function renderFileLabel(
|
|
|
105
108
|
const displayPath = transform ? transform(file.path) : file.path
|
|
106
109
|
const { basename, prefix } = splitPath(displayPath)
|
|
107
110
|
const dir = stripTrailingSlash(prefix)
|
|
108
|
-
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir
|
|
109
|
-
if (!file.renamedFrom) {
|
|
111
|
+
const showDir = fileListMode === 'flat' && pathConfig.enabled && dir !== ''
|
|
112
|
+
if (!(file.renamedFrom != null && file.renamedFrom !== '')) {
|
|
110
113
|
return (
|
|
111
114
|
<text selectable={false} wrapMode="none">
|
|
112
115
|
<span fg={t.text}>{basename}</span>
|
|
@@ -211,7 +214,9 @@ function renderFileRow(
|
|
|
211
214
|
// Repo disambiguation prefix: only in flat mode, only when the file came
|
|
212
215
|
// from a sub-repo (root repo files get an empty prefix).
|
|
213
216
|
const repoTag =
|
|
214
|
-
fileListMode === 'flat' && file.repoPath
|
|
217
|
+
fileListMode === 'flat' && file.repoPath != null && file.repoPath !== ''
|
|
218
|
+
? (repoPrefixes[file.repoPath] ?? '')
|
|
219
|
+
: ''
|
|
215
220
|
return (
|
|
216
221
|
<box key={row.key} flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
217
222
|
<box width={2} flexShrink={0} justifyContent="center">
|
|
@@ -238,7 +243,7 @@ function renderTreeSection(
|
|
|
238
243
|
section: GitFileSection,
|
|
239
244
|
title: string,
|
|
240
245
|
files: GitFileEntry[],
|
|
241
|
-
rows:
|
|
246
|
+
rows: (GitTreeFolderRow | GitTreeFileRow)[],
|
|
242
247
|
addedW: number,
|
|
243
248
|
removedW: number,
|
|
244
249
|
fileListMode: GitFileListMode,
|
|
@@ -337,6 +342,7 @@ const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
|
337
342
|
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
338
343
|
|
|
339
344
|
export const GitPanel = memo(function GitPanel({
|
|
345
|
+
baseLabel,
|
|
340
346
|
collapsedFolders = {},
|
|
341
347
|
compact = false,
|
|
342
348
|
diffCountConfig = DEFAULT_DIFF_COUNT_CONFIG,
|
|
@@ -350,7 +356,8 @@ export const GitPanel = memo(function GitPanel({
|
|
|
350
356
|
const t = useTheme()
|
|
351
357
|
useTransparent()
|
|
352
358
|
const repoPrefixes = useAppStore((s) => s.multiRepo.prefixes)
|
|
353
|
-
const
|
|
359
|
+
const isSingleSection = headOffset > 0 || (baseLabel != null && baseLabel !== '')
|
|
360
|
+
const sectionOrder = isSingleSection ? HISTORICAL_SECTION_ORDER : BASE_SECTION_ORDER
|
|
354
361
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
355
362
|
const tree = useMemo(
|
|
356
363
|
() => buildGitTreeRows(gitPanel.files, collapsedFolders, fileListMode, compact),
|
|
@@ -361,7 +368,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
361
368
|
[gitPanel.files]
|
|
362
369
|
)
|
|
363
370
|
|
|
364
|
-
const statusNode = renderStatus(gitPanel, !!projectPath)
|
|
371
|
+
const statusNode = renderStatus(gitPanel, !!(projectPath != null && projectPath !== ''))
|
|
365
372
|
|
|
366
373
|
const hasRemoteTracking = gitPanel.ahead > 0 || gitPanel.behind > 0
|
|
367
374
|
const toggleSection =
|
|
@@ -372,7 +379,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
372
379
|
|
|
373
380
|
useEffect(() => {
|
|
374
381
|
const scrollbox = scrollRef.current
|
|
375
|
-
if (!scrollbox || !selectedEntryKey) return
|
|
382
|
+
if (!scrollbox || !(selectedEntryKey != null && selectedEntryKey !== '')) return
|
|
376
383
|
const selectedIndex = tree.visibleRows.findIndex((row) => row.key === selectedEntryKey)
|
|
377
384
|
if (selectedIndex < 0) return
|
|
378
385
|
const viewportHeight = Math.max(1, scrollbox.viewport.height)
|
|
@@ -403,7 +410,7 @@ export const GitPanel = memo(function GitPanel({
|
|
|
403
410
|
.some((k) => (tree.sections.find((e) => e.section === k)?.files.length ?? 0) > 0)
|
|
404
411
|
return renderTreeSection(
|
|
405
412
|
key,
|
|
406
|
-
sectionTitle(key, headOffset),
|
|
413
|
+
sectionTitle(key, headOffset, baseLabel),
|
|
407
414
|
section?.files ?? [],
|
|
408
415
|
section?.rows ?? [],
|
|
409
416
|
addedW,
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
-
import { memo, useEffect, useRef } from 'react'
|
|
2
|
+
import { memo, useEffect, useRef, useState } from 'react'
|
|
3
3
|
|
|
4
4
|
import type { DiffData, GitDiffView } from '../../../state/types'
|
|
5
5
|
import type { ThemeId } from '../../themes'
|
|
6
6
|
|
|
7
7
|
import { diffHash } from '../../../git/diff-hash'
|
|
8
|
+
import { getMergeBase } from '../../../git/divergence'
|
|
8
9
|
import { fetchDiff } from '../../../git/git-diff'
|
|
9
10
|
import { useGitPanelPolling } from '../../../git/git-poller'
|
|
10
11
|
import { useRepoDiscovery } from '../../../git/use-repo-discovery'
|
|
11
12
|
import { useAppStore } from '../../../state/app-store'
|
|
12
13
|
import { dispatchGlobal } from '../../../state/dispatch-ref'
|
|
13
14
|
import { getSelectedGitFile, gitFileKey } from '../../../state/git-tree'
|
|
15
|
+
import { getActiveWorktree, getSessionProjectPath } from '../../../state/session-worktrees'
|
|
14
16
|
import { setGitDiffScroller } from '../../git-view-controls'
|
|
15
17
|
import { useTheme } from '../../theme'
|
|
16
18
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
@@ -64,7 +66,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
64
66
|
</box>
|
|
65
67
|
)
|
|
66
68
|
}
|
|
67
|
-
if (diff.errorMessage) {
|
|
69
|
+
if (diff.errorMessage != null && diff.errorMessage !== '') {
|
|
68
70
|
return (
|
|
69
71
|
<box flexGrow={1} padding={1}>
|
|
70
72
|
<text fg={t.error}>{diff.errorMessage}</text>
|
|
@@ -77,7 +79,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
const placeholder = placeholderText(diff)
|
|
80
|
-
if (placeholder) {
|
|
82
|
+
if (placeholder != null && placeholder !== '') {
|
|
81
83
|
return (
|
|
82
84
|
<box flexGrow={1} padding={1}>
|
|
83
85
|
<text fg={t.textMuted}>{placeholder}</text>
|
|
@@ -87,7 +89,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
87
89
|
|
|
88
90
|
return (
|
|
89
91
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
90
|
-
{diff.oldPath ? (
|
|
92
|
+
{diff.oldPath != null && diff.oldPath !== '' ? (
|
|
91
93
|
<box paddingLeft={1} paddingRight={1}>
|
|
92
94
|
<text fg={t.textMuted}>
|
|
93
95
|
renamed: {diff.oldPath} → {diff.path}
|
|
@@ -123,13 +125,41 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
123
125
|
const focusMode = useAppStore((s) => s.focusMode)
|
|
124
126
|
const diffRef = useRef<PierreDiffHandle | null>(null)
|
|
125
127
|
|
|
126
|
-
const currentSession =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
const currentSession =
|
|
129
|
+
currentSessionId != null && currentSessionId !== ''
|
|
130
|
+
? sessions.find((s) => s.id === currentSessionId)
|
|
131
|
+
: undefined
|
|
132
|
+
const projectPath = getSessionProjectPath(currentSession)
|
|
133
|
+
|
|
134
|
+
// Review-vs-base: when toggled on for a worktree that records a baseRef,
|
|
135
|
+
// resolve the fork point and diff the working tree against it.
|
|
136
|
+
const activeWorktree = getActiveWorktree(currentSession)
|
|
137
|
+
const reviewBaseRef =
|
|
138
|
+
gitMode.reviewBase && activeWorktree?.baseRef != null && activeWorktree.baseRef !== ''
|
|
139
|
+
? activeWorktree.baseRef
|
|
140
|
+
: null
|
|
141
|
+
const [compareRef, setCompareRef] = useState<string | undefined>(undefined)
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
if (reviewBaseRef == null || projectPath == null || projectPath === '') {
|
|
144
|
+
setCompareRef(undefined)
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
let cancelled = false
|
|
148
|
+
void getMergeBase(projectPath, reviewBaseRef).then((mergeBase) => {
|
|
149
|
+
if (!cancelled) setCompareRef(mergeBase)
|
|
150
|
+
})
|
|
151
|
+
return () => {
|
|
152
|
+
cancelled = true
|
|
153
|
+
}
|
|
154
|
+
}, [reviewBaseRef, projectPath])
|
|
130
155
|
|
|
131
156
|
useRepoDiscovery(projectPath)
|
|
132
|
-
useGitPanelPolling({
|
|
157
|
+
useGitPanelPolling({
|
|
158
|
+
compareRef,
|
|
159
|
+
enabled: focusMode === 'git',
|
|
160
|
+
headOffset: gitMode.headOffset,
|
|
161
|
+
projectPath,
|
|
162
|
+
})
|
|
133
163
|
|
|
134
164
|
const fileBarWidth = Math.max(20, Math.floor(dimensions.width * gitPane.diffModeRatio))
|
|
135
165
|
|
|
@@ -140,8 +170,12 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
140
170
|
selectedEntryKey: gitMode.selectedEntryKey,
|
|
141
171
|
})
|
|
142
172
|
const selectedDiffKey = selectedFile ? gitFileKey(selectedFile) : null
|
|
143
|
-
const diff =
|
|
144
|
-
|
|
173
|
+
const diff =
|
|
174
|
+
selectedDiffKey != null && selectedDiffKey !== '' ? gitMode.diffs[selectedDiffKey] : undefined
|
|
175
|
+
const loading =
|
|
176
|
+
selectedDiffKey != null && selectedDiffKey !== ''
|
|
177
|
+
? !!(gitMode.loading[selectedDiffKey] === true)
|
|
178
|
+
: false
|
|
145
179
|
|
|
146
180
|
useEffect(() => {
|
|
147
181
|
setGitDiffScroller((delta: number) => {
|
|
@@ -160,6 +194,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
160
194
|
}, [])
|
|
161
195
|
|
|
162
196
|
useDiffPrefetch(gitMode.selectedEntryKey, gitPane.prefetchRadius, {
|
|
197
|
+
compareRef,
|
|
163
198
|
enabled: focusMode === 'git',
|
|
164
199
|
headOffset: gitMode.headOffset,
|
|
165
200
|
projectPath,
|
|
@@ -168,11 +203,16 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
168
203
|
|
|
169
204
|
useEffect(() => {
|
|
170
205
|
if (focusMode !== 'git') return
|
|
171
|
-
if (!selectedFile || !projectPath) return
|
|
172
|
-
if (!selectedDiffKey) return
|
|
206
|
+
if (!selectedFile || !(projectPath != null && projectPath !== '')) return
|
|
207
|
+
if (!(selectedDiffKey != null && selectedDiffKey !== '')) return
|
|
173
208
|
if (diff || loading) return
|
|
174
209
|
dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
|
|
175
|
-
void fetchDiff(
|
|
210
|
+
void fetchDiff(
|
|
211
|
+
selectedFile.repoPath ?? projectPath,
|
|
212
|
+
selectedFile,
|
|
213
|
+
gitMode.headOffset,
|
|
214
|
+
compareRef
|
|
215
|
+
)
|
|
176
216
|
.then((d) =>
|
|
177
217
|
dispatchGlobal({
|
|
178
218
|
diff: d,
|
|
@@ -184,7 +224,16 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
184
224
|
.catch(() =>
|
|
185
225
|
dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
|
|
186
226
|
)
|
|
187
|
-
}, [
|
|
227
|
+
}, [
|
|
228
|
+
focusMode,
|
|
229
|
+
projectPath,
|
|
230
|
+
selectedFile,
|
|
231
|
+
selectedDiffKey,
|
|
232
|
+
diff,
|
|
233
|
+
loading,
|
|
234
|
+
gitMode.headOffset,
|
|
235
|
+
compareRef,
|
|
236
|
+
])
|
|
188
237
|
|
|
189
238
|
const pendingPath = gitMode.pendingDeletePath
|
|
190
239
|
const pendingIsUntracked =
|
|
@@ -199,13 +248,13 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
199
248
|
}
|
|
200
249
|
const actionMessage = gitMode.actionMessage
|
|
201
250
|
let footerNode: React.ReactNode = null
|
|
202
|
-
if (pendingHint) {
|
|
251
|
+
if (pendingHint != null && pendingHint !== '') {
|
|
203
252
|
footerNode = (
|
|
204
253
|
<text fg={t.warning}>
|
|
205
254
|
<strong>{pendingHint}</strong>
|
|
206
255
|
</text>
|
|
207
256
|
)
|
|
208
|
-
} else if (actionMessage) {
|
|
257
|
+
} else if (actionMessage != null && actionMessage !== '') {
|
|
209
258
|
footerNode = actionMessage.split('\n').map((line, idx) => (
|
|
210
259
|
<text key={idx} fg={t.primary}>
|
|
211
260
|
{line}
|
|
@@ -213,6 +262,8 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
213
262
|
))
|
|
214
263
|
}
|
|
215
264
|
|
|
265
|
+
const baseLabel = compareRef != null && reviewBaseRef != null ? `vs ${reviewBaseRef}` : undefined
|
|
266
|
+
|
|
216
267
|
return (
|
|
217
268
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
218
269
|
<box flexDirection="row" flexGrow={1}>
|
|
@@ -228,7 +279,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
228
279
|
<text fg={t.text}>
|
|
229
280
|
<strong>aimux · git</strong>
|
|
230
281
|
</text>
|
|
231
|
-
{gitPanel.branch ? (
|
|
282
|
+
{gitPanel.branch != null && gitPanel.branch !== '' ? (
|
|
232
283
|
<box flexDirection="row">
|
|
233
284
|
<text fg={t.text}>{'\u{e702}'} </text>
|
|
234
285
|
<text fg={t.text}>{gitPanel.branch}</text>
|
|
@@ -242,9 +293,18 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
242
293
|
<text fg={t.textMuted}>[ newer · ] older</text>
|
|
243
294
|
</box>
|
|
244
295
|
) : null}
|
|
296
|
+
{baseLabel != null ? (
|
|
297
|
+
<box flexDirection="row" gap={1}>
|
|
298
|
+
<text fg={t.primary}>
|
|
299
|
+
<strong>{baseLabel}</strong>
|
|
300
|
+
</text>
|
|
301
|
+
<text fg={t.textMuted}>b: back</text>
|
|
302
|
+
</box>
|
|
303
|
+
) : null}
|
|
245
304
|
<text fg={t.textMuted}>{'·'.repeat(Math.max(0, fileBarWidth - 2))}</text>
|
|
246
305
|
</box>
|
|
247
306
|
<GitPanel
|
|
307
|
+
baseLabel={baseLabel}
|
|
248
308
|
collapsedFolders={gitMode.collapsedFolders}
|
|
249
309
|
compact={gitPane.treeCompaction}
|
|
250
310
|
fileListMode={gitPane.fileListMode}
|
|
@@ -292,7 +352,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
292
352
|
/>
|
|
293
353
|
</box>
|
|
294
354
|
</box>
|
|
295
|
-
{footerNode ? (
|
|
355
|
+
{footerNode != null ? (
|
|
296
356
|
<box paddingLeft={1} paddingRight={1} backgroundColor={sidebarBg} flexDirection="column">
|
|
297
357
|
{footerNode}
|
|
298
358
|
</box>
|
|
@@ -76,14 +76,14 @@ export const ImageDiffView = memo(function ImageDiffView({ diff }: ImageDiffView
|
|
|
76
76
|
const showBoth = before && after
|
|
77
77
|
return (
|
|
78
78
|
<box flexDirection="column" flexGrow={1} overflow="hidden" backgroundColor={t.background}>
|
|
79
|
-
{diff.oldPath ? (
|
|
79
|
+
{diff.oldPath != null && diff.oldPath !== '' ? (
|
|
80
80
|
<box paddingLeft={1} paddingRight={1}>
|
|
81
81
|
<text fg={t.textMuted}>
|
|
82
82
|
renamed: {diff.oldPath} → {diff.path}
|
|
83
83
|
</text>
|
|
84
84
|
</box>
|
|
85
85
|
) : null}
|
|
86
|
-
{banner ? (
|
|
86
|
+
{banner != null && banner !== '' ? (
|
|
87
87
|
<box paddingLeft={1} paddingRight={1}>
|
|
88
88
|
<text fg={protocol === 'kitty' ? t.textMuted : t.warning}>{banner}</text>
|
|
89
89
|
</box>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { BoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
2
3
|
import { memo, useEffect, useRef, useState } from 'react'
|
|
3
4
|
|
|
4
5
|
import { convertToPng, isPng } from '../../../terminal-graphics/format-fallback'
|
|
@@ -47,7 +48,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
47
48
|
let cancelled = false
|
|
48
49
|
stateRef.current = { imageId: null, lastKey: null, uploaded: false }
|
|
49
50
|
setError(null)
|
|
50
|
-
|
|
51
|
+
void (async () => {
|
|
51
52
|
let pngBytes: Uint8Array | null = null
|
|
52
53
|
if (mime === 'image/png' || isPng(bytes)) {
|
|
53
54
|
pngBytes = bytes
|
|
@@ -60,7 +61,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
60
61
|
}
|
|
61
62
|
pngBytes = result.png
|
|
62
63
|
}
|
|
63
|
-
if (cancelled
|
|
64
|
+
if (cancelled) return
|
|
64
65
|
const id = nextImageId()
|
|
65
66
|
writeRaw(uploadPngEscape(pngBytes, id))
|
|
66
67
|
stateRef.current.imageId = id
|
|
@@ -76,7 +77,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
76
77
|
}
|
|
77
78
|
}, [bytes, mime])
|
|
78
79
|
|
|
79
|
-
if (error) {
|
|
80
|
+
if (error != null && error !== '') {
|
|
80
81
|
return (
|
|
81
82
|
<box flexGrow={1} alignItems="center" justifyContent="center" padding={1}>
|
|
82
83
|
<text fg={t.warning}>({error})</text>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { GitPaneMode, GitPanePosition, GitPaneState } from '../../../../state/types'
|
|
2
2
|
import type { ContextMenuItem } from '../../../context-menu/controller'
|
|
3
3
|
|
|
4
|
-
const GIT_PANE_MENU_OPTIONS:
|
|
4
|
+
const GIT_PANE_MENU_OPTIONS: {
|
|
5
5
|
label: string
|
|
6
6
|
mode: GitPaneMode
|
|
7
7
|
position: GitPanePosition
|
|
8
|
-
}
|
|
8
|
+
}[] = [
|
|
9
9
|
{ label: 'Move to top', mode: 'embedded', position: 'top' },
|
|
10
10
|
{ label: 'Move to bottom', mode: 'embedded', position: 'bottom' },
|
|
11
11
|
{ label: 'Move to left', mode: 'pane', position: 'left' },
|