@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
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import type { TerminalContentOrigin } from '
|
|
4
|
-
import type { FocusMode, TabSession } from '
|
|
3
|
+
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
4
|
+
import type { FocusMode, TabSession } from '../../../state/types'
|
|
5
5
|
|
|
6
|
-
import { logInputDebug } from '
|
|
6
|
+
import { logInputDebug } from '../../../debug/input-log'
|
|
7
7
|
import {
|
|
8
8
|
computePaneRects,
|
|
9
9
|
type LayoutNode,
|
|
10
10
|
PANE_BORDER,
|
|
11
11
|
type PaneRect,
|
|
12
12
|
type SplitDirection,
|
|
13
|
-
} from '
|
|
14
|
-
import { useTokens } from '
|
|
13
|
+
} from '../../../state/layout-tree'
|
|
14
|
+
import { useTokens } from '../../theme'
|
|
15
15
|
import { TerminalPane } from './terminal-pane'
|
|
16
16
|
|
|
17
17
|
const PANE_CHROME = PANE_BORDER
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AppState } from '
|
|
1
|
+
import type { AppState } from '../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { version as APP_VERSION } from '
|
|
4
|
-
import { useAppStore } from '
|
|
5
|
-
import { useKeymap } from '
|
|
6
|
-
import { getStatusBarModel } from '
|
|
7
|
-
import { getCurrentTokens, useBg, useTokens } from '
|
|
8
|
-
import { AIUsageIndicator } from '
|
|
3
|
+
import { version as APP_VERSION } from '../../../../package.json'
|
|
4
|
+
import { useAppStore } from '../../../state/app-store'
|
|
5
|
+
import { useKeymap } from '../../keymap-context'
|
|
6
|
+
import { getStatusBarModel } from '../../status-bar-model'
|
|
7
|
+
import { getCurrentTokens, useBg, useTokens } from '../../theme'
|
|
8
|
+
import { AIUsageIndicator } from '../overlays/ai-usage/ai-usage-indicator'
|
|
9
9
|
|
|
10
10
|
function getModeColor(focusMode: AppState['focusMode']): string {
|
|
11
11
|
const t = getCurrentTokens()
|
|
@@ -2,19 +2,19 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
2
2
|
|
|
3
3
|
import { memo, type ReactNode } from 'react'
|
|
4
4
|
|
|
5
|
-
import type { TerminalContentOrigin } from '
|
|
6
|
-
import type { TabSession, TerminalSnapshot, TerminalSpan } from '
|
|
5
|
+
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
|
+
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
7
|
|
|
8
|
-
import { logInputDebug } from '
|
|
9
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
10
|
-
import { type ContextMenuItem, openContextMenu } from '
|
|
11
|
-
import { getCurrentTokens, useBg, useTokens } from '
|
|
12
|
-
import { ContextMenuBox } from '
|
|
8
|
+
import { logInputDebug } from '../../../debug/input-log'
|
|
9
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
10
|
+
import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
|
|
11
|
+
import { getCurrentTokens, useBg, useTokens } from '../../theme'
|
|
12
|
+
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
13
13
|
|
|
14
14
|
interface TerminalPaneProps {
|
|
15
15
|
tab?: TabSession
|
|
16
16
|
tabId?: string
|
|
17
|
-
focusMode: import('
|
|
17
|
+
focusMode: import('../../../state/types').FocusMode
|
|
18
18
|
isActive?: boolean
|
|
19
19
|
contentOrigin: TerminalContentOrigin
|
|
20
20
|
mouseForwardingEnabled: boolean
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
UsagePaceStage,
|
|
6
|
+
UsageSnapshot,
|
|
7
|
+
UsageWindow,
|
|
8
|
+
} from '../../../../services/ai-usage/types'
|
|
9
|
+
|
|
10
|
+
import { useAIUsageStore } from '../../../../state/ai-usage-store'
|
|
11
|
+
import { useTokens } from '../../../theme'
|
|
12
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
13
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
14
|
+
|
|
15
|
+
const TOOL_TITLE: Record<AIUsageTool, string> = {
|
|
16
|
+
claude: 'Claude',
|
|
17
|
+
codex: 'Codex',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BAR_SEGMENTS = 32
|
|
21
|
+
const BAR_FILLED_CHAR = '\u{2501}'
|
|
22
|
+
const BAR_EMPTY_CHAR = '\u{2500}'
|
|
23
|
+
|
|
24
|
+
function buildBar(percent: number | null): { empty: string; filled: string } {
|
|
25
|
+
const p = percent ?? 0
|
|
26
|
+
let filledCount = 0
|
|
27
|
+
for (let i = 0; i < BAR_SEGMENTS; i++) {
|
|
28
|
+
if (p > i * (100 / BAR_SEGMENTS)) filledCount++
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
empty: BAR_EMPTY_CHAR.repeat(BAR_SEGMENTS - filledCount),
|
|
32
|
+
filled: BAR_FILLED_CHAR.repeat(filledCount),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatRelative(iso: string, now: number = Date.now()): string {
|
|
37
|
+
const diffMs = now - new Date(iso).getTime()
|
|
38
|
+
if (!Number.isFinite(diffMs) || diffMs < 0) return 'just now'
|
|
39
|
+
const s = Math.floor(diffMs / 1000)
|
|
40
|
+
if (s < 10) return 'just now'
|
|
41
|
+
if (s < 60) return `${s}s ago`
|
|
42
|
+
const m = Math.floor(s / 60)
|
|
43
|
+
if (m < 60) return `${m}m ago`
|
|
44
|
+
const h = Math.floor(m / 60)
|
|
45
|
+
if (h < 24) return `${h}h ago`
|
|
46
|
+
const d = Math.floor(h / 24)
|
|
47
|
+
return `${d}d ago`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function paceStageIsAhead(stage: UsagePaceStage): boolean {
|
|
51
|
+
return stage === 'ahead' || stage === 'farAhead' || stage === 'slightlyAhead'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function paceStageIsBehind(stage: UsagePaceStage): boolean {
|
|
55
|
+
return stage === 'behind' || stage === 'farBehind' || stage === 'slightlyBehind'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function AIUsageModal() {
|
|
59
|
+
const t = useTokens()
|
|
60
|
+
const snapshots = useAIUsageStore((s) => s.snapshots)
|
|
61
|
+
|
|
62
|
+
const tools: AIUsageTool[] = ['claude', 'codex']
|
|
63
|
+
const sections = tools
|
|
64
|
+
.map((tool) => ({ snap: snapshots[tool], tool }))
|
|
65
|
+
.filter((s): s is { snap: UsageSnapshot; tool: AIUsageTool } => s.snap !== undefined)
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ModalShell
|
|
69
|
+
title="AI usage"
|
|
70
|
+
keybindsModeId="modal.ai-usage"
|
|
71
|
+
width={uiTokens.modalWidth.md}
|
|
72
|
+
listGap={1}
|
|
73
|
+
>
|
|
74
|
+
{sections.length === 0 ? (
|
|
75
|
+
<text fg={t.muted} selectable={false}>
|
|
76
|
+
no data yet — collecting…
|
|
77
|
+
</text>
|
|
78
|
+
) : (
|
|
79
|
+
<box flexDirection="column" gap={1}>
|
|
80
|
+
{sections.map(({ snap, tool }) => (
|
|
81
|
+
<ToolSection key={tool} snap={snap} tool={tool} />
|
|
82
|
+
))}
|
|
83
|
+
</box>
|
|
84
|
+
)}
|
|
85
|
+
</ModalShell>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface ToolSectionProps {
|
|
90
|
+
snap: UsageSnapshot
|
|
91
|
+
tool: AIUsageTool
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ToolSection({ snap, tool }: ToolSectionProps) {
|
|
95
|
+
const t = useTokens()
|
|
96
|
+
const isHardError = Boolean(snap.error) && !snap.stale
|
|
97
|
+
const relative = formatRelative(snap.lastUpdated)
|
|
98
|
+
|
|
99
|
+
let body: ReactNode
|
|
100
|
+
if (isHardError) {
|
|
101
|
+
body = (
|
|
102
|
+
<text fg={t.palette.error} selectable={false}>
|
|
103
|
+
{`error: ${snap.error ?? ''}`}
|
|
104
|
+
</text>
|
|
105
|
+
)
|
|
106
|
+
} else if (snap.windows.length === 0) {
|
|
107
|
+
body = (
|
|
108
|
+
<text fg={t.muted} selectable={false}>
|
|
109
|
+
no window data
|
|
110
|
+
</text>
|
|
111
|
+
)
|
|
112
|
+
} else {
|
|
113
|
+
body = snap.windows.map((window) => <WindowRow key={window.kind} window={window} />)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<box flexDirection="column">
|
|
118
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
119
|
+
<text fg={t.accent} selectable={false}>
|
|
120
|
+
{TOOL_TITLE[tool]}
|
|
121
|
+
</text>
|
|
122
|
+
{snap.planTier ? (
|
|
123
|
+
<text fg={t.muted} selectable={false}>
|
|
124
|
+
{snap.planTier}
|
|
125
|
+
</text>
|
|
126
|
+
) : null}
|
|
127
|
+
</box>
|
|
128
|
+
<text fg={t.muted} selectable={false}>
|
|
129
|
+
{`Updated ${relative}`}
|
|
130
|
+
</text>
|
|
131
|
+
{body}
|
|
132
|
+
</box>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function WindowRow({ window }: { window: UsageWindow }) {
|
|
137
|
+
const t = useTokens()
|
|
138
|
+
const percent = window.percent
|
|
139
|
+
const { empty, filled } = buildBar(percent)
|
|
140
|
+
|
|
141
|
+
let barColor = t.palette.success
|
|
142
|
+
if (percent !== null) {
|
|
143
|
+
if (percent >= 85) barColor = t.palette.error
|
|
144
|
+
else if (percent >= 60) barColor = t.palette.warning
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const pctText = percent === null ? '—' : `${Math.round(percent)}% used`
|
|
148
|
+
const resetText = window.timeRemaining ? `Resets in ${window.timeRemaining}` : null
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<box flexDirection="column" paddingTop={1}>
|
|
152
|
+
<text fg={t.palette.ink} selectable={false}>
|
|
153
|
+
{window.label}
|
|
154
|
+
</text>
|
|
155
|
+
<box flexDirection="row">
|
|
156
|
+
<text fg={barColor} selectable={false}>
|
|
157
|
+
{filled}
|
|
158
|
+
</text>
|
|
159
|
+
<text fg={t.muted} selectable={false}>
|
|
160
|
+
{empty}
|
|
161
|
+
</text>
|
|
162
|
+
</box>
|
|
163
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
164
|
+
<text fg={t.muted} selectable={false}>
|
|
165
|
+
{pctText}
|
|
166
|
+
</text>
|
|
167
|
+
{resetText ? (
|
|
168
|
+
<text fg={t.muted} selectable={false}>
|
|
169
|
+
{resetText}
|
|
170
|
+
</text>
|
|
171
|
+
) : null}
|
|
172
|
+
</box>
|
|
173
|
+
{window.pace ? <PaceLine pace={window.pace} /> : null}
|
|
174
|
+
</box>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function PaceLine({ pace }: { pace: NonNullable<UsageWindow['pace']> }) {
|
|
179
|
+
const t = useTokens()
|
|
180
|
+
let color = t.muted
|
|
181
|
+
if (paceStageIsBehind(pace.stage)) color = t.palette.warning
|
|
182
|
+
else if (paceStageIsAhead(pace.stage)) color = t.palette.success
|
|
183
|
+
|
|
184
|
+
const suffix = pace.rightText ? ` · ${pace.rightText}` : ''
|
|
185
|
+
return (
|
|
186
|
+
<text fg={color} selectable={false}>
|
|
187
|
+
{`Pace: ${pace.label}${suffix}`}
|
|
188
|
+
</text>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
@@ -2,12 +2,12 @@ import type { ModeId } from '@brimveyn/aimux-config'
|
|
|
2
2
|
|
|
3
3
|
import { useLayoutEffect, useMemo } from 'react'
|
|
4
4
|
|
|
5
|
-
import { collectHelpEntries, HELP_MODE_LABELS } from '
|
|
6
|
-
import { dispatchGlobal } from '
|
|
7
|
-
import { useKeymap } from '
|
|
8
|
-
import { useTokens } from '
|
|
9
|
-
import { uiTokens } from '
|
|
10
|
-
import { Picker, type PickerItem } from '
|
|
5
|
+
import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
|
|
6
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
7
|
+
import { useKeymap } from '../../../keymap-context'
|
|
8
|
+
import { useTokens } from '../../../theme'
|
|
9
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
10
|
+
import { Picker, type PickerItem } from '../shared/picker'
|
|
11
11
|
|
|
12
12
|
interface HelpModalProps {
|
|
13
13
|
filter: string | null
|
package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useTokens } from '
|
|
2
|
-
import { uiTokens } from '
|
|
3
|
-
import { ListItem } from '
|
|
4
|
-
import { ModalShell } from '
|
|
1
|
+
import { useTokens } from '../../../theme'
|
|
2
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
3
|
+
import { ListItem } from '../../primitives/list-item'
|
|
4
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
5
5
|
|
|
6
6
|
interface UpdateAvailableModalProps {
|
|
7
7
|
currentVersion: string
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
3
|
|
|
4
|
-
import type { ModeId } from '
|
|
5
|
-
|
|
6
|
-
import { useAppStore } from '
|
|
7
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
8
|
-
import { useTokens } from '
|
|
9
|
-
import { uiTokens } from '
|
|
10
|
-
import { InputField } from '
|
|
11
|
-
import { ModalShell } from '
|
|
4
|
+
import type { ModeId } from '../../../../input/modes/types'
|
|
5
|
+
|
|
6
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
7
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
8
|
+
import { useTokens } from '../../../theme'
|
|
9
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
10
|
+
import { InputField } from '../../primitives/input-field'
|
|
11
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
12
12
|
|
|
13
13
|
interface GitCommitModalProps {
|
|
14
14
|
activeField: 'title' | 'body'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { DirectoryResult } from '../../../../state/types'
|
|
2
|
+
|
|
3
|
+
import { abbreviatePath } from '../../../path-format'
|
|
4
|
+
import { getCurrentTokens, useTokens } from '../../../theme'
|
|
5
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
6
|
+
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
7
|
+
|
|
8
|
+
const VISIBLE_ROWS = 8
|
|
9
|
+
|
|
10
|
+
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
11
|
+
if (result.type === 'worktree') return '\u{e728}'
|
|
12
|
+
if (result.type === 'workspace') return '\u{f07c}'
|
|
13
|
+
return '\u{e702}'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
17
|
+
const t = getCurrentTokens()
|
|
18
|
+
if (result.type === 'worktree') return t.palette.warning
|
|
19
|
+
if (result.type === 'workspace') return t.accent
|
|
20
|
+
return t.palette.primary
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CreateSessionModalProps {
|
|
24
|
+
activeField: 'directory' | 'name'
|
|
25
|
+
directoryQuery: string
|
|
26
|
+
sessionName: string
|
|
27
|
+
results: DirectoryResult[]
|
|
28
|
+
selectedIndex: number
|
|
29
|
+
pendingProjectPath: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CreateSessionModal({
|
|
33
|
+
activeField,
|
|
34
|
+
directoryQuery,
|
|
35
|
+
pendingProjectPath,
|
|
36
|
+
results,
|
|
37
|
+
selectedIndex,
|
|
38
|
+
sessionName,
|
|
39
|
+
}: CreateSessionModalProps) {
|
|
40
|
+
const t = useTokens()
|
|
41
|
+
const dirActive = activeField === 'directory'
|
|
42
|
+
const nameActive = activeField === 'name'
|
|
43
|
+
|
|
44
|
+
const items: FormOptionItem[] = results.map((result) => {
|
|
45
|
+
return {
|
|
46
|
+
key: result.path,
|
|
47
|
+
leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
|
|
48
|
+
title: (active) => (
|
|
49
|
+
<text fg={active ? t.palette.ink : t.muted}>{abbreviatePath(result.path)}</text>
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Form
|
|
56
|
+
title="Create workspace"
|
|
57
|
+
keybindsModeId="modal.create-session"
|
|
58
|
+
width={uiTokens.modalWidth.xl}
|
|
59
|
+
>
|
|
60
|
+
<AutoComplete
|
|
61
|
+
active={dirActive}
|
|
62
|
+
label="Search projects"
|
|
63
|
+
placeholder="Type a project name..."
|
|
64
|
+
value={directoryQuery}
|
|
65
|
+
displayValue={pendingProjectPath ? abbreviatePath(pendingProjectPath) : directoryQuery}
|
|
66
|
+
items={items}
|
|
67
|
+
selectedIndex={selectedIndex}
|
|
68
|
+
maxVisibleRows={VISIBLE_ROWS}
|
|
69
|
+
emptyState={
|
|
70
|
+
<text fg={t.muted}>
|
|
71
|
+
{directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
|
|
72
|
+
</text>
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<TextField active={nameActive} label="Workspace name" value={sessionName} />
|
|
77
|
+
</Form>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
2
|
+
import { Form, TextField } from '../shared/form'
|
|
3
|
+
|
|
4
|
+
export function SessionNameModal({ title, value }: { title: string; value: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<Form title={title} keybindsModeId="modal.session-name" width={uiTokens.modalWidth.md}>
|
|
7
|
+
<TextField active value={value} />
|
|
8
|
+
</Form>
|
|
9
|
+
)
|
|
10
|
+
}
|
package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx}
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { SessionRecord } from '
|
|
1
|
+
import type { SessionRecord } from '../../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
4
|
-
import { filterSessions } from '
|
|
5
|
-
import { abbreviatePath } from '
|
|
6
|
-
import { orderSessionsForDisplay } from '
|
|
7
|
-
import { useTokens } from '
|
|
8
|
-
import { uiTokens } from '
|
|
9
|
-
import { Picker, type PickerItem } from '
|
|
3
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
4
|
+
import { filterSessions } from '../../../../state/selectors'
|
|
5
|
+
import { abbreviatePath } from '../../../path-format'
|
|
6
|
+
import { orderSessionsForDisplay } from '../../../session-ordering'
|
|
7
|
+
import { useTokens } from '../../../theme'
|
|
8
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
9
|
+
import { Picker, type PickerItem } from '../shared/picker'
|
|
10
10
|
|
|
11
11
|
interface SessionPickerModalProps {
|
|
12
12
|
sessions: SessionRecord[]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useTokens } from '../../../theme'
|
|
6
|
+
import { InputField } from '../../primitives/input-field'
|
|
7
|
+
import { ListItem } from '../../primitives/list-item'
|
|
8
|
+
import { ModalShell } from './modal-shell'
|
|
9
|
+
|
|
10
|
+
interface FormProps {
|
|
11
|
+
children: ReactNode
|
|
12
|
+
footer?: ReactNode
|
|
13
|
+
keybindsModeId?: ModeId
|
|
14
|
+
listGap?: number
|
|
15
|
+
subtitle?: string
|
|
16
|
+
title: string
|
|
17
|
+
width: number | `${number}%`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FieldLabelProps {
|
|
21
|
+
active?: boolean
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
description?: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TextFieldProps {
|
|
27
|
+
active: boolean
|
|
28
|
+
cursorPos?: number
|
|
29
|
+
description?: ReactNode
|
|
30
|
+
label?: ReactNode
|
|
31
|
+
placeholder?: string
|
|
32
|
+
value: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FormOptionContent = ReactNode | ((active: boolean) => ReactNode)
|
|
36
|
+
|
|
37
|
+
export interface FormOptionItem {
|
|
38
|
+
key: string
|
|
39
|
+
leading?: FormOptionContent
|
|
40
|
+
subtitle?: FormOptionContent
|
|
41
|
+
title: FormOptionContent
|
|
42
|
+
trailing?: FormOptionContent
|
|
43
|
+
onClick?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AutoCompleteProps {
|
|
47
|
+
active: boolean
|
|
48
|
+
cursorPos?: number
|
|
49
|
+
displayValue?: string
|
|
50
|
+
emptyState?: ReactNode
|
|
51
|
+
items: FormOptionItem[]
|
|
52
|
+
label: ReactNode
|
|
53
|
+
maxVisibleRows?: number
|
|
54
|
+
onHover?: (index: number) => void
|
|
55
|
+
placeholder?: string
|
|
56
|
+
selectedIndex: number
|
|
57
|
+
value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Form({
|
|
61
|
+
children,
|
|
62
|
+
footer,
|
|
63
|
+
keybindsModeId,
|
|
64
|
+
listGap,
|
|
65
|
+
subtitle,
|
|
66
|
+
title,
|
|
67
|
+
width,
|
|
68
|
+
}: FormProps) {
|
|
69
|
+
return (
|
|
70
|
+
<ModalShell
|
|
71
|
+
title={title}
|
|
72
|
+
subtitle={subtitle}
|
|
73
|
+
width={width}
|
|
74
|
+
keybindsModeId={keybindsModeId}
|
|
75
|
+
listGap={listGap}
|
|
76
|
+
footer={footer}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</ModalShell>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function FieldLabel({ active = false, children, description }: FieldLabelProps) {
|
|
84
|
+
const t = useTokens()
|
|
85
|
+
return (
|
|
86
|
+
<box flexDirection="column">
|
|
87
|
+
{children ? <text fg={active ? t.palette.ink : t.muted}>{children}</text> : null}
|
|
88
|
+
{description ? <text fg={t.muted}>{description}</text> : null}
|
|
89
|
+
</box>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function TextField({
|
|
94
|
+
active,
|
|
95
|
+
cursorPos,
|
|
96
|
+
description,
|
|
97
|
+
label,
|
|
98
|
+
placeholder,
|
|
99
|
+
value,
|
|
100
|
+
}: TextFieldProps) {
|
|
101
|
+
return (
|
|
102
|
+
<box flexDirection="column">
|
|
103
|
+
{label || description ? (
|
|
104
|
+
<FieldLabel active={active} description={description}>
|
|
105
|
+
{label}
|
|
106
|
+
</FieldLabel>
|
|
107
|
+
) : null}
|
|
108
|
+
<InputField active={active} value={value} cursorPos={cursorPos} placeholder={placeholder} />
|
|
109
|
+
</box>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveItemContent(content: FormOptionContent | undefined, active: boolean): ReactNode {
|
|
114
|
+
return typeof content === 'function' ? content(active) : content
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function AutoComplete({
|
|
118
|
+
active,
|
|
119
|
+
cursorPos,
|
|
120
|
+
displayValue,
|
|
121
|
+
emptyState,
|
|
122
|
+
items,
|
|
123
|
+
label,
|
|
124
|
+
maxVisibleRows = 8,
|
|
125
|
+
onHover,
|
|
126
|
+
placeholder,
|
|
127
|
+
selectedIndex,
|
|
128
|
+
value,
|
|
129
|
+
}: AutoCompleteProps) {
|
|
130
|
+
const scrollOffset = Math.max(0, selectedIndex - maxVisibleRows + 1)
|
|
131
|
+
const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisibleRows)
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<box flexDirection="column" gap={1}>
|
|
135
|
+
<TextField
|
|
136
|
+
active={active}
|
|
137
|
+
label={label}
|
|
138
|
+
value={active ? value : (displayValue ?? value)}
|
|
139
|
+
cursorPos={cursorPos}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
/>
|
|
142
|
+
<box flexDirection="column" height={maxVisibleRows}>
|
|
143
|
+
{items.length === 0
|
|
144
|
+
? (emptyState ?? null)
|
|
145
|
+
: visibleItems.map((item, index) => {
|
|
146
|
+
const optionIndex = scrollOffset + index
|
|
147
|
+
const optionActive = active && optionIndex === selectedIndex
|
|
148
|
+
return (
|
|
149
|
+
<ListItem
|
|
150
|
+
key={item.key}
|
|
151
|
+
active={optionActive}
|
|
152
|
+
leading={resolveItemContent(item.leading, optionActive)}
|
|
153
|
+
title={resolveItemContent(item.title, optionActive)}
|
|
154
|
+
subtitle={resolveItemContent(item.subtitle, optionActive)}
|
|
155
|
+
trailing={resolveItemContent(item.trailing, optionActive)}
|
|
156
|
+
onHover={onHover ? () => onHover(optionIndex) : undefined}
|
|
157
|
+
onClick={item.onClick}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
})}
|
|
161
|
+
</box>
|
|
162
|
+
</box>
|
|
163
|
+
)
|
|
164
|
+
}
|
package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import { describeBindings } from '
|
|
4
|
-
import { useKeymap } from '
|
|
5
|
-
import { useTokens } from '
|
|
6
|
-
import { Surface } from '
|
|
3
|
+
import { describeBindings } from '../../../../input/keymap/describe-bindings'
|
|
4
|
+
import { useKeymap } from '../../../keymap-context'
|
|
5
|
+
import { useTokens } from '../../../theme'
|
|
6
|
+
import { Surface } from '../../primitives/surface'
|
|
7
7
|
|
|
8
8
|
const KEYS_COLUMN_WIDTH = 12
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
16
16
|
const t = useTokens()
|
|
17
17
|
const config = useKeymap()
|
|
18
18
|
const bindings = describeBindings(config, modeId, {
|
|
19
|
-
|
|
19
|
+
mergeAlternativesByDescription: true,
|
|
20
20
|
withDescriptionOnly: true,
|
|
21
21
|
})
|
|
22
22
|
|
|
@@ -24,10 +24,10 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
24
24
|
const entries = typeof limit === 'number' ? bindings.slice(0, limit) : bindings
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<box position="absolute" bottom={
|
|
27
|
+
<box position="absolute" bottom={0} right={0}>
|
|
28
28
|
<Surface tone="elevated" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
|
29
29
|
{entries.map((binding) => (
|
|
30
|
-
<box key={
|
|
30
|
+
<box key={binding.description ?? binding.keys} flexDirection="row">
|
|
31
31
|
<box width={KEYS_COLUMN_WIDTH}>
|
|
32
32
|
<text fg={t.accent}>{binding.keysDisplay}</text>
|
|
33
33
|
</box>
|
|
@@ -3,7 +3,7 @@ import type { ModeId } from '@brimveyn/aimux-config'
|
|
|
3
3
|
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
4
4
|
import { type ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
-
import { useTokens, useTransparent } from '
|
|
6
|
+
import { useTokens, useTransparent } from '../../../theme'
|
|
7
7
|
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
8
8
|
|
|
9
9
|
interface ModalShellProps {
|
|
@@ -4,9 +4,9 @@ import type { ScrollBoxRenderable } from '@opentui/core'
|
|
|
4
4
|
import { useTerminalDimensions } from '@opentui/react'
|
|
5
5
|
import { type ReactNode, useLayoutEffect, useRef } from 'react'
|
|
6
6
|
|
|
7
|
-
import { useTokens } from '
|
|
8
|
-
import { BareInput } from '
|
|
9
|
-
import { ListItem } from '
|
|
7
|
+
import { useTokens } from '../../../theme'
|
|
8
|
+
import { BareInput } from '../../primitives/bare-input'
|
|
9
|
+
import { ListItem } from '../../primitives/list-item'
|
|
10
10
|
import { ModalShell } from './modal-shell'
|
|
11
11
|
|
|
12
12
|
export interface PickerItem {
|