@brimveyn/aimux 1.23.7 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/input/keymap/key-chord.ts +10 -0
- package/src/input/keymap/key-format.ts +14 -1
- package/src/input/modes/handlers/shared.ts +48 -0
- package/src/state/actions.ts +2 -1
- package/src/state/reducers/modal-state.ts +24 -28
- package/src/state/text-cursor.ts +117 -0
- package/src/ui/breaking-update-screen.tsx +7 -1
- package/src/ui/components/layout/bar-footer.tsx +3 -5
- package/src/ui/components/layout/bar.tsx +41 -16
- package/src/ui/components/layout/sidebar/project-list.tsx +6 -16
- package/src/ui/components/layout/sidebar/workspace-row.tsx +6 -9
- package/src/ui/components/layout/status-bar.tsx +9 -3
- package/src/ui/components/layout/terminal-pane.tsx +18 -16
- package/src/ui/components/modals/app/help-modal.tsx +20 -15
- package/src/ui/components/modals/app/update-available-modal.tsx +3 -1
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/projects/create-project-modal.tsx +9 -3
- package/src/ui/components/modals/projects/project-picker-modal.tsx +6 -6
- package/src/ui/components/modals/settings/settings-search-modal.tsx +19 -14
- package/src/ui/components/modals/shared/modal-shell.tsx +12 -2
- package/src/ui/components/modals/shared/picker.tsx +8 -4
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +7 -5
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +10 -12
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +14 -10
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +19 -14
- package/src/ui/components/modals/workspace/workspace-move-modal.tsx +4 -2
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +10 -4
- package/src/ui/components/primitives/list-item.tsx +24 -2
- package/src/ui/components/primitives/surface.tsx +4 -1
- package/src/ui/components/settings/row-value.tsx +8 -1
- package/src/ui/components/settings/settings-footer.tsx +3 -2
- package/src/ui/components/settings/settings-row.tsx +3 -0
- package/src/ui/components/settings/settings-search-bar.tsx +4 -6
- package/src/ui/components/settings/settings-view.tsx +25 -21
- package/src/ui/components/stats/stats-view.tsx +4 -2
- package/src/ui/selection-ink.ts +20 -0
|
@@ -44,12 +44,6 @@ interface TerminalPaneProps {
|
|
|
44
44
|
junctionEdges?: JunctionEdges
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
48
|
-
const t = getCurrentTheme()
|
|
49
|
-
if (!isActive) return t.border
|
|
50
|
-
return focusMode === 'terminal-input' ? t.accent : t.primary
|
|
51
|
-
}
|
|
52
|
-
|
|
53
47
|
function renderSpan(span: TerminalSpan, key: string, softCursor: boolean): ReactNode {
|
|
54
48
|
let node: ReactNode = span.text
|
|
55
49
|
|
|
@@ -177,15 +171,19 @@ const NOOP = (): void => {}
|
|
|
177
171
|
// wraps) a wheel event bumps _scrollY, and opentui's onResize never re-clamps it,
|
|
178
172
|
// so the offset sticks for the renderable's life: the viewport shifts up, dead
|
|
179
173
|
// rows appear at the bottom, and the prompt scrolls off-screen after `clear`.
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
174
|
+
//
|
|
175
|
+
// Kill the built-in wheel handler at its dispatch point: processMouseEvent calls
|
|
176
|
+
// onMouseEvent() unconditionally after any React mouse listener, and
|
|
177
|
+
// TextBufferRenderable.onMouseEvent() unconditionally calls handleScroll() for a
|
|
178
|
+
// scroll event. Overriding onMouseEvent (rather than only handleScroll) is a
|
|
179
|
+
// single deterministic no-op standing between every wheel step and _scrollY, so
|
|
180
|
+
// the drift can no longer slip in on a path the later reset misses. Selection is
|
|
181
|
+
// unaffected: it is driven by the renderer (startSelection/updateSelection), not
|
|
182
|
+
// by onMouseEvent, which exists only to scroll. Both are protected; reach them
|
|
183
|
+
// via Reflect, mirroring the scrollY reset already used elsewhere in the pane.
|
|
186
184
|
const pinTerminalScroll = (node: TextRenderable | null): void => {
|
|
187
185
|
if (!node) return
|
|
188
|
-
Reflect.set(node, '
|
|
186
|
+
Reflect.set(node, 'onMouseEvent', NOOP)
|
|
189
187
|
if (node.scrollY !== 0) node.scrollY = 0
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -498,9 +496,13 @@ export function TerminalPane({
|
|
|
498
496
|
return (
|
|
499
497
|
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
500
498
|
<ContextMenuBox
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
padding
|
|
499
|
+
// No frame, in any state — not around the pane, not between two split
|
|
500
|
+
// panes, and nothing that colours itself by focus mode. What was the
|
|
501
|
+
// border is now plain padding: same one-cell inset, so the geometry
|
|
502
|
+
// PANE_BORDER describes and the resize/hit-test gutter both still hold,
|
|
503
|
+
// but the cells are painted in the editor background like everything
|
|
504
|
+
// else. Surfaces are told apart by their background alone.
|
|
505
|
+
padding={1}
|
|
504
506
|
flexDirection="column"
|
|
505
507
|
flexGrow={1}
|
|
506
508
|
backgroundColor={editorBg}
|
|
@@ -5,6 +5,7 @@ import { useCallback, 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 { useSelectionInk } from '../../../selection-ink'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -24,6 +25,7 @@ function matchesFilter(needle: string, ...fields: (string | undefined)[]): boole
|
|
|
24
25
|
|
|
25
26
|
export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModalProps) {
|
|
26
27
|
const t = useTheme()
|
|
28
|
+
const ink = useSelectionInk()
|
|
27
29
|
const config = useKeymap()
|
|
28
30
|
const allEntries = useMemo(() => collectHelpEntries(config), [config])
|
|
29
31
|
const scoped = useMemo(
|
|
@@ -50,21 +52,24 @@ export function HelpModal({ cursorPos, filter, scope, selectedIndex }: HelpModal
|
|
|
50
52
|
|
|
51
53
|
const items = useMemo<PickerItem[]>(
|
|
52
54
|
() =>
|
|
53
|
-
filtered.map((entry, index) =>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
filtered.map((entry, index) => {
|
|
56
|
+
const active = index === selectedIndex
|
|
57
|
+
return {
|
|
58
|
+
group: entry.modeLabel,
|
|
59
|
+
key: `${entry.mode}::${entry.keysDisplay}::${entry.description ?? ''}::${index}`,
|
|
60
|
+
title: (
|
|
61
|
+
<text fg={active ? ink : t.textMuted} wrapMode="none">
|
|
62
|
+
{entry.description ?? ''}
|
|
63
|
+
</text>
|
|
64
|
+
),
|
|
65
|
+
trailing: (
|
|
66
|
+
<text fg={active ? ink : t.textMuted} wrapMode="none">
|
|
67
|
+
{entry.keysDisplay}
|
|
68
|
+
</text>
|
|
69
|
+
),
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
[filtered, ink, selectedIndex, t]
|
|
68
73
|
)
|
|
69
74
|
|
|
70
75
|
const handleHover = useCallback(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
1
2
|
import { useTheme } from '../../../theme'
|
|
2
3
|
import { uiTokens } from '../../../ui-tokens'
|
|
3
4
|
import { ListItem } from '../../primitives/list-item'
|
|
@@ -20,6 +21,7 @@ export function UpdateAvailableModal({
|
|
|
20
21
|
selectedIndex,
|
|
21
22
|
}: UpdateAvailableModalProps) {
|
|
22
23
|
const t = useTheme()
|
|
24
|
+
const ink = useSelectionInk()
|
|
23
25
|
return (
|
|
24
26
|
<ModalShell
|
|
25
27
|
title="Update available"
|
|
@@ -36,7 +38,7 @@ export function UpdateAvailableModal({
|
|
|
36
38
|
key={option.label}
|
|
37
39
|
active={active}
|
|
38
40
|
direction="row"
|
|
39
|
-
title={<text fg={active ?
|
|
41
|
+
title={<text fg={active ? ink : t.textMuted}>{option.label}</text>}
|
|
40
42
|
/>
|
|
41
43
|
)
|
|
42
44
|
})}
|
|
@@ -172,9 +172,9 @@ export function GitCommitModal({
|
|
|
172
172
|
|
|
173
173
|
{isConfirm || isGenerating || !isAutoCommitEnabled() ? null : (
|
|
174
174
|
<box flexDirection="row" gap={1} marginTop={1} alignItems="center">
|
|
175
|
+
{/* Filled, not framed — the same button the setup widget uses. */}
|
|
175
176
|
<box
|
|
176
|
-
|
|
177
|
-
borderColor={t.border}
|
|
177
|
+
backgroundColor={t.backgroundElement}
|
|
178
178
|
paddingLeft={1}
|
|
179
179
|
paddingRight={1}
|
|
180
180
|
flexDirection="row"
|
|
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
|
|
|
3
3
|
import type { DirectoryResult } from '../../../../state/types'
|
|
4
4
|
|
|
5
5
|
import { abbreviatePath } from '../../../path-format'
|
|
6
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
6
7
|
import { getCurrentTheme, useTheme } from '../../../theme'
|
|
7
8
|
import { uiTokens } from '../../../ui-tokens'
|
|
8
9
|
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
@@ -40,6 +41,7 @@ export function CreateProjectModal({
|
|
|
40
41
|
selectedIndex,
|
|
41
42
|
}: CreateProjectModalProps) {
|
|
42
43
|
const t = useTheme()
|
|
44
|
+
const ink = useSelectionInk()
|
|
43
45
|
const dirActive = activeField === 'directory'
|
|
44
46
|
const nameActive = activeField === 'name'
|
|
45
47
|
|
|
@@ -47,12 +49,16 @@ export function CreateProjectModal({
|
|
|
47
49
|
() =>
|
|
48
50
|
results.map((result) => ({
|
|
49
51
|
key: result.path,
|
|
50
|
-
leading:
|
|
52
|
+
leading: (active) => (
|
|
53
|
+
<text fg={active ? ink : getDirectoryResultColor(result)}>
|
|
54
|
+
{getDirectoryResultIcon(result)}
|
|
55
|
+
</text>
|
|
56
|
+
),
|
|
51
57
|
title: (active) => (
|
|
52
|
-
<text fg={active ?
|
|
58
|
+
<text fg={active ? ink : t.textMuted}>{abbreviatePath(result.path)}</text>
|
|
53
59
|
),
|
|
54
60
|
})),
|
|
55
|
-
[results, t]
|
|
61
|
+
[ink, results, t]
|
|
56
62
|
)
|
|
57
63
|
|
|
58
64
|
return (
|
|
@@ -6,6 +6,7 @@ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-
|
|
|
6
6
|
import { filterProjects } from '../../../../state/selectors'
|
|
7
7
|
import { abbreviatePath } from '../../../path-format'
|
|
8
8
|
import { orderProjectsForDisplay } from '../../../project-ordering'
|
|
9
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
9
10
|
import { useTheme } from '../../../theme'
|
|
10
11
|
import { uiTokens } from '../../../ui-tokens'
|
|
11
12
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -44,6 +45,7 @@ export function ProjectPickerModal({
|
|
|
44
45
|
selectedIndex,
|
|
45
46
|
}: ProjectPickerModalProps) {
|
|
46
47
|
const t = useTheme()
|
|
48
|
+
const ink = useSelectionInk()
|
|
47
49
|
const ordered = useMemo(() => orderProjectsForDisplay(projects), [projects])
|
|
48
50
|
const filtered = useMemo(() => filterProjects(ordered, filter), [filter, ordered])
|
|
49
51
|
const hasFilter = !!(filter != null && filter !== '')
|
|
@@ -59,10 +61,10 @@ export function ProjectPickerModal({
|
|
|
59
61
|
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-project' }),
|
|
60
62
|
subtitle:
|
|
61
63
|
project.projectPath != null && project.projectPath !== '' ? (
|
|
62
|
-
<text fg={t.textMuted}>{abbreviatePath(project.projectPath)}</text>
|
|
64
|
+
<text fg={active ? ink : t.textMuted}>{abbreviatePath(project.projectPath)}</text>
|
|
63
65
|
) : undefined,
|
|
64
66
|
title: (
|
|
65
|
-
<text fg={active ?
|
|
67
|
+
<text fg={active ? ink : t.textMuted}>
|
|
66
68
|
{formatProjectLine(project, currentProjectId, currentTabCount, displayIndex)}
|
|
67
69
|
</text>
|
|
68
70
|
),
|
|
@@ -72,13 +74,11 @@ export function ProjectPickerModal({
|
|
|
72
74
|
key: '__create-new__',
|
|
73
75
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-project' }),
|
|
74
76
|
title: (
|
|
75
|
-
<text fg={selectedIndex === filtered.length ?
|
|
76
|
-
Create new project
|
|
77
|
-
</text>
|
|
77
|
+
<text fg={selectedIndex === filtered.length ? ink : t.textMuted}>Create new project</text>
|
|
78
78
|
),
|
|
79
79
|
}
|
|
80
80
|
return [...projectItems, createNewItem]
|
|
81
|
-
}, [currentProjectId, currentTabCount, filtered, ordered, selectedIndex, t])
|
|
81
|
+
}, [currentProjectId, currentTabCount, filtered, ink, ordered, selectedIndex, t])
|
|
82
82
|
|
|
83
83
|
const handleHover = useCallback(
|
|
84
84
|
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
@@ -3,6 +3,7 @@ import { useCallback, useMemo } from 'react'
|
|
|
3
3
|
import { filterSettingRows } from '../../../../settings/search'
|
|
4
4
|
import { useAppStore } from '../../../../state/app-store'
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
6
7
|
import { useTheme } from '../../../theme'
|
|
7
8
|
import { uiTokens } from '../../../ui-tokens'
|
|
8
9
|
import { RowValue } from '../../settings/row-value'
|
|
@@ -28,25 +29,29 @@ export function SettingsSearchModal({
|
|
|
28
29
|
selectedIndex,
|
|
29
30
|
}: SettingsSearchModalProps) {
|
|
30
31
|
const t = useTheme()
|
|
32
|
+
const ink = useSelectionInk()
|
|
31
33
|
const projects = useAppStore((s) => s.projects)
|
|
32
34
|
const hits = useMemo(() => filterSettingRows(projects, filter), [projects, filter])
|
|
33
35
|
|
|
34
36
|
const items = useMemo<PickerItem[]>(
|
|
35
37
|
() =>
|
|
36
|
-
hits.map((hit) =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
hits.map((hit, index) => {
|
|
39
|
+
const active = index === selectedIndex
|
|
40
|
+
return {
|
|
41
|
+
group: hit.sectionLabel,
|
|
42
|
+
key: hit.row.id,
|
|
43
|
+
// Acts on the selection, which the hover just set — the same two-step
|
|
44
|
+
// every other picker's rows use.
|
|
45
|
+
onClick: () => runSideEffectGlobal({ type: 'confirm-settings-search' }),
|
|
46
|
+
subtitle:
|
|
47
|
+
hit.row.description != null && hit.row.description !== '' ? (
|
|
48
|
+
<text fg={active ? ink : t.textMuted}>{hit.row.description}</text>
|
|
49
|
+
) : undefined,
|
|
50
|
+
title: <text fg={active ? ink : t.text}>{hit.row.label}</text>,
|
|
51
|
+
trailing: <RowValue fg={active ? ink : undefined} row={hit.row} />,
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
[hits, ink, selectedIndex, t]
|
|
50
55
|
)
|
|
51
56
|
|
|
52
57
|
const handleHover = useCallback(
|
|
@@ -37,11 +37,21 @@ export function ModalShell({
|
|
|
37
37
|
justifyContent="center"
|
|
38
38
|
alignItems="center"
|
|
39
39
|
>
|
|
40
|
+
{/* No frame, like everything else — the panel background is what lifts the
|
|
41
|
+
modal off the terminal, and the extra column either side is what makes
|
|
42
|
+
it read as a card rather than a box drawn on the screen.
|
|
43
|
+
|
|
44
|
+
Transparent mode is the one exception that keeps the border: there is
|
|
45
|
+
no background there to lift anything, so without a rule the modal has
|
|
46
|
+
no edge at all. */}
|
|
40
47
|
<box
|
|
41
|
-
border
|
|
48
|
+
border={transparent}
|
|
42
49
|
borderColor={t.border}
|
|
43
50
|
backgroundColor={bg}
|
|
44
|
-
|
|
51
|
+
paddingTop={1}
|
|
52
|
+
paddingBottom={1}
|
|
53
|
+
paddingLeft={2}
|
|
54
|
+
paddingRight={2}
|
|
45
55
|
width={width}
|
|
46
56
|
renderAfter={transparent ? fillBorderedBoxInterior : undefined}
|
|
47
57
|
>
|
|
@@ -4,6 +4,7 @@ import type { MouseEvent as OtuiMouseEvent, ScrollBoxRenderable } from '@opentui
|
|
|
4
4
|
import { useTerminalDimensions } from '@opentui/react'
|
|
5
5
|
import { type ReactNode, useCallback, useLayoutEffect, useMemo, useRef } from 'react'
|
|
6
6
|
|
|
7
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
7
8
|
import { useTheme } from '../../../theme'
|
|
8
9
|
import { BareInput } from '../../primitives/bare-input'
|
|
9
10
|
import { ListItem } from '../../primitives/list-item'
|
|
@@ -48,7 +49,10 @@ function PickerItemCtas({
|
|
|
48
49
|
onDelete?: () => void
|
|
49
50
|
onWorkspace?: () => void
|
|
50
51
|
}) {
|
|
51
|
-
|
|
52
|
+
// These only ever render on the selected row, which is filled with `primary` —
|
|
53
|
+
// so they wear its ink, not the primary/warning/error they would carry
|
|
54
|
+
// anywhere else. `[edit]` in primary on primary is nothing at all.
|
|
55
|
+
const ink = useSelectionInk()
|
|
52
56
|
const handleEdit = useCallback(
|
|
53
57
|
(event: OtuiMouseEvent) => {
|
|
54
58
|
event.stopPropagation()
|
|
@@ -74,17 +78,17 @@ function PickerItemCtas({
|
|
|
74
78
|
<box flexDirection="row" gap={1}>
|
|
75
79
|
{onEdit ? (
|
|
76
80
|
<box onMouseDown={handleEdit}>
|
|
77
|
-
<text fg={
|
|
81
|
+
<text fg={ink}>[edit]</text>
|
|
78
82
|
</box>
|
|
79
83
|
) : null}
|
|
80
84
|
{onWorkspace ? (
|
|
81
85
|
<box onMouseDown={handleWorkspace}>
|
|
82
|
-
<text fg={
|
|
86
|
+
<text fg={ink}>[WT]</text>
|
|
83
87
|
</box>
|
|
84
88
|
) : null}
|
|
85
89
|
{onDelete ? (
|
|
86
90
|
<box onMouseDown={handleDelete}>
|
|
87
|
-
<text fg={
|
|
91
|
+
<text fg={ink}>[del]</text>
|
|
88
92
|
</box>
|
|
89
93
|
) : null}
|
|
90
94
|
</box>
|
|
@@ -5,6 +5,7 @@ import type { SnippetRecord } from '../../../../state/types'
|
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { filterSnippets } from '../../../../state/selectors'
|
|
7
7
|
import { isConfigSnippetId } from '../../../../state/snippet-catalog'
|
|
8
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -33,6 +34,7 @@ export function SnippetPickerModal({
|
|
|
33
34
|
snippets,
|
|
34
35
|
}: SnippetPickerModalProps) {
|
|
35
36
|
const t = useTheme()
|
|
37
|
+
const ink = useSelectionInk()
|
|
36
38
|
const filtered = useMemo(() => filterSnippets(snippets, filter), [filter, snippets])
|
|
37
39
|
|
|
38
40
|
const items = useMemo<PickerItem[]>(
|
|
@@ -52,21 +54,21 @@ export function SnippetPickerModal({
|
|
|
52
54
|
onEdit: fromConfig
|
|
53
55
|
? undefined
|
|
54
56
|
: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
55
|
-
subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
57
|
+
subtitle: <text fg={active ? ink : t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
56
58
|
title: (
|
|
57
59
|
<box flexDirection="row">
|
|
58
|
-
<text fg={active ?
|
|
60
|
+
<text fg={active ? ink : t.textMuted}>
|
|
59
61
|
<strong>{snippet.name}</strong>
|
|
60
62
|
</text>
|
|
61
63
|
{snippet.trigger != null && snippet.trigger !== '' ? (
|
|
62
|
-
<text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
64
|
+
<text fg={active ? ink : t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
63
65
|
) : null}
|
|
64
|
-
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
66
|
+
{fromConfig ? <text fg={active ? ink : t.textMuted}>{' [config]'}</text> : null}
|
|
65
67
|
</box>
|
|
66
68
|
),
|
|
67
69
|
}
|
|
68
70
|
}),
|
|
69
|
-
[filtered, selectedIndex, t]
|
|
71
|
+
[filtered, ink, selectedIndex, t]
|
|
70
72
|
)
|
|
71
73
|
|
|
72
74
|
const handleHover = useCallback(
|
|
@@ -5,6 +5,7 @@ import type { AssistantId } from '../../../../state/types'
|
|
|
5
5
|
import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
|
|
6
6
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
7
7
|
import { getNewTabAssistantOptions } from '../../../../state/selectors'
|
|
8
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Form, TextField } from '../shared/form'
|
|
@@ -35,6 +36,7 @@ export function NewTabModal({
|
|
|
35
36
|
selectedIndex,
|
|
36
37
|
}: NewTabModalProps) {
|
|
37
38
|
const t = useTheme()
|
|
39
|
+
const ink = useSelectionInk()
|
|
38
40
|
const excludeTerminal = pendingPrompt != null && pendingPrompt.trim() !== ''
|
|
39
41
|
const footerText =
|
|
40
42
|
pendingPrompt == null
|
|
@@ -66,16 +68,16 @@ export function NewTabModal({
|
|
|
66
68
|
dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
67
69
|
subtitle: (
|
|
68
70
|
<box flexDirection="column">
|
|
69
|
-
<text fg={t.textMuted}>{option.description}</text>
|
|
71
|
+
<text fg={active ? ink : t.textMuted}>{option.description}</text>
|
|
70
72
|
{customCmd != null && customCmd !== '' ? (
|
|
71
|
-
<text fg={t.primary}>{customCmd}</text>
|
|
73
|
+
<text fg={active ? ink : t.primary}>{customCmd}</text>
|
|
72
74
|
) : null}
|
|
73
75
|
</box>
|
|
74
76
|
),
|
|
75
|
-
title: <text fg={active ?
|
|
77
|
+
title: <text fg={active ? ink : t.textMuted}>{option.label}</text>,
|
|
76
78
|
}
|
|
77
79
|
}),
|
|
78
|
-
[customCommands, filtered, selectedIndex, t]
|
|
80
|
+
[customCommands, filtered, ink, selectedIndex, t]
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
if (editingCommand !== null) {
|
|
@@ -86,20 +88,16 @@ export function NewTabModal({
|
|
|
86
88
|
keybindsModeId="modal.new-tab.editing-command"
|
|
87
89
|
width={uiTokens.modalWidth.md}
|
|
88
90
|
>
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
value={editBuffer}
|
|
93
|
-
cursorPos={cursorPos}
|
|
94
|
-
placeholder={option.command}
|
|
95
|
-
/>
|
|
91
|
+
{/* No description: the placeholder is the default command, so the field
|
|
92
|
+
already shows what leaving it blank gets you. */}
|
|
93
|
+
<TextField active value={editBuffer} cursorPos={cursorPos} placeholder={option.command} />
|
|
96
94
|
</Form>
|
|
97
95
|
)
|
|
98
96
|
}
|
|
99
97
|
|
|
100
98
|
return (
|
|
101
99
|
<Picker
|
|
102
|
-
title="New tab
|
|
100
|
+
title="New tab"
|
|
103
101
|
keybindsModeId="modal.new-tab.command-edit"
|
|
104
102
|
width={uiTokens.modalWidth.md}
|
|
105
103
|
gap={1}
|
|
@@ -4,6 +4,7 @@ import type { ThemeId } from '../../../themes'
|
|
|
4
4
|
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { filterThemeIds, themeDisplayName } from '../../../filter-themes'
|
|
7
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
7
8
|
import { useMode, useTheme, useTransparent } from '../../../theme'
|
|
8
9
|
import { uiTokens } from '../../../ui-tokens'
|
|
9
10
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -27,6 +28,7 @@ export function ThemePickerModal({
|
|
|
27
28
|
selectedIndex,
|
|
28
29
|
}: ThemePickerModalProps) {
|
|
29
30
|
const t = useTheme()
|
|
31
|
+
const ink = useSelectionInk()
|
|
30
32
|
const transparent = useTransparent()
|
|
31
33
|
const mode = useMode()
|
|
32
34
|
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
@@ -48,11 +50,11 @@ export function ThemePickerModal({
|
|
|
48
50
|
dispatchGlobal({ type: 'close-modal' })
|
|
49
51
|
runSideEffectGlobal({ action: 'confirm', type: 'apply-theme' })
|
|
50
52
|
},
|
|
51
|
-
title: <text fg={active ?
|
|
52
|
-
trailing: isCurrent ? <text fg={t.primary}>current</text> : undefined,
|
|
53
|
+
title: <text fg={active ? ink : t.textMuted}>{themeDisplayName(id)}</text>,
|
|
54
|
+
trailing: isCurrent ? <text fg={active ? ink : t.primary}>current</text> : undefined,
|
|
53
55
|
}
|
|
54
56
|
}),
|
|
55
|
-
[currentThemeId, effectiveIndex, filtered, t]
|
|
57
|
+
[currentThemeId, effectiveIndex, filtered, ink, t]
|
|
56
58
|
)
|
|
57
59
|
|
|
58
60
|
const handleHover = useCallback(
|
|
@@ -68,13 +70,15 @@ export function ThemePickerModal({
|
|
|
68
70
|
filter={filter}
|
|
69
71
|
cursorPos={cursorPos}
|
|
70
72
|
footer={
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
<text fg={t.textMuted}>
|
|
74
|
+
{[
|
|
75
|
+
filtered.length === 0 ? '' : `${effectiveIndex + 1}/${filtered.length}`,
|
|
76
|
+
`transparent ${transparent ? 'on' : 'off'} (ctrl-t)`,
|
|
77
|
+
`mode ${mode} (ctrl-l)`,
|
|
78
|
+
]
|
|
79
|
+
.filter((part) => part !== '')
|
|
80
|
+
.join(' · ')}
|
|
81
|
+
</text>
|
|
78
82
|
}
|
|
79
83
|
items={items}
|
|
80
84
|
selectedIndex={effectiveIndex}
|
|
@@ -4,6 +4,7 @@ import type { WorkspaceRecord } from '../../../../state/types'
|
|
|
4
4
|
|
|
5
5
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { buildBaseRefOptions } from '../../../../state/selectors'
|
|
7
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
7
8
|
import { useTheme } from '../../../theme'
|
|
8
9
|
import { uiTokens } from '../../../ui-tokens'
|
|
9
10
|
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
@@ -35,6 +36,7 @@ export function CreateWorkspaceModal({
|
|
|
35
36
|
workspaces,
|
|
36
37
|
}: CreateWorkspaceModalProps) {
|
|
37
38
|
const t = useTheme()
|
|
39
|
+
const ink = useSelectionInk()
|
|
38
40
|
|
|
39
41
|
const handleHover = useCallback(
|
|
40
42
|
(index: number) => dispatchGlobal({ index, type: 'set-modal-selection-index' }),
|
|
@@ -45,24 +47,30 @@ export function CreateWorkspaceModal({
|
|
|
45
47
|
() =>
|
|
46
48
|
buildBaseRefOptions(workspaces, baseBranches, baseQuery).map((option) => ({
|
|
47
49
|
key: option.ref,
|
|
48
|
-
leading: (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
leading: (active) => {
|
|
51
|
+
let fg = t.textMuted
|
|
52
|
+
if (active) fg = ink
|
|
53
|
+
else if (option.kind === 'workspace') fg = t.warning
|
|
54
|
+
return <text fg={fg}>{option.kind === 'workspace' ? '\u{e728}' : '\u{e702}'}</text>
|
|
55
|
+
},
|
|
53
56
|
subtitle:
|
|
54
|
-
option.kind === 'workspace'
|
|
55
|
-
<text fg={t.textMuted}>workspace: {option.detail}</text>
|
|
56
|
-
|
|
57
|
-
title: (active) => <text fg={active ?
|
|
57
|
+
option.kind === 'workspace'
|
|
58
|
+
? (active) => <text fg={active ? ink : t.textMuted}>workspace: {option.detail}</text>
|
|
59
|
+
: null,
|
|
60
|
+
title: (active) => <text fg={active ? ink : t.textMuted}>{option.label}</text>,
|
|
58
61
|
})),
|
|
59
|
-
[baseBranches, baseQuery, t, workspaces]
|
|
62
|
+
[baseBranches, baseQuery, ink, t, workspaces]
|
|
60
63
|
)
|
|
61
64
|
|
|
62
65
|
const baseActive = activeField === 'base'
|
|
63
66
|
return (
|
|
67
|
+
// The title asks the question and the subtitle answers what happens with the
|
|
68
|
+
// answer, so the prompt field needs no label of its own — a heading, a
|
|
69
|
+
// question, a paragraph and a placeholder all saying the same thing was four
|
|
70
|
+
// lines of chrome above an empty box.
|
|
64
71
|
<Form
|
|
65
72
|
title="New workspace"
|
|
73
|
+
subtitle="What do you want to work on? Names the workspace and its branch, and is sent to the assistant."
|
|
66
74
|
keybindsModeId="modal.create-workspace"
|
|
67
75
|
width={uiTokens.modalWidth.xl}
|
|
68
76
|
>
|
|
@@ -70,11 +78,9 @@ export function CreateWorkspaceModal({
|
|
|
70
78
|
<box flexDirection="column">
|
|
71
79
|
<TextField
|
|
72
80
|
active={activeField === 'prompt'}
|
|
73
|
-
label="What do you want to work on? (optional)"
|
|
74
|
-
description="Sent to the assistant, and names the workspace and its branch. Leave empty for a bare workspace."
|
|
75
81
|
value={prompt}
|
|
76
82
|
cursorPos={activeField === 'prompt' ? cursorPos : undefined}
|
|
77
|
-
placeholder="Describe the task, or leave empty..."
|
|
83
|
+
placeholder="Describe the task, or leave empty for a bare workspace..."
|
|
78
84
|
minLines={PROMPT_LINES}
|
|
79
85
|
/>
|
|
80
86
|
{branchError != null && branchError !== '' ? (
|
|
@@ -94,7 +100,6 @@ export function CreateWorkspaceModal({
|
|
|
94
100
|
onHover={handleHover}
|
|
95
101
|
emptyState={<text fg={t.textMuted}>No branches found</text>}
|
|
96
102
|
/>
|
|
97
|
-
<text fg={t.textMuted}>Ctrl+Enter newline · Tab picks a base · Enter creates</text>
|
|
98
103
|
</box>
|
|
99
104
|
</Form>
|
|
100
105
|
)
|
|
@@ -4,6 +4,7 @@ import type { ModalWorkspaceMove, WorkspaceRecord } from '../../../../state/type
|
|
|
4
4
|
|
|
5
5
|
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { formatDivergence } from '../../../../state/project-workspaces'
|
|
7
|
+
import { useSelectionInk } from '../../../selection-ink'
|
|
7
8
|
import { useTheme } from '../../../theme'
|
|
8
9
|
import { uiTokens } from '../../../ui-tokens'
|
|
9
10
|
import { ListItem } from '../../primitives/list-item'
|
|
@@ -34,6 +35,7 @@ export function WorkspaceMoveModal({
|
|
|
34
35
|
workspaces,
|
|
35
36
|
}: WorkspaceMoveModalProps) {
|
|
36
37
|
const t = useTheme()
|
|
38
|
+
const ink = useSelectionInk()
|
|
37
39
|
const source = useMemo(
|
|
38
40
|
() => workspaces.find((w) => w.id === sourceWorkspaceId),
|
|
39
41
|
[sourceWorkspaceId, workspaces]
|
|
@@ -106,11 +108,11 @@ export function WorkspaceMoveModal({
|
|
|
106
108
|
onHoverIndex={handleSelectIndex}
|
|
107
109
|
onClickIndex={handleSelectIndex}
|
|
108
110
|
title={
|
|
109
|
-
<text fg={active ?
|
|
111
|
+
<text fg={active ? ink : t.textMuted} wrapMode="none">
|
|
110
112
|
{label}
|
|
111
113
|
{workspace.source === 'primary' ? ' (primary)' : ''}
|
|
112
114
|
{ahead !== '' ? ` ${ahead}` : ''}
|
|
113
|
-
{dirty ? <span fg={t.warning}> ●</span> : null}
|
|
115
|
+
{dirty ? <span fg={active ? ink : t.warning}> ●</span> : null}
|
|
114
116
|
</text>
|
|
115
117
|
}
|
|
116
118
|
/>
|