@brimveyn/aimux 1.5.4 → 1.6.1

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.
Files changed (67) hide show
  1. package/README.md +14 -2
  2. package/package.json +5 -3
  3. package/src/app-runtime/side-effects.ts +41 -6
  4. package/src/app.tsx +17 -5
  5. package/src/config.ts +29 -4
  6. package/src/diff-parser/clean-last-newline.ts +5 -0
  7. package/src/diff-parser/constants.ts +13 -0
  8. package/src/diff-parser/index.ts +12 -0
  9. package/src/diff-parser/parse-line-type.ts +29 -0
  10. package/src/diff-parser/parse-patch-files.ts +369 -0
  11. package/src/diff-parser/types.ts +75 -0
  12. package/src/git/git-diff.ts +24 -12
  13. package/src/git/git-poller.ts +11 -3
  14. package/src/git/git-status.ts +98 -17
  15. package/src/input/modes/bridge.ts +8 -0
  16. package/src/input/modes/transitions.ts +2 -1
  17. package/src/input/modes/types.ts +4 -1
  18. package/src/pty/terminal-snapshot.ts +5 -5
  19. package/src/state/git-tree.ts +220 -0
  20. package/src/state/reducers/git-mode-state.ts +276 -36
  21. package/src/state/reducers/git-panel-state.ts +35 -4
  22. package/src/state/reducers/modal-state.ts +43 -10
  23. package/src/state/store.ts +5 -1
  24. package/src/state/types.ts +46 -6
  25. package/src/state/workspace-save.ts +2 -0
  26. package/src/ui/components/create-session-modal.tsx +25 -8
  27. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  28. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  29. package/src/ui/components/diff-renderer/fold-strip.tsx +86 -0
  30. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  31. package/src/ui/components/diff-renderer/index.ts +1 -0
  32. package/src/ui/components/diff-renderer/pierre-diff.tsx +171 -0
  33. package/src/ui/components/diff-renderer/split-view.tsx +211 -0
  34. package/src/ui/components/diff-renderer/stacked-view.tsx +168 -0
  35. package/src/ui/components/git-commit-modal.tsx +16 -3
  36. package/src/ui/components/git-pane-widget.tsx +6 -1
  37. package/src/ui/components/git-panel.tsx +215 -86
  38. package/src/ui/components/git-view.tsx +103 -90
  39. package/src/ui/components/help-modal.tsx +45 -12
  40. package/src/ui/components/input-field.tsx +4 -3
  41. package/src/ui/components/list-item.tsx +11 -2
  42. package/src/ui/components/modal-filter-bar.tsx +3 -2
  43. package/src/ui/components/modal-keybinds-overlay.tsx +4 -3
  44. package/src/ui/components/modal-shell.tsx +5 -4
  45. package/src/ui/components/new-tab-modal.tsx +17 -4
  46. package/src/ui/components/pending-chord-overlay.tsx +5 -4
  47. package/src/ui/components/session-bar.tsx +13 -6
  48. package/src/ui/components/session-picker-modal.tsx +28 -10
  49. package/src/ui/components/sidebar.tsx +26 -11
  50. package/src/ui/components/snippet-editor-modal.tsx +18 -3
  51. package/src/ui/components/snippet-picker-modal.tsx +13 -4
  52. package/src/ui/components/split-layout.tsx +3 -2
  53. package/src/ui/components/status-bar.tsx +15 -10
  54. package/src/ui/components/surface.tsx +6 -6
  55. package/src/ui/components/tab-item.tsx +28 -17
  56. package/src/ui/components/terminal-pane.tsx +28 -16
  57. package/src/ui/components/theme-picker-modal.tsx +113 -17
  58. package/src/ui/components/update-available-modal.tsx +13 -2
  59. package/src/ui/filter-themes.ts +15 -0
  60. package/src/ui/keymap-context.ts +10 -2
  61. package/src/ui/root.tsx +29 -7
  62. package/src/ui/shiki.ts +49 -0
  63. package/src/ui/status-bar-model.ts +32 -4
  64. package/src/ui/theme-store.ts +36 -0
  65. package/src/ui/theme.ts +4 -17
  66. package/src/ui/themes.ts +23 -277
  67. package/src/ui/syntax.ts +0 -102
@@ -0,0 +1,48 @@
1
+ import type { ThemedToken } from 'shiki'
2
+
3
+ import { ensureShikiLang, ensureShikiTheme, getShikiHighlighter } from '../../shiki'
4
+
5
+ export interface HighlightSpan {
6
+ bold?: boolean
7
+ fg?: string
8
+ italic?: boolean
9
+ text: string
10
+ underline?: boolean
11
+ }
12
+
13
+ // Shiki FontStyle: 1 = italic, 2 = bold, 4 = underline.
14
+ export function tokenToSpan(token: ThemedToken): HighlightSpan {
15
+ const fs = token.fontStyle ?? 0
16
+ return {
17
+ bold: (fs & 2) !== 0 || undefined,
18
+ fg: token.color,
19
+ italic: (fs & 1) !== 0 || undefined,
20
+ text: token.content,
21
+ underline: (fs & 4) !== 0 || undefined,
22
+ }
23
+ }
24
+
25
+ export async function tokenizeSide(
26
+ lines: string[],
27
+ lang: string,
28
+ theme: string
29
+ ): Promise<ThemedToken[][]> {
30
+ if (lines.length === 0) return []
31
+ const highlighter = await getShikiHighlighter()
32
+ const [langOk, themeOk] = await Promise.all([
33
+ ensureShikiLang(highlighter, lang),
34
+ ensureShikiTheme(highlighter, theme),
35
+ ])
36
+ if (!langOk || !themeOk) return []
37
+ try {
38
+ const result = highlighter.codeToTokens(lines.join(''), {
39
+ // eslint-disable-next-line typescript/no-explicit-any
40
+ lang: lang as any,
41
+ // eslint-disable-next-line typescript/no-explicit-any
42
+ theme: theme as any,
43
+ })
44
+ return result.tokens
45
+ } catch {
46
+ return []
47
+ }
48
+ }
@@ -0,0 +1 @@
1
+ export { type DiffView, PierreDiff, type PierreDiffHandle } from './pierre-diff'
@@ -0,0 +1,171 @@
1
+ import type { ScrollBoxRenderable } from '@opentui/core'
2
+ import type { ThemedToken } from 'shiki'
3
+
4
+ import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
5
+
6
+ import type { FoldState } from '../../../state/types'
7
+ import type { ThemeId } from '../../themes'
8
+
9
+ import { parsePatchFiles } from '../../../diff-parser'
10
+ import { useAppStore } from '../../../state/app-store'
11
+ import { dispatchGlobal } from '../../../state/dispatch-ref'
12
+ import { useTheme } from '../../theme'
13
+ import { buildSplitRows, buildUnifiedRows, firstChangeRowOffset, gutterWidth } from './build-rows'
14
+ import { filetypeFromPath } from './filetype'
15
+ import { tokenizeSide } from './highlight'
16
+ import { SplitView, type SplitViewHandle } from './split-view'
17
+ import { StackedView, type StackedViewHandle } from './stacked-view'
18
+
19
+ export type DiffView = 'split' | 'stacked'
20
+
21
+ export interface PierreDiffHandle {
22
+ leftScroll: ScrollBoxRenderable | null
23
+ rightScroll: ScrollBoxRenderable | null
24
+ }
25
+
26
+ export interface DiffHighlights {
27
+ add: ThemedToken[][]
28
+ del: ThemedToken[][]
29
+ }
30
+
31
+ export interface FoldDispatch {
32
+ adjust: (foldId: string, side: 'top' | 'bottom', delta: number) => void
33
+ set: (foldId: string, top: number, bottom: number) => void
34
+ }
35
+
36
+ interface Props {
37
+ cacheKey: string
38
+ diff: string
39
+ path: string
40
+ themeId: ThemeId
41
+ view: DiffView
42
+ }
43
+
44
+ const EMPTY_HIGHLIGHTS: DiffHighlights = { add: [], del: [] }
45
+ const EMPTY_FOLDS: Record<string, FoldState> = {}
46
+
47
+ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDiff(
48
+ { cacheKey, diff, path, themeId, view },
49
+ ref
50
+ ) {
51
+ const theme = useTheme()
52
+ const file = useMemo(() => {
53
+ const patches = parsePatchFiles(diff)
54
+ return patches[0]?.files[0]
55
+ }, [diff])
56
+
57
+ const filetype = useMemo(() => filetypeFromPath(path), [path])
58
+
59
+ const terminalCols = useAppStore((s) => s.layout.terminalCols)
60
+ const sidebarWidth = useAppStore((s) => s.sidebar.width)
61
+ const contentWidth = useMemo(() => {
62
+ if (!file) return 0
63
+ const gw = gutterWidth(file)
64
+ const usable = Math.max(0, terminalCols - sidebarWidth - 1)
65
+ const paneCols = view === 'split' ? Math.max(0, Math.floor(usable / 2) - 1) : usable
66
+ const prefix = view === 'split' ? gw + 4 : gw * 2 + 5
67
+ return Math.max(1, paneCols - prefix)
68
+ }, [file, view, terminalCols, sidebarWidth])
69
+
70
+ const folds = useAppStore((s) => s.gitMode.folds[cacheKey]) ?? EMPTY_FOLDS
71
+ const foldDispatch = useMemo<FoldDispatch>(
72
+ () => ({
73
+ adjust: (foldId, side, delta) =>
74
+ dispatchGlobal({ delta, foldId, key: cacheKey, side, type: 'git-mode-fold-adjust' }),
75
+ set: (foldId, top, bottom) =>
76
+ dispatchGlobal({ bottom, foldId, key: cacheKey, top, type: 'git-mode-fold-set' }),
77
+ }),
78
+ [cacheKey]
79
+ )
80
+
81
+ const [highlights, setHighlights] = useState<DiffHighlights>(EMPTY_HIGHLIGHTS)
82
+
83
+ useEffect(() => {
84
+ setHighlights(EMPTY_HIGHLIGHTS)
85
+ if (!file || !filetype) return
86
+ let cancelled = false
87
+ void Promise.all([
88
+ tokenizeSide(file.additionLines, filetype, themeId),
89
+ tokenizeSide(file.deletionLines, filetype, themeId),
90
+ ]).then(([add, del]) => {
91
+ if (cancelled) return
92
+ setHighlights({ add, del })
93
+ })
94
+ return () => {
95
+ cancelled = true
96
+ }
97
+ }, [file, filetype, themeId])
98
+
99
+ const splitRef = useRef<SplitViewHandle | null>(null)
100
+ const stackedRef = useRef<StackedViewHandle | null>(null)
101
+
102
+ useImperativeHandle(
103
+ ref,
104
+ () => ({
105
+ get leftScroll() {
106
+ if (view === 'split') return splitRef.current?.leftScroll ?? null
107
+ return stackedRef.current?.scroll ?? null
108
+ },
109
+ get rightScroll() {
110
+ if (view === 'split') return splitRef.current?.rightScroll ?? null
111
+ return stackedRef.current?.scroll ?? null
112
+ },
113
+ }),
114
+ [view]
115
+ )
116
+
117
+ useEffect(() => {
118
+ if (!file) return
119
+ const rows =
120
+ view === 'split'
121
+ ? buildSplitRows(file, EMPTY_FOLDS, contentWidth)
122
+ : buildUnifiedRows(file, EMPTY_FOLDS, contentWidth)
123
+ const offset = firstChangeRowOffset(rows)
124
+ if (offset < 0) return
125
+ const target = Math.max(0, offset - 2)
126
+ const apply = (): void => {
127
+ if (view === 'split') {
128
+ const left = splitRef.current?.leftScroll
129
+ const right = splitRef.current?.rightScroll
130
+ if (left) left.scrollTop = target
131
+ if (right && right !== left) right.scrollTop = target
132
+ } else {
133
+ const node = stackedRef.current?.scroll
134
+ if (node) node.scrollTop = target
135
+ }
136
+ }
137
+ const raf = requestAnimationFrame(apply)
138
+ return () => cancelAnimationFrame(raf)
139
+ }, [cacheKey, file, view, contentWidth])
140
+
141
+ if (!file) {
142
+ return (
143
+ <box flexGrow={1} padding={1}>
144
+ <text fg={theme.colors['descriptionForeground']}>(could not parse diff)</text>
145
+ </box>
146
+ )
147
+ }
148
+
149
+ if (view === 'stacked') {
150
+ return (
151
+ <StackedView
152
+ ref={stackedRef}
153
+ contentWidth={contentWidth}
154
+ file={file}
155
+ foldDispatch={foldDispatch}
156
+ folds={folds}
157
+ highlights={highlights}
158
+ />
159
+ )
160
+ }
161
+ return (
162
+ <SplitView
163
+ ref={splitRef}
164
+ contentWidth={contentWidth}
165
+ file={file}
166
+ foldDispatch={foldDispatch}
167
+ folds={folds}
168
+ highlights={highlights}
169
+ />
170
+ )
171
+ })
@@ -0,0 +1,211 @@
1
+ import type { ThemedToken } from 'shiki'
2
+
3
+ import {
4
+ type MouseEvent as OtuiMouseEvent,
5
+ type ScrollBoxRenderable,
6
+ TextAttributes,
7
+ } from '@opentui/core'
8
+ import { forwardRef, useImperativeHandle, useRef } from 'react'
9
+
10
+ import type { FileDiffMetadata } from '../../../diff-parser'
11
+ import type { FoldState } from '../../../state/types'
12
+ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
13
+
14
+ import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
15
+ import { scrollGitDiff } from '../../git-view-controls'
16
+ import { useTheme } from '../../theme'
17
+ import { buildSplitRows, gutterWidth, type SplitCell, type SplitRowOrHeader } from './build-rows'
18
+ import { FoldStrip } from './fold-strip'
19
+ import { tokenToSpan } from './highlight'
20
+
21
+ export interface SplitViewHandle {
22
+ leftScroll: ScrollBoxRenderable | null
23
+ rightScroll: ScrollBoxRenderable | null
24
+ }
25
+
26
+ interface Props {
27
+ file: FileDiffMetadata
28
+ highlights: DiffHighlights
29
+ folds: Record<string, FoldState>
30
+ foldDispatch: FoldDispatch
31
+ contentWidth: number
32
+ }
33
+
34
+ function handleScroll(e: OtuiMouseEvent): void {
35
+ const delta = getScrollViewportDelta(e)
36
+ if (delta === null) return
37
+ e.preventDefault()
38
+ e.stopPropagation()
39
+ scrollGitDiff(delta)
40
+ }
41
+
42
+ export const SplitView = forwardRef<SplitViewHandle, Props>(function SplitView(
43
+ { contentWidth, file, foldDispatch, folds, highlights },
44
+ ref
45
+ ) {
46
+ const theme = useTheme()
47
+ const leftRef = useRef<ScrollBoxRenderable | null>(null)
48
+ const rightRef = useRef<ScrollBoxRenderable | null>(null)
49
+
50
+ useImperativeHandle(
51
+ ref,
52
+ () => ({
53
+ get leftScroll() {
54
+ return leftRef.current
55
+ },
56
+ get rightScroll() {
57
+ return rightRef.current
58
+ },
59
+ }),
60
+ []
61
+ )
62
+
63
+ const rows = buildSplitRows(file, folds, contentWidth)
64
+ const gw = gutterWidth(file)
65
+
66
+ return (
67
+ <box flexDirection="row" flexGrow={1} overflow="hidden" onMouseScroll={handleScroll}>
68
+ <scrollbox
69
+ ref={leftRef}
70
+ flexGrow={1}
71
+ scrollY
72
+ viewportCulling
73
+ contentOptions={{ flexDirection: 'column', gap: 0 }}
74
+ verticalScrollbarOptions={{ visible: false }}
75
+ onMouseScroll={handleScroll}
76
+ >
77
+ {rows.map((row, i) => (
78
+ <SideRow
79
+ key={i}
80
+ cell={row.type === 'row' ? row.left : null}
81
+ foldDispatch={foldDispatch}
82
+ gw={gw}
83
+ header={row.type === 'hunk-header' ? row : null}
84
+ rowHeight={row.type === 'row' ? row.height : 1}
85
+ tokens={highlights.del}
86
+ />
87
+ ))}
88
+ </scrollbox>
89
+ <box width={1} backgroundColor={theme.colors['editor.background']} />
90
+ <scrollbox
91
+ ref={rightRef}
92
+ flexGrow={1}
93
+ scrollY
94
+ viewportCulling
95
+ contentOptions={{ flexDirection: 'column', gap: 0 }}
96
+ onMouseScroll={handleScroll}
97
+ >
98
+ {rows.map((row, i) => (
99
+ <SideRow
100
+ key={i}
101
+ cell={row.type === 'row' ? row.right : null}
102
+ foldDispatch={foldDispatch}
103
+ gw={gw}
104
+ header={row.type === 'hunk-header' ? row : null}
105
+ rowHeight={row.type === 'row' ? row.height : 1}
106
+ tokens={highlights.add}
107
+ />
108
+ ))}
109
+ </scrollbox>
110
+ </box>
111
+ )
112
+ })
113
+
114
+ function SideRow({
115
+ cell,
116
+ foldDispatch,
117
+ gw,
118
+ header,
119
+ rowHeight,
120
+ tokens,
121
+ }: {
122
+ cell: SplitCell | null
123
+ foldDispatch: FoldDispatch
124
+ gw: number
125
+ header: Extract<SplitRowOrHeader, { type: 'hunk-header' }> | null
126
+ rowHeight: number
127
+ tokens: ThemedToken[][]
128
+ }) {
129
+ if (header) return <HunkHeaderRow row={header} />
130
+ if (!cell) return null
131
+ if (cell.type === 'fold') return <FoldStrip dispatch={foldDispatch} fold={cell.fold} />
132
+ return <HalfRow cell={cell} gw={gw} height={rowHeight} tokens={tokens} />
133
+ }
134
+
135
+ function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-header' }> }) {
136
+ const theme = useTheme()
137
+ return (
138
+ <box
139
+ flexDirection="row"
140
+ backgroundColor={theme.colors['sideBarSectionHeader.background']}
141
+ paddingLeft={1}
142
+ paddingRight={1}
143
+ >
144
+ <text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
145
+ {row.context ? (
146
+ <text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
147
+ ) : null}
148
+ </box>
149
+ )
150
+ }
151
+
152
+ function HalfRow({
153
+ cell,
154
+ gw,
155
+ height,
156
+ tokens,
157
+ }: {
158
+ cell: Exclude<SplitCell, { type: 'fold' }>
159
+ gw: number
160
+ height: number
161
+ tokens: ThemedToken[][]
162
+ }) {
163
+ const theme = useTheme()
164
+ if (cell.type === 'filler') {
165
+ return <box backgroundColor={theme.colors['sideBarSectionHeader.background']} height={height} />
166
+ }
167
+ let bg: string | undefined
168
+ let sign = ' '
169
+ let signColor = theme.colors['descriptionForeground']
170
+ if (cell.type === 'addition') {
171
+ bg = theme.colors['diffEditor.insertedLineBackground']
172
+ sign = '+'
173
+ signColor = theme.colors['gitDecoration.addedResourceForeground']
174
+ } else if (cell.type === 'deletion') {
175
+ bg = theme.colors['diffEditor.removedLineBackground']
176
+ sign = '-'
177
+ signColor = theme.colors['editorError.foreground']
178
+ }
179
+ const num = String(cell.lineNumber).padStart(gw, ' ')
180
+ const lineTokens = tokens[cell.lineIdx]
181
+ return (
182
+ <box flexDirection="row" backgroundColor={bg} height={height}>
183
+ <text fg={theme.colors['descriptionForeground']}>{` ${num} `}</text>
184
+ <text fg={signColor}>{`${sign} `}</text>
185
+ <LineContent content={cell.content} tokens={lineTokens} />
186
+ </box>
187
+ )
188
+ }
189
+
190
+ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
191
+ const theme = useTheme()
192
+ if (!tokens || tokens.length === 0) {
193
+ return <text fg={theme.colors['editor.foreground']}>{content}</text>
194
+ }
195
+ return (
196
+ <text>
197
+ {tokens.map((t, i) => {
198
+ const s = tokenToSpan(t)
199
+ let attributes = 0
200
+ if (s.bold) attributes |= TextAttributes.BOLD
201
+ if (s.italic) attributes |= TextAttributes.ITALIC
202
+ if (s.underline) attributes |= TextAttributes.UNDERLINE
203
+ return (
204
+ <span key={i} fg={s.fg ?? theme.colors['editor.foreground']} attributes={attributes}>
205
+ {s.text}
206
+ </span>
207
+ )
208
+ })}
209
+ </text>
210
+ )
211
+ }
@@ -0,0 +1,168 @@
1
+ import type { ThemedToken } from 'shiki'
2
+
3
+ import {
4
+ type MouseEvent as OtuiMouseEvent,
5
+ type ScrollBoxRenderable,
6
+ TextAttributes,
7
+ } from '@opentui/core'
8
+ import { forwardRef, useImperativeHandle, useRef } from 'react'
9
+
10
+ import type { FileDiffMetadata } from '../../../diff-parser'
11
+ import type { FoldState } from '../../../state/types'
12
+ import type { DiffHighlights, FoldDispatch } from './pierre-diff'
13
+
14
+ import { getScrollViewportDelta } from '../../../app-runtime/terminal-mouse-adapter'
15
+ import { scrollGitDiff } from '../../git-view-controls'
16
+ import { useTheme } from '../../theme'
17
+ import { buildUnifiedRows, gutterWidth, type UnifiedRowOrHeader } from './build-rows'
18
+ import { FoldStrip } from './fold-strip'
19
+ import { tokenToSpan } from './highlight'
20
+
21
+ export interface StackedViewHandle {
22
+ scroll: ScrollBoxRenderable | null
23
+ }
24
+
25
+ interface Props {
26
+ file: FileDiffMetadata
27
+ highlights: DiffHighlights
28
+ folds: Record<string, FoldState>
29
+ foldDispatch: FoldDispatch
30
+ contentWidth: number
31
+ }
32
+
33
+ function handleScroll(e: OtuiMouseEvent): void {
34
+ const delta = getScrollViewportDelta(e)
35
+ if (delta === null) return
36
+ e.preventDefault()
37
+ e.stopPropagation()
38
+ scrollGitDiff(delta)
39
+ }
40
+
41
+ export const StackedView = forwardRef<StackedViewHandle, Props>(function StackedView(
42
+ { contentWidth, file, foldDispatch, folds, highlights },
43
+ ref
44
+ ) {
45
+ const scrollRef = useRef<ScrollBoxRenderable | null>(null)
46
+
47
+ useImperativeHandle(
48
+ ref,
49
+ () => ({
50
+ get scroll() {
51
+ return scrollRef.current
52
+ },
53
+ }),
54
+ []
55
+ )
56
+
57
+ const rows = buildUnifiedRows(file, folds, contentWidth)
58
+ const gw = gutterWidth(file)
59
+
60
+ return (
61
+ <scrollbox
62
+ ref={scrollRef}
63
+ flexGrow={1}
64
+ scrollY
65
+ viewportCulling
66
+ contentOptions={{ flexDirection: 'column', gap: 0 }}
67
+ onMouseScroll={handleScroll}
68
+ >
69
+ {rows.map((row, i) => (
70
+ <UnifiedRowRender
71
+ key={i}
72
+ foldDispatch={foldDispatch}
73
+ gw={gw}
74
+ highlights={highlights}
75
+ row={row}
76
+ />
77
+ ))}
78
+ </scrollbox>
79
+ )
80
+ })
81
+
82
+ function UnifiedRowRender({
83
+ foldDispatch,
84
+ gw,
85
+ highlights,
86
+ row,
87
+ }: {
88
+ foldDispatch: FoldDispatch
89
+ gw: number
90
+ highlights: DiffHighlights
91
+ row: UnifiedRowOrHeader
92
+ }) {
93
+ const theme = useTheme()
94
+ if (row.type === 'hunk-header') {
95
+ return (
96
+ <box
97
+ flexDirection="row"
98
+ backgroundColor={theme.colors['sideBarSectionHeader.background']}
99
+ paddingLeft={1}
100
+ paddingRight={1}
101
+ >
102
+ <text fg={theme.colors['descriptionForeground']}>{row.spec}</text>
103
+ {row.context ? (
104
+ <text fg={theme.colors['editor.lineHighlightBackground']}> {row.context}</text>
105
+ ) : null}
106
+ </box>
107
+ )
108
+ }
109
+ if (row.type === 'fold') {
110
+ return <FoldStrip dispatch={foldDispatch} fold={row.fold} />
111
+ }
112
+ const pad = (n: number | undefined): string =>
113
+ n === undefined ? ' '.repeat(gw) : String(n).padStart(gw, ' ')
114
+ if (row.type === 'context') {
115
+ const tokens = highlights.add[row.lineIdx]
116
+ return (
117
+ <box flexDirection="row" height={row.height}>
118
+ <text
119
+ fg={theme.colors['descriptionForeground']}
120
+ >{` ${pad(row.delLineNumber)} ${pad(row.addLineNumber)} `}</text>
121
+ <text fg={theme.colors['editor.foreground']}> </text>
122
+ <LineContent content={row.content} tokens={tokens} />
123
+ </box>
124
+ )
125
+ }
126
+ const bg =
127
+ row.type === 'addition'
128
+ ? theme.colors['diffEditor.insertedLineBackground']
129
+ : theme.colors['diffEditor.removedLineBackground']
130
+ const sign = row.type === 'addition' ? '+' : '-'
131
+ const signColor =
132
+ row.type === 'addition'
133
+ ? theme.colors['gitDecoration.addedResourceForeground']
134
+ : theme.colors['editorError.foreground']
135
+ const delNum = row.type === 'deletion' ? row.lineNumber : undefined
136
+ const addNum = row.type === 'addition' ? row.lineNumber : undefined
137
+ const tokens = row.type === 'addition' ? highlights.add[row.lineIdx] : highlights.del[row.lineIdx]
138
+ return (
139
+ <box flexDirection="row" backgroundColor={bg} height={row.height}>
140
+ <text fg={theme.colors['descriptionForeground']}>{` ${pad(delNum)} ${pad(addNum)} `}</text>
141
+ <text fg={signColor}>{`${sign} `}</text>
142
+ <LineContent content={row.content} tokens={tokens} />
143
+ </box>
144
+ )
145
+ }
146
+
147
+ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken[] | undefined }) {
148
+ const theme = useTheme()
149
+ if (!tokens || tokens.length === 0) {
150
+ return <text fg={theme.colors['editor.foreground']}>{content}</text>
151
+ }
152
+ return (
153
+ <text>
154
+ {tokens.map((t, i) => {
155
+ const s = tokenToSpan(t)
156
+ let attributes = 0
157
+ if (s.bold) attributes |= TextAttributes.BOLD
158
+ if (s.italic) attributes |= TextAttributes.ITALIC
159
+ if (s.underline) attributes |= TextAttributes.UNDERLINE
160
+ return (
161
+ <span key={i} fg={s.fg ?? theme.colors['editor.foreground']} attributes={attributes}>
162
+ {s.text}
163
+ </span>
164
+ )
165
+ })}
166
+ </text>
167
+ )
168
+ }
@@ -1,4 +1,4 @@
1
- import { theme } from '../theme'
1
+ import { useTheme } from '../theme'
2
2
  import { uiTokens } from '../ui-tokens'
3
3
  import { InputField } from './input-field'
4
4
  import { ModalShell } from './modal-shell'
@@ -11,13 +11,20 @@ interface GitCommitModalProps {
11
11
  }
12
12
 
13
13
  export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
14
+ const theme = useTheme()
14
15
  const titleActive = activeField === 'title'
15
16
  const bodyActive = activeField === 'body'
16
17
 
17
18
  return (
18
19
  <ModalShell title="Commit" keybindsModeId="modal.git-commit" width={uiTokens.modalWidth.xl}>
19
20
  <box flexDirection="column">
20
- <text fg={titleActive ? theme.text : theme.textMuted}>Title</text>
21
+ <text
22
+ fg={
23
+ titleActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
24
+ }
25
+ >
26
+ Title
27
+ </text>
21
28
  <InputField
22
29
  active={titleActive}
23
30
  cursorPos={titleActive ? cursorPos : undefined}
@@ -26,7 +33,13 @@ export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommi
26
33
  </box>
27
34
 
28
35
  <box flexDirection="column">
29
- <text fg={bodyActive ? theme.text : theme.textMuted}>Body (optional)</text>
36
+ <text
37
+ fg={
38
+ bodyActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
39
+ }
40
+ >
41
+ Body (optional)
42
+ </text>
30
43
  <InputField
31
44
  active={bodyActive}
32
45
  cursorPos={bodyActive ? cursorPos : undefined}
@@ -12,6 +12,8 @@ interface GitPaneWidgetProps {
12
12
 
13
13
  export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: GitPaneWidgetProps) {
14
14
  const gitPanel = useAppStore((s) => s.gitPanel)
15
+ const gitMode = useAppStore((s) => s.gitMode)
16
+ const gitFileListMode = useAppStore((s) => s.gitPane.fileListMode)
15
17
  const pathConfig = useAppStore((s) => s.gitPane.path)
16
18
  const diffCountConfig = useAppStore((s) => s.gitPane.diffCount)
17
19
  const currentSessionId = useAppStore((s) => s.currentSessionId)
@@ -21,7 +23,7 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
21
23
  : undefined
22
24
  const projectPath = currentSession?.projectPath
23
25
 
24
- useGitPanelPolling({ enabled: pollingEnabled, projectPath })
26
+ useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
25
27
 
26
28
  const lastGoodRef = useRef<GitPanelState | null>(null)
27
29
  const prevProjectPathRef = useRef(projectPath)
@@ -37,10 +39,13 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
37
39
 
38
40
  return (
39
41
  <GitPanel
42
+ collapsedFolders={gitMode.collapsedFolders}
40
43
  diffCountConfig={diffCountConfig}
44
+ fileListMode={gitFileListMode}
41
45
  gitPanel={display}
42
46
  pathConfig={pathConfig}
43
47
  projectPath={projectPath}
48
+ selectedEntryKey={gitMode.selectedEntryKey}
44
49
  />
45
50
  )
46
51
  })