@brimveyn/aimux 1.13.0 → 1.14.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/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +12 -14
- package/src/app-runtime/multi-click-clipboard-guard.ts +24 -12
- package/src/app-runtime/pty-write.ts +6 -9
- package/src/app-runtime/side-effects.ts +65 -56
- package/src/app-runtime/snippet-actions.ts +2 -3
- package/src/app-runtime/use-backend-runtime.ts +4 -7
- package/src/app-runtime/use-mouse-handlers.ts +30 -1
- package/src/app-runtime/use-renderer-bindings.ts +4 -12
- package/src/app-runtime/use-terminal-resize.ts +6 -18
- package/src/app.tsx +0 -3
- package/src/daemon/daemon.ts +20 -27
- package/src/daemon/session-manager.ts +3 -15
- package/src/daemon/session-registry.ts +11 -30
- package/src/git/command-queue.ts +18 -2
- package/src/input/terminal-text-extraction.ts +7 -8
- package/src/ipc/manager-protocol.ts +6 -38
- package/src/ipc/protocol.ts +9 -37
- package/src/pty/pty-manager.ts +20 -31
- package/src/pty/terminal-snapshot.ts +43 -0
- package/src/session-backend/local-session-backend.ts +7 -31
- package/src/session-backend/remote-session-backend.ts +51 -63
- package/src/session-backend/types.ts +2 -15
- package/src/state/reducers/tab-state.ts +2 -20
- package/src/state/session-persistence.ts +2 -22
- package/src/state/types.ts +3 -11
- package/src/state/validation.ts +0 -8
- package/src/terminal-manager/manager-client.ts +5 -29
- package/src/terminal-manager/terminal-manager.ts +2 -17
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +30 -23
- package/src/ui/components/git/diff-renderer/split-view.tsx +33 -7
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +23 -3
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +9 -5
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +8 -6
- package/src/ui/components/git/git-panel.tsx +114 -75
- package/src/ui/components/git/git-view.tsx +26 -18
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +10 -10
- package/src/ui/components/layout/session-bar.tsx +134 -116
- package/src/ui/components/layout/sidebar/sidebar.tsx +89 -53
- package/src/ui/components/layout/sidebar/tab-item.tsx +65 -54
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +154 -114
- package/src/ui/components/modals/app/help-modal.tsx +25 -18
- package/src/ui/components/modals/git/git-commit-modal.tsx +15 -8
- package/src/ui/components/modals/sessions/create-session-modal.tsx +13 -9
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +35 -28
- package/src/ui/components/modals/shared/form.tsx +2 -1
- package/src/ui/components/modals/shared/picker.tsx +49 -33
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +42 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +82 -66
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +23 -6
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +11 -6
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/primitives/list-item.tsx +20 -4
- package/src/ui/root.tsx +77 -48
|
@@ -34,6 +34,30 @@ import { tokenToSpan } from './highlight'
|
|
|
34
34
|
import { useSegmentVirtualization } from './use-segment-virtualization'
|
|
35
35
|
|
|
36
36
|
const OVERSCAN = 24
|
|
37
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
38
|
+
const HIDDEN_SCROLLBAR_OPTIONS = { visible: false }
|
|
39
|
+
|
|
40
|
+
// Stable per-row key derived from the line identities in the row, so rows keep a
|
|
41
|
+
// consistent identity across fold expand/collapse rather than relying on position.
|
|
42
|
+
function splitCellKey(cell: SplitCell): string {
|
|
43
|
+
switch (cell.type) {
|
|
44
|
+
case 'context':
|
|
45
|
+
return `c${cell.lineNumber}`
|
|
46
|
+
case 'addition':
|
|
47
|
+
return `a${cell.lineNumber}`
|
|
48
|
+
case 'deletion':
|
|
49
|
+
return `d${cell.lineNumber}`
|
|
50
|
+
case 'fold':
|
|
51
|
+
return `f${cell.fold.foldId}`
|
|
52
|
+
case 'filler':
|
|
53
|
+
return 'x'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function splitRowKey(row: SplitRowOrHeader): string {
|
|
58
|
+
if (row.type === 'hunk-header') return `hh:${row.spec}`
|
|
59
|
+
return `${splitCellKey(row.left)}|${splitCellKey(row.right)}`
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
export interface SplitViewHandle {
|
|
39
63
|
leftScroll: ScrollBoxRenderable | null
|
|
@@ -154,15 +178,15 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
154
178
|
flexGrow={1}
|
|
155
179
|
scrollY
|
|
156
180
|
viewportCulling
|
|
157
|
-
contentOptions={
|
|
158
|
-
verticalScrollbarOptions={
|
|
181
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
182
|
+
verticalScrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
159
183
|
onMouseScroll={handleScroll}
|
|
160
184
|
>
|
|
161
185
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
162
186
|
{renderedSegments.map((rendered) =>
|
|
163
|
-
rendered.rows.map((row
|
|
187
|
+
rendered.rows.map((row) => (
|
|
164
188
|
<SideRow
|
|
165
|
-
key={`${rendered.segment.id}:left:${
|
|
189
|
+
key={`${rendered.segment.id}:left:${splitRowKey(row)}`}
|
|
166
190
|
cell={row.type === 'row' ? row.left : null}
|
|
167
191
|
foldDispatch={foldDispatch}
|
|
168
192
|
gw={gw}
|
|
@@ -180,14 +204,14 @@ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
|
|
|
180
204
|
flexGrow={1}
|
|
181
205
|
scrollY
|
|
182
206
|
viewportCulling
|
|
183
|
-
contentOptions={
|
|
207
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
184
208
|
onMouseScroll={handleScroll}
|
|
185
209
|
>
|
|
186
210
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
187
211
|
{renderedSegments.map((rendered) =>
|
|
188
|
-
rendered.rows.map((row
|
|
212
|
+
rendered.rows.map((row) => (
|
|
189
213
|
<SideRow
|
|
190
|
-
key={`${rendered.segment.id}:right:${
|
|
214
|
+
key={`${rendered.segment.id}:right:${splitRowKey(row)}`}
|
|
191
215
|
cell={row.type === 'row' ? row.right : null}
|
|
192
216
|
foldDispatch={foldDispatch}
|
|
193
217
|
gw={gw}
|
|
@@ -291,6 +315,8 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
291
315
|
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
292
316
|
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
293
317
|
return (
|
|
318
|
+
// Syntax tokens are positional within a single line and never reorder.
|
|
319
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
294
320
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
295
321
|
{s.text}
|
|
296
322
|
</span>
|
|
@@ -33,6 +33,24 @@ import { tokenToSpan } from './highlight'
|
|
|
33
33
|
import { useSegmentVirtualization } from './use-segment-virtualization'
|
|
34
34
|
|
|
35
35
|
const OVERSCAN = 24
|
|
36
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
37
|
+
|
|
38
|
+
// Stable per-row key derived from the line identities in the row, so rows keep a
|
|
39
|
+
// consistent identity across fold expand/collapse rather than relying on position.
|
|
40
|
+
function unifiedRowKey(row: UnifiedRowOrHeader): string {
|
|
41
|
+
switch (row.type) {
|
|
42
|
+
case 'hunk-header':
|
|
43
|
+
return `hh:${row.spec}`
|
|
44
|
+
case 'fold':
|
|
45
|
+
return `f:${row.fold.foldId}`
|
|
46
|
+
case 'context':
|
|
47
|
+
return `c:${row.delLineNumber}:${row.addLineNumber}`
|
|
48
|
+
case 'addition':
|
|
49
|
+
return `a:${row.lineNumber}`
|
|
50
|
+
case 'deletion':
|
|
51
|
+
return `d:${row.lineNumber}`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
|
|
37
55
|
export interface StackedViewHandle {
|
|
38
56
|
scroll: ScrollBoxRenderable | null
|
|
@@ -148,14 +166,14 @@ export const StackedView = forwardRef<StackedViewHandle, Props>(function Stacked
|
|
|
148
166
|
flexGrow={1}
|
|
149
167
|
scrollY
|
|
150
168
|
viewportCulling
|
|
151
|
-
contentOptions={
|
|
169
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
152
170
|
onMouseScroll={handleScroll}
|
|
153
171
|
>
|
|
154
172
|
{visibleWindow.topSpacer > 0 ? <box height={visibleWindow.topSpacer} /> : null}
|
|
155
173
|
{renderedSegments.map((rendered) =>
|
|
156
|
-
rendered.rows.map((row
|
|
174
|
+
rendered.rows.map((row) => (
|
|
157
175
|
<UnifiedRowRender
|
|
158
|
-
key={`${rendered.segment.id}:${
|
|
176
|
+
key={`${rendered.segment.id}:${unifiedRowKey(row)}`}
|
|
159
177
|
foldDispatch={foldDispatch}
|
|
160
178
|
gw={gw}
|
|
161
179
|
highlights={highlights}
|
|
@@ -236,6 +254,8 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
|
|
|
236
254
|
if (s.italic === true) attributes |= TextAttributes.ITALIC
|
|
237
255
|
if (s.underline === true) attributes |= TextAttributes.UNDERLINE
|
|
238
256
|
return (
|
|
257
|
+
// Syntax tokens are positional within a single line and never reorder.
|
|
258
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
239
259
|
<span key={i} fg={s.fg ?? t.text} attributes={attributes}>
|
|
240
260
|
{s.text}
|
|
241
261
|
</span>
|
|
@@ -67,11 +67,15 @@ class PrefetchQueue {
|
|
|
67
67
|
if (!task) break
|
|
68
68
|
this.inflight.set(task.key, task.controller)
|
|
69
69
|
this.running += 1
|
|
70
|
-
void
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
void (async () => {
|
|
71
|
+
try {
|
|
72
|
+
await this.execute(task)
|
|
73
|
+
} finally {
|
|
74
|
+
this.inflight.delete(task.key)
|
|
75
|
+
this.running -= 1
|
|
76
|
+
this.pump()
|
|
77
|
+
}
|
|
78
|
+
})()
|
|
75
79
|
}
|
|
76
80
|
}
|
|
77
81
|
}
|
|
@@ -124,8 +124,9 @@ export function useDiffPreparation(
|
|
|
124
124
|
const owner = requestOwner
|
|
125
125
|
const version = requestVersionRef.current
|
|
126
126
|
setPreparing(true)
|
|
127
|
-
void
|
|
128
|
-
|
|
127
|
+
void (async () => {
|
|
128
|
+
try {
|
|
129
|
+
const result = await prepareDiff(diff, path, { signal: controller.signal })
|
|
129
130
|
if (
|
|
130
131
|
controller.signal.aborted ||
|
|
131
132
|
requestVersionRef.current !== version ||
|
|
@@ -142,9 +143,9 @@ export function useDiffPreparation(
|
|
|
142
143
|
key: cacheKey,
|
|
143
144
|
type: 'git-mode-set-parsed',
|
|
144
145
|
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
} catch {
|
|
147
|
+
// Aborted or failed preparation; nothing to surface here.
|
|
148
|
+
} finally {
|
|
148
149
|
if (
|
|
149
150
|
!controller.signal.aborted &&
|
|
150
151
|
requestVersionRef.current === version &&
|
|
@@ -152,7 +153,8 @@ export function useDiffPreparation(
|
|
|
152
153
|
) {
|
|
153
154
|
setPreparing(false)
|
|
154
155
|
}
|
|
155
|
-
}
|
|
156
|
+
}
|
|
157
|
+
})()
|
|
156
158
|
|
|
157
159
|
return () => {
|
|
158
160
|
controller.abort()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { memo, type ReactNode, useEffect, useMemo, useRef } from 'react'
|
|
3
|
+
import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
GitFileEntry,
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type GitTreeFileRow,
|
|
19
19
|
type GitTreeFolderRow,
|
|
20
20
|
} from '../../../state/git-tree'
|
|
21
|
-
import { getCurrentTheme,
|
|
21
|
+
import { getCurrentTheme, useTheme, useTransparent } from '../../theme'
|
|
22
22
|
|
|
23
23
|
interface GitPanelProps {
|
|
24
24
|
collapsedFolders?: Record<string, true>
|
|
@@ -167,23 +167,30 @@ function renderDiffCount(
|
|
|
167
167
|
)
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const FolderRow = memo(function FolderRow({
|
|
171
|
+
isSelected,
|
|
172
|
+
row,
|
|
173
|
+
}: {
|
|
174
|
+
row: GitTreeFolderRow
|
|
175
|
+
isSelected: boolean
|
|
176
|
+
}) {
|
|
177
|
+
const t = useTheme()
|
|
178
|
+
const transparent = useTransparent()
|
|
179
|
+
const bg = isSelected && !transparent ? t.backgroundElement : undefined
|
|
180
|
+
const onSelect = useCallback(() => {
|
|
174
181
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
175
|
-
}
|
|
176
|
-
const onToggle = ()
|
|
182
|
+
}, [row.key])
|
|
183
|
+
const onToggle = useCallback(() => {
|
|
177
184
|
dispatchGlobal({ key: row.key, type: 'git-mode-toggle-folder' })
|
|
178
|
-
}
|
|
185
|
+
}, [row.key])
|
|
179
186
|
return (
|
|
180
|
-
<box
|
|
187
|
+
<box flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
181
188
|
<box flexGrow={1} overflow="hidden" paddingLeft={row.depth * 2}>
|
|
182
189
|
<box flexDirection="row" gap={1} onMouseDown={onToggle}>
|
|
183
190
|
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
184
191
|
{row.isCollapsed ? '▸' : '▾'}
|
|
185
192
|
</text>
|
|
186
|
-
<text selectable={false} fg={
|
|
193
|
+
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
187
194
|
{row.isCollapsed ? '\uf07b' : '\uf07c'}
|
|
188
195
|
</text>
|
|
189
196
|
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
@@ -193,24 +200,35 @@ function renderFolderRow(row: GitTreeFolderRow, isSelected: boolean): ReactNode
|
|
|
193
200
|
</box>
|
|
194
201
|
</box>
|
|
195
202
|
)
|
|
196
|
-
}
|
|
203
|
+
})
|
|
197
204
|
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
isSelected
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
205
|
+
const FileRow = memo(function FileRow({
|
|
206
|
+
addedW,
|
|
207
|
+
diffCountConfig,
|
|
208
|
+
fileListMode,
|
|
209
|
+
isSelected,
|
|
210
|
+
pathConfig,
|
|
211
|
+
removedW,
|
|
212
|
+
repoPrefixes,
|
|
213
|
+
row,
|
|
214
|
+
}: {
|
|
215
|
+
row: GitTreeFileRow
|
|
216
|
+
addedW: number
|
|
217
|
+
removedW: number
|
|
218
|
+
isSelected: boolean
|
|
219
|
+
fileListMode: GitFileListMode
|
|
220
|
+
pathConfig: GitPanePathConfig
|
|
221
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
206
222
|
repoPrefixes: Record<string, string>
|
|
207
|
-
)
|
|
223
|
+
}) {
|
|
224
|
+
const t = useTheme()
|
|
225
|
+
const transparent = useTransparent()
|
|
208
226
|
const file = row.file
|
|
209
227
|
const hasNumstat = file.added !== null || file.removed !== null
|
|
210
|
-
const bg = isSelected && !
|
|
211
|
-
const onSelect = ()
|
|
228
|
+
const bg = isSelected && !transparent ? t.backgroundElement : undefined
|
|
229
|
+
const onSelect = useCallback(() => {
|
|
212
230
|
dispatchGlobal({ key: row.key, type: 'git-mode-select-entry-by-key' })
|
|
213
|
-
}
|
|
231
|
+
}, [row.key])
|
|
214
232
|
// Repo disambiguation prefix: only in flat mode, only when the file came
|
|
215
233
|
// from a sub-repo (root repo files get an empty prefix).
|
|
216
234
|
const repoTag =
|
|
@@ -218,7 +236,7 @@ function renderFileRow(
|
|
|
218
236
|
? (repoPrefixes[file.repoPath] ?? '')
|
|
219
237
|
: ''
|
|
220
238
|
return (
|
|
221
|
-
<box
|
|
239
|
+
<box flexDirection="row" gap={1} backgroundColor={bg} onMouseDown={onSelect}>
|
|
222
240
|
<box width={2} flexShrink={0} justifyContent="center">
|
|
223
241
|
<text selectable={false} fg={statusColor(file.status)} bg={bg}>
|
|
224
242
|
<strong>{displayStatus(file)}</strong>
|
|
@@ -226,7 +244,7 @@ function renderFileRow(
|
|
|
226
244
|
</box>
|
|
227
245
|
{repoTag ? (
|
|
228
246
|
<box flexShrink={0}>
|
|
229
|
-
<text selectable={false} fg={
|
|
247
|
+
<text selectable={false} fg={t.primary} bg={bg}>
|
|
230
248
|
<strong>{repoTag}</strong>
|
|
231
249
|
</text>
|
|
232
250
|
</box>
|
|
@@ -237,32 +255,44 @@ function renderFileRow(
|
|
|
237
255
|
{renderDiffCount(file, addedW, removedW, bg, diffCountConfig, hasNumstat)}
|
|
238
256
|
</box>
|
|
239
257
|
)
|
|
240
|
-
}
|
|
258
|
+
})
|
|
241
259
|
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
260
|
+
const TreeSection = memo(function TreeSection({
|
|
261
|
+
addedW,
|
|
262
|
+
diffCountConfig,
|
|
263
|
+
fileListMode,
|
|
264
|
+
files,
|
|
265
|
+
marginTop,
|
|
266
|
+
pathConfig,
|
|
267
|
+
removedW,
|
|
268
|
+
repoPrefixes,
|
|
269
|
+
rows,
|
|
270
|
+
selectedEntryKey,
|
|
271
|
+
showListModeToggle,
|
|
272
|
+
title,
|
|
273
|
+
}: {
|
|
274
|
+
title: string
|
|
275
|
+
files: GitFileEntry[]
|
|
276
|
+
rows: (GitTreeFolderRow | GitTreeFileRow)[]
|
|
277
|
+
addedW: number
|
|
278
|
+
removedW: number
|
|
279
|
+
fileListMode: GitFileListMode
|
|
280
|
+
selectedEntryKey: string | null | undefined
|
|
281
|
+
showListModeToggle: boolean
|
|
282
|
+
pathConfig: GitPanePathConfig
|
|
283
|
+
diffCountConfig: GitPaneDiffCountConfig
|
|
284
|
+
marginTop: number
|
|
255
285
|
repoPrefixes: Record<string, string>
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
const t2 = getCurrentTheme()
|
|
286
|
+
}) {
|
|
287
|
+
const t2 = useTheme()
|
|
259
288
|
const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
|
|
260
|
-
const toggleListMode = ()
|
|
289
|
+
const toggleListMode = useCallback(() => {
|
|
261
290
|
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
262
291
|
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
263
|
-
}
|
|
292
|
+
}, [nextFileListMode])
|
|
293
|
+
if (files.length === 0) return null
|
|
264
294
|
return (
|
|
265
|
-
<box
|
|
295
|
+
<box flexDirection="column" marginTop={marginTop}>
|
|
266
296
|
<box flexDirection="row" justifyContent="space-between">
|
|
267
297
|
<text selectable={false} fg={t2.text}>
|
|
268
298
|
<strong>
|
|
@@ -284,22 +314,25 @@ function renderTreeSection(
|
|
|
284
314
|
) : null}
|
|
285
315
|
</box>
|
|
286
316
|
{rows.map((row) =>
|
|
287
|
-
row.kind === 'folder'
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
317
|
+
row.kind === 'folder' ? (
|
|
318
|
+
<FolderRow key={row.key} row={row} isSelected={row.key === selectedEntryKey} />
|
|
319
|
+
) : (
|
|
320
|
+
<FileRow
|
|
321
|
+
key={row.key}
|
|
322
|
+
row={row}
|
|
323
|
+
addedW={addedW}
|
|
324
|
+
removedW={removedW}
|
|
325
|
+
isSelected={row.key === selectedEntryKey}
|
|
326
|
+
fileListMode={fileListMode}
|
|
327
|
+
pathConfig={pathConfig}
|
|
328
|
+
diffCountConfig={diffCountConfig}
|
|
329
|
+
repoPrefixes={repoPrefixes}
|
|
330
|
+
/>
|
|
331
|
+
)
|
|
299
332
|
)}
|
|
300
333
|
</box>
|
|
301
334
|
)
|
|
302
|
-
}
|
|
335
|
+
})
|
|
303
336
|
|
|
304
337
|
interface StatusPlaceholder {
|
|
305
338
|
label: string
|
|
@@ -340,6 +373,10 @@ function computeStatusPlaceholder(
|
|
|
340
373
|
|
|
341
374
|
const DEFAULT_PATH_CONFIG: GitPanePathConfig = { enabled: true }
|
|
342
375
|
const DEFAULT_DIFF_COUNT_CONFIG: GitPaneDiffCountConfig = { enabled: true }
|
|
376
|
+
const HIDDEN_SCROLLBAR_OPTIONS = { visible: false }
|
|
377
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
378
|
+
const EMPTY_FILES: GitFileEntry[] = []
|
|
379
|
+
const EMPTY_ROWS: (GitTreeFolderRow | GitTreeFileRow)[] = []
|
|
343
380
|
|
|
344
381
|
export const GitPanel = memo(function GitPanel({
|
|
345
382
|
baseLabel,
|
|
@@ -399,29 +436,31 @@ export const GitPanel = memo(function GitPanel({
|
|
|
399
436
|
flexGrow={1}
|
|
400
437
|
ref={scrollRef}
|
|
401
438
|
scrollY
|
|
402
|
-
scrollbarOptions={
|
|
439
|
+
scrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
403
440
|
viewportCulling
|
|
404
|
-
contentOptions={
|
|
441
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
405
442
|
>
|
|
406
443
|
{sectionOrder.map((key, idx) => {
|
|
407
444
|
const section = tree.sections.find((entry) => entry.section === key)
|
|
408
445
|
const priorHasFiles = sectionOrder
|
|
409
446
|
.slice(0, idx)
|
|
410
447
|
.some((k) => (tree.sections.find((e) => e.section === k)?.files.length ?? 0) > 0)
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
448
|
+
return (
|
|
449
|
+
<TreeSection
|
|
450
|
+
key={key}
|
|
451
|
+
title={sectionTitle(key, headOffset, baseLabel)}
|
|
452
|
+
files={section?.files ?? EMPTY_FILES}
|
|
453
|
+
rows={section?.rows ?? EMPTY_ROWS}
|
|
454
|
+
addedW={addedW}
|
|
455
|
+
removedW={removedW}
|
|
456
|
+
fileListMode={fileListMode}
|
|
457
|
+
selectedEntryKey={selectedEntryKey}
|
|
458
|
+
showListModeToggle={key === toggleSection}
|
|
459
|
+
pathConfig={pathConfig}
|
|
460
|
+
diffCountConfig={diffCountConfig}
|
|
461
|
+
marginTop={priorHasFiles ? 1 : 0}
|
|
462
|
+
repoPrefixes={repoPrefixes}
|
|
463
|
+
/>
|
|
425
464
|
)
|
|
426
465
|
})}
|
|
427
466
|
</scrollbox>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
|
|
1
3
|
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
3
5
|
|
|
4
6
|
import type { DiffData, GitDiffView } from '../../../state/types'
|
|
5
7
|
import type { ThemeId } from '../../themes'
|
|
@@ -145,9 +147,10 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
145
147
|
return
|
|
146
148
|
}
|
|
147
149
|
let cancelled = false
|
|
148
|
-
void
|
|
150
|
+
void (async () => {
|
|
151
|
+
const mergeBase = await getMergeBase(projectPath, reviewBaseRef)
|
|
149
152
|
if (!cancelled) setCompareRef(mergeBase)
|
|
150
|
-
})
|
|
153
|
+
})()
|
|
151
154
|
return () => {
|
|
152
155
|
cancelled = true
|
|
153
156
|
}
|
|
@@ -207,23 +210,24 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
207
210
|
if (!(selectedDiffKey != null && selectedDiffKey !== '')) return
|
|
208
211
|
if (diff || loading) return
|
|
209
212
|
dispatchGlobal({ key: selectedDiffKey, loading: true, type: 'git-mode-set-loading' })
|
|
210
|
-
void
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
void (async () => {
|
|
214
|
+
try {
|
|
215
|
+
const d = await fetchDiff(
|
|
216
|
+
selectedFile.repoPath ?? projectPath,
|
|
217
|
+
selectedFile,
|
|
218
|
+
gitMode.headOffset,
|
|
219
|
+
compareRef
|
|
220
|
+
)
|
|
217
221
|
dispatchGlobal({
|
|
218
222
|
diff: d,
|
|
219
223
|
hash: diffHash(d.rawDiff),
|
|
220
224
|
key: selectedDiffKey,
|
|
221
225
|
type: 'git-mode-set-diff',
|
|
222
226
|
})
|
|
223
|
-
|
|
224
|
-
.catch(() =>
|
|
227
|
+
} catch {
|
|
225
228
|
dispatchGlobal({ key: selectedDiffKey, loading: false, type: 'git-mode-set-loading' })
|
|
226
|
-
|
|
229
|
+
}
|
|
230
|
+
})()
|
|
227
231
|
}, [
|
|
228
232
|
focusMode,
|
|
229
233
|
projectPath,
|
|
@@ -256,6 +260,8 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
256
260
|
)
|
|
257
261
|
} else if (actionMessage != null && actionMessage !== '') {
|
|
258
262
|
footerNode = actionMessage.split('\n').map((line, idx) => (
|
|
263
|
+
// Message lines are positional and may repeat (e.g. blank lines); index is the identity.
|
|
264
|
+
// eslint-disable-next-line react/no-array-index-key
|
|
259
265
|
<text key={idx} fg={t.primary}>
|
|
260
266
|
{line}
|
|
261
267
|
</text>
|
|
@@ -264,6 +270,12 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
264
270
|
|
|
265
271
|
const baseLabel = compareRef != null && reviewBaseRef != null ? `vs ${reviewBaseRef}` : undefined
|
|
266
272
|
|
|
273
|
+
const handleExitDiff = useCallback((event: OtuiMouseEvent) => {
|
|
274
|
+
event.preventDefault()
|
|
275
|
+
event.stopPropagation()
|
|
276
|
+
dispatchGlobal({ type: 'exit-git-mode' })
|
|
277
|
+
}, [])
|
|
278
|
+
|
|
267
279
|
return (
|
|
268
280
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
269
281
|
<box flexDirection="row" flexGrow={1}>
|
|
@@ -331,11 +343,7 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
331
343
|
paddingLeft={1}
|
|
332
344
|
paddingRight={1}
|
|
333
345
|
backgroundColor={actionBg}
|
|
334
|
-
onMouseDown={
|
|
335
|
-
event.preventDefault()
|
|
336
|
-
event.stopPropagation()
|
|
337
|
-
dispatchGlobal({ type: 'exit-git-mode' })
|
|
338
|
-
}}
|
|
346
|
+
onMouseDown={handleExitDiff}
|
|
339
347
|
>
|
|
340
348
|
<text fg={t.text}>
|
|
341
349
|
<strong>Exit diff</strong>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BoxRenderable } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import { memo, useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
|
4
4
|
|
|
5
5
|
import { convertToPng, isPng } from '../../../terminal-graphics/format-fallback'
|
|
6
6
|
import {
|
|
@@ -77,15 +77,7 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
77
77
|
}
|
|
78
78
|
}, [bytes, mime])
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<box flexGrow={1} alignItems="center" justifyContent="center" padding={1}>
|
|
83
|
-
<text fg={t.warning}>({error})</text>
|
|
84
|
-
</box>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function renderAfter(this: BoxRenderable): void {
|
|
80
|
+
const renderAfter = useCallback(function (this: BoxRenderable): void {
|
|
89
81
|
const state = stateRef.current
|
|
90
82
|
if (!state.uploaded || state.imageId === null) return
|
|
91
83
|
const key = `${this.screenX},${this.screenY},${this.width},${this.height}`
|
|
@@ -94,6 +86,14 @@ export const TerminalImagePane = memo(function TerminalImagePane({
|
|
|
94
86
|
const seq = buildPlacement(state.imageId, this.screenX, this.screenY)
|
|
95
87
|
// Queue write to land AFTER opentui's native cell flush in this frame.
|
|
96
88
|
process.nextTick(() => writeRaw(seq))
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
if (error != null && error !== '') {
|
|
92
|
+
return (
|
|
93
|
+
<box flexGrow={1} alignItems="center" justifyContent="center" padding={1}>
|
|
94
|
+
<text fg={t.warning}>({error})</text>
|
|
95
|
+
</box>
|
|
96
|
+
)
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
return <box flexGrow={1} renderAfter={renderAfter} />
|