@brimveyn/aimux 1.8.0 → 1.9.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 (80) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +6 -1
  3. package/src/app.tsx +20 -16
  4. package/src/input/keymap/describe-bindings.ts +63 -2
  5. package/src/input/modes/bridge.ts +1 -0
  6. package/src/input/modes/transitions.ts +3 -1
  7. package/src/input/modes/types.ts +1 -0
  8. package/src/pty/terminal-snapshot.ts +7 -5
  9. package/src/services/ai-usage/adapters/claude.ts +102 -26
  10. package/src/services/ai-usage/adapters/codex.ts +83 -32
  11. package/src/services/ai-usage/cache.ts +60 -0
  12. package/src/services/ai-usage/pace.ts +79 -0
  13. package/src/services/ai-usage/provider.ts +43 -20
  14. package/src/services/ai-usage/types.ts +31 -0
  15. package/src/state/ai-usage-store.ts +10 -3
  16. package/src/state/reducers/git-mode-state.ts +58 -0
  17. package/src/state/reducers/modal-state.ts +13 -0
  18. package/src/state/types.ts +18 -6
  19. package/src/ui/breaking-update-screen.tsx +2 -2
  20. package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
  21. package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
  22. package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
  23. package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
  24. package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
  25. package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
  26. package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
  27. package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +8 -17
  28. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
  29. package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
  30. package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +9 -5
  31. package/src/ui/components/{git-view.tsx → git/git-view.tsx} +70 -37
  32. package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
  33. package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +4 -4
  34. package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
  35. package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
  36. package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
  37. package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
  38. package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
  39. package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
  40. package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
  41. package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
  42. package/src/ui/components/modals/app/ai-usage-modal.tsx +190 -0
  43. package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
  44. package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
  45. package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
  46. package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
  47. package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
  48. package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
  49. package/src/ui/components/modals/shared/form.tsx +164 -0
  50. package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
  51. package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
  52. package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
  53. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
  54. package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
  55. package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
  56. package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
  57. package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +56 -24
  58. package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
  59. package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
  60. package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
  61. package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
  62. package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
  63. package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
  64. package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
  65. package/src/ui/root.tsx +26 -23
  66. package/src/ui/theme-store.ts +30 -26
  67. package/src/ui/themes.ts +1 -0
  68. package/src/ui/ai-usage/controller.ts +0 -35
  69. package/src/ui/components/ai-usage-popover.tsx +0 -152
  70. package/src/ui/components/create-session-modal.tsx +0 -96
  71. package/src/ui/components/diff-renderer/build-rows.ts +0 -349
  72. package/src/ui/components/diff-renderer/highlight.ts +0 -44
  73. package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
  74. package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
  75. package/src/ui/components/session-name-modal.tsx +0 -11
  76. package/src/ui/components/snippet-editor-modal.tsx +0 -40
  77. /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
  78. /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
  79. /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
  80. /package/src/ui/components/{use-sidebar-auto-scroll.ts → layout/sidebar/use-sidebar-auto-scroll.ts} +0 -0
@@ -0,0 +1,60 @@
1
+ import type { AIUsageTool } from '@brimveyn/aimux-config'
2
+
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ import type { UsageSnapshot } from './types'
8
+
9
+ const CACHE_DIR = join(homedir(), '.cache', 'aimux')
10
+ const CACHE_PATH = join(CACHE_DIR, 'ai-usage.json')
11
+
12
+ interface CacheEntry {
13
+ fetchedAt: number
14
+ snapshot: UsageSnapshot
15
+ }
16
+
17
+ type CacheFile = Partial<Record<AIUsageTool, CacheEntry>>
18
+
19
+ function readCacheFile(): CacheFile {
20
+ try {
21
+ if (!existsSync(CACHE_PATH)) return {}
22
+ const raw = readFileSync(CACHE_PATH, 'utf8')
23
+ const parsed = JSON.parse(raw) as unknown
24
+ if (typeof parsed !== 'object' || parsed === null) return {}
25
+ return parsed as CacheFile
26
+ } catch {
27
+ return {}
28
+ }
29
+ }
30
+
31
+ export interface CachedSnapshot {
32
+ snapshot: UsageSnapshot
33
+ ageMs: number
34
+ }
35
+
36
+ export function loadCachedSnapshot(tool: AIUsageTool, maxAgeMs: number): CachedSnapshot | null {
37
+ const cache = readCacheFile()
38
+ const entry = cache[tool]
39
+ if (!entry) return null
40
+ if (typeof entry.fetchedAt !== 'number') return null
41
+ const ageMs = Date.now() - entry.fetchedAt
42
+ if (ageMs > maxAgeMs) return null
43
+ if (!entry.snapshot || entry.snapshot.tool !== tool) return null
44
+ return { ageMs, snapshot: entry.snapshot }
45
+ }
46
+
47
+ export function saveCachedSnapshot(snapshot: UsageSnapshot): void {
48
+ if (snapshot.error) return
49
+ if (snapshot.percent === null) return
50
+ try {
51
+ mkdirSync(CACHE_DIR, { recursive: true })
52
+ const cache = readCacheFile()
53
+ cache[snapshot.tool] = { fetchedAt: Date.now(), snapshot }
54
+ const tmpPath = `${CACHE_PATH}.${process.pid}.tmp`
55
+ writeFileSync(tmpPath, `${JSON.stringify(cache, null, 2)}\n`)
56
+ renameSync(tmpPath, CACHE_PATH)
57
+ } catch {
58
+ // swallow — cache is best-effort
59
+ }
60
+ }
@@ -0,0 +1,79 @@
1
+ import type { UsagePace, UsagePaceStage } from './types'
2
+
3
+ function stageFor(delta: number): UsagePaceStage {
4
+ const abs = Math.abs(delta)
5
+ if (abs <= 2) return 'onTrack'
6
+ if (abs <= 6) return delta >= 0 ? 'slightlyBehind' : 'slightlyAhead'
7
+ if (abs <= 12) return delta >= 0 ? 'behind' : 'ahead'
8
+ return delta >= 0 ? 'farBehind' : 'farAhead'
9
+ }
10
+
11
+ function formatDuration(seconds: number): string {
12
+ const total = Math.max(0, Math.round(seconds))
13
+ const h = Math.floor(total / 3600)
14
+ const m = Math.floor((total % 3600) / 60)
15
+ if (h >= 24) {
16
+ const d = Math.floor(h / 24)
17
+ const hh = h % 24
18
+ return hh > 0 ? `${d}d ${hh}h` : `${d}d`
19
+ }
20
+ if (h > 0) return m > 0 ? `${h}h ${m}m` : `${h}h`
21
+ return `${m}m`
22
+ }
23
+
24
+ export function computePace(opts: {
25
+ percent: number | null
26
+ resetAtMs: number | null
27
+ windowSeconds: number | null
28
+ now?: number
29
+ }): UsagePace | null {
30
+ const { percent, resetAtMs, windowSeconds } = opts
31
+ const now = opts.now ?? Date.now()
32
+ if (percent === null || resetAtMs === null || !windowSeconds || windowSeconds <= 0) {
33
+ return null
34
+ }
35
+
36
+ const timeUntilReset = (resetAtMs - now) / 1000
37
+ if (timeUntilReset <= 0 || timeUntilReset > windowSeconds) return null
38
+
39
+ const elapsed = Math.max(0, Math.min(windowSeconds, windowSeconds - timeUntilReset))
40
+ const expected = Math.max(0, Math.min(100, (elapsed / windowSeconds) * 100))
41
+ const actual = Math.max(0, Math.min(100, percent))
42
+ const delta = actual - expected
43
+ const rounded = Math.round(delta)
44
+ const stage = stageFor(rounded)
45
+
46
+ let rightText: string | null = null
47
+ if (elapsed > 0 && actual > 0) {
48
+ const rate = actual / elapsed
49
+ if (rate > 0) {
50
+ const remaining = Math.max(0, 100 - actual)
51
+ const candidate = remaining / rate
52
+ if (candidate >= timeUntilReset) {
53
+ rightText = 'Lasts to reset'
54
+ } else {
55
+ rightText = `Runs out in ${formatDuration(candidate)}`
56
+ }
57
+ }
58
+ } else if (elapsed > 0 && actual === 0) {
59
+ rightText = 'Lasts to reset'
60
+ }
61
+
62
+ let label: string
63
+ if (stage === 'onTrack') {
64
+ label = 'On pace'
65
+ } else if (rounded > 0) {
66
+ label = `Behind (+${rounded}%)`
67
+ } else {
68
+ label = `Ahead (${rounded}%)`
69
+ }
70
+
71
+ return { delta, label, rightText, stage }
72
+ }
73
+
74
+ export function formatTimeRemaining(resetAtMs: number | null, now?: number): string | null {
75
+ if (resetAtMs === null) return null
76
+ const diff = resetAtMs - (now ?? Date.now())
77
+ if (diff <= 0) return null
78
+ return formatDuration(diff / 1000)
79
+ }
@@ -4,6 +4,7 @@ import type { UsageSnapshot } from './types'
4
4
 
5
5
  import { fetchClaudeUsage } from './adapters/claude'
6
6
  import { fetchCodexUsage } from './adapters/codex'
7
+ import { loadCachedSnapshot, saveCachedSnapshot } from './cache'
7
8
 
8
9
  const DEFAULT_POLL_SECONDS = 60
9
10
  const DEFAULT_TOOLS: AIUsageTool[] = ['claude', 'codex']
@@ -34,32 +35,54 @@ export function startAIUsageService(
34
35
 
35
36
  const tick = async (): Promise<void> => {
36
37
  if (stopped) return
37
- const results = await Promise.allSettled(tools.map((t) => fetchFor(t, config)))
38
- if (stopped) return
39
- for (let i = 0; i < results.length; i++) {
40
- const result = results[i]
41
- const tool = tools[i]
42
- if (!result || !tool) continue
43
- if (result.status === 'fulfilled') {
44
- onUpdate(result.value)
38
+
39
+ const toFetch: AIUsageTool[] = []
40
+ let maxCachedAgeMs = 0
41
+ for (const tool of tools) {
42
+ const cached = loadCachedSnapshot(tool, pollMs)
43
+ if (cached) {
44
+ onUpdate(cached.snapshot)
45
+ if (cached.ageMs > maxCachedAgeMs) maxCachedAgeMs = cached.ageMs
45
46
  } else {
46
- onUpdate({
47
- burnRatePerHour: null,
48
- costUSD: null,
49
- error: result.reason instanceof Error ? result.reason.message : String(result.reason),
50
- lastUpdated: new Date().toISOString(),
51
- percent: null,
52
- resetAt: null,
53
- timeRemaining: null,
54
- tokens: { cache: 0, input: 0, output: 0, total: 0 },
55
- tool,
56
- })
47
+ toFetch.push(tool)
48
+ }
49
+ }
50
+
51
+ if (toFetch.length > 0) {
52
+ const results = await Promise.allSettled(toFetch.map((t) => fetchFor(t, config)))
53
+ if (stopped) return
54
+ for (let i = 0; i < results.length; i++) {
55
+ const result = results[i]
56
+ const tool = toFetch[i]
57
+ if (!result || !tool) continue
58
+ if (result.status === 'fulfilled') {
59
+ saveCachedSnapshot(result.value)
60
+ onUpdate(result.value)
61
+ } else {
62
+ onUpdate({
63
+ burnRatePerHour: null,
64
+ costUSD: null,
65
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
66
+ lastUpdated: new Date().toISOString(),
67
+ percent: null,
68
+ planTier: null,
69
+ resetAt: null,
70
+ timeRemaining: null,
71
+ tokens: { cache: 0, input: 0, output: 0, total: 0 },
72
+ tool,
73
+ windows: [],
74
+ })
75
+ }
57
76
  }
58
77
  }
78
+
59
79
  if (!stopped) {
80
+ const minDelayMs = 5_000
81
+ const nextDelayMs =
82
+ toFetch.length > 0 ? pollMs : Math.max(minDelayMs, pollMs - maxCachedAgeMs)
60
83
  timer = setTimeout(() => {
61
84
  void tick()
62
- }, pollMs)
85
+ }, nextDelayMs)
63
86
  }
64
87
  }
65
88
 
@@ -2,6 +2,34 @@ import type { AIUsageTool } from '@brimveyn/aimux-config'
2
2
 
3
3
  export type { AIUsageTool }
4
4
 
5
+ export type UsageWindowKind = 'session' | 'weekly' | 'sonnet' | 'opus' | 'primary' | 'secondary'
6
+
7
+ export type UsagePaceStage =
8
+ | 'farAhead'
9
+ | 'ahead'
10
+ | 'slightlyAhead'
11
+ | 'onTrack'
12
+ | 'slightlyBehind'
13
+ | 'behind'
14
+ | 'farBehind'
15
+
16
+ export interface UsagePace {
17
+ delta: number
18
+ stage: UsagePaceStage
19
+ label: string
20
+ rightText: string | null
21
+ }
22
+
23
+ export interface UsageWindow {
24
+ kind: UsageWindowKind
25
+ label: string
26
+ percent: number | null
27
+ resetAt: string | null
28
+ timeRemaining: string | null
29
+ windowSeconds: number | null
30
+ pace: UsagePace | null
31
+ }
32
+
5
33
  export interface UsageSnapshot {
6
34
  tool: AIUsageTool
7
35
  percent: number | null
@@ -16,5 +44,8 @@ export interface UsageSnapshot {
16
44
  timeRemaining: string | null
17
45
  burnRatePerHour: number | null
18
46
  lastUpdated: string
47
+ planTier: string | null
48
+ windows: UsageWindow[]
19
49
  error?: string
50
+ stale?: boolean
20
51
  }
@@ -18,9 +18,16 @@ export const aiUsageStore = createStore<AIUsageState>((set) => ({
18
18
  enabled: false,
19
19
  setEnabled: (enabled: boolean) => set({ enabled }),
20
20
  setSnapshot: (snap: UsageSnapshot) =>
21
- set((state) => ({
22
- snapshots: { ...state.snapshots, [snap.tool]: snap },
23
- })),
21
+ set((state) => {
22
+ const prev = state.snapshots[snap.tool]
23
+ const isFailure = Boolean(snap.error)
24
+ const hasPriorValue = prev && prev.percent !== null
25
+ const merged: UsageSnapshot =
26
+ isFailure && hasPriorValue && prev
27
+ ? { ...prev, error: snap.error, lastUpdated: snap.lastUpdated, stale: true }
28
+ : snap
29
+ return { snapshots: { ...state.snapshots, [snap.tool]: merged } }
30
+ }),
24
31
  snapshots: {},
25
32
  }))
26
33
 
@@ -274,6 +274,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
274
274
  }
275
275
  }
276
276
  case 'git-mode-set-parsed': {
277
+ const prevParsed = state.gitMode.parsedFiles[action.key]
278
+ if (prevParsed && prevParsed.hash === action.hash) return state
277
279
  return {
278
280
  ...state,
279
281
  gitMode: {
@@ -303,6 +305,62 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
303
305
  },
304
306
  }
305
307
  }
308
+ case 'git-mode-merge-highlights': {
309
+ const k = `${action.key}|${action.themeId}`
310
+ const existing = state.gitMode.highlights[k]
311
+ const sameContent = existing && existing.hash === action.hash
312
+ if (sameContent) {
313
+ const existingAdd = existing.add as unknown[]
314
+ const existingDel = existing.del as unknown[]
315
+ let changesAnything = false
316
+ outer: for (const patch of action.add) {
317
+ const tokens = patch.tokens as unknown[]
318
+ for (let i = 0; i < tokens.length; i++) {
319
+ if (existingAdd[patch.start + i] === undefined) {
320
+ changesAnything = true
321
+ break outer
322
+ }
323
+ }
324
+ }
325
+ if (!changesAnything) {
326
+ outer: for (const patch of action.del) {
327
+ const tokens = patch.tokens as unknown[]
328
+ for (let i = 0; i < tokens.length; i++) {
329
+ if (existingDel[patch.start + i] === undefined) {
330
+ changesAnything = true
331
+ break outer
332
+ }
333
+ }
334
+ }
335
+ }
336
+ if (!changesAnything) return state
337
+ }
338
+ const nextAdd = sameContent ? ([...(existing.add as unknown[])] as unknown[]) : []
339
+ const nextDel = sameContent ? ([...(existing.del as unknown[])] as unknown[]) : []
340
+ for (const patch of action.add) {
341
+ const tokens = patch.tokens as unknown[]
342
+ for (let i = 0; i < tokens.length; i++) nextAdd[patch.start + i] = tokens[i]
343
+ }
344
+ for (const patch of action.del) {
345
+ const tokens = patch.tokens as unknown[]
346
+ for (let i = 0; i < tokens.length; i++) nextDel[patch.start + i] = tokens[i]
347
+ }
348
+ return {
349
+ ...state,
350
+ gitMode: {
351
+ ...state.gitMode,
352
+ highlights: {
353
+ ...state.gitMode.highlights,
354
+ [k]: {
355
+ add: nextAdd,
356
+ del: nextDel,
357
+ hash: action.hash,
358
+ themeId: action.themeId,
359
+ },
360
+ },
361
+ },
362
+ }
363
+ }
306
364
  case 'git-mode-invalidate-diffs': {
307
365
  if (action.paths.length === 0) return state
308
366
  return clearDiffCacheForPaths(state, new Set(action.paths))
@@ -54,6 +54,19 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
54
54
  },
55
55
  }
56
56
  }
57
+ case 'open-ai-usage-modal': {
58
+ return {
59
+ ...state,
60
+ focusMode: 'modal',
61
+ modal: {
62
+ cursorPos: 0,
63
+ editBuffer: '',
64
+ selectedIndex: 0,
65
+ sessionTargetId: null,
66
+ type: 'ai-usage',
67
+ },
68
+ }
69
+ }
57
70
  case 'open-help-modal': {
58
71
  const keymap = getActiveKeymap()
59
72
  const scope = action.scope ?? null
@@ -1,8 +1,6 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
2
  import type { ThemedToken } from 'shiki'
3
3
 
4
- import type { FileDiffMetadata } from '../diff-parser'
5
-
6
4
  export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
7
5
 
8
6
  export type AssistantId = BuiltinAssistantId | (string & {})
@@ -44,6 +42,7 @@ export type ModalType =
44
42
  | 'split-picker'
45
43
  | 'git-commit'
46
44
  | 'update-available'
45
+ | 'ai-usage'
47
46
  | null
48
47
 
49
48
  export interface TerminalSpan {
@@ -223,8 +222,7 @@ export interface FoldState {
223
222
 
224
223
  export interface ParsedDiffEntry {
225
224
  hash: string
226
- // Stored as unknown at the state boundary; concrete type is FileDiffMetadata
227
- // | null, narrowed at read sites via getParsedFile().
225
+ // Stored as unknown at the state boundary and narrowed at read sites.
228
226
  file: unknown
229
227
  }
230
228
 
@@ -236,9 +234,9 @@ export interface HighlightsEntry {
236
234
  del: unknown
237
235
  }
238
236
 
239
- export function getParsedFile(entry: ParsedDiffEntry | undefined): FileDiffMetadata | null {
237
+ export function getParsedPayload<T>(entry: ParsedDiffEntry | undefined): T | null {
240
238
  if (!entry) return null
241
- return entry.file as FileDiffMetadata | null
239
+ return entry.file as T | null
242
240
  }
243
241
 
244
242
  export function getHighlightsTokens(entry: HighlightsEntry | undefined): {
@@ -348,6 +346,10 @@ export interface ModalUpdateAvailable extends ModalBase {
348
346
  latestVersion: string
349
347
  }
350
348
 
349
+ export interface ModalAIUsage extends ModalBase {
350
+ type: 'ai-usage'
351
+ }
352
+
351
353
  export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
352
354
 
353
355
  export interface DirectoryResult {
@@ -369,6 +371,7 @@ export type ModalState =
369
371
  | ModalSnippetEditor
370
372
  | ModalGitCommit
371
373
  | ModalUpdateAvailable
374
+ | ModalAIUsage
372
375
 
373
376
  export interface LayoutState {
374
377
  terminalCols: number
@@ -434,6 +437,7 @@ export type ModalAction =
434
437
  | { type: 'open-theme-picker' }
435
438
  | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
436
439
  | { type: 'set-modal-selection-index'; index: number }
440
+ | { type: 'open-ai-usage-modal' }
437
441
 
438
442
  // -- Session actions --
439
443
  export type SessionAction =
@@ -601,6 +605,14 @@ export type GitModeAction =
601
605
  add: unknown
602
606
  del: unknown
603
607
  }
608
+ | {
609
+ type: 'git-mode-merge-highlights'
610
+ key: string
611
+ hash: string
612
+ themeId: string
613
+ add: Array<{ start: number; tokens: unknown }>
614
+ del: Array<{ start: number; tokens: unknown }>
615
+ }
604
616
  | { type: 'git-mode-invalidate-diffs'; paths: string[] }
605
617
  | { type: 'git-mode-set-loading'; key: string; loading: boolean }
606
618
  | { type: 'git-mode-set-pending-delete'; path: string | null }
@@ -1,7 +1,7 @@
1
1
  import { useKeyboard } from '@opentui/react'
2
2
 
3
- import { ListItem } from './components/list-item'
4
- import { ModalShell } from './components/modal-shell'
3
+ import { ModalShell } from './components/modals/shared/modal-shell'
4
+ import { ListItem } from './components/primitives/list-item'
5
5
  import { uiTokens } from './ui-tokens'
6
6
 
7
7
  interface BreakingUpdateScreenProps {