@brimveyn/aimux 1.6.2 → 1.7.1
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/manager-protocol.ts +2 -2
- 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 +25 -2
- 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,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
|
}
|
package/src/ui/root.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import { StatusBar } from './components/status-bar'
|
|
|
24
24
|
import { TerminalPane } from './components/terminal-pane'
|
|
25
25
|
import { ThemePickerModal } from './components/theme-picker-modal'
|
|
26
26
|
import { UpdateAvailableModal } from './components/update-available-modal'
|
|
27
|
-
import {
|
|
27
|
+
import { useBg } from './theme'
|
|
28
28
|
|
|
29
29
|
function getCreateSessionFields(modal: ModalState) {
|
|
30
30
|
if (modal.type !== 'create-session') {
|
|
@@ -82,7 +82,10 @@ function renderModal(
|
|
|
82
82
|
<NewTabModal
|
|
83
83
|
selectedIndex={modal.selectedIndex}
|
|
84
84
|
customCommands={options.customCommands}
|
|
85
|
-
|
|
85
|
+
filter={modal.editBuffer}
|
|
86
|
+
cursorPos={modal.cursorPos}
|
|
87
|
+
editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
|
|
88
|
+
editBuffer={modal.editBuffer ?? ''}
|
|
86
89
|
/>
|
|
87
90
|
)
|
|
88
91
|
case 'session-picker':
|
|
@@ -93,6 +96,7 @@ function renderModal(
|
|
|
93
96
|
currentSessionId={options.currentSessionId}
|
|
94
97
|
currentTabCount={options.currentTabCount}
|
|
95
98
|
filter={modal.editBuffer}
|
|
99
|
+
cursorPos={modal.cursorPos}
|
|
96
100
|
/>
|
|
97
101
|
)
|
|
98
102
|
case 'session-name':
|
|
@@ -121,6 +125,7 @@ function renderModal(
|
|
|
121
125
|
snippets={options.snippets}
|
|
122
126
|
selectedIndex={modal.selectedIndex}
|
|
123
127
|
filter={modal.editBuffer}
|
|
128
|
+
cursorPos={modal.cursorPos}
|
|
124
129
|
/>
|
|
125
130
|
)
|
|
126
131
|
case 'snippet-editor':
|
|
@@ -138,6 +143,7 @@ function renderModal(
|
|
|
138
143
|
selectedIndex={modal.selectedIndex}
|
|
139
144
|
currentThemeId={options.themeId}
|
|
140
145
|
filter={modal.editBuffer}
|
|
146
|
+
cursorPos={modal.cursorPos}
|
|
141
147
|
/>
|
|
142
148
|
)
|
|
143
149
|
case 'update-available':
|
|
@@ -154,6 +160,7 @@ function renderModal(
|
|
|
154
160
|
filter={modal.editBuffer}
|
|
155
161
|
selectedIndex={modal.selectedIndex}
|
|
156
162
|
scope={modal.scope}
|
|
163
|
+
cursorPos={modal.cursorPos}
|
|
157
164
|
/>
|
|
158
165
|
)
|
|
159
166
|
case 'git-commit': {
|
|
@@ -215,7 +222,7 @@ export function RootView({
|
|
|
215
222
|
terminalRows,
|
|
216
223
|
themeId,
|
|
217
224
|
}: RootViewProps) {
|
|
218
|
-
const
|
|
225
|
+
const editorBg = useBg('editor.background')
|
|
219
226
|
const tabs = useAppStore((s) => s.tabs)
|
|
220
227
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
221
228
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
@@ -241,12 +248,7 @@ export function RootView({
|
|
|
241
248
|
const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
|
|
242
249
|
if (inGitMode) {
|
|
243
250
|
return (
|
|
244
|
-
<box
|
|
245
|
-
flexDirection="column"
|
|
246
|
-
width="100%"
|
|
247
|
-
height="100%"
|
|
248
|
-
backgroundColor={theme.colors['editor.background']}
|
|
249
|
-
>
|
|
251
|
+
<box flexDirection="column" width="100%" height="100%" backgroundColor={editorBg}>
|
|
250
252
|
<GitView themeId={themeId} />
|
|
251
253
|
<StatusBar />
|
|
252
254
|
<PendingChordOverlay />
|
|
@@ -265,12 +267,7 @@ export function RootView({
|
|
|
265
267
|
}
|
|
266
268
|
|
|
267
269
|
return (
|
|
268
|
-
<box
|
|
269
|
-
flexDirection="column"
|
|
270
|
-
width="100%"
|
|
271
|
-
height="100%"
|
|
272
|
-
backgroundColor={theme.colors['editor.background']}
|
|
273
|
-
>
|
|
270
|
+
<box flexDirection="column" width="100%" height="100%" backgroundColor={editorBg}>
|
|
274
271
|
{sessionBarPosition === 'top' && <SessionBar />}
|
|
275
272
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
276
273
|
<Sidebar onTabActivate={onPaneActivate} />
|
|
@@ -343,18 +340,12 @@ export function RootView({
|
|
|
343
340
|
}
|
|
344
341
|
|
|
345
342
|
function GitPaneInPaneMode({ ratio }: { ratio: number }) {
|
|
346
|
-
const
|
|
343
|
+
const bg = useBg('sideBar.background')
|
|
347
344
|
// Ratio maps to a fixed column count (20..80), mirroring the reservation in
|
|
348
345
|
// use-terminal-resize so the terminal-content area stays in sync.
|
|
349
346
|
const width = Math.max(20, Math.min(80, Math.round(ratio * 80)))
|
|
350
347
|
return (
|
|
351
|
-
<box
|
|
352
|
-
flexDirection="column"
|
|
353
|
-
width={width}
|
|
354
|
-
flexShrink={0}
|
|
355
|
-
backgroundColor={theme.colors['sideBar.background']}
|
|
356
|
-
overflow="hidden"
|
|
357
|
-
>
|
|
348
|
+
<box flexDirection="column" width={width} flexShrink={0} backgroundColor={bg} overflow="hidden">
|
|
358
349
|
<GitPaneWidget pollingEnabled />
|
|
359
350
|
</box>
|
|
360
351
|
)
|
|
@@ -114,17 +114,17 @@ export function getStatusBarModel(
|
|
|
114
114
|
function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null {
|
|
115
115
|
switch (modalType) {
|
|
116
116
|
case 'help':
|
|
117
|
-
return 'modal.help'
|
|
117
|
+
return 'modal.help.filtering'
|
|
118
118
|
case 'new-tab':
|
|
119
|
-
return 'modal.new-tab'
|
|
119
|
+
return 'modal.new-tab.command-edit'
|
|
120
120
|
case 'session-picker':
|
|
121
|
-
return 'modal.session-picker'
|
|
121
|
+
return 'modal.session-picker.filtering'
|
|
122
122
|
case 'snippet-picker':
|
|
123
|
-
return 'modal.snippet-picker'
|
|
123
|
+
return 'modal.snippet-picker.filtering'
|
|
124
124
|
case 'split-picker':
|
|
125
125
|
return 'modal.split-picker'
|
|
126
126
|
case 'theme-picker':
|
|
127
|
-
return 'modal.theme-picker'
|
|
127
|
+
return 'modal.theme-picker.filtering'
|
|
128
128
|
case 'update-available':
|
|
129
129
|
return 'modal.update-available'
|
|
130
130
|
default:
|
package/src/ui/theme-store.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { useStore } from 'zustand'
|
|
2
2
|
import { createStore } from 'zustand/vanilla'
|
|
3
3
|
|
|
4
|
-
import { type Theme, type ThemeId, THEMES } from './themes'
|
|
4
|
+
import { type Theme, type ThemeColorMap, type ThemeId, THEMES } from './themes'
|
|
5
5
|
|
|
6
6
|
interface ThemeStore {
|
|
7
7
|
theme: Theme
|
|
8
|
+
transparent: boolean
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const DEFAULT = THEMES['aimux']
|
|
11
12
|
if (!DEFAULT) throw new Error('default theme missing')
|
|
12
13
|
|
|
13
|
-
const themeStore = createStore<ThemeStore>(() => ({ theme: DEFAULT }))
|
|
14
|
+
const themeStore = createStore<ThemeStore>(() => ({ theme: DEFAULT, transparent: false }))
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Subscribe to the active theme. Pass a selector to subscribe to a narrower
|
|
@@ -34,3 +35,26 @@ export function applyTheme(id: ThemeId): void {
|
|
|
34
35
|
if (!entry) return
|
|
35
36
|
themeStore.setState({ theme: entry })
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
/** Subscribe to the transparent-mode flag. */
|
|
40
|
+
export function useTransparent(): boolean {
|
|
41
|
+
return useStore(themeStore, (s) => s.transparent)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getTransparent(): boolean {
|
|
45
|
+
return themeStore.getState().transparent
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setTransparent(value: boolean): void {
|
|
49
|
+
if (themeStore.getState().transparent === value) return
|
|
50
|
+
themeStore.setState({ transparent: value })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a background color through the transparent-mode flag. Returns
|
|
55
|
+
* `undefined` when transparent mode is on, letting the terminal emulator's
|
|
56
|
+
* own background show through.
|
|
57
|
+
*/
|
|
58
|
+
export function useBg(key: keyof ThemeColorMap): string | undefined {
|
|
59
|
+
return useStore(themeStore, (s) => (s.transparent ? undefined : s.theme.colors[key]))
|
|
60
|
+
}
|
package/src/ui/theme.ts
CHANGED
|
@@ -2,4 +2,12 @@
|
|
|
2
2
|
// React components should import `useTheme` from `./theme-store`; non-React
|
|
3
3
|
// callers use `getCurrentTheme()`. `applyTheme` moved to the store module.
|
|
4
4
|
|
|
5
|
-
export {
|
|
5
|
+
export {
|
|
6
|
+
applyTheme,
|
|
7
|
+
getCurrentTheme,
|
|
8
|
+
getTransparent,
|
|
9
|
+
setTransparent,
|
|
10
|
+
useBg,
|
|
11
|
+
useTheme,
|
|
12
|
+
useTransparent,
|
|
13
|
+
} from './theme-store'
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useTheme } from '../theme'
|
|
2
|
-
import { Surface } from './surface'
|
|
3
|
-
|
|
4
|
-
interface ModalFilterBarProps {
|
|
5
|
-
filter: string | null
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function ModalFilterBar({ filter }: ModalFilterBarProps) {
|
|
9
|
-
const theme = useTheme()
|
|
10
|
-
if (filter === null) {
|
|
11
|
-
return null
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<Surface tone="input" paddingLeft={1} paddingRight={1} paddingTop={1} paddingBottom={1}>
|
|
16
|
-
<text fg={theme.colors['editor.foreground']}>/{filter}_</text>
|
|
17
|
-
</Surface>
|
|
18
|
-
)
|
|
19
|
-
}
|