@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.
- package/package.json +2 -2
- package/src/app-runtime/side-effects.ts +6 -1
- package/src/app.tsx +20 -16
- package/src/input/keymap/describe-bindings.ts +63 -2
- package/src/input/modes/bridge.ts +1 -0
- package/src/input/modes/transitions.ts +3 -1
- package/src/input/modes/types.ts +1 -0
- package/src/pty/terminal-snapshot.ts +7 -5
- package/src/services/ai-usage/adapters/claude.ts +102 -26
- package/src/services/ai-usage/adapters/codex.ts +83 -32
- package/src/services/ai-usage/cache.ts +60 -0
- package/src/services/ai-usage/pace.ts +79 -0
- package/src/services/ai-usage/provider.ts +43 -20
- package/src/services/ai-usage/types.ts +31 -0
- package/src/state/ai-usage-store.ts +10 -3
- package/src/state/reducers/git-mode-state.ts +58 -0
- package/src/state/reducers/modal-state.ts +13 -0
- package/src/state/types.ts +18 -6
- package/src/ui/breaking-update-screen.tsx +2 -2
- package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
- package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
- package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
- package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +8 -17
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
- package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
- package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +9 -5
- package/src/ui/components/{git-view.tsx → git/git-view.tsx} +70 -37
- package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
- package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +4 -4
- package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
- package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
- package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
- package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
- package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
- package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
- package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
- package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
- package/src/ui/components/modals/app/ai-usage-modal.tsx +190 -0
- package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
- package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
- package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
- package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
- package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
- package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
- package/src/ui/components/modals/shared/form.tsx +164 -0
- package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
- package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
- package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
- package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
- package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
- package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
- package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +56 -24
- package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
- package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
- package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
- package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
- package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
- package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
- package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
- package/src/ui/root.tsx +26 -23
- package/src/ui/theme-store.ts +30 -26
- package/src/ui/themes.ts +1 -0
- package/src/ui/ai-usage/controller.ts +0 -35
- package/src/ui/components/ai-usage-popover.tsx +0 -152
- package/src/ui/components/create-session-modal.tsx +0 -96
- package/src/ui/components/diff-renderer/build-rows.ts +0 -349
- package/src/ui/components/diff-renderer/highlight.ts +0 -44
- package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
- package/src/ui/components/session-name-modal.tsx +0 -11
- package/src/ui/components/snippet-editor-modal.tsx +0 -40
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
- /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
- /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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
package/src/state/types.ts
CHANGED
|
@@ -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
|
|
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
|
|
237
|
+
export function getParsedPayload<T>(entry: ParsedDiffEntry | undefined): T | null {
|
|
240
238
|
if (!entry) return null
|
|
241
|
-
return entry.file as
|
|
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 {
|
|
4
|
-
import {
|
|
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 {
|