@brimveyn/aimux 1.5.0 → 1.6.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/README.md +14 -2
- package/package.json +5 -3
- package/src/app-runtime/side-effects.ts +41 -6
- package/src/app-runtime/use-renderer-bindings.ts +1 -1
- package/src/app.tsx +17 -5
- package/src/config.ts +29 -4
- package/src/diff-parser/clean-last-newline.ts +5 -0
- package/src/diff-parser/constants.ts +13 -0
- package/src/diff-parser/index.ts +12 -0
- package/src/diff-parser/parse-line-type.ts +29 -0
- package/src/diff-parser/parse-patch-files.ts +369 -0
- package/src/diff-parser/types.ts +75 -0
- package/src/index.tsx +2 -2
- package/src/input/modes/bridge.ts +8 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +4 -1
- package/src/pty/terminal-snapshot.ts +4 -4
- package/src/state/git-tree.ts +220 -0
- package/src/state/reducers/git-mode-state.ts +232 -35
- package/src/state/reducers/git-panel-state.ts +29 -3
- package/src/state/reducers/modal-state.ts +43 -10
- package/src/state/store.ts +5 -1
- package/src/state/types.ts +41 -5
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/create-session-modal.tsx +23 -7
- package/src/ui/components/diff-renderer/build-rows.ts +349 -0
- package/src/ui/components/diff-renderer/filetype.ts +29 -0
- package/src/ui/components/diff-renderer/fold-strip.tsx +84 -0
- package/src/ui/components/diff-renderer/highlight.ts +48 -0
- package/src/ui/components/diff-renderer/index.ts +1 -0
- package/src/ui/components/diff-renderer/pierre-diff.tsx +170 -0
- package/src/ui/components/diff-renderer/split-view.tsx +207 -0
- package/src/ui/components/diff-renderer/stacked-view.tsx +166 -0
- package/src/ui/components/git-commit-modal.tsx +14 -2
- package/src/ui/components/git-pane-widget.tsx +5 -0
- package/src/ui/components/git-panel.tsx +176 -79
- package/src/ui/components/git-view.tsx +89 -87
- package/src/ui/components/help-modal.tsx +43 -11
- package/src/ui/components/input-field.tsx +2 -2
- package/src/ui/components/list-item.tsx +9 -1
- package/src/ui/components/modal-filter-bar.tsx +1 -1
- package/src/ui/components/modal-keybinds-overlay.tsx +2 -2
- package/src/ui/components/modal-shell.tsx +3 -3
- package/src/ui/components/new-tab-modal.tsx +15 -3
- package/src/ui/components/pending-chord-overlay.tsx +3 -3
- package/src/ui/components/session-bar.tsx +10 -5
- package/src/ui/components/session-picker-modal.tsx +26 -9
- package/src/ui/components/sidebar.tsx +22 -10
- package/src/ui/components/snippet-editor-modal.tsx +16 -2
- package/src/ui/components/snippet-picker-modal.tsx +11 -3
- package/src/ui/components/split-layout.tsx +1 -1
- package/src/ui/components/status-bar.tsx +12 -9
- package/src/ui/components/surface.tsx +5 -5
- package/src/ui/components/tab-item.tsx +22 -16
- package/src/ui/components/terminal-pane.tsx +25 -15
- package/src/ui/components/theme-picker-modal.tsx +111 -16
- package/src/ui/components/update-available-modal.tsx +11 -1
- package/src/ui/filter-themes.ts +10 -0
- package/src/ui/house-themes.ts +313 -0
- package/src/ui/keymap-context.ts +10 -2
- package/src/ui/root.tsx +26 -6
- package/src/ui/shiki.ts +49 -0
- package/src/ui/status-bar-model.ts +19 -1
- package/src/ui/theme.ts +22 -10
- package/src/ui/themes.generated.ts +59794 -0
- package/src/ui/themes.ts +68 -274
- package/src/ui/syntax.ts +0 -102
|
@@ -2,6 +2,7 @@ import type { SessionRecord } from '../../state/types'
|
|
|
2
2
|
|
|
3
3
|
import { filterSessions } from '../../state/selectors'
|
|
4
4
|
import { abbreviatePath } from '../path-format'
|
|
5
|
+
import { orderSessionsForDisplay } from '../session-ordering'
|
|
5
6
|
import { theme } from '../theme'
|
|
6
7
|
import { uiTokens } from '../ui-tokens'
|
|
7
8
|
import { ListItem } from './list-item'
|
|
@@ -19,13 +20,14 @@ interface SessionPickerModalProps {
|
|
|
19
20
|
function formatSessionLine(
|
|
20
21
|
session: SessionRecord,
|
|
21
22
|
currentSessionId: string | null,
|
|
22
|
-
currentTabCount: number
|
|
23
|
+
currentTabCount: number,
|
|
24
|
+
displayIndex: number
|
|
23
25
|
): string {
|
|
24
26
|
const tabCount =
|
|
25
27
|
session.id === currentSessionId
|
|
26
28
|
? currentTabCount
|
|
27
29
|
: (session.workspaceSnapshot?.tabs.length ?? 0)
|
|
28
|
-
return
|
|
30
|
+
return `[${displayIndex}] ${session.name} (${tabCount} tab${tabCount === 1 ? '' : 's'})`
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function getEmptyStateMessage(hasFilter: boolean): string {
|
|
@@ -43,7 +45,9 @@ export function SessionPickerModal({
|
|
|
43
45
|
selectedIndex,
|
|
44
46
|
sessions,
|
|
45
47
|
}: SessionPickerModalProps) {
|
|
46
|
-
const
|
|
48
|
+
const ordered = orderSessionsForDisplay(sessions)
|
|
49
|
+
const baselineOrder = ordered.map((s) => s.id)
|
|
50
|
+
const filtered = filterSessions(ordered, filter)
|
|
47
51
|
const hasFilter = !!filter
|
|
48
52
|
const showFilteredEmptyState = filtered.length === 0 && sessions.length > 0
|
|
49
53
|
const showInitialEmptyState = filtered.length === 0 && sessions.length === 0
|
|
@@ -56,25 +60,32 @@ export function SessionPickerModal({
|
|
|
56
60
|
footer={<ModalFilterBar filter={filter} />}
|
|
57
61
|
>
|
|
58
62
|
{showFilteredEmptyState ? (
|
|
59
|
-
<text fg={theme.
|
|
63
|
+
<text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(hasFilter)}</text>
|
|
60
64
|
) : null}
|
|
61
65
|
{showInitialEmptyState ? (
|
|
62
|
-
<text fg={theme.
|
|
66
|
+
<text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(false)}</text>
|
|
63
67
|
) : null}
|
|
64
68
|
{filtered.map((session, index) => {
|
|
65
69
|
const active = index === selectedIndex
|
|
70
|
+
const displayIndex = baselineOrder.indexOf(session.id) + 1
|
|
66
71
|
return (
|
|
67
72
|
<ListItem
|
|
68
73
|
key={session.id}
|
|
69
74
|
active={active}
|
|
70
75
|
title={
|
|
71
|
-
<text
|
|
72
|
-
{
|
|
76
|
+
<text
|
|
77
|
+
fg={
|
|
78
|
+
active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
73
82
|
</text>
|
|
74
83
|
}
|
|
75
84
|
subtitle={
|
|
76
85
|
session.projectPath ? (
|
|
77
|
-
<text fg={theme.
|
|
86
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
87
|
+
{abbreviatePath(session.projectPath)}
|
|
88
|
+
</text>
|
|
78
89
|
) : undefined
|
|
79
90
|
}
|
|
80
91
|
/>
|
|
@@ -83,7 +94,13 @@ export function SessionPickerModal({
|
|
|
83
94
|
<ListItem
|
|
84
95
|
active={selectedIndex === filtered.length}
|
|
85
96
|
title={
|
|
86
|
-
<text
|
|
97
|
+
<text
|
|
98
|
+
fg={
|
|
99
|
+
selectedIndex === filtered.length
|
|
100
|
+
? theme.colors['editor.foreground']
|
|
101
|
+
: theme.colors['descriptionForeground']
|
|
102
|
+
}
|
|
103
|
+
>
|
|
87
104
|
Create new session
|
|
88
105
|
</text>
|
|
89
106
|
}
|
|
@@ -31,19 +31,21 @@ const SidebarTop = memo(function SidebarTop() {
|
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<box flexDirection="column" flexShrink={0} gap={0}>
|
|
34
|
-
<text fg={theme.
|
|
34
|
+
<text fg={theme.colors['textLink.foreground']}>
|
|
35
35
|
<strong>aimux</strong>
|
|
36
36
|
</text>
|
|
37
|
-
<text fg={theme.
|
|
37
|
+
<text fg={theme.colors['terminal.ansiMagenta']}>
|
|
38
38
|
{currentSession ? currentSession.name : 'No session selected'}
|
|
39
39
|
</text>
|
|
40
40
|
{branch ? (
|
|
41
41
|
<box flexDirection="row">
|
|
42
|
-
<text fg={theme.
|
|
43
|
-
<text fg={theme.
|
|
42
|
+
<text fg={theme.colors['textLink.foreground']}>{'\u{e702}'} </text>
|
|
43
|
+
<text fg={theme.colors['descriptionForeground']}>{branch}</text>
|
|
44
44
|
</box>
|
|
45
45
|
) : null}
|
|
46
|
-
<text fg={theme.
|
|
46
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
47
|
+
{'·'.repeat(Math.max(0, sidebarWidth - 2))}
|
|
48
|
+
</text>
|
|
47
49
|
</box>
|
|
48
50
|
)
|
|
49
51
|
})
|
|
@@ -56,11 +58,17 @@ function renderGroupGutter(
|
|
|
56
58
|
) {
|
|
57
59
|
return (
|
|
58
60
|
<box flexDirection="column" width={1} overflow="hidden">
|
|
59
|
-
<text
|
|
61
|
+
<text
|
|
62
|
+
fg={theme.colors['terminal.ansiMagenta']}
|
|
63
|
+
bg={isActive ? theme.colors['list.activeSelectionBackground'] : undefined}
|
|
64
|
+
>
|
|
60
65
|
{/* oxlint-disable-next-line no-nested-ternary */}
|
|
61
66
|
{isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
|
|
62
67
|
</text>
|
|
63
|
-
<text
|
|
68
|
+
<text
|
|
69
|
+
fg={theme.colors['terminal.ansiMagenta']}
|
|
70
|
+
bg={isActive ? theme.colors['list.activeSelectionBackground'] : undefined}
|
|
71
|
+
>
|
|
64
72
|
{isGroupEnd ? GUTTER_END : GUTTER_PAD}
|
|
65
73
|
</text>
|
|
66
74
|
</box>
|
|
@@ -101,7 +109,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
101
109
|
>
|
|
102
110
|
{tabs.length === 0 ? (
|
|
103
111
|
<box paddingTop={1}>
|
|
104
|
-
<text fg={theme.
|
|
112
|
+
<text fg={theme.colors['descriptionForeground']}>No tabs yet. Press Ctrl+n.</text>
|
|
105
113
|
</box>
|
|
106
114
|
) : (
|
|
107
115
|
tabs.map((tab, index) => {
|
|
@@ -157,7 +165,11 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
|
|
|
157
165
|
const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.ratio) * 100)) : 1
|
|
158
166
|
const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.ratio * 100)) : 0
|
|
159
167
|
|
|
160
|
-
const separator =
|
|
168
|
+
const separator = (
|
|
169
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
170
|
+
{'·'.repeat(Math.max(0, sidebarWidth - 2))}
|
|
171
|
+
</text>
|
|
172
|
+
)
|
|
161
173
|
const gitBody = gitEmbedded ? (
|
|
162
174
|
<box flexDirection="column" flexGrow={gitGrow} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
163
175
|
<GitPaneWidget pollingEnabled={gitPane.visible} />
|
|
@@ -169,7 +181,7 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
|
|
|
169
181
|
width={sidebarWidth}
|
|
170
182
|
padding={0}
|
|
171
183
|
flexDirection="column"
|
|
172
|
-
backgroundColor={theme.
|
|
184
|
+
backgroundColor={theme.colors['sideBar.background']}
|
|
173
185
|
gap={0}
|
|
174
186
|
>
|
|
175
187
|
<SidebarTop />
|
|
@@ -26,12 +26,26 @@ export function SnippetEditorModal({
|
|
|
26
26
|
width={uiTokens.modalWidth.xl}
|
|
27
27
|
>
|
|
28
28
|
<box flexDirection="column">
|
|
29
|
-
<text
|
|
29
|
+
<text
|
|
30
|
+
fg={
|
|
31
|
+
nameActive ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
Name
|
|
35
|
+
</text>
|
|
30
36
|
<InputField active={nameActive} value={snippetName} />
|
|
31
37
|
</box>
|
|
32
38
|
|
|
33
39
|
<box flexDirection="column">
|
|
34
|
-
<text
|
|
40
|
+
<text
|
|
41
|
+
fg={
|
|
42
|
+
contentActive
|
|
43
|
+
? theme.colors['editor.foreground']
|
|
44
|
+
: theme.colors['descriptionForeground']
|
|
45
|
+
}
|
|
46
|
+
>
|
|
47
|
+
Content
|
|
48
|
+
</text>
|
|
35
49
|
<InputField active={contentActive} value={snippetContent} />
|
|
36
50
|
</box>
|
|
37
51
|
</ModalShell>
|
|
@@ -32,7 +32,7 @@ export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetP
|
|
|
32
32
|
footer={<ModalFilterBar filter={filter} />}
|
|
33
33
|
>
|
|
34
34
|
{filtered.length === 0 ? (
|
|
35
|
-
<text fg={theme.
|
|
35
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
36
36
|
{filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
|
|
37
37
|
</text>
|
|
38
38
|
) : null}
|
|
@@ -43,11 +43,19 @@ export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetP
|
|
|
43
43
|
key={snippet.id}
|
|
44
44
|
active={active}
|
|
45
45
|
title={
|
|
46
|
-
<text
|
|
46
|
+
<text
|
|
47
|
+
fg={
|
|
48
|
+
active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
49
|
+
}
|
|
50
|
+
>
|
|
47
51
|
<strong>{snippet.name}</strong>
|
|
48
52
|
</text>
|
|
49
53
|
}
|
|
50
|
-
subtitle={
|
|
54
|
+
subtitle={
|
|
55
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
56
|
+
{truncateContent(snippet.content)}
|
|
57
|
+
</text>
|
|
58
|
+
}
|
|
51
59
|
/>
|
|
52
60
|
)
|
|
53
61
|
})}
|
|
@@ -134,7 +134,7 @@ export function SplitLayout({
|
|
|
134
134
|
<box
|
|
135
135
|
minWidth={node.direction === 'vertical' ? 1 : undefined}
|
|
136
136
|
minHeight={node.direction === 'horizontal' ? 1 : undefined}
|
|
137
|
-
backgroundColor={theme.border}
|
|
137
|
+
backgroundColor={theme.colors['editorGroup.border']}
|
|
138
138
|
onMouseDown={(e: OtuiMouseEvent) => {
|
|
139
139
|
e.preventDefault()
|
|
140
140
|
if (!onSeparatorDragStart) return
|
|
@@ -9,16 +9,16 @@ import { theme } from '../theme'
|
|
|
9
9
|
function getModeColor(focusMode: AppState['focusMode']): string {
|
|
10
10
|
switch (focusMode) {
|
|
11
11
|
case 'terminal-input':
|
|
12
|
-
return theme.
|
|
12
|
+
return theme.colors['textLink.foreground']
|
|
13
13
|
case 'modal':
|
|
14
|
-
return theme.
|
|
14
|
+
return theme.colors['editorWarning.foreground']
|
|
15
15
|
case 'command-edit':
|
|
16
|
-
return theme.
|
|
16
|
+
return theme.colors['editorWarning.foreground']
|
|
17
17
|
case 'git':
|
|
18
|
-
return theme.
|
|
18
|
+
return theme.colors['gitDecoration.addedResourceForeground']
|
|
19
19
|
case 'navigation':
|
|
20
20
|
default:
|
|
21
|
-
return theme.
|
|
21
|
+
return theme.colors['terminal.ansiMagenta']
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -52,16 +52,19 @@ export function StatusBar() {
|
|
|
52
52
|
paddingTop={0}
|
|
53
53
|
paddingBottom={0}
|
|
54
54
|
flexDirection="column"
|
|
55
|
-
backgroundColor={theme.
|
|
55
|
+
backgroundColor={theme.colors['sideBarSectionHeader.background']}
|
|
56
56
|
>
|
|
57
57
|
<box width="100%" flexDirection="row">
|
|
58
58
|
<text fg={getModeColor(state.focusMode)}>[{getModeLabel(state.focusMode)}]</text>
|
|
59
59
|
<text> </text>
|
|
60
|
-
<text fg={theme.
|
|
60
|
+
<text fg={theme.colors['editor.foreground']}>{model.left}</text>
|
|
61
61
|
</box>
|
|
62
62
|
<box width="100%" flexDirection="row" justifyContent="space-between">
|
|
63
|
-
<text fg={theme.
|
|
64
|
-
<
|
|
63
|
+
<text fg={theme.colors['descriptionForeground']}>{model.right}</text>
|
|
64
|
+
<box flexDirection="row" gap={2}>
|
|
65
|
+
{model.help ? <text fg={theme.colors['descriptionForeground']}>{model.help}</text> : null}
|
|
66
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>v{APP_VERSION}</text>
|
|
67
|
+
</box>
|
|
65
68
|
</box>
|
|
66
69
|
</box>
|
|
67
70
|
)
|
|
@@ -7,16 +7,16 @@ type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
|
|
|
7
7
|
function getSurfaceColor(tone: SurfaceTone): string {
|
|
8
8
|
switch (tone) {
|
|
9
9
|
case 'elevated':
|
|
10
|
-
return theme.
|
|
10
|
+
return theme.colors['sideBar.background']
|
|
11
11
|
case 'selected':
|
|
12
|
-
return theme.
|
|
12
|
+
return theme.colors['list.activeSelectionBackground']
|
|
13
13
|
case 'input':
|
|
14
|
-
return theme.background
|
|
14
|
+
return theme.colors['editor.background']
|
|
15
15
|
case 'inputActive':
|
|
16
|
-
return theme.
|
|
16
|
+
return theme.colors['list.activeSelectionBackground']
|
|
17
17
|
case 'muted':
|
|
18
18
|
default:
|
|
19
|
-
return theme.
|
|
19
|
+
return theme.colors['sideBarSectionHeader.background']
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -15,15 +15,15 @@ interface TabItemProps {
|
|
|
15
15
|
function getStatusColor(status: TabSession['status']): string {
|
|
16
16
|
switch (status) {
|
|
17
17
|
case 'running':
|
|
18
|
-
return theme.
|
|
18
|
+
return theme.colors['gitDecoration.addedResourceForeground']
|
|
19
19
|
case 'disconnected':
|
|
20
|
-
return theme.
|
|
20
|
+
return theme.colors['editorWarning.foreground']
|
|
21
21
|
case 'error':
|
|
22
|
-
return theme.
|
|
22
|
+
return theme.colors['editorError.foreground']
|
|
23
23
|
case 'exited':
|
|
24
|
-
return theme.
|
|
24
|
+
return theme.colors['editorWarning.foreground']
|
|
25
25
|
default:
|
|
26
|
-
return theme.
|
|
26
|
+
return theme.colors['descriptionForeground']
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -37,32 +37,34 @@ function getIndicator(active: boolean, focused: boolean, inLayout: boolean): str
|
|
|
37
37
|
|
|
38
38
|
function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean): string {
|
|
39
39
|
if (active) {
|
|
40
|
-
return focused ? theme.
|
|
40
|
+
return focused ? theme.colors['textLink.foreground'] : theme.colors['terminal.ansiMagenta']
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
return inLayout
|
|
43
|
+
return inLayout
|
|
44
|
+
? theme.colors['descriptionForeground']
|
|
45
|
+
: theme.colors['editor.lineHighlightBackground']
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
function BusyIndicator() {
|
|
47
49
|
const frame = useBusySpinner()
|
|
48
|
-
return <text fg={theme.
|
|
50
|
+
return <text fg={theme.colors['textLink.foreground']}>{frame} busy</text>
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocusedInput: boolean }) {
|
|
52
54
|
if (tab.status === 'error') {
|
|
53
|
-
return <text fg={theme.
|
|
55
|
+
return <text fg={theme.colors['editorError.foreground']}>✗ error</text>
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (tab.status === 'disconnected') {
|
|
57
|
-
return <text fg={theme.
|
|
59
|
+
return <text fg={theme.colors['editorWarning.foreground']}>⏸ restore</text>
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
if (tab.status === 'exited') {
|
|
61
|
-
return <text fg={theme.
|
|
63
|
+
return <text fg={theme.colors['editorWarning.foreground']}>⏹ exited</text>
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
if (isFocusedInput) {
|
|
65
|
-
return <text fg={theme.
|
|
67
|
+
return <text fg={theme.colors['focusBorder']}>▸ focused</text>
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
if (tab.activity === 'busy') {
|
|
@@ -70,7 +72,7 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
|
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
if (tab.activity === 'idle') {
|
|
73
|
-
return <text fg={theme.
|
|
75
|
+
return <text fg={theme.colors['gitDecoration.addedResourceForeground']}>● idle</text>
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
return <text fg={getStatusColor(tab.status)}>{tab.status}</text>
|
|
@@ -89,16 +91,20 @@ export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }:
|
|
|
89
91
|
paddingRight={1}
|
|
90
92
|
paddingTop={0}
|
|
91
93
|
paddingBottom={0}
|
|
92
|
-
backgroundColor={active ? theme.
|
|
94
|
+
backgroundColor={active ? theme.colors['list.activeSelectionBackground'] : undefined}
|
|
93
95
|
flexDirection="column"
|
|
94
96
|
gap={0}
|
|
95
97
|
>
|
|
96
98
|
<box flexDirection="row">
|
|
97
99
|
<text fg={indicatorColor}>{indicator} </text>
|
|
98
|
-
<text
|
|
100
|
+
<text
|
|
101
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
102
|
+
>
|
|
103
|
+
{tab.title}
|
|
104
|
+
</text>
|
|
99
105
|
</box>
|
|
100
106
|
<box flexDirection="row">
|
|
101
|
-
<text fg={theme.
|
|
107
|
+
<text fg={theme.colors['descriptionForeground']}> {label} </text>
|
|
102
108
|
<ActivityIndicator tab={tab} isFocusedInput={isFocusedInput} />
|
|
103
109
|
</box>
|
|
104
110
|
</box>
|
|
@@ -46,14 +46,14 @@ function getTitle(
|
|
|
46
46
|
|
|
47
47
|
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
48
48
|
if (isActive && focusMode === 'terminal-input') {
|
|
49
|
-
return theme.
|
|
49
|
+
return theme.colors['focusBorder']
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (isActive) {
|
|
53
|
-
return theme.
|
|
53
|
+
return theme.colors['terminal.ansiMagenta']
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return theme.
|
|
56
|
+
return theme.colors['editor.lineHighlightBackground']
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
@@ -72,7 +72,7 @@ function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
|
-
<span key={key} fg={span.fg ?? theme.
|
|
75
|
+
<span key={key} fg={span.fg ?? theme.colors['editor.foreground']} bg={span.bg}>
|
|
76
76
|
{node}
|
|
77
77
|
</span>
|
|
78
78
|
)
|
|
@@ -90,7 +90,7 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
90
90
|
if (viewport && viewport.lines.length > 0) {
|
|
91
91
|
const lines = viewport.lines
|
|
92
92
|
return (
|
|
93
|
-
<text fg={theme.
|
|
93
|
+
<text fg={theme.colors['editor.foreground']}>
|
|
94
94
|
{lines.map((line, lineIndex) => (
|
|
95
95
|
<span key={`line-${lineIndex}`}>
|
|
96
96
|
{line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
|
|
@@ -101,7 +101,11 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
101
101
|
)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
return
|
|
104
|
+
return (
|
|
105
|
+
<text fg={theme.colors['editor.foreground']}>
|
|
106
|
+
{buffer.length > 0 ? buffer : 'Waiting for session output...'}
|
|
107
|
+
</text>
|
|
108
|
+
)
|
|
105
109
|
})
|
|
106
110
|
|
|
107
111
|
export function TerminalPane({
|
|
@@ -179,19 +183,19 @@ export function TerminalPane({
|
|
|
179
183
|
padding={0}
|
|
180
184
|
flexDirection="column"
|
|
181
185
|
flexGrow={1}
|
|
182
|
-
backgroundColor={theme.background}
|
|
186
|
+
backgroundColor={theme.colors['editor.background']}
|
|
183
187
|
onMouseDrag={forwardMouseEvent}
|
|
184
188
|
onMouseScroll={forwardScrollEvent}
|
|
185
189
|
onMouseUp={forwardMouseEvent}
|
|
186
190
|
>
|
|
187
191
|
{!tab ? (
|
|
188
192
|
<box flexGrow={1} justifyContent="center" alignItems="center" flexDirection="column">
|
|
189
|
-
<text fg={theme.
|
|
190
|
-
<text fg={theme.
|
|
193
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>· · ·</text>
|
|
194
|
+
<text fg={theme.colors['descriptionForeground']}> </text>
|
|
191
195
|
<box flexDirection="row">
|
|
192
|
-
<text fg={theme.
|
|
193
|
-
<text fg={theme.
|
|
194
|
-
<text fg={theme.
|
|
196
|
+
<text fg={theme.colors['descriptionForeground']}>Press </text>
|
|
197
|
+
<text fg={theme.colors['textLink.foreground']}>Ctrl+n</text>
|
|
198
|
+
<text fg={theme.colors['descriptionForeground']}> to launch an assistant</text>
|
|
195
199
|
</box>
|
|
196
200
|
</box>
|
|
197
201
|
) : (
|
|
@@ -209,12 +213,18 @@ export function TerminalPane({
|
|
|
209
213
|
)}
|
|
210
214
|
</box>
|
|
211
215
|
{tab?.status === 'exited' && tab.exitCode !== undefined ? (
|
|
212
|
-
<text fg={theme.
|
|
216
|
+
<text fg={theme.colors['editorWarning.foreground']}>
|
|
217
|
+
Process exited with code {tab.exitCode}
|
|
218
|
+
</text>
|
|
213
219
|
) : null}
|
|
214
220
|
{tab?.status === 'disconnected' ? (
|
|
215
|
-
<text fg={theme.
|
|
221
|
+
<text fg={theme.colors['editorWarning.foreground']}>
|
|
222
|
+
Restored snapshot. Press Ctrl+r to restart this session.
|
|
223
|
+
</text>
|
|
224
|
+
) : null}
|
|
225
|
+
{tab?.errorMessage ? (
|
|
226
|
+
<text fg={theme.colors['editorError.foreground']}>{tab.errorMessage}</text>
|
|
216
227
|
) : null}
|
|
217
|
-
{tab?.errorMessage ? <text fg={theme.danger}>{tab.errorMessage}</text> : null}
|
|
218
228
|
</box>
|
|
219
229
|
)
|
|
220
230
|
}
|
|
@@ -1,35 +1,130 @@
|
|
|
1
|
+
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
5
|
+
import { filterThemeIds } from '../filter-themes'
|
|
1
6
|
import { theme } from '../theme'
|
|
2
|
-
import {
|
|
7
|
+
import { type ThemeId, THEMES } from '../themes'
|
|
3
8
|
import { uiTokens } from '../ui-tokens'
|
|
4
9
|
import { ListItem } from './list-item'
|
|
10
|
+
import { ModalFilterBar } from './modal-filter-bar'
|
|
5
11
|
import { ModalShell } from './modal-shell'
|
|
6
12
|
|
|
7
13
|
interface ThemePickerModalProps {
|
|
8
|
-
selectedIndex: number
|
|
9
14
|
currentThemeId: ThemeId
|
|
15
|
+
filter: string | null
|
|
16
|
+
selectedIndex: number
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
|
|
19
|
+
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
20
|
+
const MODAL_CHROME_ROWS = 6
|
|
21
|
+
|
|
22
|
+
function clampSelection(index: number, count: number): number {
|
|
23
|
+
if (count === 0) return 0
|
|
24
|
+
return Math.max(0, Math.min(count - 1, index))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function computeWindowStart(
|
|
28
|
+
prevStart: number,
|
|
29
|
+
selectedRowIndex: number,
|
|
30
|
+
total: number,
|
|
31
|
+
windowSize: number
|
|
32
|
+
): number {
|
|
33
|
+
if (total <= windowSize) return 0
|
|
34
|
+
const margin = 1
|
|
35
|
+
const maxStart = total - windowSize
|
|
36
|
+
let start = Math.max(0, Math.min(maxStart, prevStart))
|
|
37
|
+
const topThreshold = start + margin
|
|
38
|
+
const bottomThreshold = start + windowSize - 1 - margin
|
|
39
|
+
if (selectedRowIndex < topThreshold) {
|
|
40
|
+
start = Math.max(0, selectedRowIndex - margin)
|
|
41
|
+
} else if (selectedRowIndex > bottomThreshold) {
|
|
42
|
+
start = Math.min(maxStart, selectedRowIndex - windowSize + 1 + margin)
|
|
43
|
+
}
|
|
44
|
+
return start
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ThemePickerModal({ currentThemeId, filter, selectedIndex }: ThemePickerModalProps) {
|
|
48
|
+
const dimensions = useTerminalDimensions()
|
|
49
|
+
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
50
|
+
|
|
51
|
+
useLayoutEffect(() => {
|
|
52
|
+
dispatchGlobal({ count: filtered.length, type: 'set-theme-entry-count' })
|
|
53
|
+
}, [filtered.length])
|
|
54
|
+
|
|
55
|
+
const effectiveIndex = clampSelection(selectedIndex, filtered.length)
|
|
56
|
+
const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
|
|
57
|
+
const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
|
|
58
|
+
|
|
59
|
+
const prevStartRef = useRef(0)
|
|
60
|
+
const prevFilterRef = useRef<string | null>(filter)
|
|
61
|
+
if (prevFilterRef.current !== filter) {
|
|
62
|
+
prevFilterRef.current = filter
|
|
63
|
+
prevStartRef.current = 0
|
|
64
|
+
}
|
|
65
|
+
const start = computeWindowStart(
|
|
66
|
+
prevStartRef.current,
|
|
67
|
+
effectiveIndex,
|
|
68
|
+
filtered.length,
|
|
69
|
+
listHeight
|
|
70
|
+
)
|
|
71
|
+
prevStartRef.current = start
|
|
72
|
+
const visible = filtered.slice(start, start + listHeight)
|
|
73
|
+
|
|
13
74
|
return (
|
|
14
75
|
<ModalShell
|
|
15
76
|
title="Select theme"
|
|
16
77
|
keybindsModeId="modal.theme-picker"
|
|
17
78
|
width={uiTokens.modalWidth.md}
|
|
18
79
|
listGap={0}
|
|
80
|
+
footer={
|
|
81
|
+
<box flexDirection="column" gap={0}>
|
|
82
|
+
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
83
|
+
{filtered.length === 0
|
|
84
|
+
? ''
|
|
85
|
+
: ` ${effectiveIndex + 1} / ${filtered.length}${filter ? '' : ' — type / to filter'}`}
|
|
86
|
+
</text>
|
|
87
|
+
<ModalFilterBar filter={filter} />
|
|
88
|
+
</box>
|
|
89
|
+
}
|
|
19
90
|
>
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
91
|
+
{filtered.length === 0 ? (
|
|
92
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
93
|
+
{filter ? 'No matching themes.' : 'No themes available.'}
|
|
94
|
+
</text>
|
|
95
|
+
) : (
|
|
96
|
+
<box height={listHeight} flexDirection="column" overflow="hidden">
|
|
97
|
+
{visible.map((id, i) => {
|
|
98
|
+
const rowIndex = start + i
|
|
99
|
+
const entry = THEMES[id]
|
|
100
|
+
if (!entry) return null
|
|
101
|
+
const active = rowIndex === effectiveIndex
|
|
102
|
+
const isCurrent = id === currentThemeId
|
|
103
|
+
return (
|
|
104
|
+
<ListItem
|
|
105
|
+
key={id}
|
|
106
|
+
active={active}
|
|
107
|
+
title={
|
|
108
|
+
<text
|
|
109
|
+
fg={
|
|
110
|
+
active
|
|
111
|
+
? theme.colors['editor.foreground']
|
|
112
|
+
: theme.colors['descriptionForeground']
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
{entry.name}
|
|
116
|
+
</text>
|
|
117
|
+
}
|
|
118
|
+
trailing={
|
|
119
|
+
isCurrent ? (
|
|
120
|
+
<text fg={theme.colors['textLink.foreground']}>current</text>
|
|
121
|
+
) : undefined
|
|
122
|
+
}
|
|
123
|
+
/>
|
|
124
|
+
)
|
|
125
|
+
})}
|
|
126
|
+
</box>
|
|
127
|
+
)}
|
|
33
128
|
</ModalShell>
|
|
34
129
|
)
|
|
35
130
|
}
|
|
@@ -35,7 +35,17 @@ export function UpdateAvailableModal({
|
|
|
35
35
|
key={option.label}
|
|
36
36
|
active={active}
|
|
37
37
|
direction="row"
|
|
38
|
-
title={
|
|
38
|
+
title={
|
|
39
|
+
<text
|
|
40
|
+
fg={
|
|
41
|
+
active
|
|
42
|
+
? theme.colors['editor.foreground']
|
|
43
|
+
: theme.colors['descriptionForeground']
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
{option.label}
|
|
47
|
+
</text>
|
|
48
|
+
}
|
|
39
49
|
/>
|
|
40
50
|
)
|
|
41
51
|
})}
|