@brimveyn/aimux 1.7.1 → 1.7.3
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 +3 -4
- package/src/app-runtime/pty-write.ts +15 -3
- package/src/app-runtime/side-effects.ts +10 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app.tsx +5 -8
- package/src/config.ts +2 -10
- package/src/daemon/daemon.ts +7 -2
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/types.ts +6 -1
- package/src/input/raw-input-handler.ts +12 -3
- package/src/pty/terminal-snapshot.ts +4 -4
- package/src/state/reducers/modal-state.ts +3 -2
- package/src/state/reducers/session-state.ts +0 -7
- package/src/state/types.ts +7 -1
- package/src/ui/components/bare-input.tsx +5 -5
- package/src/ui/components/context-menu-box.tsx +21 -0
- package/src/ui/components/context-menu-overlay.tsx +114 -0
- package/src/ui/components/create-session-modal.tsx +12 -41
- package/src/ui/components/diff-renderer/fold-strip.tsx +7 -7
- package/src/ui/components/diff-renderer/highlight.ts +6 -10
- package/src/ui/components/diff-renderer/pierre-diff.tsx +3 -3
- package/src/ui/components/diff-renderer/prepare-diff.ts +2 -3
- package/src/ui/components/diff-renderer/split-view.tsx +22 -29
- package/src/ui/components/diff-renderer/stacked-view.tsx +18 -31
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +0 -1
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +1 -1
- package/src/ui/components/git-commit-modal.tsx +4 -16
- package/src/ui/components/git-panel.tsx +37 -73
- package/src/ui/components/git-view.tsx +17 -19
- package/src/ui/components/help-modal.tsx +5 -13
- package/src/ui/components/input-field.tsx +5 -7
- package/src/ui/components/list-item.tsx +3 -11
- package/src/ui/components/modal-keybinds-overlay.tsx +4 -4
- package/src/ui/components/modal-shell.tsx +6 -11
- package/src/ui/components/new-tab-modal.tsx +7 -15
- package/src/ui/components/pending-chord-overlay.tsx +5 -5
- package/src/ui/components/picker.tsx +6 -6
- package/src/ui/components/session-bar.tsx +34 -20
- package/src/ui/components/session-picker-modal.tsx +7 -20
- package/src/ui/components/sidebar.tsx +36 -35
- package/src/ui/components/snippet-editor-modal.tsx +4 -18
- package/src/ui/components/snippet-picker-modal.tsx +5 -9
- package/src/ui/components/split-layout.tsx +3 -3
- package/src/ui/components/status-bar.tsx +12 -13
- package/src/ui/components/surface.tsx +8 -11
- package/src/ui/components/tab-item.tsx +54 -29
- package/src/ui/components/terminal-pane.tsx +59 -25
- package/src/ui/components/theme-picker-modal.tsx +7 -21
- package/src/ui/components/update-available-modal.tsx +3 -13
- package/src/ui/context-menu/controller.ts +34 -0
- package/src/ui/root.tsx +5 -2
- package/src/ui/shiki.ts +29 -21
- package/src/ui/theme-store.ts +110 -21
- package/src/ui/theme.ts +6 -3
- package/src/ui/themes.ts +18 -13
|
@@ -11,7 +11,7 @@ import { useAppStore } from '../../state/app-store'
|
|
|
11
11
|
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
12
12
|
import { getSelectedGitFile, gitFileKey } from '../../state/git-tree'
|
|
13
13
|
import { setGitDiffScroller } from '../git-view-controls'
|
|
14
|
-
import { useBg,
|
|
14
|
+
import { useBg, useTokens } from '../theme'
|
|
15
15
|
import { PierreDiff, type PierreDiffHandle } from './diff-renderer'
|
|
16
16
|
import { useDiffPrefetch } from './diff-renderer/use-diff-prefetch'
|
|
17
17
|
import { GitPanel } from './git-panel'
|
|
@@ -47,25 +47,25 @@ const DiffStage = memo(function DiffStage({
|
|
|
47
47
|
themeId,
|
|
48
48
|
view,
|
|
49
49
|
}: DiffStageProps) {
|
|
50
|
-
const
|
|
50
|
+
const t = useTokens()
|
|
51
51
|
if (loading && !diff) {
|
|
52
52
|
return (
|
|
53
53
|
<box flexGrow={1} padding={1}>
|
|
54
|
-
<text fg={
|
|
54
|
+
<text fg={t.muted}>Loading diff…</text>
|
|
55
55
|
</box>
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
if (!diff) {
|
|
59
59
|
return (
|
|
60
60
|
<box flexGrow={1} padding={1}>
|
|
61
|
-
<text fg={
|
|
61
|
+
<text fg={t.muted}>Select a file.</text>
|
|
62
62
|
</box>
|
|
63
63
|
)
|
|
64
64
|
}
|
|
65
65
|
if (diff.errorMessage) {
|
|
66
66
|
return (
|
|
67
67
|
<box flexGrow={1} padding={1}>
|
|
68
|
-
<text fg={
|
|
68
|
+
<text fg={t.palette.error}>{diff.errorMessage}</text>
|
|
69
69
|
</box>
|
|
70
70
|
)
|
|
71
71
|
}
|
|
@@ -74,7 +74,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
74
74
|
if (placeholder) {
|
|
75
75
|
return (
|
|
76
76
|
<box flexGrow={1} padding={1}>
|
|
77
|
-
<text fg={
|
|
77
|
+
<text fg={t.muted}>{placeholder}</text>
|
|
78
78
|
</box>
|
|
79
79
|
)
|
|
80
80
|
}
|
|
@@ -83,7 +83,7 @@ const DiffStage = memo(function DiffStage({
|
|
|
83
83
|
<box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
84
84
|
{diff.oldPath ? (
|
|
85
85
|
<box paddingLeft={1} paddingRight={1}>
|
|
86
|
-
<text fg={
|
|
86
|
+
<text fg={t.muted}>
|
|
87
87
|
renamed: {diff.oldPath} → {diff.path}
|
|
88
88
|
</text>
|
|
89
89
|
</box>
|
|
@@ -105,8 +105,8 @@ interface GitViewProps {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
108
|
-
const
|
|
109
|
-
const sidebarBg = useBg('
|
|
108
|
+
const t = useTokens()
|
|
109
|
+
const sidebarBg = useBg('elevated')
|
|
110
110
|
const dimensions = useTerminalDimensions()
|
|
111
111
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
112
112
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
@@ -193,13 +193,13 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
193
193
|
let footerNode: React.ReactNode = null
|
|
194
194
|
if (pendingHint) {
|
|
195
195
|
footerNode = (
|
|
196
|
-
<text fg={
|
|
196
|
+
<text fg={t.palette.warning}>
|
|
197
197
|
<strong>{pendingHint}</strong>
|
|
198
198
|
</text>
|
|
199
199
|
)
|
|
200
200
|
} else if (actionMessage) {
|
|
201
201
|
footerNode = actionMessage.split('\n').map((line, idx) => (
|
|
202
|
-
<text key={idx} fg={
|
|
202
|
+
<text key={idx} fg={t.palette.primary}>
|
|
203
203
|
{line}
|
|
204
204
|
</text>
|
|
205
205
|
))
|
|
@@ -215,26 +215,24 @@ export const GitView = memo(function GitView({ themeId }: GitViewProps) {
|
|
|
215
215
|
padding={0}
|
|
216
216
|
gap={0}
|
|
217
217
|
>
|
|
218
|
-
<text fg={
|
|
218
|
+
<text fg={t.palette.primary}>
|
|
219
219
|
<strong>aimux · git</strong>
|
|
220
220
|
</text>
|
|
221
221
|
{gitPanel.branch ? (
|
|
222
222
|
<box flexDirection="row">
|
|
223
|
-
<text fg={
|
|
224
|
-
<text fg={
|
|
223
|
+
<text fg={t.palette.primary}>{'\u{e702}'} </text>
|
|
224
|
+
<text fg={t.muted}>{gitPanel.branch}</text>
|
|
225
225
|
</box>
|
|
226
226
|
) : null}
|
|
227
227
|
{gitMode.headOffset > 0 ? (
|
|
228
228
|
<box flexDirection="row" gap={1}>
|
|
229
|
-
<text fg={
|
|
229
|
+
<text fg={t.palette.warning}>
|
|
230
230
|
<strong>HEAD~{gitMode.headOffset}</strong>
|
|
231
231
|
</text>
|
|
232
|
-
<text fg={
|
|
232
|
+
<text fg={t.muted}>[ newer · ] older</text>
|
|
233
233
|
</box>
|
|
234
234
|
) : null}
|
|
235
|
-
<text fg={
|
|
236
|
-
{'·'.repeat(Math.max(0, fileBarWidth - 2))}
|
|
237
|
-
</text>
|
|
235
|
+
<text fg={t.hover}>{'·'.repeat(Math.max(0, fileBarWidth - 2))}</text>
|
|
238
236
|
<GitPanel
|
|
239
237
|
collapsedFolders={gitMode.collapsedFolders}
|
|
240
238
|
compact={gitPane.treeCompaction}
|
|
@@ -5,7 +5,7 @@ import { useLayoutEffect, useMemo } from 'react'
|
|
|
5
5
|
import { collectHelpEntries, HELP_MODE_LABELS } from '../../input/keymap/help-entries'
|
|
6
6
|
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
7
7
|
import { useKeymap } from '../keymap-context'
|
|
8
|
-
import {
|
|
8
|
+
import { useTokens } from '../theme'
|
|
9
9
|
import { uiTokens } from '../ui-tokens'
|
|
10
10
|
import { Picker, type PickerItem } from './picker'
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@ function matchesFilter(needle: string, ...fields: (string | undefined)[]): boole
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
|
|
26
|
-
const
|
|
26
|
+
const t = useTokens()
|
|
27
27
|
const config = useKeymap()
|
|
28
28
|
const allEntries = useMemo(() => collectHelpEntries(config), [config])
|
|
29
29
|
const scoped = useMemo(
|
|
@@ -54,18 +54,12 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
54
54
|
group: entry.modeLabel,
|
|
55
55
|
key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
|
|
56
56
|
title: (
|
|
57
|
-
<text
|
|
58
|
-
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
59
|
-
wrapMode="none"
|
|
60
|
-
>
|
|
57
|
+
<text fg={active ? t.palette.ink : t.muted} wrapMode="none">
|
|
61
58
|
{entry.description ?? ''}
|
|
62
59
|
</text>
|
|
63
60
|
),
|
|
64
61
|
trailing: (
|
|
65
|
-
<text
|
|
66
|
-
fg={active ? theme.colors['textLink.foreground'] : theme.colors['terminal.ansiMagenta']}
|
|
67
|
-
wrapMode="none"
|
|
68
|
-
>
|
|
62
|
+
<text fg={active ? t.palette.primary : t.accent} wrapMode="none">
|
|
69
63
|
{entry.keysDisplay}
|
|
70
64
|
</text>
|
|
71
65
|
),
|
|
@@ -82,9 +76,7 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
82
76
|
items={items}
|
|
83
77
|
selectedIndex={selectedIndex}
|
|
84
78
|
emptyState={
|
|
85
|
-
<text fg={
|
|
86
|
-
{filter ? 'No matching bindings.' : 'No bindings registered.'}
|
|
87
|
-
</text>
|
|
79
|
+
<text fg={t.muted}>{filter ? 'No matching bindings.' : 'No bindings registered.'}</text>
|
|
88
80
|
}
|
|
89
81
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
90
82
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useTokens } from '../theme'
|
|
2
2
|
import { Surface } from './surface'
|
|
3
3
|
|
|
4
4
|
interface InputFieldProps {
|
|
@@ -9,15 +9,13 @@ interface InputFieldProps {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function InputField({ active, cursorPos, placeholder, value }: InputFieldProps) {
|
|
12
|
-
const
|
|
13
|
-
const fg = active ?
|
|
12
|
+
const t = useTokens()
|
|
13
|
+
const fg = active ? t.palette.ink : t.muted
|
|
14
14
|
if (!active) {
|
|
15
15
|
const showPlaceholder = !value && !!placeholder
|
|
16
16
|
return (
|
|
17
17
|
<Surface tone="input" padding={1}>
|
|
18
|
-
<text fg={showPlaceholder ?
|
|
19
|
-
{showPlaceholder ? placeholder : value}
|
|
20
|
-
</text>
|
|
18
|
+
<text fg={showPlaceholder ? t.faint : fg}>{showPlaceholder ? placeholder : value}</text>
|
|
21
19
|
</Surface>
|
|
22
20
|
)
|
|
23
21
|
}
|
|
@@ -34,7 +32,7 @@ export function InputField({ active, cursorPos, placeholder, value }: InputField
|
|
|
34
32
|
<Surface tone="inputActive" padding={1}>
|
|
35
33
|
<text fg={fg}>
|
|
36
34
|
{before}
|
|
37
|
-
<span bg={
|
|
35
|
+
<span bg={t.palette.ink} fg={t.palette.neutral}>
|
|
38
36
|
{cursorDisplay}
|
|
39
37
|
</span>
|
|
40
38
|
{trailing}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useTokens } from '../theme'
|
|
4
4
|
import { Surface } from './surface'
|
|
5
5
|
|
|
6
6
|
export type ListItemDirection = 'row' | 'column'
|
|
@@ -28,7 +28,7 @@ export function ListItem({
|
|
|
28
28
|
title,
|
|
29
29
|
trailing,
|
|
30
30
|
}: ListItemProps) {
|
|
31
|
-
const
|
|
31
|
+
const t = useTokens()
|
|
32
32
|
const isRow = direction === 'row'
|
|
33
33
|
return (
|
|
34
34
|
<box id={id} onMouseOver={onHover} onMouseDown={onClick}>
|
|
@@ -41,15 +41,7 @@ export function ListItem({
|
|
|
41
41
|
<box flexDirection="row">
|
|
42
42
|
{isRow ? null : (
|
|
43
43
|
<>
|
|
44
|
-
<text
|
|
45
|
-
fg={
|
|
46
|
-
active
|
|
47
|
-
? theme.colors['textLink.foreground']
|
|
48
|
-
: theme.colors['editor.lineHighlightBackground']
|
|
49
|
-
}
|
|
50
|
-
>
|
|
51
|
-
{active ? '›' : '·'}
|
|
52
|
-
</text>
|
|
44
|
+
<text fg={active ? t.palette.primary : t.hover}>{active ? '›' : '·'}</text>
|
|
53
45
|
<text> </text>
|
|
54
46
|
</>
|
|
55
47
|
)}
|
|
@@ -2,7 +2,7 @@ import type { ModeId } from '@brimveyn/aimux-config'
|
|
|
2
2
|
|
|
3
3
|
import { describeBindings } from '../../input/keymap/describe-bindings'
|
|
4
4
|
import { useKeymap } from '../keymap-context'
|
|
5
|
-
import {
|
|
5
|
+
import { useTokens } from '../theme'
|
|
6
6
|
import { Surface } from './surface'
|
|
7
7
|
|
|
8
8
|
const KEYS_COLUMN_WIDTH = 12
|
|
@@ -13,7 +13,7 @@ interface ModalKeybindsOverlayProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProps) {
|
|
16
|
-
const
|
|
16
|
+
const t = useTokens()
|
|
17
17
|
const config = useKeymap()
|
|
18
18
|
const bindings = describeBindings(config, modeId, {
|
|
19
19
|
dedupeByDescription: true,
|
|
@@ -29,9 +29,9 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
29
29
|
{entries.map((binding) => (
|
|
30
30
|
<box key={`${binding.keys}-${binding.description}`} flexDirection="row">
|
|
31
31
|
<box width={KEYS_COLUMN_WIDTH}>
|
|
32
|
-
<text fg={
|
|
32
|
+
<text fg={t.accent}>{binding.keysDisplay}</text>
|
|
33
33
|
</box>
|
|
34
|
-
<text fg={
|
|
34
|
+
<text fg={t.muted}>{binding.description ?? ''}</text>
|
|
35
35
|
</box>
|
|
36
36
|
))}
|
|
37
37
|
</Surface>
|
|
@@ -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 {
|
|
6
|
+
import { useTokens, useTransparent } from '../theme'
|
|
7
7
|
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
8
8
|
|
|
9
9
|
interface ModalShellProps {
|
|
@@ -32,16 +32,11 @@ const TRANSPARENT_RGBA = RGBA.fromValues(0, 0, 0, 0)
|
|
|
32
32
|
//
|
|
33
33
|
// `setCell` (→ `bufferSetCell` → zig `buffer.set`) bypasses both: it writes
|
|
34
34
|
// the cell unconditionally, no alpha check, no blending, no char preservation.
|
|
35
|
-
// That overwrites each chrome char with a space while keeping the cell bg
|
|
36
|
-
// transparent, so the terminal emulator's own (blurred) window bg still shows
|
|
37
|
-
// through. Children (modal content) then render on top as normal.
|
|
38
35
|
function fillModalInteriorWithSpaces(this: BoxRenderable, buffer: OptimizedBuffer): void {
|
|
39
36
|
const x0 = this.screenX
|
|
40
37
|
const y0 = this.screenY
|
|
41
38
|
const w = this.width
|
|
42
39
|
const h = this.height
|
|
43
|
-
// Inset by 1 on each side so we don't erase the border that renderSelf just
|
|
44
|
-
// drew. The modal always has a single-cell border.
|
|
45
40
|
const startX = x0 + 1
|
|
46
41
|
const startY = y0 + 1
|
|
47
42
|
const endX = x0 + w - 1
|
|
@@ -62,9 +57,9 @@ export function ModalShell({
|
|
|
62
57
|
title,
|
|
63
58
|
width,
|
|
64
59
|
}: ModalShellProps) {
|
|
65
|
-
const
|
|
60
|
+
const t = useTokens()
|
|
66
61
|
const transparent = useTransparent()
|
|
67
|
-
const bg = transparent ? 'transparent' :
|
|
62
|
+
const bg = transparent ? 'transparent' : t.elevated
|
|
68
63
|
return (
|
|
69
64
|
<box
|
|
70
65
|
position="absolute"
|
|
@@ -77,7 +72,7 @@ export function ModalShell({
|
|
|
77
72
|
>
|
|
78
73
|
<box
|
|
79
74
|
border
|
|
80
|
-
borderColor={
|
|
75
|
+
borderColor={t.palette.primary}
|
|
81
76
|
backgroundColor={bg}
|
|
82
77
|
padding={1}
|
|
83
78
|
width={width}
|
|
@@ -85,8 +80,8 @@ export function ModalShell({
|
|
|
85
80
|
>
|
|
86
81
|
<box width="100%" flexDirection="column" gap={listGap}>
|
|
87
82
|
<box flexDirection="column">
|
|
88
|
-
<text fg={
|
|
89
|
-
{subtitle ? <text fg={
|
|
83
|
+
<text fg={t.accent}>{title}</text>
|
|
84
|
+
{subtitle ? <text fg={t.muted}>{subtitle}</text> : null}
|
|
90
85
|
</box>
|
|
91
86
|
{children}
|
|
92
87
|
{footer ? <box>{footer}</box> : null}
|
|
@@ -3,7 +3,7 @@ import type { AssistantId } from '../../state/types'
|
|
|
3
3
|
import { getAllAssistantOptions, getAssistantOption } from '../../pty/command-registry'
|
|
4
4
|
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
5
5
|
import { filterAssistants } from '../../state/selectors'
|
|
6
|
-
import {
|
|
6
|
+
import { useTokens } from '../theme'
|
|
7
7
|
import { uiTokens } from '../ui-tokens'
|
|
8
8
|
import { InputField } from './input-field'
|
|
9
9
|
import { ModalShell } from './modal-shell'
|
|
@@ -26,7 +26,7 @@ export function NewTabModal({
|
|
|
26
26
|
filter,
|
|
27
27
|
selectedIndex,
|
|
28
28
|
}: NewTabModalProps) {
|
|
29
|
-
const
|
|
29
|
+
const t = useTokens()
|
|
30
30
|
|
|
31
31
|
if (editingCommand !== null) {
|
|
32
32
|
const option =
|
|
@@ -39,9 +39,7 @@ export function NewTabModal({
|
|
|
39
39
|
width={uiTokens.modalWidth.md}
|
|
40
40
|
>
|
|
41
41
|
<box flexDirection="column">
|
|
42
|
-
<text fg={
|
|
43
|
-
Custom command (blank to reset to default: {option.command})
|
|
44
|
-
</text>
|
|
42
|
+
<text fg={t.muted}>Custom command (blank to reset to default: {option.command})</text>
|
|
45
43
|
<InputField
|
|
46
44
|
active
|
|
47
45
|
value={editBuffer}
|
|
@@ -65,17 +63,11 @@ export function NewTabModal({
|
|
|
65
63
|
onEdit: () => dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
66
64
|
subtitle: (
|
|
67
65
|
<box flexDirection="column">
|
|
68
|
-
<text fg={
|
|
69
|
-
{customCmd ? <text fg={
|
|
66
|
+
<text fg={t.muted}>{option.description}</text>
|
|
67
|
+
{customCmd ? <text fg={t.palette.primary}>{customCmd}</text> : null}
|
|
70
68
|
</box>
|
|
71
69
|
),
|
|
72
|
-
title:
|
|
73
|
-
<text
|
|
74
|
-
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
75
|
-
>
|
|
76
|
-
{option.label}
|
|
77
|
-
</text>
|
|
78
|
-
),
|
|
70
|
+
title: <text fg={active ? t.palette.ink : t.muted}>{option.label}</text>,
|
|
79
71
|
}
|
|
80
72
|
})
|
|
81
73
|
|
|
@@ -89,7 +81,7 @@ export function NewTabModal({
|
|
|
89
81
|
cursorPos={cursorPos}
|
|
90
82
|
items={items}
|
|
91
83
|
selectedIndex={selectedIndex}
|
|
92
|
-
emptyState={<text fg={
|
|
84
|
+
emptyState={<text fg={t.muted}>No matching assistants.</text>}
|
|
93
85
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
94
86
|
/>
|
|
95
87
|
)
|
|
@@ -2,11 +2,11 @@ import { parseKeyNotation } from '../../input/keymap/key-chord'
|
|
|
2
2
|
import { formatChord } from '../../input/keymap/key-format'
|
|
3
3
|
import { useAppStore } from '../../state/app-store'
|
|
4
4
|
import { useKeymap } from '../keymap-context'
|
|
5
|
-
import {
|
|
5
|
+
import { useTokens } from '../theme'
|
|
6
6
|
import { Surface } from './surface'
|
|
7
7
|
|
|
8
8
|
export function PendingChordOverlay() {
|
|
9
|
-
const
|
|
9
|
+
const t = useTokens()
|
|
10
10
|
const pendingChords = useAppStore((s) => s.pendingChords)
|
|
11
11
|
const modalOpen = useAppStore((s) => s.modal.type !== null)
|
|
12
12
|
const config = useKeymap()
|
|
@@ -20,9 +20,9 @@ export function PendingChordOverlay() {
|
|
|
20
20
|
<box position="absolute" bottom={modalOpen ? 10 : 2} right={1}>
|
|
21
21
|
<Surface tone="elevated" paddingLeft={1} paddingRight={1}>
|
|
22
22
|
<box flexDirection="row">
|
|
23
|
-
<text fg={
|
|
24
|
-
<text fg={
|
|
25
|
-
<text fg={
|
|
23
|
+
<text fg={t.muted}>pending: </text>
|
|
24
|
+
<text fg={t.palette.primary}>{display}</text>
|
|
25
|
+
<text fg={t.muted}> …</text>
|
|
26
26
|
</box>
|
|
27
27
|
</Surface>
|
|
28
28
|
</box>
|
|
@@ -4,7 +4,7 @@ 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 {
|
|
7
|
+
import { useTokens } from '../theme'
|
|
8
8
|
import { BareInput } from './bare-input'
|
|
9
9
|
import { ListItem } from './list-item'
|
|
10
10
|
import { ModalShell } from './modal-shell'
|
|
@@ -39,7 +39,7 @@ const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
|
39
39
|
const MODAL_CHROME_ROWS = 6
|
|
40
40
|
|
|
41
41
|
function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?: () => void }) {
|
|
42
|
-
const
|
|
42
|
+
const t = useTokens()
|
|
43
43
|
return (
|
|
44
44
|
<box flexDirection="row" gap={1}>
|
|
45
45
|
{onEdit ? (
|
|
@@ -49,7 +49,7 @@ function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?:
|
|
|
49
49
|
onEdit()
|
|
50
50
|
}}
|
|
51
51
|
>
|
|
52
|
-
<text fg={
|
|
52
|
+
<text fg={t.palette.primary}>[edit]</text>
|
|
53
53
|
</box>
|
|
54
54
|
) : null}
|
|
55
55
|
{onDelete ? (
|
|
@@ -59,7 +59,7 @@ function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?:
|
|
|
59
59
|
onDelete()
|
|
60
60
|
}}
|
|
61
61
|
>
|
|
62
|
-
<text fg={
|
|
62
|
+
<text fg={t.palette.error}>[del]</text>
|
|
63
63
|
</box>
|
|
64
64
|
) : null}
|
|
65
65
|
</box>
|
|
@@ -80,7 +80,7 @@ export function Picker({
|
|
|
80
80
|
title,
|
|
81
81
|
width,
|
|
82
82
|
}: PickerProps) {
|
|
83
|
-
const
|
|
83
|
+
const t = useTokens()
|
|
84
84
|
const dimensions = useTerminalDimensions()
|
|
85
85
|
const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
|
|
86
86
|
const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
|
|
@@ -140,7 +140,7 @@ export function Picker({
|
|
|
140
140
|
if (item.group && item.group !== prevGroup) {
|
|
141
141
|
nodes.push(
|
|
142
142
|
<box key={`group::${item.group}`} paddingLeft={1} paddingTop={index === 0 ? 0 : 1}>
|
|
143
|
-
<text fg={
|
|
143
|
+
<text fg={t.palette.warning} wrapMode="none">
|
|
144
144
|
{item.group}
|
|
145
145
|
</text>
|
|
146
146
|
</box>
|
|
@@ -10,11 +10,12 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
|
10
10
|
import { IDLE_SESSION_STATUS } from '../../state/types'
|
|
11
11
|
import { useBusySpinner } from '../hooks/use-busy-spinner'
|
|
12
12
|
import { moveIdToIdPosition, orderSessionsForDisplay } from '../session-ordering'
|
|
13
|
-
import { useBg,
|
|
13
|
+
import { useBg, useTokens } from '../theme'
|
|
14
|
+
import { ContextMenuBox } from './context-menu-box'
|
|
14
15
|
|
|
15
16
|
export function SessionBar() {
|
|
16
|
-
const
|
|
17
|
-
const headerBg = useBg('
|
|
17
|
+
const t = useTokens()
|
|
18
|
+
const headerBg = useBg('elevated')
|
|
18
19
|
const sessions = useAppStore((s) => s.sessions)
|
|
19
20
|
const currentId = useAppStore((s) => s.currentSessionId)
|
|
20
21
|
const bar = useAppStore((s) => s.sessionBar)
|
|
@@ -22,11 +23,7 @@ export function SessionBar() {
|
|
|
22
23
|
|
|
23
24
|
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
24
25
|
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
25
|
-
// Hysteresis: the id of the chip we most recently swapped with. While the
|
|
26
|
-
// cursor remains over that chip we refuse to swap back (prevents oscillation
|
|
27
|
-
// when a long chip's new bounds still cover the cursor after a swap).
|
|
28
26
|
const lastSwapWithRef = useRef<string | null>(null)
|
|
29
|
-
// Live bounds of each chip after render, keyed by session id.
|
|
30
27
|
const chipRefs = useRef(new Map<string, BoxRenderable>())
|
|
31
28
|
|
|
32
29
|
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
@@ -125,6 +122,22 @@ export function SessionBar() {
|
|
|
125
122
|
onMouseDrag={handleMouseDrag}
|
|
126
123
|
onMouseUp={commitDrop}
|
|
127
124
|
onMouseDragEnd={cancelDrag}
|
|
125
|
+
rightClickMenu={[
|
|
126
|
+
[
|
|
127
|
+
'Rename',
|
|
128
|
+
() =>
|
|
129
|
+
dispatchGlobal({
|
|
130
|
+
initialName: session.name,
|
|
131
|
+
returnToSessionPicker: false,
|
|
132
|
+
sessionTargetId: session.id,
|
|
133
|
+
type: 'open-session-name-modal',
|
|
134
|
+
}),
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
'Close',
|
|
138
|
+
() => runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' }),
|
|
139
|
+
],
|
|
140
|
+
]}
|
|
128
141
|
/>
|
|
129
142
|
)
|
|
130
143
|
})}
|
|
@@ -133,13 +146,13 @@ export function SessionBar() {
|
|
|
133
146
|
flexDirection="row"
|
|
134
147
|
paddingLeft={1}
|
|
135
148
|
paddingRight={1}
|
|
136
|
-
backgroundColor={
|
|
149
|
+
backgroundColor={t.selected}
|
|
137
150
|
onMouseDown={(e) => {
|
|
138
151
|
e.stopPropagation()
|
|
139
152
|
dispatchGlobal({ returnToSessionPicker: false, type: 'open-create-session-modal' })
|
|
140
153
|
}}
|
|
141
154
|
>
|
|
142
|
-
<text fg={
|
|
155
|
+
<text fg={t.palette.ink} selectable={false}>
|
|
143
156
|
+ New
|
|
144
157
|
</text>
|
|
145
158
|
</box>
|
|
@@ -166,6 +179,7 @@ interface SessionChipProps {
|
|
|
166
179
|
onMouseDrag: (event: OtuiMouseEvent) => void
|
|
167
180
|
onMouseUp: (event: OtuiMouseEvent) => void
|
|
168
181
|
onMouseDragEnd: (event: OtuiMouseEvent) => void
|
|
182
|
+
rightClickMenu: Array<[string, () => void]>
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
function SessionChip({
|
|
@@ -177,29 +191,29 @@ function SessionChip({
|
|
|
177
191
|
onMouseDragEnd,
|
|
178
192
|
onMouseUp,
|
|
179
193
|
onRef,
|
|
194
|
+
rightClickMenu,
|
|
180
195
|
session,
|
|
181
196
|
status,
|
|
182
197
|
}: SessionChipProps) {
|
|
183
|
-
const
|
|
184
|
-
const selectionBg = useBg('
|
|
198
|
+
const t = useTokens()
|
|
199
|
+
const selectionBg = useBg('selected')
|
|
185
200
|
const showSpinner = status.working
|
|
186
201
|
const showWaiting = status.waiting
|
|
187
202
|
const spinner = useBusySpinner(showSpinner)
|
|
188
|
-
const labelColor = active
|
|
189
|
-
? theme.colors['editor.foreground']
|
|
190
|
-
: theme.colors['descriptionForeground']
|
|
203
|
+
const labelColor = active ? t.palette.ink : t.muted
|
|
191
204
|
const bgColor = dragging || active ? selectionBg : undefined
|
|
192
|
-
const idleColor =
|
|
193
|
-
const workingColor =
|
|
194
|
-
const waitingColor =
|
|
205
|
+
const idleColor = t.palette.success
|
|
206
|
+
const workingColor = t.palette.primary
|
|
207
|
+
const waitingColor = t.palette.warning
|
|
195
208
|
|
|
196
209
|
return (
|
|
197
|
-
<
|
|
210
|
+
<ContextMenuBox
|
|
198
211
|
ref={onRef}
|
|
199
212
|
flexDirection="row"
|
|
200
213
|
paddingLeft={1}
|
|
201
214
|
paddingRight={1}
|
|
202
215
|
backgroundColor={bgColor}
|
|
216
|
+
rightClickMenu={rightClickMenu}
|
|
203
217
|
onMouseDown={(e) => {
|
|
204
218
|
e.preventDefault()
|
|
205
219
|
onMouseDown(e)
|
|
@@ -233,9 +247,9 @@ function SessionChip({
|
|
|
233
247
|
<text fg={labelColor} selectable={false}>
|
|
234
248
|
[{index}] {session.name}
|
|
235
249
|
</text>
|
|
236
|
-
<text fg={
|
|
250
|
+
<text fg={t.hover} selectable={false}>
|
|
237
251
|
{' '}
|
|
238
252
|
</text>
|
|
239
|
-
</
|
|
253
|
+
</ContextMenuBox>
|
|
240
254
|
)
|
|
241
255
|
}
|
|
@@ -4,7 +4,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
|
4
4
|
import { filterSessions } from '../../state/selectors'
|
|
5
5
|
import { abbreviatePath } from '../path-format'
|
|
6
6
|
import { orderSessionsForDisplay } from '../session-ordering'
|
|
7
|
-
import {
|
|
7
|
+
import { useTokens } from '../theme'
|
|
8
8
|
import { uiTokens } from '../ui-tokens'
|
|
9
9
|
import { Picker, type PickerItem } from './picker'
|
|
10
10
|
|
|
@@ -31,10 +31,7 @@ function formatSessionLine(
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function getEmptyStateMessage(hasFilter: boolean): string {
|
|
34
|
-
if (hasFilter)
|
|
35
|
-
return 'No matching sessions.'
|
|
36
|
-
}
|
|
37
|
-
|
|
34
|
+
if (hasFilter) return 'No matching sessions.'
|
|
38
35
|
return 'No sessions yet. Press Enter or n to create your first session.'
|
|
39
36
|
}
|
|
40
37
|
|
|
@@ -46,7 +43,7 @@ export function SessionPickerModal({
|
|
|
46
43
|
selectedIndex,
|
|
47
44
|
sessions,
|
|
48
45
|
}: SessionPickerModalProps) {
|
|
49
|
-
const
|
|
46
|
+
const t = useTokens()
|
|
50
47
|
const ordered = orderSessionsForDisplay(sessions)
|
|
51
48
|
const baselineOrder = ordered.map((s) => s.id)
|
|
52
49
|
const filtered = filterSessions(ordered, filter)
|
|
@@ -60,14 +57,10 @@ export function SessionPickerModal({
|
|
|
60
57
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
61
58
|
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
62
59
|
subtitle: session.projectPath ? (
|
|
63
|
-
<text fg={
|
|
64
|
-
{abbreviatePath(session.projectPath)}
|
|
65
|
-
</text>
|
|
60
|
+
<text fg={t.muted}>{abbreviatePath(session.projectPath)}</text>
|
|
66
61
|
) : undefined,
|
|
67
62
|
title: (
|
|
68
|
-
<text
|
|
69
|
-
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
70
|
-
>
|
|
63
|
+
<text fg={active ? t.palette.ink : t.muted}>
|
|
71
64
|
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
72
65
|
</text>
|
|
73
66
|
),
|
|
@@ -78,13 +71,7 @@ export function SessionPickerModal({
|
|
|
78
71
|
key: '__create-new__',
|
|
79
72
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
80
73
|
title: (
|
|
81
|
-
<text
|
|
82
|
-
fg={
|
|
83
|
-
selectedIndex === filtered.length
|
|
84
|
-
? theme.colors['editor.foreground']
|
|
85
|
-
: theme.colors['descriptionForeground']
|
|
86
|
-
}
|
|
87
|
-
>
|
|
74
|
+
<text fg={selectedIndex === filtered.length ? t.palette.ink : t.muted}>
|
|
88
75
|
Create new session
|
|
89
76
|
</text>
|
|
90
77
|
),
|
|
@@ -104,7 +91,7 @@ export function SessionPickerModal({
|
|
|
104
91
|
selectedIndex={selectedIndex}
|
|
105
92
|
emptyState={
|
|
106
93
|
filtered.length === 0 ? (
|
|
107
|
-
<text fg={
|
|
94
|
+
<text fg={t.muted}>{getEmptyStateMessage(hasFilter)}</text>
|
|
108
95
|
) : undefined
|
|
109
96
|
}
|
|
110
97
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|