@brimveyn/aimux 1.6.1 → 1.7.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 +3 -0
- package/package.json +2 -2
- package/src/app-runtime/backend-attach-runtime.ts +6 -0
- package/src/app-runtime/backend-runtime-events.ts +21 -21
- package/src/app-runtime/side-effects.ts +43 -12
- package/src/app-runtime/use-backend-runtime.ts +2 -20
- package/src/app-runtime/use-directory-search.ts +6 -1
- package/src/app.tsx +6 -1
- package/src/config.ts +28 -1
- package/src/daemon/daemon.ts +197 -15
- package/src/daemon/session-manager.ts +9 -0
- package/src/daemon/session-registry.ts +5 -5
- package/src/git/diff-hash.ts +10 -0
- package/src/index.tsx +10 -2
- package/src/input/keymap/help-entries.ts +5 -5
- package/src/input/modes/bridge.ts +5 -8
- package/src/input/modes/transitions.ts +15 -23
- package/src/input/modes/types.ts +3 -5
- package/src/ipc/protocol.ts +49 -5
- package/src/platform/project-search.ts +45 -12
- package/src/pty/assistant-status-detection-loop.ts +192 -0
- package/src/pty/assistant-status-detector.ts +226 -0
- package/src/pty/pty-manager.ts +3 -37
- package/src/session-backend/bootstrap.ts +4 -1
- package/src/session-backend/local-session-backend.ts +43 -56
- package/src/session-backend/remote-session-backend.ts +15 -0
- package/src/session-backend/types.ts +10 -1
- package/src/state/git-tree.ts +42 -16
- package/src/state/reducers/diff-cache.ts +64 -0
- package/src/state/reducers/git-mode-state.ts +91 -33
- package/src/state/reducers/git-panel-state.ts +33 -2
- package/src/state/reducers/modal-state.ts +91 -134
- package/src/state/reducers/session-state.ts +13 -6
- package/src/state/reducers/tab-state.ts +0 -10
- package/src/state/selectors.ts +12 -0
- package/src/state/session-persistence.ts +20 -15
- package/src/state/store.ts +13 -3
- package/src/state/types.ts +87 -14
- package/src/ui/breaking-update-screen.tsx +31 -0
- package/src/ui/components/bare-input.tsx +44 -0
- package/src/ui/components/create-session-modal.tsx +16 -8
- package/src/ui/components/diff-renderer/fold-strip.tsx +5 -13
- package/src/ui/components/diff-renderer/pierre-diff.tsx +9 -31
- package/src/ui/components/diff-renderer/prepare-diff.ts +66 -0
- package/src/ui/components/diff-renderer/split-view.tsx +35 -16
- package/src/ui/components/diff-renderer/stacked-view.tsx +30 -12
- package/src/ui/components/diff-renderer/use-diff-prefetch.ts +191 -0
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +106 -0
- package/src/ui/components/git-pane-widget.tsx +2 -0
- package/src/ui/components/git-panel.tsx +27 -10
- package/src/ui/components/git-view.tsx +23 -9
- package/src/ui/components/help-modal.tsx +45 -160
- package/src/ui/components/input-field.tsx +6 -2
- package/src/ui/components/list-item.tsx +36 -28
- package/src/ui/components/modal-shell.tsx +51 -13
- package/src/ui/components/new-tab-modal.tsx +78 -45
- package/src/ui/components/picker.tsx +179 -0
- package/src/ui/components/session-bar.tsx +47 -21
- package/src/ui/components/session-picker-modal.tsx +58 -56
- package/src/ui/components/sidebar.tsx +49 -22
- package/src/ui/components/snippet-picker-modal.tsx +43 -34
- package/src/ui/components/status-bar.tsx +3 -2
- package/src/ui/components/surface.tsx +11 -8
- package/src/ui/components/tab-item.tsx +38 -22
- package/src/ui/components/terminal-pane.tsx +8 -8
- package/src/ui/components/theme-picker-modal.tsx +51 -91
- package/src/ui/root.tsx +14 -23
- package/src/ui/status-bar-model.ts +5 -5
- package/src/ui/theme-store.ts +26 -2
- package/src/ui/theme.ts +9 -1
- package/src/ui/components/modal-filter-bar.tsx +0 -19
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Theme } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
+
import { type ScrollBoxRenderable } from '@opentui/core'
|
|
3
4
|
import { memo, useMemo, useRef } from 'react'
|
|
4
5
|
|
|
5
6
|
import { useAppStore } from '../../state/app-store'
|
|
6
|
-
import {
|
|
7
|
+
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
8
|
+
import { getCurrentTheme, useBg, useTheme } from '../theme'
|
|
7
9
|
import { GitPaneWidget } from './git-pane-widget'
|
|
8
10
|
import { buildTabGroupInfo } from './sidebar-group-metadata'
|
|
9
11
|
import { TabItem } from './tab-item'
|
|
@@ -19,6 +21,20 @@ const GUTTER_MIDDLE = '├'
|
|
|
19
21
|
const GUTTER_END = '╰'
|
|
20
22
|
const GUTTER_PAD = '│'
|
|
21
23
|
|
|
24
|
+
function getRowBackground({
|
|
25
|
+
alternate,
|
|
26
|
+
isActive,
|
|
27
|
+
theme,
|
|
28
|
+
}: {
|
|
29
|
+
isActive: boolean
|
|
30
|
+
alternate: boolean
|
|
31
|
+
theme: Theme
|
|
32
|
+
}): string | undefined {
|
|
33
|
+
if (isActive) return theme.colors['list.activeSelectionBackground']
|
|
34
|
+
if (alternate) return theme.colors['editor.lineHighlightBackground']
|
|
35
|
+
return undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
const SidebarTop = memo(function SidebarTop() {
|
|
23
39
|
const theme = useTheme()
|
|
24
40
|
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
@@ -44,6 +60,19 @@ const SidebarTop = memo(function SidebarTop() {
|
|
|
44
60
|
<text fg={theme.colors['descriptionForeground']}>{branch}</text>
|
|
45
61
|
</box>
|
|
46
62
|
) : null}
|
|
63
|
+
<box
|
|
64
|
+
flexDirection="row"
|
|
65
|
+
paddingY={1}
|
|
66
|
+
backgroundColor={theme.colors['list.activeSelectionBackground']}
|
|
67
|
+
justifyContent="center"
|
|
68
|
+
marginTop={1}
|
|
69
|
+
onMouseDown={(e) => {
|
|
70
|
+
e.stopPropagation()
|
|
71
|
+
dispatchGlobal({ type: 'open-new-tab-modal' })
|
|
72
|
+
}}
|
|
73
|
+
>
|
|
74
|
+
<text fg={theme.colors['editor.foreground']}>+ New assistant</text>
|
|
75
|
+
</box>
|
|
47
76
|
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
48
77
|
{'·'.repeat(Math.max(0, sidebarWidth - 2))}
|
|
49
78
|
</text>
|
|
@@ -51,25 +80,14 @@ const SidebarTop = memo(function SidebarTop() {
|
|
|
51
80
|
)
|
|
52
81
|
})
|
|
53
82
|
|
|
54
|
-
function renderGroupGutter(
|
|
55
|
-
isGroupStart: boolean,
|
|
56
|
-
isGroupMiddle: boolean,
|
|
57
|
-
isGroupEnd: boolean,
|
|
58
|
-
isActive: boolean
|
|
59
|
-
) {
|
|
83
|
+
function renderGroupGutter(isGroupStart: boolean, isGroupMiddle: boolean, isGroupEnd: boolean) {
|
|
60
84
|
return (
|
|
61
85
|
<box flexDirection="column" width={1} overflow="hidden">
|
|
62
|
-
<text
|
|
63
|
-
fg={getCurrentTheme().colors['terminal.ansiMagenta']}
|
|
64
|
-
bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
|
|
65
|
-
>
|
|
86
|
+
<text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
|
|
66
87
|
{/* oxlint-disable-next-line no-nested-ternary */}
|
|
67
88
|
{isGroupStart ? GUTTER_START : isGroupMiddle ? GUTTER_MIDDLE : GUTTER_PAD}
|
|
68
89
|
</text>
|
|
69
|
-
<text
|
|
70
|
-
fg={getCurrentTheme().colors['terminal.ansiMagenta']}
|
|
71
|
-
bg={isActive ? getCurrentTheme().colors['list.activeSelectionBackground'] : undefined}
|
|
72
|
-
>
|
|
90
|
+
<text fg={getCurrentTheme().colors['terminal.ansiMagenta']}>
|
|
73
91
|
{isGroupEnd ? GUTTER_END : GUTTER_PAD}
|
|
74
92
|
</text>
|
|
75
93
|
</box>
|
|
@@ -116,6 +134,7 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
116
134
|
) : (
|
|
117
135
|
tabs.map((tab, index) => {
|
|
118
136
|
const isActive = tab.id === activeTabId
|
|
137
|
+
const alternate = index % 2 === 1
|
|
119
138
|
const info = tabGroupInfo.get(tab.id)
|
|
120
139
|
const inLayout = !!info?.inLayout
|
|
121
140
|
const inGroup = info ? index >= info.groupStart && index <= info.groupEnd : false
|
|
@@ -126,19 +145,20 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
126
145
|
return (
|
|
127
146
|
<box
|
|
128
147
|
key={tab.id}
|
|
148
|
+
backgroundColor={getRowBackground({ alternate, isActive, theme })}
|
|
129
149
|
flexDirection="row"
|
|
130
|
-
onMouseDown={
|
|
150
|
+
onMouseDown={(event) => {
|
|
151
|
+
event.stopPropagation()
|
|
152
|
+
onTabActivate?.(tab.id)
|
|
153
|
+
}}
|
|
131
154
|
>
|
|
132
|
-
{inGroup
|
|
133
|
-
? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd, isActive)
|
|
134
|
-
: null}
|
|
155
|
+
{inGroup ? renderGroupGutter(isGroupStart, isGroupMiddle, isGroupEnd) : null}
|
|
135
156
|
<box flexGrow={1}>
|
|
136
157
|
<TabItem
|
|
137
158
|
id={`sidebar-tab-${tab.id}`}
|
|
138
159
|
tab={tab}
|
|
139
160
|
active={isActive}
|
|
140
161
|
focused={focusMode === 'navigation'}
|
|
141
|
-
isFocusedInput={isActive && focusMode === 'terminal-input'}
|
|
142
162
|
inLayout={inLayout}
|
|
143
163
|
/>
|
|
144
164
|
</box>
|
|
@@ -152,9 +172,11 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
|
|
|
152
172
|
|
|
153
173
|
export function Sidebar({ onTabActivate }: SidebarProps) {
|
|
154
174
|
const theme = useTheme()
|
|
175
|
+
const sidebarBg = useBg('sideBar.background')
|
|
155
176
|
const sidebarVisible = useAppStore((s) => s.sidebar.visible)
|
|
156
177
|
const sidebarWidth = useAppStore((s) => s.sidebar.width)
|
|
157
178
|
const gitPane = useAppStore((s) => s.gitPane)
|
|
179
|
+
const focusMode = useAppStore((s) => s.focusMode)
|
|
158
180
|
|
|
159
181
|
if (!sidebarVisible) {
|
|
160
182
|
return null
|
|
@@ -184,8 +206,13 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
|
|
|
184
206
|
width={sidebarWidth}
|
|
185
207
|
padding={0}
|
|
186
208
|
flexDirection="column"
|
|
187
|
-
backgroundColor={
|
|
209
|
+
backgroundColor={sidebarBg}
|
|
188
210
|
gap={0}
|
|
211
|
+
onMouseDown={() => {
|
|
212
|
+
if (focusMode === 'terminal-input') {
|
|
213
|
+
dispatchGlobal({ focusMode: 'navigation', type: 'set-focus-mode' })
|
|
214
|
+
}
|
|
215
|
+
}}
|
|
189
216
|
>
|
|
190
217
|
<SidebarTop />
|
|
191
218
|
{gitOnTop ? (
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { SnippetRecord } from '../../state/types'
|
|
2
2
|
|
|
3
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
3
4
|
import { filterSnippets } from '../../state/selectors'
|
|
4
5
|
import { useTheme } from '../theme'
|
|
5
6
|
import { uiTokens } from '../ui-tokens'
|
|
6
|
-
import {
|
|
7
|
-
import { ModalFilterBar } from './modal-filter-bar'
|
|
8
|
-
import { ModalShell } from './modal-shell'
|
|
7
|
+
import { Picker, type PickerItem } from './picker'
|
|
9
8
|
|
|
10
9
|
interface SnippetPickerModalProps {
|
|
11
10
|
snippets: SnippetRecord[]
|
|
12
11
|
selectedIndex: number
|
|
13
12
|
filter: string | null
|
|
13
|
+
cursorPos?: number
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const MAX_PREVIEW_LENGTH = 60
|
|
@@ -21,45 +21,54 @@ function truncateContent(content: string): string {
|
|
|
21
21
|
return `${normalized.slice(0, MAX_PREVIEW_LENGTH - 3)}...`
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function SnippetPickerModal({
|
|
24
|
+
export function SnippetPickerModal({
|
|
25
|
+
cursorPos,
|
|
26
|
+
filter,
|
|
27
|
+
selectedIndex,
|
|
28
|
+
snippets,
|
|
29
|
+
}: SnippetPickerModalProps) {
|
|
25
30
|
const theme = useTheme()
|
|
26
31
|
const filtered = filterSnippets(snippets, filter)
|
|
27
32
|
|
|
33
|
+
const items: PickerItem[] = filtered.map((snippet, index) => {
|
|
34
|
+
const active = index === selectedIndex
|
|
35
|
+
return {
|
|
36
|
+
key: snippet.id,
|
|
37
|
+
onClick: () => {
|
|
38
|
+
dispatchGlobal({ type: 'close-modal' })
|
|
39
|
+
runSideEffectGlobal({ type: 'paste-selected-snippet' })
|
|
40
|
+
},
|
|
41
|
+
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
|
|
42
|
+
onEdit: () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
43
|
+
subtitle: (
|
|
44
|
+
<text fg={theme.colors['descriptionForeground']}>{truncateContent(snippet.content)}</text>
|
|
45
|
+
),
|
|
46
|
+
title: (
|
|
47
|
+
<text
|
|
48
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
49
|
+
>
|
|
50
|
+
<strong>{snippet.name}</strong>
|
|
51
|
+
</text>
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
28
56
|
return (
|
|
29
|
-
<
|
|
57
|
+
<Picker
|
|
30
58
|
title="Snippets"
|
|
31
|
-
keybindsModeId="modal.snippet-picker"
|
|
59
|
+
keybindsModeId="modal.snippet-picker.filtering"
|
|
32
60
|
width={uiTokens.modalWidth.xl}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
{
|
|
61
|
+
gap={1}
|
|
62
|
+
filter={filter}
|
|
63
|
+
cursorPos={cursorPos}
|
|
64
|
+
items={items}
|
|
65
|
+
selectedIndex={selectedIndex}
|
|
66
|
+
emptyState={
|
|
36
67
|
<text fg={theme.colors['descriptionForeground']}>
|
|
37
68
|
{filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
|
|
38
69
|
</text>
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<ListItem
|
|
44
|
-
key={snippet.id}
|
|
45
|
-
active={active}
|
|
46
|
-
title={
|
|
47
|
-
<text
|
|
48
|
-
fg={
|
|
49
|
-
active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
50
|
-
}
|
|
51
|
-
>
|
|
52
|
-
<strong>{snippet.name}</strong>
|
|
53
|
-
</text>
|
|
54
|
-
}
|
|
55
|
-
subtitle={
|
|
56
|
-
<text fg={theme.colors['descriptionForeground']}>
|
|
57
|
-
{truncateContent(snippet.content)}
|
|
58
|
-
</text>
|
|
59
|
-
}
|
|
60
|
-
/>
|
|
61
|
-
)
|
|
62
|
-
})}
|
|
63
|
-
</ModalShell>
|
|
70
|
+
}
|
|
71
|
+
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
72
|
+
/>
|
|
64
73
|
)
|
|
65
74
|
}
|
|
@@ -4,7 +4,7 @@ import { version as APP_VERSION } from '../../../package.json'
|
|
|
4
4
|
import { useAppStore } from '../../state/app-store'
|
|
5
5
|
import { useKeymap } from '../keymap-context'
|
|
6
6
|
import { getStatusBarModel } from '../status-bar-model'
|
|
7
|
-
import { getCurrentTheme, useTheme } from '../theme'
|
|
7
|
+
import { getCurrentTheme, useBg, useTheme } from '../theme'
|
|
8
8
|
|
|
9
9
|
function getModeColor(focusMode: AppState['focusMode']): string {
|
|
10
10
|
const t = getCurrentTheme()
|
|
@@ -41,6 +41,7 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
|
|
|
41
41
|
|
|
42
42
|
export function StatusBar() {
|
|
43
43
|
const theme = useTheme()
|
|
44
|
+
const headerBg = useBg('sideBarSectionHeader.background')
|
|
44
45
|
const state = useAppStore((s) => s)
|
|
45
46
|
const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
|
|
46
47
|
const config = useKeymap()
|
|
@@ -54,7 +55,7 @@ export function StatusBar() {
|
|
|
54
55
|
paddingTop={0}
|
|
55
56
|
paddingBottom={0}
|
|
56
57
|
flexDirection="column"
|
|
57
|
-
backgroundColor={
|
|
58
|
+
backgroundColor={headerBg}
|
|
58
59
|
>
|
|
59
60
|
<box width="100%" flexDirection="row">
|
|
60
61
|
<text fg={getModeColor(state.focusMode)}>[{getModeLabel(state.focusMode)}]</text>
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
import type { ReactNode } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { ThemeColorMap } from '../themes'
|
|
4
|
+
|
|
5
|
+
import { useBg } from '../theme'
|
|
4
6
|
|
|
5
7
|
type SurfaceTone = 'muted' | 'elevated' | 'selected' | 'input' | 'inputActive'
|
|
6
8
|
|
|
7
|
-
function
|
|
9
|
+
function toneToColorKey(tone: SurfaceTone): keyof ThemeColorMap {
|
|
8
10
|
switch (tone) {
|
|
9
11
|
case 'elevated':
|
|
10
|
-
return
|
|
12
|
+
return 'sideBar.background'
|
|
11
13
|
case 'selected':
|
|
12
|
-
return
|
|
14
|
+
return 'list.activeSelectionBackground'
|
|
13
15
|
case 'input':
|
|
14
|
-
return
|
|
16
|
+
return 'editor.background'
|
|
15
17
|
case 'inputActive':
|
|
16
|
-
return
|
|
18
|
+
return 'list.activeSelectionBackground'
|
|
17
19
|
case 'muted':
|
|
18
20
|
default:
|
|
19
|
-
return
|
|
21
|
+
return 'sideBarSectionHeader.background'
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -45,9 +47,10 @@ export function Surface({
|
|
|
45
47
|
tone = 'muted',
|
|
46
48
|
width,
|
|
47
49
|
}: SurfaceProps) {
|
|
50
|
+
const bg = useBg(toneToColorKey(tone))
|
|
48
51
|
return (
|
|
49
52
|
<box
|
|
50
|
-
backgroundColor={
|
|
53
|
+
backgroundColor={bg}
|
|
51
54
|
flexDirection={flexDirection}
|
|
52
55
|
gap={gap}
|
|
53
56
|
padding={padding}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
1
3
|
import type { TabSession } from '../../state/types'
|
|
2
4
|
|
|
5
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
3
6
|
import { useBusySpinner } from '../hooks/use-busy-spinner'
|
|
4
7
|
import { getCurrentTheme, useTheme } from '../theme'
|
|
5
8
|
|
|
@@ -8,7 +11,6 @@ interface TabItemProps {
|
|
|
8
11
|
tab: TabSession
|
|
9
12
|
active: boolean
|
|
10
13
|
focused: boolean
|
|
11
|
-
isFocusedInput: boolean
|
|
12
14
|
inLayout?: boolean
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -20,8 +22,6 @@ function getStatusColor(status: TabSession['status']): string {
|
|
|
20
22
|
return getCurrentTheme().colors['editorWarning.foreground']
|
|
21
23
|
case 'error':
|
|
22
24
|
return getCurrentTheme().colors['editorError.foreground']
|
|
23
|
-
case 'exited':
|
|
24
|
-
return getCurrentTheme().colors['editorWarning.foreground']
|
|
25
25
|
default:
|
|
26
26
|
return getCurrentTheme().colors['descriptionForeground']
|
|
27
27
|
}
|
|
@@ -50,10 +50,15 @@ function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean)
|
|
|
50
50
|
function BusyIndicator() {
|
|
51
51
|
const theme = useTheme()
|
|
52
52
|
const frame = useBusySpinner()
|
|
53
|
-
return <text fg={theme.colors['textLink.foreground']}>{frame}
|
|
53
|
+
return <text fg={theme.colors['textLink.foreground']}>{frame} working</text>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function WaitingIndicator() {
|
|
57
|
+
const theme = useTheme()
|
|
58
|
+
return <text fg={theme.colors['editorWarning.foreground']}>? waiting</text>
|
|
54
59
|
}
|
|
55
60
|
|
|
56
|
-
function ActivityIndicator({
|
|
61
|
+
function ActivityIndicator({ tab }: { tab: TabSession }) {
|
|
57
62
|
const theme = useTheme()
|
|
58
63
|
if (tab.status === 'error') {
|
|
59
64
|
return <text fg={theme.colors['editorError.foreground']}>✗ error</text>
|
|
@@ -63,16 +68,12 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
|
|
|
63
68
|
return <text fg={theme.colors['editorWarning.foreground']}>⏸ restore</text>
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
if (tab.
|
|
67
|
-
return <
|
|
71
|
+
if (tab.activity === 'working') {
|
|
72
|
+
return <BusyIndicator />
|
|
68
73
|
}
|
|
69
74
|
|
|
70
|
-
if (
|
|
71
|
-
return <
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (tab.activity === 'busy') {
|
|
75
|
-
return <BusyIndicator />
|
|
75
|
+
if (tab.activity === 'waiting-input') {
|
|
76
|
+
return <WaitingIndicator />
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
if (tab.activity === 'idle') {
|
|
@@ -82,12 +83,13 @@ function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocused
|
|
|
82
83
|
return <text fg={getStatusColor(tab.status)}>{tab.status}</text>
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
export function TabItem({ active, focused, id, inLayout,
|
|
86
|
+
export function TabItem({ active, focused, id, inLayout, tab }: TabItemProps) {
|
|
86
87
|
const theme = useTheme()
|
|
87
88
|
const label = tab.command.split(' ')[0]
|
|
88
89
|
const isInLayout = inLayout ?? false
|
|
89
90
|
const indicator = getIndicator(active, focused, isInLayout)
|
|
90
91
|
const indicatorColor = getIndicatorColor(active, focused, isInLayout)
|
|
92
|
+
const [hovered, setHovered] = useState(false)
|
|
91
93
|
|
|
92
94
|
return (
|
|
93
95
|
<box
|
|
@@ -96,21 +98,35 @@ export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }:
|
|
|
96
98
|
paddingRight={1}
|
|
97
99
|
paddingTop={0}
|
|
98
100
|
paddingBottom={0}
|
|
99
|
-
backgroundColor={active ? theme.colors['list.activeSelectionBackground'] : undefined}
|
|
100
101
|
flexDirection="column"
|
|
101
102
|
gap={0}
|
|
103
|
+
onMouseOver={() => setHovered(true)}
|
|
104
|
+
onMouseOut={() => setHovered(false)}
|
|
102
105
|
>
|
|
103
|
-
<box flexDirection="row">
|
|
106
|
+
<box flexDirection="row" alignItems="center">
|
|
104
107
|
<text fg={indicatorColor}>{indicator} </text>
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
<box flexGrow={1}>
|
|
109
|
+
<text
|
|
110
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
111
|
+
>
|
|
112
|
+
{tab.title}
|
|
113
|
+
</text>
|
|
114
|
+
</box>
|
|
115
|
+
{hovered ? (
|
|
116
|
+
<box
|
|
117
|
+
onMouseDown={(event) => {
|
|
118
|
+
event.stopPropagation()
|
|
119
|
+
dispatchGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
120
|
+
runSideEffectGlobal({ tabId: tab.id, type: 'close-tab' })
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<text fg={theme.colors['descriptionForeground']}>×</text>
|
|
124
|
+
</box>
|
|
125
|
+
) : null}
|
|
110
126
|
</box>
|
|
111
127
|
<box flexDirection="row">
|
|
112
128
|
<text fg={theme.colors['descriptionForeground']}> {label} </text>
|
|
113
|
-
<ActivityIndicator tab={tab}
|
|
129
|
+
<ActivityIndicator tab={tab} />
|
|
114
130
|
</box>
|
|
115
131
|
</box>
|
|
116
132
|
)
|
|
@@ -6,7 +6,7 @@ import type { TerminalContentOrigin } from '../../input/raw-input-handler'
|
|
|
6
6
|
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
|
|
7
7
|
|
|
8
8
|
import { logInputDebug } from '../../debug/input-log'
|
|
9
|
-
import { getCurrentTheme, useTheme } from '../theme'
|
|
9
|
+
import { getCurrentTheme, useBg, useTheme } from '../theme'
|
|
10
10
|
|
|
11
11
|
interface TerminalPaneProps {
|
|
12
12
|
tab?: TabSession
|
|
@@ -125,6 +125,7 @@ export function TerminalPane({
|
|
|
125
125
|
tabId,
|
|
126
126
|
}: TerminalPaneProps) {
|
|
127
127
|
const theme = useTheme()
|
|
128
|
+
const editorBg = useBg('editor.background')
|
|
128
129
|
const paneIsActive = isActive ?? true
|
|
129
130
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
130
131
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
@@ -185,7 +186,8 @@ export function TerminalPane({
|
|
|
185
186
|
padding={0}
|
|
186
187
|
flexDirection="column"
|
|
187
188
|
flexGrow={1}
|
|
188
|
-
backgroundColor={
|
|
189
|
+
backgroundColor={editorBg}
|
|
190
|
+
onMouseDown={forwardMouseEvent}
|
|
189
191
|
onMouseDrag={forwardMouseEvent}
|
|
190
192
|
onMouseScroll={forwardScrollEvent}
|
|
191
193
|
onMouseUp={forwardMouseEvent}
|
|
@@ -205,7 +207,10 @@ export function TerminalPane({
|
|
|
205
207
|
flexDirection="column"
|
|
206
208
|
flexGrow={1}
|
|
207
209
|
width="100%"
|
|
208
|
-
onMouseDown={
|
|
210
|
+
onMouseDown={(e) => {
|
|
211
|
+
e.stopPropagation()
|
|
212
|
+
forwardMouseEvent(e)
|
|
213
|
+
}}
|
|
209
214
|
onMouseUp={forwardMouseEvent}
|
|
210
215
|
onMouseDrag={forwardMouseEvent}
|
|
211
216
|
onMouseScroll={forwardScrollEvent}
|
|
@@ -214,11 +219,6 @@ export function TerminalPane({
|
|
|
214
219
|
</box>
|
|
215
220
|
)}
|
|
216
221
|
</box>
|
|
217
|
-
{tab?.status === 'exited' && tab.exitCode !== undefined ? (
|
|
218
|
-
<text fg={theme.colors['editorWarning.foreground']}>
|
|
219
|
-
Process exited with code {tab.exitCode}
|
|
220
|
-
</text>
|
|
221
|
-
) : null}
|
|
222
222
|
{tab?.status === 'disconnected' ? (
|
|
223
223
|
<text fg={theme.colors['editorWarning.foreground']}>
|
|
224
224
|
Restored snapshot. Press Ctrl+r to restart this session.
|
|
@@ -1,52 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useLayoutEffect, useMemo, useRef } from 'react'
|
|
1
|
+
import { useLayoutEffect, useMemo } from 'react'
|
|
3
2
|
|
|
4
|
-
import { dispatchGlobal } from '../../state/dispatch-ref'
|
|
3
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
5
4
|
import { filterThemeIds } from '../filter-themes'
|
|
6
|
-
import { useTheme } from '../theme'
|
|
5
|
+
import { useTheme, useTransparent } from '../theme'
|
|
7
6
|
import { type ThemeId, THEMES } from '../themes'
|
|
8
7
|
import { uiTokens } from '../ui-tokens'
|
|
9
|
-
import {
|
|
10
|
-
import { ModalFilterBar } from './modal-filter-bar'
|
|
11
|
-
import { ModalShell } from './modal-shell'
|
|
8
|
+
import { Picker, type PickerItem } from './picker'
|
|
12
9
|
|
|
13
10
|
interface ThemePickerModalProps {
|
|
14
11
|
currentThemeId: ThemeId
|
|
15
12
|
filter: string | null
|
|
16
13
|
selectedIndex: number
|
|
14
|
+
cursorPos?: number
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
20
|
-
const MODAL_CHROME_ROWS = 6
|
|
21
|
-
|
|
22
17
|
function clampSelection(index: number, count: number): number {
|
|
23
18
|
if (count === 0) return 0
|
|
24
19
|
return Math.max(0, Math.min(count - 1, index))
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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) {
|
|
22
|
+
export function ThemePickerModal({
|
|
23
|
+
currentThemeId,
|
|
24
|
+
cursorPos,
|
|
25
|
+
filter,
|
|
26
|
+
selectedIndex,
|
|
27
|
+
}: ThemePickerModalProps) {
|
|
48
28
|
const theme = useTheme()
|
|
49
|
-
const
|
|
29
|
+
const transparent = useTransparent()
|
|
50
30
|
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
51
31
|
|
|
52
32
|
useLayoutEffect(() => {
|
|
@@ -54,78 +34,58 @@ export function ThemePickerModal({ currentThemeId, filter, selectedIndex }: Them
|
|
|
54
34
|
}, [filtered.length])
|
|
55
35
|
|
|
56
36
|
const effectiveIndex = clampSelection(selectedIndex, filtered.length)
|
|
57
|
-
const maxHeight = Math.max(6, Math.floor(dimensions.height * VIEWPORT_HEIGHT_RATIO))
|
|
58
|
-
const listHeight = Math.max(1, maxHeight - MODAL_CHROME_ROWS)
|
|
59
37
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
38
|
+
const items: PickerItem[] = filtered.flatMap((id, rowIndex) => {
|
|
39
|
+
const entry = THEMES[id]
|
|
40
|
+
if (!entry) return []
|
|
41
|
+
const active = rowIndex === effectiveIndex
|
|
42
|
+
const isCurrent = id === currentThemeId
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
key: id,
|
|
46
|
+
onClick: () => {
|
|
47
|
+
dispatchGlobal({ type: 'close-modal' })
|
|
48
|
+
runSideEffectGlobal({ action: 'confirm', type: 'apply-theme' })
|
|
49
|
+
},
|
|
50
|
+
title: (
|
|
51
|
+
<text
|
|
52
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
53
|
+
>
|
|
54
|
+
{entry.name}
|
|
55
|
+
</text>
|
|
56
|
+
),
|
|
57
|
+
trailing: isCurrent ? (
|
|
58
|
+
<text fg={theme.colors['textLink.foreground']}>current</text>
|
|
59
|
+
) : undefined,
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
})
|
|
74
63
|
|
|
75
64
|
return (
|
|
76
|
-
<
|
|
65
|
+
<Picker
|
|
77
66
|
title="Select theme"
|
|
78
|
-
keybindsModeId="modal.theme-picker"
|
|
67
|
+
keybindsModeId="modal.theme-picker.filtering"
|
|
79
68
|
width={uiTokens.modalWidth.md}
|
|
80
|
-
|
|
69
|
+
filter={filter}
|
|
70
|
+
cursorPos={cursorPos}
|
|
81
71
|
footer={
|
|
82
72
|
<box flexDirection="column" gap={0}>
|
|
83
73
|
<text fg={theme.colors['editor.lineHighlightBackground']}>
|
|
84
|
-
{filtered.length === 0
|
|
85
|
-
|
|
86
|
-
|
|
74
|
+
{filtered.length === 0 ? '' : ` ${effectiveIndex + 1} / ${filtered.length}`}
|
|
75
|
+
</text>
|
|
76
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
77
|
+
{` transparent: ${transparent ? 'on' : 'off'} (ctrl-t)`}
|
|
87
78
|
</text>
|
|
88
|
-
<ModalFilterBar filter={filter} />
|
|
89
79
|
</box>
|
|
90
80
|
}
|
|
91
|
-
|
|
92
|
-
{
|
|
81
|
+
items={items}
|
|
82
|
+
selectedIndex={effectiveIndex}
|
|
83
|
+
emptyState={
|
|
93
84
|
<text fg={theme.colors['descriptionForeground']}>
|
|
94
85
|
{filter ? 'No matching themes.' : 'No themes available.'}
|
|
95
86
|
</text>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const rowIndex = start + i
|
|
100
|
-
const entry = THEMES[id]
|
|
101
|
-
if (!entry) return null
|
|
102
|
-
const active = rowIndex === effectiveIndex
|
|
103
|
-
const isCurrent = id === currentThemeId
|
|
104
|
-
return (
|
|
105
|
-
<ListItem
|
|
106
|
-
key={id}
|
|
107
|
-
active={active}
|
|
108
|
-
title={
|
|
109
|
-
<text
|
|
110
|
-
fg={
|
|
111
|
-
active
|
|
112
|
-
? theme.colors['editor.foreground']
|
|
113
|
-
: theme.colors['descriptionForeground']
|
|
114
|
-
}
|
|
115
|
-
>
|
|
116
|
-
{entry.name}
|
|
117
|
-
</text>
|
|
118
|
-
}
|
|
119
|
-
trailing={
|
|
120
|
-
isCurrent ? (
|
|
121
|
-
<text fg={theme.colors['textLink.foreground']}>current</text>
|
|
122
|
-
) : undefined
|
|
123
|
-
}
|
|
124
|
-
/>
|
|
125
|
-
)
|
|
126
|
-
})}
|
|
127
|
-
</box>
|
|
128
|
-
)}
|
|
129
|
-
</ModalShell>
|
|
87
|
+
}
|
|
88
|
+
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
89
|
+
/>
|
|
130
90
|
)
|
|
131
91
|
}
|