@brimveyn/aimux 1.6.2 → 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 +24 -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 +2 -1
- package/src/config.ts +9 -0
- 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/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 +2 -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/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 +11 -3
- package/src/state/types.ts +29 -13
- 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/split-view.tsx +11 -17
- package/src/ui/components/diff-renderer/stacked-view.tsx +7 -14
- package/src/ui/components/git-panel.tsx +10 -3
- package/src/ui/components/git-view.tsx +4 -8
- 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,13 +1,12 @@
|
|
|
1
1
|
import type { SessionRecord } from '../../state/types'
|
|
2
2
|
|
|
3
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
|
|
3
4
|
import { filterSessions } from '../../state/selectors'
|
|
4
5
|
import { abbreviatePath } from '../path-format'
|
|
5
6
|
import { orderSessionsForDisplay } from '../session-ordering'
|
|
6
7
|
import { useTheme } from '../theme'
|
|
7
8
|
import { uiTokens } from '../ui-tokens'
|
|
8
|
-
import {
|
|
9
|
-
import { ModalFilterBar } from './modal-filter-bar'
|
|
10
|
-
import { ModalShell } from './modal-shell'
|
|
9
|
+
import { Picker, type PickerItem } from './picker'
|
|
11
10
|
|
|
12
11
|
interface SessionPickerModalProps {
|
|
13
12
|
sessions: SessionRecord[]
|
|
@@ -15,6 +14,7 @@ interface SessionPickerModalProps {
|
|
|
15
14
|
currentSessionId: string | null
|
|
16
15
|
currentTabCount: number
|
|
17
16
|
filter: string | null
|
|
17
|
+
cursorPos?: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function formatSessionLine(
|
|
@@ -41,6 +41,7 @@ function getEmptyStateMessage(hasFilter: boolean): string {
|
|
|
41
41
|
export function SessionPickerModal({
|
|
42
42
|
currentSessionId,
|
|
43
43
|
currentTabCount,
|
|
44
|
+
cursorPos,
|
|
44
45
|
filter,
|
|
45
46
|
selectedIndex,
|
|
46
47
|
sessions,
|
|
@@ -50,62 +51,63 @@ export function SessionPickerModal({
|
|
|
50
51
|
const baselineOrder = ordered.map((s) => s.id)
|
|
51
52
|
const filtered = filterSessions(ordered, filter)
|
|
52
53
|
const hasFilter = !!filter
|
|
53
|
-
|
|
54
|
-
const
|
|
54
|
+
|
|
55
|
+
const sessionItems: PickerItem[] = filtered.map((session, index) => {
|
|
56
|
+
const active = index === selectedIndex
|
|
57
|
+
const displayIndex = baselineOrder.indexOf(session.id) + 1
|
|
58
|
+
return {
|
|
59
|
+
key: session.id,
|
|
60
|
+
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
61
|
+
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
62
|
+
subtitle: session.projectPath ? (
|
|
63
|
+
<text fg={theme.colors['descriptionForeground']}>
|
|
64
|
+
{abbreviatePath(session.projectPath)}
|
|
65
|
+
</text>
|
|
66
|
+
) : undefined,
|
|
67
|
+
title: (
|
|
68
|
+
<text
|
|
69
|
+
fg={active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']}
|
|
70
|
+
>
|
|
71
|
+
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
72
|
+
</text>
|
|
73
|
+
),
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const createNewItem: PickerItem = {
|
|
78
|
+
key: '__create-new__',
|
|
79
|
+
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
80
|
+
title: (
|
|
81
|
+
<text
|
|
82
|
+
fg={
|
|
83
|
+
selectedIndex === filtered.length
|
|
84
|
+
? theme.colors['editor.foreground']
|
|
85
|
+
: theme.colors['descriptionForeground']
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
Create new session
|
|
89
|
+
</text>
|
|
90
|
+
),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const items = [...sessionItems, createNewItem]
|
|
55
94
|
|
|
56
95
|
return (
|
|
57
|
-
<
|
|
96
|
+
<Picker
|
|
58
97
|
title="Sessions"
|
|
59
|
-
keybindsModeId="modal.session-picker"
|
|
98
|
+
keybindsModeId="modal.session-picker.filtering"
|
|
60
99
|
width={uiTokens.modalWidth.lg}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<ListItem
|
|
74
|
-
key={session.id}
|
|
75
|
-
active={active}
|
|
76
|
-
title={
|
|
77
|
-
<text
|
|
78
|
-
fg={
|
|
79
|
-
active ? theme.colors['editor.foreground'] : theme.colors['descriptionForeground']
|
|
80
|
-
}
|
|
81
|
-
>
|
|
82
|
-
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
83
|
-
</text>
|
|
84
|
-
}
|
|
85
|
-
subtitle={
|
|
86
|
-
session.projectPath ? (
|
|
87
|
-
<text fg={theme.colors['descriptionForeground']}>
|
|
88
|
-
{abbreviatePath(session.projectPath)}
|
|
89
|
-
</text>
|
|
90
|
-
) : undefined
|
|
91
|
-
}
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
})}
|
|
95
|
-
<ListItem
|
|
96
|
-
active={selectedIndex === filtered.length}
|
|
97
|
-
title={
|
|
98
|
-
<text
|
|
99
|
-
fg={
|
|
100
|
-
selectedIndex === filtered.length
|
|
101
|
-
? theme.colors['editor.foreground']
|
|
102
|
-
: theme.colors['descriptionForeground']
|
|
103
|
-
}
|
|
104
|
-
>
|
|
105
|
-
Create new session
|
|
106
|
-
</text>
|
|
107
|
-
}
|
|
108
|
-
/>
|
|
109
|
-
</ModalShell>
|
|
100
|
+
gap={1}
|
|
101
|
+
filter={filter}
|
|
102
|
+
cursorPos={cursorPos}
|
|
103
|
+
items={items}
|
|
104
|
+
selectedIndex={selectedIndex}
|
|
105
|
+
emptyState={
|
|
106
|
+
filtered.length === 0 ? (
|
|
107
|
+
<text fg={theme.colors['descriptionForeground']}>{getEmptyStateMessage(hasFilter)}</text>
|
|
108
|
+
) : undefined
|
|
109
|
+
}
|
|
110
|
+
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
111
|
+
/>
|
|
110
112
|
)
|
|
111
113
|
}
|
|
@@ -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.
|