@brimveyn/aimux 1.1.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/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +79 -0
- package/src/app-runtime/backend-attach-runtime.ts +135 -0
- package/src/app-runtime/backend-runtime-events.ts +78 -0
- package/src/app-runtime/click-selection-resolver.ts +130 -0
- package/src/app-runtime/pty-write.ts +32 -0
- package/src/app-runtime/render-invalidation.ts +20 -0
- package/src/app-runtime/selection-clipboard.ts +69 -0
- package/src/app-runtime/selection-scroll.ts +143 -0
- package/src/app-runtime/session-actions.ts +170 -0
- package/src/app-runtime/side-effects.ts +434 -0
- package/src/app-runtime/snippet-actions.ts +90 -0
- package/src/app-runtime/split-drag-controller.ts +15 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +86 -0
- package/src/app-runtime/terminal-mouse-adapter.ts +31 -0
- package/src/app-runtime/use-backend-runtime.ts +99 -0
- package/src/app-runtime/use-directory-search.ts +52 -0
- package/src/app-runtime/use-mouse-handlers.ts +183 -0
- package/src/app-runtime/use-renderer-bindings.ts +163 -0
- package/src/app-runtime/use-terminal-resize.ts +141 -0
- package/src/app-runtime/use-workspace-autosave.ts +39 -0
- package/src/app.tsx +247 -0
- package/src/config/loader.ts +36 -0
- package/src/config.ts +146 -0
- package/src/daemon/daemon.ts +236 -0
- package/src/daemon/runtime-paths.ts +69 -0
- package/src/daemon/session-manager.ts +109 -0
- package/src/daemon/session-registry.ts +207 -0
- package/src/debug/input-log.ts +29 -0
- package/src/doctor.ts +106 -0
- package/src/git/git-poller.ts +49 -0
- package/src/git/git-status.ts +183 -0
- package/src/index.tsx +56 -0
- package/src/input/keymap/build-handlers.ts +34 -0
- package/src/input/keymap/key-chord.ts +201 -0
- package/src/input/keymap/keymap-mode-handler.ts +88 -0
- package/src/input/keymap/sequence-resolver.ts +117 -0
- package/src/input/keymap/trie.ts +103 -0
- package/src/input/modes/bridge.ts +58 -0
- package/src/input/modes/handlers/index.ts +18 -0
- package/src/input/modes/handlers/shared.ts +70 -0
- package/src/input/modes/registry.ts +34 -0
- package/src/input/modes/transitions.ts +37 -0
- package/src/input/modes/types.ts +63 -0
- package/src/input/mouse-forwarding.ts +98 -0
- package/src/input/multi-click-detector.ts +55 -0
- package/src/input/paste.ts +12 -0
- package/src/input/raw-input-handler.ts +191 -0
- package/src/input/terminal-text-extraction.ts +78 -0
- package/src/ipc/protocol.ts +341 -0
- package/src/platform/clipboard.ts +13 -0
- package/src/platform/daemon-control.ts +62 -0
- package/src/platform/id.ts +7 -0
- package/src/platform/project-search.ts +75 -0
- package/src/pty/command-registry.ts +69 -0
- package/src/pty/pty-manager.ts +268 -0
- package/src/pty/terminal-snapshot.ts +210 -0
- package/src/restart-daemon.ts +28 -0
- package/src/session-backend/bootstrap.ts +107 -0
- package/src/session-backend/local-session-backend.ts +164 -0
- package/src/session-backend/remote-session-backend.ts +373 -0
- package/src/session-backend/types.ts +47 -0
- package/src/state/app-store.ts +19 -0
- package/src/state/layout-resize.ts +56 -0
- package/src/state/layout-tree.ts +323 -0
- package/src/state/reducers/git-panel-state.ts +102 -0
- package/src/state/reducers/modal-state.ts +358 -0
- package/src/state/reducers/session-state.ts +86 -0
- package/src/state/reducers/tab-state.ts +531 -0
- package/src/state/reducers/ui-state.ts +29 -0
- package/src/state/selectors.ts +30 -0
- package/src/state/session-catalog.ts +96 -0
- package/src/state/session-persistence.ts +210 -0
- package/src/state/snippet-catalog.ts +90 -0
- package/src/state/store.ts +93 -0
- package/src/state/terminal-modes.ts +11 -0
- package/src/state/types.ts +367 -0
- package/src/state/validation.ts +154 -0
- package/src/state/workspace-save.ts +33 -0
- package/src/ui/components/create-session-modal.tsx +100 -0
- package/src/ui/components/git-panel.tsx +200 -0
- package/src/ui/components/help-modal.tsx +79 -0
- package/src/ui/components/input-field.tsx +18 -0
- package/src/ui/components/list-item.tsx +30 -0
- package/src/ui/components/modal-filter-bar.tsx +18 -0
- package/src/ui/components/modal-shell.tsx +47 -0
- package/src/ui/components/new-tab-modal.tsx +52 -0
- package/src/ui/components/pending-chord-indicator.tsx +41 -0
- package/src/ui/components/session-name-modal.tsx +15 -0
- package/src/ui/components/session-picker-modal.tsx +93 -0
- package/src/ui/components/sidebar-group-metadata.ts +44 -0
- package/src/ui/components/sidebar-scroll.ts +33 -0
- package/src/ui/components/sidebar.tsx +225 -0
- package/src/ui/components/snippet-editor-modal.tsx +39 -0
- package/src/ui/components/snippet-picker-modal.tsx +56 -0
- package/src/ui/components/split-layout.tsx +201 -0
- package/src/ui/components/status-bar.tsx +60 -0
- package/src/ui/components/surface.tsx +63 -0
- package/src/ui/components/tab-item.tsx +118 -0
- package/src/ui/components/terminal-pane.tsx +215 -0
- package/src/ui/components/theme-picker-modal.tsx +35 -0
- package/src/ui/components/use-sidebar-auto-scroll.ts +63 -0
- package/src/ui/components/use-sidebar-branch.ts +36 -0
- package/src/ui/directory-search.ts +1 -0
- package/src/ui/git-branch.ts +11 -0
- package/src/ui/path-format.ts +6 -0
- package/src/ui/root.tsx +261 -0
- package/src/ui/status-bar-model.ts +87 -0
- package/src/ui/theme.ts +9 -0
- package/src/ui/themes.ts +255 -0
- package/src/ui/ui-tokens.ts +11 -0
- package/src/update.ts +62 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import type { TabSession } from '../../state/types'
|
|
4
|
+
|
|
5
|
+
import { theme } from '../theme'
|
|
6
|
+
|
|
7
|
+
interface TabItemProps {
|
|
8
|
+
id?: string
|
|
9
|
+
tab: TabSession
|
|
10
|
+
active: boolean
|
|
11
|
+
focused: boolean
|
|
12
|
+
isFocusedInput: boolean
|
|
13
|
+
inLayout?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getStatusColor(status: TabSession['status']): string {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'running':
|
|
19
|
+
return theme.success
|
|
20
|
+
case 'disconnected':
|
|
21
|
+
return theme.warning
|
|
22
|
+
case 'error':
|
|
23
|
+
return theme.danger
|
|
24
|
+
case 'exited':
|
|
25
|
+
return theme.warning
|
|
26
|
+
default:
|
|
27
|
+
return theme.textMuted
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const BUSY_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
32
|
+
const BUSY_FRAME_INTERVAL_MS = 80
|
|
33
|
+
|
|
34
|
+
function getIndicator(active: boolean, focused: boolean, inLayout: boolean): string {
|
|
35
|
+
if (active) {
|
|
36
|
+
return focused ? '›' : '•'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return inLayout ? '·' : ' '
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean): string {
|
|
43
|
+
if (active) {
|
|
44
|
+
return focused ? theme.accent : theme.accentAlt
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return inLayout ? theme.textMuted : theme.dim
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function BusyIndicator() {
|
|
51
|
+
const [frame, setFrame] = useState(0)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const interval = setInterval(() => {
|
|
55
|
+
setFrame((prev) => (prev + 1) % BUSY_FRAMES.length)
|
|
56
|
+
}, BUSY_FRAME_INTERVAL_MS)
|
|
57
|
+
return () => clearInterval(interval)
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
return <text fg={theme.accent}>{BUSY_FRAMES[frame]} busy</text>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ActivityIndicator({ isFocusedInput, tab }: { tab: TabSession; isFocusedInput: boolean }) {
|
|
64
|
+
if (tab.status === 'error') {
|
|
65
|
+
return <text fg={theme.danger}>✗ error</text>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (tab.status === 'disconnected') {
|
|
69
|
+
return <text fg={theme.warning}>⏸ restore</text>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (tab.status === 'exited') {
|
|
73
|
+
return <text fg={theme.warning}>⏹ exited</text>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (isFocusedInput) {
|
|
77
|
+
return <text fg={theme.borderActive}>▸ focused</text>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (tab.activity === 'busy') {
|
|
81
|
+
return <BusyIndicator />
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (tab.activity === 'idle') {
|
|
85
|
+
return <text fg={theme.success}>● idle</text>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return <text fg={getStatusColor(tab.status)}>{tab.status}</text>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function TabItem({ active, focused, id, inLayout, isFocusedInput, tab }: TabItemProps) {
|
|
92
|
+
const label = tab.command.split(' ')[0]
|
|
93
|
+
const isInLayout = inLayout ?? false
|
|
94
|
+
const indicator = getIndicator(active, focused, isInLayout)
|
|
95
|
+
const indicatorColor = getIndicatorColor(active, focused, isInLayout)
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<box
|
|
99
|
+
id={id}
|
|
100
|
+
paddingLeft={1}
|
|
101
|
+
paddingRight={1}
|
|
102
|
+
paddingTop={0}
|
|
103
|
+
paddingBottom={0}
|
|
104
|
+
backgroundColor={active ? theme.panelHighlight : undefined}
|
|
105
|
+
flexDirection="column"
|
|
106
|
+
gap={0}
|
|
107
|
+
>
|
|
108
|
+
<box flexDirection="row">
|
|
109
|
+
<text fg={indicatorColor}>{indicator} </text>
|
|
110
|
+
<text fg={active ? theme.text : theme.textMuted}>{tab.title}</text>
|
|
111
|
+
</box>
|
|
112
|
+
<box flexDirection="row">
|
|
113
|
+
<text fg={theme.textMuted}> {label} </text>
|
|
114
|
+
<ActivityIndicator tab={tab} isFocusedInput={isFocusedInput} />
|
|
115
|
+
</box>
|
|
116
|
+
</box>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { TerminalContentOrigin } from '../../input/raw-input-handler'
|
|
5
|
+
import type { TabSession, TerminalSpan } from '../../state/types'
|
|
6
|
+
|
|
7
|
+
import { logInputDebug } from '../../debug/input-log'
|
|
8
|
+
import { theme } from '../theme'
|
|
9
|
+
|
|
10
|
+
interface TerminalPaneProps {
|
|
11
|
+
tab?: TabSession
|
|
12
|
+
tabId?: string
|
|
13
|
+
focusMode: import('../../state/types').FocusMode
|
|
14
|
+
isActive?: boolean
|
|
15
|
+
contentOrigin: TerminalContentOrigin
|
|
16
|
+
mouseForwardingEnabled: boolean
|
|
17
|
+
localScrollbackEnabled: boolean
|
|
18
|
+
onTerminalMouseEvent: (event: OtuiMouseEvent, origin: TerminalContentOrigin) => void
|
|
19
|
+
onTerminalScrollEvent: (event: OtuiMouseEvent) => void
|
|
20
|
+
onTerminalClick?: (event: OtuiMouseEvent, origin: TerminalContentOrigin, tabId?: string) => void
|
|
21
|
+
onPaneActivate?: (tabId: string) => void
|
|
22
|
+
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
23
|
+
onSeparatorDragEnd?: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTitle(
|
|
27
|
+
tab: TabSession | undefined,
|
|
28
|
+
isActive: boolean,
|
|
29
|
+
focusMode: TerminalPaneProps['focusMode']
|
|
30
|
+
): string {
|
|
31
|
+
if (!tab) {
|
|
32
|
+
return 'No active session'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isActive && focusMode === 'terminal-input') {
|
|
36
|
+
return `● ${tab.title} · ${tab.status}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isActive) {
|
|
40
|
+
return `▸ ${tab.title} · ${tab.status}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `${tab.title} · ${tab.status}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
47
|
+
if (isActive && focusMode === 'terminal-input') {
|
|
48
|
+
return theme.borderActive
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (isActive) {
|
|
52
|
+
return theme.accentAlt
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return theme.dim
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
59
|
+
let node: ReactNode = span.text
|
|
60
|
+
|
|
61
|
+
if (span.underline) {
|
|
62
|
+
node = <u>{node}</u>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (span.italic) {
|
|
66
|
+
node = <em>{node}</em>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (span.bold) {
|
|
70
|
+
node = <strong>{node}</strong>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<span key={key} fg={span.fg ?? theme.text} bg={span.bg}>
|
|
75
|
+
{node}
|
|
76
|
+
</span>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderViewport(tab: TabSession): ReactNode {
|
|
81
|
+
if (tab.viewport && tab.viewport.lines.length > 0) {
|
|
82
|
+
const lines = tab.viewport.lines
|
|
83
|
+
return (
|
|
84
|
+
<text fg={theme.text}>
|
|
85
|
+
{lines.map((line, lineIndex) => (
|
|
86
|
+
<span key={`line-${lineIndex}`}>
|
|
87
|
+
{line.spans.map((span, spanIndex) => renderSpan(span, `s-${spanIndex}`))}
|
|
88
|
+
{lineIndex < lines.length - 1 ? '\n' : ''}
|
|
89
|
+
</span>
|
|
90
|
+
))}
|
|
91
|
+
</text>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<text fg={theme.text}>
|
|
97
|
+
{tab.buffer.length > 0 ? tab.buffer : 'Waiting for session output...'}
|
|
98
|
+
</text>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function TerminalPane({
|
|
103
|
+
contentOrigin,
|
|
104
|
+
focusMode,
|
|
105
|
+
isActive,
|
|
106
|
+
localScrollbackEnabled,
|
|
107
|
+
mouseForwardingEnabled,
|
|
108
|
+
onPaneActivate,
|
|
109
|
+
onSeparatorDrag,
|
|
110
|
+
onSeparatorDragEnd,
|
|
111
|
+
onTerminalClick,
|
|
112
|
+
onTerminalMouseEvent,
|
|
113
|
+
onTerminalScrollEvent,
|
|
114
|
+
tab,
|
|
115
|
+
tabId,
|
|
116
|
+
}: TerminalPaneProps) {
|
|
117
|
+
const paneIsActive = isActive ?? true
|
|
118
|
+
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
119
|
+
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
120
|
+
const forwardMouseEvent = (event: OtuiMouseEvent) => {
|
|
121
|
+
if (event.type === 'down') {
|
|
122
|
+
logInputDebug('pane.mouseDown', {
|
|
123
|
+
button: event.button,
|
|
124
|
+
canForward: canForwardMouse,
|
|
125
|
+
eventType: event.type,
|
|
126
|
+
tabId,
|
|
127
|
+
willClick: !canForwardMouse && !!tab && event.button === 0 && !!onTerminalClick,
|
|
128
|
+
x: event.x,
|
|
129
|
+
y: event.y,
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
if (event.type === 'drag' && onSeparatorDrag?.(event)) {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (event.type === 'up') {
|
|
137
|
+
onSeparatorDragEnd?.()
|
|
138
|
+
}
|
|
139
|
+
if (tabId && onPaneActivate && event.type === 'down') {
|
|
140
|
+
onPaneActivate(tabId)
|
|
141
|
+
}
|
|
142
|
+
if (!canForwardMouse) {
|
|
143
|
+
if (tab && event.type === 'down' && event.button === 0 && onTerminalClick) {
|
|
144
|
+
onTerminalClick(event, contentOrigin, tabId)
|
|
145
|
+
}
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
event.preventDefault()
|
|
150
|
+
event.stopPropagation()
|
|
151
|
+
onTerminalMouseEvent(event, contentOrigin)
|
|
152
|
+
}
|
|
153
|
+
const forwardScrollEvent = (event: OtuiMouseEvent) => {
|
|
154
|
+
if (!canForwardMouse && !canUseLocalScrollback) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
event.stopPropagation()
|
|
160
|
+
|
|
161
|
+
if (canForwardMouse) {
|
|
162
|
+
onTerminalMouseEvent(event, contentOrigin)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
onTerminalScrollEvent(event)
|
|
167
|
+
}
|
|
168
|
+
return (
|
|
169
|
+
<box flexDirection="column" flexGrow={1} gap={0}>
|
|
170
|
+
<box
|
|
171
|
+
border
|
|
172
|
+
borderColor={getBorderColor(paneIsActive, focusMode)}
|
|
173
|
+
title={getTitle(tab, paneIsActive, focusMode)}
|
|
174
|
+
padding={0}
|
|
175
|
+
flexDirection="column"
|
|
176
|
+
flexGrow={1}
|
|
177
|
+
backgroundColor={theme.background}
|
|
178
|
+
onMouseDrag={forwardMouseEvent}
|
|
179
|
+
onMouseScroll={forwardScrollEvent}
|
|
180
|
+
onMouseUp={forwardMouseEvent}
|
|
181
|
+
>
|
|
182
|
+
{!tab ? (
|
|
183
|
+
<box flexGrow={1} justifyContent="center" alignItems="center" flexDirection="column">
|
|
184
|
+
<text fg={theme.dim}>· · ·</text>
|
|
185
|
+
<text fg={theme.textMuted}> </text>
|
|
186
|
+
<box flexDirection="row">
|
|
187
|
+
<text fg={theme.textMuted}>Press </text>
|
|
188
|
+
<text fg={theme.accent}>Ctrl+n</text>
|
|
189
|
+
<text fg={theme.textMuted}> to launch an assistant</text>
|
|
190
|
+
</box>
|
|
191
|
+
</box>
|
|
192
|
+
) : (
|
|
193
|
+
<box
|
|
194
|
+
flexDirection="column"
|
|
195
|
+
flexGrow={1}
|
|
196
|
+
width="100%"
|
|
197
|
+
onMouseDown={forwardMouseEvent}
|
|
198
|
+
onMouseUp={forwardMouseEvent}
|
|
199
|
+
onMouseDrag={forwardMouseEvent}
|
|
200
|
+
onMouseScroll={forwardScrollEvent}
|
|
201
|
+
>
|
|
202
|
+
{renderViewport(tab)}
|
|
203
|
+
</box>
|
|
204
|
+
)}
|
|
205
|
+
</box>
|
|
206
|
+
{tab?.status === 'exited' && tab.exitCode !== undefined ? (
|
|
207
|
+
<text fg={theme.warning}>Process exited with code {tab.exitCode}</text>
|
|
208
|
+
) : null}
|
|
209
|
+
{tab?.status === 'disconnected' ? (
|
|
210
|
+
<text fg={theme.warning}>Restored snapshot. Press Ctrl+r to restart this session.</text>
|
|
211
|
+
) : null}
|
|
212
|
+
{tab?.errorMessage ? <text fg={theme.danger}>{tab.errorMessage}</text> : null}
|
|
213
|
+
</box>
|
|
214
|
+
)
|
|
215
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { theme } from '../theme'
|
|
2
|
+
import { THEME_IDS, type ThemeId, THEMES } from '../themes'
|
|
3
|
+
import { uiTokens } from '../ui-tokens'
|
|
4
|
+
import { ListItem } from './list-item'
|
|
5
|
+
import { ModalShell } from './modal-shell'
|
|
6
|
+
|
|
7
|
+
interface ThemePickerModalProps {
|
|
8
|
+
selectedIndex: number
|
|
9
|
+
currentThemeId: ThemeId
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ThemePickerModal({ currentThemeId, selectedIndex }: ThemePickerModalProps) {
|
|
13
|
+
return (
|
|
14
|
+
<ModalShell
|
|
15
|
+
title="Select theme"
|
|
16
|
+
help="j/k move, Enter confirm, Esc cancel."
|
|
17
|
+
width={uiTokens.modalWidth.md}
|
|
18
|
+
listGap={0}
|
|
19
|
+
>
|
|
20
|
+
{THEME_IDS.map((id, index) => {
|
|
21
|
+
const entry = THEMES[id]
|
|
22
|
+
const active = index === selectedIndex
|
|
23
|
+
const isCurrent = id === currentThemeId
|
|
24
|
+
return (
|
|
25
|
+
<ListItem
|
|
26
|
+
key={id}
|
|
27
|
+
active={active}
|
|
28
|
+
title={<text fg={active ? theme.text : theme.textMuted}>{entry.name}</text>}
|
|
29
|
+
trailing={isCurrent ? <text fg={theme.accent}>current</text> : undefined}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
})}
|
|
33
|
+
</ModalShell>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
|
|
5
|
+
import { getSidebarScrollTarget } from './sidebar-scroll'
|
|
6
|
+
|
|
7
|
+
interface UseSidebarAutoScrollOptions {
|
|
8
|
+
scrollRef: React.RefObject<ScrollBoxRenderable | null>
|
|
9
|
+
visible: boolean
|
|
10
|
+
activeTabId: string | null
|
|
11
|
+
activeIndex: number
|
|
12
|
+
tabCount: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useSidebarAutoScroll({
|
|
16
|
+
activeIndex,
|
|
17
|
+
activeTabId,
|
|
18
|
+
scrollRef,
|
|
19
|
+
tabCount,
|
|
20
|
+
visible,
|
|
21
|
+
}: UseSidebarAutoScrollOptions): void {
|
|
22
|
+
const previousActiveIndexRef = useRef(-1)
|
|
23
|
+
const previousVisibilityRef = useRef(visible)
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!visible) {
|
|
27
|
+
previousVisibilityRef.current = false
|
|
28
|
+
previousActiveIndexRef.current = activeIndex
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const scrollbox = scrollRef.current
|
|
33
|
+
if (!scrollbox) {
|
|
34
|
+
previousVisibilityRef.current = visible
|
|
35
|
+
previousActiveIndexRef.current = activeIndex
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!previousVisibilityRef.current && activeTabId) {
|
|
40
|
+
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
41
|
+
previousVisibilityRef.current = true
|
|
42
|
+
previousActiveIndexRef.current = activeIndex
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const scrollTarget = getSidebarScrollTarget({
|
|
47
|
+
nextActiveIndex: activeIndex,
|
|
48
|
+
previousActiveIndex: previousActiveIndexRef.current,
|
|
49
|
+
tabCount,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (scrollTarget === 'top') {
|
|
53
|
+
scrollbox.scrollTo({ x: 0, y: 0 })
|
|
54
|
+
} else if (scrollTarget === 'bottom') {
|
|
55
|
+
scrollbox.scrollTo({ x: 0, y: scrollbox.scrollHeight })
|
|
56
|
+
} else if (scrollTarget === 'active-item' && activeTabId) {
|
|
57
|
+
scrollbox.scrollChildIntoView(`sidebar-tab-${activeTabId}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
previousVisibilityRef.current = visible
|
|
61
|
+
previousActiveIndexRef.current = activeIndex
|
|
62
|
+
}, [activeIndex, activeTabId, scrollRef, tabCount, visible])
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { getCurrentBranch } from '../git-branch'
|
|
4
|
+
|
|
5
|
+
const BRANCH_POLL_INTERVAL_MS = 5_000
|
|
6
|
+
|
|
7
|
+
export function useSidebarBranch(projectPath: string | undefined): string | null {
|
|
8
|
+
const [branch, setBranch] = useState<string | null>(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!projectPath) {
|
|
12
|
+
setBranch(null)
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let isCurrent = true
|
|
17
|
+
const updateBranch = async () => {
|
|
18
|
+
const nextBranch = await getCurrentBranch(projectPath)
|
|
19
|
+
if (isCurrent) {
|
|
20
|
+
setBranch(nextBranch)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
void updateBranch()
|
|
25
|
+
const interval = setInterval(() => {
|
|
26
|
+
void updateBranch()
|
|
27
|
+
}, BRANCH_POLL_INTERVAL_MS)
|
|
28
|
+
|
|
29
|
+
return () => {
|
|
30
|
+
isCurrent = false
|
|
31
|
+
clearInterval(interval)
|
|
32
|
+
}
|
|
33
|
+
}, [projectPath])
|
|
34
|
+
|
|
35
|
+
return branch
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { searchProjectDirectories as searchDirectories } from '../platform/project-search'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { $ } from 'bun'
|
|
2
|
+
|
|
3
|
+
export async function getCurrentBranch(cwd: string): Promise<string | null> {
|
|
4
|
+
try {
|
|
5
|
+
const result = await $`git -C ${cwd} branch --show-current`.quiet()
|
|
6
|
+
const branch = result.text().trim()
|
|
7
|
+
return branch || null
|
|
8
|
+
} catch {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
}
|