@brimveyn/aimux 1.1.0 → 1.2.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.
@@ -0,0 +1,237 @@
1
+ import type { DiffRenderable } from '@opentui/core'
2
+
3
+ import { memo, useEffect, useRef } from 'react'
4
+
5
+ import type { DiffData } from '../../state/types'
6
+
7
+ import { fetchDiff } from '../../git/git-diff'
8
+ import { useGitPanelPolling } from '../../git/git-poller'
9
+ import { useAppStore } from '../../state/app-store'
10
+ import { dispatchGlobal } from '../../state/dispatch-ref'
11
+ import { setGitDiffScroller } from '../git-view-controls'
12
+ import { getSyntaxClient, getSyntaxStyle } from '../syntax'
13
+ import { theme } from '../theme'
14
+ import { fileKey, GitPanel } from './git-panel'
15
+
16
+ interface CodePaneLike {
17
+ scrollY: number
18
+ maxScrollY: number
19
+ }
20
+
21
+ interface DiffRenderableInternals {
22
+ leftCodeRenderable?: CodePaneLike
23
+ rightCodeRenderable?: CodePaneLike
24
+ }
25
+
26
+ function filetypeFromPath(path: string): string | undefined {
27
+ const dot = path.lastIndexOf('.')
28
+ if (dot < 0) return undefined
29
+ const ext = path.slice(dot + 1).toLowerCase()
30
+ const map: Record<string, string> = {
31
+ cjs: 'javascript',
32
+ js: 'javascript',
33
+ jsx: 'javascript',
34
+ markdown: 'markdown',
35
+ md: 'markdown',
36
+ mdx: 'markdown',
37
+ mjs: 'javascript',
38
+ ts: 'typescript',
39
+ tsx: 'typescript',
40
+ zig: 'zig',
41
+ }
42
+ return map[ext]
43
+ }
44
+
45
+ interface DiffStageProps {
46
+ diff: DiffData | undefined
47
+ loading: boolean
48
+ diffRef: React.RefObject<DiffRenderable | null>
49
+ }
50
+
51
+ function placeholderText(diff: DiffData): string | null {
52
+ if (diff.status === 'binary') {
53
+ const before = diff.binarySizeBefore ?? 0
54
+ const after = diff.binarySizeAfter ?? 0
55
+ return `(binary file — ${before} → ${after} bytes)`
56
+ }
57
+ if (diff.rawDiff.length === 0) {
58
+ if (diff.status === 'new') return '(new file — no diff)'
59
+ if (diff.status === 'deleted') return '(deleted — no diff)'
60
+ return '(no changes)'
61
+ }
62
+ return null
63
+ }
64
+
65
+ const DiffStage = memo(function DiffStage({ diff, diffRef, loading }: DiffStageProps) {
66
+ if (loading && !diff) {
67
+ return (
68
+ <box flexGrow={1} padding={1}>
69
+ <text fg={theme.textMuted}>Loading diff…</text>
70
+ </box>
71
+ )
72
+ }
73
+ if (!diff) {
74
+ return (
75
+ <box flexGrow={1} padding={1}>
76
+ <text fg={theme.textMuted}>Select a file.</text>
77
+ </box>
78
+ )
79
+ }
80
+ if (diff.errorMessage) {
81
+ return (
82
+ <box flexGrow={1} padding={1}>
83
+ <text fg={theme.danger}>{diff.errorMessage}</text>
84
+ </box>
85
+ )
86
+ }
87
+
88
+ const placeholder = placeholderText(diff)
89
+ if (placeholder) {
90
+ return (
91
+ <box flexGrow={1} padding={1}>
92
+ <text fg={theme.textMuted}>{placeholder}</text>
93
+ </box>
94
+ )
95
+ }
96
+
97
+ const filetype = filetypeFromPath(diff.path)
98
+
99
+ return (
100
+ <box flexDirection="column" flexGrow={1} overflow="hidden">
101
+ {diff.oldPath ? (
102
+ <box paddingLeft={1} paddingRight={1}>
103
+ <text fg={theme.textMuted}>
104
+ renamed: {diff.oldPath} → {diff.path}
105
+ </text>
106
+ </box>
107
+ ) : null}
108
+ <diff
109
+ ref={diffRef}
110
+ diff={diff.rawDiff}
111
+ view="split"
112
+ syncScroll
113
+ showLineNumbers
114
+ wrapMode="none"
115
+ filetype={filetype}
116
+ treeSitterClient={filetype ? getSyntaxClient() : undefined}
117
+ syntaxStyle={filetype ? getSyntaxStyle() : undefined}
118
+ addedBg={theme.diffAddBg}
119
+ removedBg={theme.diffRemoveBg}
120
+ addedSignColor={theme.success}
121
+ removedSignColor={theme.danger}
122
+ flexGrow={1}
123
+ />
124
+ </box>
125
+ )
126
+ })
127
+
128
+ export const GitView = memo(function GitView() {
129
+ const sidebarWidth = useAppStore((s) => s.sidebar.width)
130
+ const gitPanel = useAppStore((s) => s.gitPanel)
131
+ const gitMode = useAppStore((s) => s.gitMode)
132
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
133
+ const sessions = useAppStore((s) => s.sessions)
134
+ const focusMode = useAppStore((s) => s.focusMode)
135
+ const diffRef = useRef<DiffRenderable | null>(null)
136
+
137
+ const currentSession = currentSessionId
138
+ ? sessions.find((s) => s.id === currentSessionId)
139
+ : undefined
140
+ const projectPath = currentSession?.projectPath
141
+
142
+ useGitPanelPolling({ enabled: focusMode === 'git', projectPath })
143
+
144
+ const selectedFile = gitPanel.files[gitMode.selectedFileIndex]
145
+ const diff = selectedFile ? gitMode.diffs[selectedFile.path] : undefined
146
+ const loading = selectedFile ? !!gitMode.loading[selectedFile.path] : false
147
+
148
+ useEffect(() => {
149
+ setGitDiffScroller((delta: number) => {
150
+ const node = diffRef.current as unknown as DiffRenderableInternals | null
151
+ if (!node) return
152
+ const left = node.leftCodeRenderable
153
+ const right = node.rightCodeRenderable
154
+ if (!left || !right) return
155
+ const cap = Math.max(left.maxScrollY, right.maxScrollY)
156
+ const base = left.scrollY
157
+ const nextScroll = Math.max(0, Math.min(cap, base + delta))
158
+ left.scrollY = nextScroll
159
+ right.scrollY = nextScroll
160
+ })
161
+ return () => setGitDiffScroller(null)
162
+ }, [])
163
+
164
+ useEffect(() => {
165
+ if (focusMode !== 'git') return
166
+ if (!selectedFile || !projectPath) return
167
+ const path = selectedFile.path
168
+ if (diff || loading) return
169
+ dispatchGlobal({ loading: true, path, type: 'git-mode-set-loading' })
170
+ void fetchDiff(projectPath, selectedFile)
171
+ .then((d) => dispatchGlobal({ diff: d, path, type: 'git-mode-set-diff' }))
172
+ .catch(() => dispatchGlobal({ loading: false, path, type: 'git-mode-set-loading' }))
173
+ }, [focusMode, projectPath, selectedFile, diff, loading])
174
+
175
+ const pendingPath = gitMode.pendingDeletePath
176
+ const pendingIsUntracked =
177
+ pendingPath !== null &&
178
+ selectedFile?.path === pendingPath &&
179
+ selectedFile.section === 'untracked'
180
+ let pendingHint: string | null = null
181
+ if (pendingPath !== null) {
182
+ pendingHint = pendingIsUntracked
183
+ ? 'press d again to delete file'
184
+ : 'press d again to discard changes'
185
+ }
186
+ const actionMessage = gitMode.actionMessage
187
+ let footerNode: React.ReactNode = null
188
+ if (pendingHint) {
189
+ footerNode = (
190
+ <text fg={theme.warning}>
191
+ <strong>{pendingHint}</strong>
192
+ </text>
193
+ )
194
+ } else if (actionMessage) {
195
+ footerNode = actionMessage.split('\n').map((line, idx) => (
196
+ <text key={idx} fg={theme.accent}>
197
+ {line}
198
+ </text>
199
+ ))
200
+ }
201
+
202
+ return (
203
+ <box flexDirection="column" flexGrow={1}>
204
+ <box flexDirection="row" flexGrow={1}>
205
+ <box
206
+ width={sidebarWidth}
207
+ flexDirection="column"
208
+ backgroundColor={theme.panel}
209
+ padding={0}
210
+ gap={0}
211
+ >
212
+ <text fg={theme.accent}>
213
+ <strong>aimux · git</strong>
214
+ </text>
215
+ {gitPanel.branch ? (
216
+ <box flexDirection="row">
217
+ <text fg={theme.accent}>{'\u{e702}'} </text>
218
+ <text fg={theme.textMuted}>{gitPanel.branch}</text>
219
+ </box>
220
+ ) : null}
221
+ <text fg={theme.dim}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
222
+ <GitPanel
223
+ gitPanel={gitPanel}
224
+ projectPath={projectPath}
225
+ selectedFileKey={selectedFile ? fileKey(selectedFile) : null}
226
+ />
227
+ </box>
228
+ <DiffStage diff={diff} diffRef={diffRef} loading={loading} />
229
+ </box>
230
+ {footerNode ? (
231
+ <box paddingLeft={1} paddingRight={1} backgroundColor={theme.panel} flexDirection="column">
232
+ {footerNode}
233
+ </box>
234
+ ) : null}
235
+ </box>
236
+ )
237
+ })
@@ -4,14 +4,35 @@ import { Surface } from './surface'
4
4
  interface InputFieldProps {
5
5
  active: boolean
6
6
  value: string
7
+ cursorPos?: number
7
8
  }
8
9
 
9
- export function InputField({ active, value }: InputFieldProps) {
10
+ export function InputField({ active, cursorPos, value }: InputFieldProps) {
11
+ const fg = active ? theme.text : theme.textMuted
12
+ if (!active) {
13
+ return (
14
+ <Surface tone="input" padding={1}>
15
+ <text fg={fg}>{value}</text>
16
+ </Surface>
17
+ )
18
+ }
19
+
20
+ const safePos =
21
+ cursorPos === undefined ? value.length : Math.max(0, Math.min(value.length, cursorPos))
22
+ const before = value.slice(0, safePos)
23
+ const atChar = safePos < value.length ? value.charAt(safePos) : undefined
24
+ const cursorOnLineEnd = atChar === undefined || atChar === '\n'
25
+ const cursorDisplay = cursorOnLineEnd ? ' ' : (atChar as string)
26
+ const trailing = cursorOnLineEnd ? value.slice(safePos) : value.slice(safePos + 1)
27
+
10
28
  return (
11
- <Surface tone={active ? 'inputActive' : 'input'} padding={1}>
12
- <text fg={active ? theme.text : theme.textMuted}>
13
- {value}
14
- {active ? '_' : ''}
29
+ <Surface tone="inputActive" padding={1}>
30
+ <text fg={fg}>
31
+ {before}
32
+ <span bg={theme.text} fg={theme.background}>
33
+ {cursorDisplay}
34
+ </span>
35
+ {trailing}
15
36
  </text>
16
37
  </Surface>
17
38
  )
@@ -12,6 +12,10 @@ function getModeColor(focusMode: AppState['focusMode']): string {
12
12
  return theme.warning
13
13
  case 'modal':
14
14
  return theme.warning
15
+ case 'command-edit':
16
+ return theme.warning
17
+ case 'git':
18
+ return theme.success
15
19
  case 'navigation':
16
20
  default:
17
21
  return theme.accentAlt
@@ -26,6 +30,10 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
26
30
  return 'layout'
27
31
  case 'modal':
28
32
  return 'modal'
33
+ case 'command-edit':
34
+ return 'edit'
35
+ case 'git':
36
+ return 'git'
29
37
  case 'navigation':
30
38
  default:
31
39
  return 'nav'
@@ -0,0 +1,11 @@
1
+ type Scroller = (delta: number) => void
2
+
3
+ let activeScroller: Scroller | null = null
4
+
5
+ export function setGitDiffScroller(scroller: Scroller | null): void {
6
+ activeScroller = scroller
7
+ }
8
+
9
+ export function scrollGitDiff(delta: number): void {
10
+ activeScroller?.(delta)
11
+ }
package/src/ui/root.tsx CHANGED
@@ -7,9 +7,10 @@ import type { ThemeId } from './themes'
7
7
  import { useAppStore } from '../state/app-store'
8
8
  import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
9
9
  import { CreateSessionModal } from './components/create-session-modal'
10
+ import { GitCommitModal } from './components/git-commit-modal'
11
+ import { GitView } from './components/git-view'
10
12
  import { HelpModal } from './components/help-modal'
11
13
  import { NewTabModal } from './components/new-tab-modal'
12
- import { PendingChordIndicator } from './components/pending-chord-indicator'
13
14
  import { SessionNameModal } from './components/session-name-modal'
14
15
  import { SessionPickerModal } from './components/session-picker-modal'
15
16
  import { Sidebar } from './components/sidebar'
@@ -133,6 +134,20 @@ function renderModal(
133
134
  )
134
135
  case 'help':
135
136
  return <HelpModal />
137
+ case 'git-commit': {
138
+ const titleText =
139
+ modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
140
+ const bodyText =
141
+ modal.activeField === 'title' ? modal.contentBuffer : (modal.editBuffer ?? '')
142
+ return (
143
+ <GitCommitModal
144
+ activeField={modal.activeField}
145
+ body={bodyText}
146
+ cursorPos={modal.cursorPos ?? (modal.editBuffer ?? '').length}
147
+ title={titleText}
148
+ />
149
+ )
150
+ }
136
151
  case null:
137
152
  return null
138
153
  default:
@@ -195,6 +210,26 @@ export function RootView({
195
210
  const snippetEditorFields = getSnippetEditorFields(modal)
196
211
  const splitChrome = PANE_BORDER * 2
197
212
 
213
+ const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
214
+ if (inGitMode) {
215
+ return (
216
+ <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
217
+ <GitView />
218
+ <StatusBar />
219
+ {renderModal(modal, {
220
+ createSessionFields,
221
+ currentSessionId,
222
+ currentTabCount: tabs.length,
223
+ customCommands,
224
+ sessions,
225
+ snippetEditorFields,
226
+ snippets,
227
+ themeId,
228
+ })}
229
+ </box>
230
+ )
231
+ }
232
+
198
233
  return (
199
234
  <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
200
235
  <box flexDirection="row" gap={0} padding={0} flexGrow={1}>
@@ -255,7 +290,6 @@ export function RootView({
255
290
  snippets,
256
291
  themeId,
257
292
  })}
258
- <PendingChordIndicator />
259
293
  </box>
260
294
  )
261
295
  }
@@ -49,6 +49,14 @@ function getInputHint(activeTab?: TabSession): string {
49
49
  return 'Ctrl+z unfocus Ctrl+w layout typing goes to active tab'
50
50
  }
51
51
 
52
+ function getGitHint(modalType: AppState['modal']['type']): string {
53
+ if (modalType === 'git-commit') {
54
+ return 'Tab switch field Enter newline Ctrl+Enter commit Esc cancel'
55
+ }
56
+
57
+ return 'j/k file Ctrl+d/u page a stage d unstage/delete c commit p push Esc exit'
58
+ }
59
+
52
60
  export function getStatusBarModel(state: AppState, activeTab?: TabSession): StatusBarModel {
53
61
  const currentSession = state.currentSessionId
54
62
  ? state.sessions.find((session) => session.id === state.currentSessionId)
@@ -77,6 +85,16 @@ export function getStatusBarModel(state: AppState, activeTab?: TabSession): Stat
77
85
  left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
78
86
  right: 'h/j/k/l focus |/- split H/L resize q close Esc cancel',
79
87
  }
88
+ case 'git':
89
+ return {
90
+ left: `${sessionIcon} ${sessionLabel}`,
91
+ right: getGitHint(state.modal.type),
92
+ }
93
+ case 'command-edit':
94
+ return {
95
+ left: `${sessionIcon} ${sessionLabel}`,
96
+ right: state.modal.type === 'git-commit' ? getGitHint('git-commit') : 'Esc cancel',
97
+ }
80
98
  case 'navigation':
81
99
  default:
82
100
  return {
@@ -0,0 +1,102 @@
1
+ import { getTreeSitterClient, SyntaxStyle, type ThemeTokenStyle } from '@opentui/core'
2
+
3
+ import { onThemeChange, theme } from './theme'
4
+
5
+ let cachedStyle: SyntaxStyle | null = null
6
+ let clientInitPromise: Promise<void> | null = null
7
+
8
+ onThemeChange(() => {
9
+ cachedStyle = null
10
+ })
11
+
12
+ function buildTokens(): ThemeTokenStyle[] {
13
+ return [
14
+ {
15
+ scope: ['comment', 'spell'],
16
+ style: { dim: true, foreground: theme.textMuted, italic: true },
17
+ },
18
+ { scope: ['comment.documentation'], style: { foreground: theme.textMuted, italic: true } },
19
+
20
+ {
21
+ scope: ['string', 'character', 'character.special', 'string.special'],
22
+ style: { foreground: theme.warning },
23
+ },
24
+ {
25
+ scope: ['string.escape', 'string.regexp', 'string.special.url'],
26
+ style: { foreground: theme.accentAlt },
27
+ },
28
+
29
+ {
30
+ scope: ['number', 'boolean', 'constant.builtin', 'constant.numeric'],
31
+ style: { foreground: theme.danger },
32
+ },
33
+ { scope: ['constant', 'constant.character'], style: { foreground: theme.danger } },
34
+
35
+ { scope: ['keyword'], style: { bold: true, foreground: theme.accentAlt } },
36
+ {
37
+ scope: [
38
+ 'keyword.conditional',
39
+ 'keyword.exception',
40
+ 'keyword.import',
41
+ 'keyword.modifier',
42
+ 'keyword.repeat',
43
+ ],
44
+ style: { bold: true, foreground: theme.accentAlt },
45
+ },
46
+ {
47
+ scope: [
48
+ 'keyword.coroutine',
49
+ 'keyword.directive',
50
+ 'keyword.function',
51
+ 'keyword.return',
52
+ 'keyword.type',
53
+ ],
54
+ style: { bold: true, foreground: theme.accentAlt, italic: true },
55
+ },
56
+ { scope: ['keyword.operator', 'operator'], style: { foreground: theme.accent } },
57
+
58
+ {
59
+ scope: ['function', 'function.call', 'function.method', 'function.method.call'],
60
+ style: { foreground: theme.success },
61
+ },
62
+ { scope: ['function.builtin'], style: { foreground: theme.success, italic: true } },
63
+ { scope: ['constructor'], style: { bold: true, foreground: theme.success } },
64
+
65
+ { scope: ['type', 'type.definition'], style: { foreground: theme.accent, italic: true } },
66
+ { scope: ['type.builtin'], style: { bold: true, foreground: theme.accent } },
67
+
68
+ { scope: ['variable'], style: { foreground: theme.text } },
69
+ { scope: ['variable.builtin'], style: { foreground: theme.danger, italic: true } },
70
+ { scope: ['variable.parameter', 'parameter'], style: { foreground: theme.text, italic: true } },
71
+ { scope: ['variable.member', 'property', 'field'], style: { foreground: theme.accent } },
72
+
73
+ { scope: ['attribute'], style: { foreground: theme.warning, italic: true } },
74
+ { scope: ['tag'], style: { foreground: theme.danger } },
75
+ { scope: ['tag.attribute'], style: { foreground: theme.warning, italic: true } },
76
+
77
+ { scope: ['label'], style: { foreground: theme.warning } },
78
+ { scope: ['module', 'namespace'], style: { foreground: theme.accentAlt, italic: true } },
79
+ { scope: ['module.builtin'], style: { bold: true, foreground: theme.accentAlt, italic: true } },
80
+
81
+ {
82
+ scope: ['punctuation.bracket', 'punctuation.delimiter'],
83
+ style: { foreground: theme.textMuted },
84
+ },
85
+ { scope: ['punctuation.special'], style: { foreground: theme.warning } },
86
+ ]
87
+ }
88
+
89
+ export function getSyntaxStyle(): SyntaxStyle {
90
+ if (!cachedStyle) {
91
+ cachedStyle = SyntaxStyle.fromTheme(buildTokens())
92
+ }
93
+ return cachedStyle
94
+ }
95
+
96
+ export function getSyntaxClient() {
97
+ const client = getTreeSitterClient()
98
+ if (!clientInitPromise) {
99
+ clientInitPromise = client.initialize().catch(() => {})
100
+ }
101
+ return client
102
+ }
package/src/ui/theme.ts CHANGED
@@ -2,8 +2,17 @@ import { type ThemeColors, type ThemeId, THEMES } from './themes'
2
2
 
3
3
  export const theme: ThemeColors = { ...THEMES.aimux.colors }
4
4
 
5
+ type ThemeListener = () => void
6
+ const themeListeners = new Set<ThemeListener>()
7
+
8
+ export function onThemeChange(listener: ThemeListener): () => void {
9
+ themeListeners.add(listener)
10
+ return () => themeListeners.delete(listener)
11
+ }
12
+
5
13
  export function applyTheme(id: ThemeId): void {
6
14
  const entry = THEMES[id]
7
15
  if (!entry) return
8
16
  Object.assign(theme, entry.colors)
17
+ for (const listener of themeListeners) listener()
9
18
  }
package/src/ui/themes.ts CHANGED
@@ -14,6 +14,8 @@ export interface ThemeColors {
14
14
  danger: string
15
15
  success: string
16
16
  dim: string
17
+ diffAddBg: string
18
+ diffRemoveBg: string
17
19
  }
18
20
 
19
21
  export type ThemeId =
@@ -38,6 +40,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
38
40
  border: '#2d3f52',
39
41
  borderActive: '#7cd1b8',
40
42
  danger: '#f38ba8',
43
+ diffAddBg: '#1e3d2b',
44
+ diffRemoveBg: '#3b1e27',
41
45
  dim: '#243242',
42
46
  overlay: '#0b1016',
43
47
  panel: '#16202b',
@@ -58,6 +62,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
58
62
  border: '#45475a',
59
63
  borderActive: '#cba6f7',
60
64
  danger: '#f38ba8',
65
+ diffAddBg: '#273c32',
66
+ diffRemoveBg: '#3b2838',
61
67
  dim: '#313244',
62
68
  overlay: '#181825',
63
69
  panel: '#232334',
@@ -78,6 +84,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
78
84
  border: '#44475a',
79
85
  borderActive: '#bd93f9',
80
86
  danger: '#ff5555',
87
+ diffAddBg: '#2f4630',
88
+ diffRemoveBg: '#4a2f3b',
81
89
  dim: '#383a4a',
82
90
  overlay: '#1f2029',
83
91
  panel: '#2d2f3d',
@@ -98,6 +106,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
98
106
  border: '#2a3440',
99
107
  borderActive: '#bd93f9',
100
108
  danger: '#ff5555',
109
+ diffAddBg: '#162b1d',
110
+ diffRemoveBg: '#2a1820',
101
111
  dim: '#1e2630',
102
112
  overlay: '#090d11',
103
113
  panel: '#131920',
@@ -118,6 +128,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
118
128
  border: '#4f585e',
119
129
  borderActive: '#a7c080',
120
130
  danger: '#e67e80',
131
+ diffAddBg: '#35473a',
132
+ diffRemoveBg: '#45353a',
121
133
  dim: '#3d484d',
122
134
  overlay: '#232a2e',
123
135
  panel: '#343f44',
@@ -138,6 +150,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
138
150
  border: '#504945',
139
151
  borderActive: '#b8bb26',
140
152
  danger: '#fb4934',
153
+ diffAddBg: '#2f3a28',
154
+ diffRemoveBg: '#3a2828',
141
155
  dim: '#3c3836',
142
156
  overlay: '#1f1d1b',
143
157
  panel: '#2e2e2e',
@@ -158,6 +172,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
158
172
  border: '#363646',
159
173
  borderActive: '#7e9cd8',
160
174
  danger: '#e82424',
175
+ diffAddBg: '#28332a',
176
+ diffRemoveBg: '#35252b',
161
177
  dim: '#2a2a37',
162
178
  overlay: '#171720',
163
179
  panel: '#24242e',
@@ -178,6 +194,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
178
194
  border: '#434c5e',
179
195
  borderActive: '#88c0d0',
180
196
  danger: '#bf616a',
197
+ diffAddBg: '#2f3d38',
198
+ diffRemoveBg: '#3d3035',
181
199
  dim: '#3b4252',
182
200
  overlay: '#252a33',
183
201
  panel: '#333a47',
@@ -198,6 +216,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
198
216
  border: '#3e4452',
199
217
  borderActive: '#61afef',
200
218
  danger: '#e06c75',
219
+ diffAddBg: '#2c3b2f',
220
+ diffRemoveBg: '#3d2c33',
201
221
  dim: '#3b4048',
202
222
  overlay: '#1f2329',
203
223
  panel: '#2c313a',
@@ -218,6 +238,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
218
238
  border: '#2e5560',
219
239
  borderActive: '#268bd2',
220
240
  danger: '#dc322f',
241
+ diffAddBg: '#1a3a30',
242
+ diffRemoveBg: '#3a1f28',
221
243
  dim: '#073642',
222
244
  overlay: '#00242c',
223
245
  panel: '#003340',
@@ -238,6 +260,8 @@ export const THEMES: Record<ThemeId, { name: string; colors: ThemeColors }> = {
238
260
  border: '#3b4261',
239
261
  borderActive: '#7aa2f7',
240
262
  danger: '#f7768e',
263
+ diffAddBg: '#22362d',
264
+ diffRemoveBg: '#362330',
241
265
  dim: '#292e42',
242
266
  overlay: '#141722',
243
267
  panel: '#1f2335',
@@ -1,41 +0,0 @@
1
- import { useAppStore } from '../../state/app-store'
2
- import { theme } from '../theme'
3
-
4
- function formatChord(chord: string): string {
5
- if (chord === 'space') return '␣'
6
- if (chord === 'return') return '⏎'
7
- if (chord === 'escape') return 'Esc'
8
- if (chord === 'tab') return 'Tab'
9
- if (chord === 'backspace') return '⌫'
10
- if (chord === 'up') return '↑'
11
- if (chord === 'down') return '↓'
12
- if (chord === 'left') return '←'
13
- if (chord === 'right') return '→'
14
- return chord
15
- }
16
-
17
- function formatSequence(chords: string[]): string {
18
- return chords.map(formatChord).join(' ')
19
- }
20
-
21
- export function PendingChordIndicator() {
22
- const pendingChords = useAppStore((s) => s.pendingChords)
23
-
24
- if (!pendingChords || pendingChords.length === 0) return null
25
-
26
- const label = formatSequence(pendingChords)
27
-
28
- return (
29
- <box
30
- position="absolute"
31
- bottom={2}
32
- right={2}
33
- paddingLeft={1}
34
- paddingRight={1}
35
- backgroundColor={theme.accent}
36
- borderColor={theme.borderActive}
37
- >
38
- <text fg={theme.background}>{label}…</text>
39
- </box>
40
- )
41
- }