@brimveyn/aimux 1.9.0 → 1.9.2
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/package.json +2 -2
- package/src/app-runtime/side-effects.ts +13 -3
- package/src/app.tsx +26 -17
- package/src/git/git-poller.ts +56 -3
- package/src/git/repo-discovery.ts +95 -0
- package/src/git/use-repo-discovery.ts +28 -0
- package/src/input/keymap/describe-bindings.ts +63 -2
- package/src/pty/terminal-snapshot.ts +7 -5
- package/src/state/reducers/git-mode-state.ts +58 -0
- package/src/state/reducers/git-panel-state.ts +3 -0
- package/src/state/reducers/multi-repo-state.ts +60 -0
- package/src/state/store.ts +6 -0
- package/src/state/types.ts +38 -6
- package/src/ui/breaking-update-screen.tsx +2 -2
- package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
- package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
- package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
- package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
- package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +9 -18
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
- package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
- package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +32 -10
- package/src/ui/components/{git-view.tsx → git/git-view.tsx} +73 -38
- package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
- package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +6 -4
- package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
- package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
- package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
- package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
- package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
- package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
- package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
- package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
- package/src/ui/components/{ai-usage-modal.tsx → modals/app/ai-usage-modal.tsx} +10 -6
- package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
- package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
- package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
- package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
- package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
- package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
- package/src/ui/components/modals/shared/form.tsx +164 -0
- package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
- package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
- package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
- package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
- package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
- package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
- package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +3 -3
- package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
- package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
- package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
- package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
- package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
- package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
- package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
- package/src/ui/root.tsx +24 -22
- package/src/ui/theme-store.ts +30 -26
- package/src/ui/themes.ts +1 -0
- package/src/ui/components/create-session-modal.tsx +0 -96
- package/src/ui/components/diff-renderer/build-rows.ts +0 -349
- package/src/ui/components/diff-renderer/highlight.ts +0 -44
- package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
- package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
- package/src/ui/components/session-name-modal.tsx +0 -11
- package/src/ui/components/snippet-editor-modal.tsx +0 -40
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
- /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
- /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
- /package/src/ui/components/{use-sidebar-auto-scroll.ts → layout/sidebar/use-sidebar-auto-scroll.ts} +0 -0
|
@@ -2,18 +2,22 @@ import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
2
2
|
|
|
3
3
|
import { useMemo, useRef, useState } from 'react'
|
|
4
4
|
|
|
5
|
-
import type { SessionRecord, SessionStatus } from '
|
|
5
|
+
import type { SessionRecord, SessionStatus } from '../../../state/types'
|
|
6
6
|
|
|
7
|
-
import { useAppStore } from '
|
|
8
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
7
|
+
import { useAppStore } from '../../../state/app-store'
|
|
8
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
9
9
|
// eslint-disable-next-line no-duplicate-imports
|
|
10
|
-
import { IDLE_SESSION_STATUS } from '
|
|
11
|
-
import { useBusySpinner } from '
|
|
12
|
-
import { moveIdToIdPosition, orderSessionsForDisplay } from '
|
|
13
|
-
import { useBg, useTokens } from '
|
|
14
|
-
import { ContextMenuBox } from '
|
|
10
|
+
import { IDLE_SESSION_STATUS } from '../../../state/types'
|
|
11
|
+
import { useBusySpinner } from '../../hooks/use-busy-spinner'
|
|
12
|
+
import { moveIdToIdPosition, orderSessionsForDisplay } from '../../session-ordering'
|
|
13
|
+
import { useBg, useTokens } from '../../theme'
|
|
14
|
+
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface SessionBarProps {
|
|
17
|
+
forceVisible?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SessionBar({ forceVisible = false }: SessionBarProps) {
|
|
17
21
|
const t = useTokens()
|
|
18
22
|
const headerBg = useBg('elevated')
|
|
19
23
|
const sessions = useAppStore((s) => s.sessions)
|
|
@@ -27,7 +31,7 @@ export function SessionBar() {
|
|
|
27
31
|
const chipRefs = useRef(new Map<string, BoxRenderable>())
|
|
28
32
|
|
|
29
33
|
const ordered = useMemo(() => orderSessionsForDisplay(sessions), [sessions])
|
|
30
|
-
if (!bar.visible || ordered.length === 0) return null
|
|
34
|
+
if ((!bar.visible && !forceVisible) || ordered.length === 0) return null
|
|
31
35
|
|
|
32
36
|
const visibleSessions =
|
|
33
37
|
dragOrder !== null
|
|
@@ -103,6 +107,7 @@ export function SessionBar() {
|
|
|
103
107
|
<box
|
|
104
108
|
width="100%"
|
|
105
109
|
flexDirection="row"
|
|
110
|
+
flexShrink={0}
|
|
106
111
|
paddingLeft={1}
|
|
107
112
|
paddingRight={1}
|
|
108
113
|
backgroundColor={headerBg}
|
|
@@ -205,6 +210,7 @@ function SessionChip({
|
|
|
205
210
|
const idleColor = t.palette.success
|
|
206
211
|
const workingColor = t.palette.primary
|
|
207
212
|
const waitingColor = t.palette.warning
|
|
213
|
+
const [hovered, setHovered] = useState(false)
|
|
208
214
|
|
|
209
215
|
return (
|
|
210
216
|
<ContextMenuBox
|
|
@@ -214,6 +220,8 @@ function SessionChip({
|
|
|
214
220
|
paddingRight={1}
|
|
215
221
|
backgroundColor={bgColor}
|
|
216
222
|
rightClickMenu={rightClickMenu}
|
|
223
|
+
onMouseOver={() => setHovered(true)}
|
|
224
|
+
onMouseOut={() => setHovered(false)}
|
|
217
225
|
onMouseDown={(e) => {
|
|
218
226
|
e.preventDefault()
|
|
219
227
|
onMouseDown(e)
|
|
@@ -247,9 +255,24 @@ function SessionChip({
|
|
|
247
255
|
<text fg={labelColor} selectable={false}>
|
|
248
256
|
[{index}] {session.name}
|
|
249
257
|
</text>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
258
|
+
{hovered ? (
|
|
259
|
+
<box
|
|
260
|
+
paddingLeft={1}
|
|
261
|
+
onMouseDown={(event) => {
|
|
262
|
+
event.preventDefault()
|
|
263
|
+
event.stopPropagation()
|
|
264
|
+
runSideEffectGlobal({ sessionId: session.id, type: 'delete-session' })
|
|
265
|
+
}}
|
|
266
|
+
>
|
|
267
|
+
<text fg={t.muted} selectable={false}>
|
|
268
|
+
×
|
|
269
|
+
</text>
|
|
270
|
+
</box>
|
|
271
|
+
) : (
|
|
272
|
+
<text fg={t.hover} selectable={false}>
|
|
273
|
+
{' '}
|
|
274
|
+
</text>
|
|
275
|
+
)}
|
|
253
276
|
</ContextMenuBox>
|
|
254
277
|
)
|
|
255
278
|
}
|
package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { TabSession } from '
|
|
1
|
+
import type { TabSession } from '../../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { allLeafIds, type LayoutNode } from '
|
|
3
|
+
import { allLeafIds, type LayoutNode } from '../../../../state/layout-tree'
|
|
4
4
|
|
|
5
5
|
export interface TabGroupInfo {
|
|
6
6
|
inLayout: boolean
|
|
@@ -5,12 +5,12 @@ import {
|
|
|
5
5
|
} from '@opentui/core'
|
|
6
6
|
import { memo, useMemo, useRef } from 'react'
|
|
7
7
|
|
|
8
|
-
import { useAppStore } from '
|
|
9
|
-
import { dispatchGlobal } from '
|
|
10
|
-
import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
9
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
10
|
+
import { getCurrentTokens, type ThemeTokens, useBg, useTokens } from '../../../theme'
|
|
11
|
+
import { buildGitPaneContextMenu } from '../../git/pane/git-pane-context-menu'
|
|
12
|
+
import { GitPaneWidget } from '../../git/pane/git-pane-widget'
|
|
13
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
14
14
|
import { buildTabGroupInfo } from './sidebar-group-metadata'
|
|
15
15
|
import { TabItem } from './tab-item'
|
|
16
16
|
import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
|
|
3
|
-
import type { TabSession } from '
|
|
3
|
+
import type { TabSession } from '../../../../state/types'
|
|
4
4
|
|
|
5
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
6
|
-
import { useBusySpinner } from '
|
|
7
|
-
import { getCurrentTokens, useTokens } from '
|
|
8
|
-
import { ContextMenuBox } from '
|
|
5
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
|
+
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
7
|
+
import { getCurrentTokens, useTokens } from '../../../theme'
|
|
8
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
9
9
|
|
|
10
10
|
interface TabItemProps {
|
|
11
11
|
id?: string
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
-
import type { TerminalContentOrigin } from '
|
|
4
|
-
import type { FocusMode, TabSession } from '
|
|
3
|
+
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
4
|
+
import type { FocusMode, TabSession } from '../../../state/types'
|
|
5
5
|
|
|
6
|
-
import { logInputDebug } from '
|
|
6
|
+
import { logInputDebug } from '../../../debug/input-log'
|
|
7
7
|
import {
|
|
8
8
|
computePaneRects,
|
|
9
9
|
type LayoutNode,
|
|
10
10
|
PANE_BORDER,
|
|
11
11
|
type PaneRect,
|
|
12
12
|
type SplitDirection,
|
|
13
|
-
} from '
|
|
14
|
-
import { useTokens } from '
|
|
13
|
+
} from '../../../state/layout-tree'
|
|
14
|
+
import { useTokens } from '../../theme'
|
|
15
15
|
import { TerminalPane } from './terminal-pane'
|
|
16
16
|
|
|
17
17
|
const PANE_CHROME = PANE_BORDER
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { AppState } from '
|
|
1
|
+
import type { AppState } from '../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { version as APP_VERSION } from '
|
|
4
|
-
import { useAppStore } from '
|
|
5
|
-
import { useKeymap } from '
|
|
6
|
-
import { getStatusBarModel } from '
|
|
7
|
-
import { getCurrentTokens, useBg, useTokens } from '
|
|
8
|
-
import { AIUsageIndicator } from '
|
|
3
|
+
import { version as APP_VERSION } from '../../../../package.json'
|
|
4
|
+
import { useAppStore } from '../../../state/app-store'
|
|
5
|
+
import { useKeymap } from '../../keymap-context'
|
|
6
|
+
import { getStatusBarModel } from '../../status-bar-model'
|
|
7
|
+
import { getCurrentTokens, useBg, useTokens } from '../../theme'
|
|
8
|
+
import { AIUsageIndicator } from '../overlays/ai-usage/ai-usage-indicator'
|
|
9
9
|
|
|
10
10
|
function getModeColor(focusMode: AppState['focusMode']): string {
|
|
11
11
|
const t = getCurrentTokens()
|
|
@@ -2,19 +2,19 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
|
2
2
|
|
|
3
3
|
import { memo, type ReactNode } from 'react'
|
|
4
4
|
|
|
5
|
-
import type { TerminalContentOrigin } from '
|
|
6
|
-
import type { TabSession, TerminalSnapshot, TerminalSpan } from '
|
|
5
|
+
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
|
+
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
7
|
|
|
8
|
-
import { logInputDebug } from '
|
|
9
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
10
|
-
import { type ContextMenuItem, openContextMenu } from '
|
|
11
|
-
import { getCurrentTokens, useBg, useTokens } from '
|
|
12
|
-
import { ContextMenuBox } from '
|
|
8
|
+
import { logInputDebug } from '../../../debug/input-log'
|
|
9
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
10
|
+
import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
|
|
11
|
+
import { getCurrentTokens, useBg, useTokens } from '../../theme'
|
|
12
|
+
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
13
13
|
|
|
14
14
|
interface TerminalPaneProps {
|
|
15
15
|
tab?: TabSession
|
|
16
16
|
tabId?: string
|
|
17
|
-
focusMode: import('
|
|
17
|
+
focusMode: import('../../../state/types').FocusMode
|
|
18
18
|
isActive?: boolean
|
|
19
19
|
contentOrigin: TerminalContentOrigin
|
|
20
20
|
mouseForwardingEnabled: boolean
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { AIUsageTool } from '@brimveyn/aimux-config'
|
|
2
2
|
import type { ReactNode } from 'react'
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import type {
|
|
5
|
+
UsagePaceStage,
|
|
6
|
+
UsageSnapshot,
|
|
7
|
+
UsageWindow,
|
|
8
|
+
} from '../../../../services/ai-usage/types'
|
|
9
|
+
|
|
10
|
+
import { useAIUsageStore } from '../../../../state/ai-usage-store'
|
|
11
|
+
import { useTokens } from '../../../theme'
|
|
12
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
13
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
10
14
|
|
|
11
15
|
const TOOL_TITLE: Record<AIUsageTool, string> = {
|
|
12
16
|
claude: 'Claude',
|
|
@@ -2,12 +2,12 @@ import type { ModeId } from '@brimveyn/aimux-config'
|
|
|
2
2
|
|
|
3
3
|
import { useLayoutEffect, useMemo } from 'react'
|
|
4
4
|
|
|
5
|
-
import { collectHelpEntries, HELP_MODE_LABELS } from '
|
|
6
|
-
import { dispatchGlobal } from '
|
|
7
|
-
import { useKeymap } from '
|
|
8
|
-
import { useTokens } from '
|
|
9
|
-
import { uiTokens } from '
|
|
10
|
-
import { Picker, type PickerItem } from '
|
|
5
|
+
import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
|
|
6
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
7
|
+
import { useKeymap } from '../../../keymap-context'
|
|
8
|
+
import { useTokens } from '../../../theme'
|
|
9
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
10
|
+
import { Picker, type PickerItem } from '../shared/picker'
|
|
11
11
|
|
|
12
12
|
interface HelpModalProps {
|
|
13
13
|
filter: string | null
|
package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useTokens } from '
|
|
2
|
-
import { uiTokens } from '
|
|
3
|
-
import { ListItem } from '
|
|
4
|
-
import { ModalShell } from '
|
|
1
|
+
import { useTokens } from '../../../theme'
|
|
2
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
3
|
+
import { ListItem } from '../../primitives/list-item'
|
|
4
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
5
5
|
|
|
6
6
|
interface UpdateAvailableModalProps {
|
|
7
7
|
currentVersion: string
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
|
|
2
2
|
import { useEffect, useState } from 'react'
|
|
3
3
|
|
|
4
|
-
import type { ModeId } from '
|
|
5
|
-
|
|
6
|
-
import { useAppStore } from '
|
|
7
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
8
|
-
import { useTokens } from '
|
|
9
|
-
import { uiTokens } from '
|
|
10
|
-
import { InputField } from '
|
|
11
|
-
import { ModalShell } from '
|
|
4
|
+
import type { ModeId } from '../../../../input/modes/types'
|
|
5
|
+
|
|
6
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
7
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
8
|
+
import { useTokens } from '../../../theme'
|
|
9
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
10
|
+
import { InputField } from '../../primitives/input-field'
|
|
11
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
12
12
|
|
|
13
13
|
interface GitCommitModalProps {
|
|
14
14
|
activeField: 'title' | 'body'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { DirectoryResult } from '../../../../state/types'
|
|
2
|
+
|
|
3
|
+
import { abbreviatePath } from '../../../path-format'
|
|
4
|
+
import { getCurrentTokens, useTokens } from '../../../theme'
|
|
5
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
6
|
+
import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
|
|
7
|
+
|
|
8
|
+
const VISIBLE_ROWS = 8
|
|
9
|
+
|
|
10
|
+
function getDirectoryResultIcon(result: DirectoryResult): string {
|
|
11
|
+
if (result.type === 'worktree') return '\u{e728}'
|
|
12
|
+
if (result.type === 'workspace') return '\u{f07c}'
|
|
13
|
+
return '\u{e702}'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getDirectoryResultColor(result: DirectoryResult): string {
|
|
17
|
+
const t = getCurrentTokens()
|
|
18
|
+
if (result.type === 'worktree') return t.palette.warning
|
|
19
|
+
if (result.type === 'workspace') return t.accent
|
|
20
|
+
return t.palette.primary
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CreateSessionModalProps {
|
|
24
|
+
activeField: 'directory' | 'name'
|
|
25
|
+
directoryQuery: string
|
|
26
|
+
sessionName: string
|
|
27
|
+
results: DirectoryResult[]
|
|
28
|
+
selectedIndex: number
|
|
29
|
+
pendingProjectPath: string | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function CreateSessionModal({
|
|
33
|
+
activeField,
|
|
34
|
+
directoryQuery,
|
|
35
|
+
pendingProjectPath,
|
|
36
|
+
results,
|
|
37
|
+
selectedIndex,
|
|
38
|
+
sessionName,
|
|
39
|
+
}: CreateSessionModalProps) {
|
|
40
|
+
const t = useTokens()
|
|
41
|
+
const dirActive = activeField === 'directory'
|
|
42
|
+
const nameActive = activeField === 'name'
|
|
43
|
+
|
|
44
|
+
const items: FormOptionItem[] = results.map((result) => {
|
|
45
|
+
return {
|
|
46
|
+
key: result.path,
|
|
47
|
+
leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
|
|
48
|
+
title: (active) => (
|
|
49
|
+
<text fg={active ? t.palette.ink : t.muted}>{abbreviatePath(result.path)}</text>
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Form
|
|
56
|
+
title="Create workspace"
|
|
57
|
+
keybindsModeId="modal.create-session"
|
|
58
|
+
width={uiTokens.modalWidth.xl}
|
|
59
|
+
>
|
|
60
|
+
<AutoComplete
|
|
61
|
+
active={dirActive}
|
|
62
|
+
label="Search projects"
|
|
63
|
+
placeholder="Type a project name..."
|
|
64
|
+
value={directoryQuery}
|
|
65
|
+
displayValue={pendingProjectPath ? abbreviatePath(pendingProjectPath) : directoryQuery}
|
|
66
|
+
items={items}
|
|
67
|
+
selectedIndex={selectedIndex}
|
|
68
|
+
maxVisibleRows={VISIBLE_ROWS}
|
|
69
|
+
emptyState={
|
|
70
|
+
<text fg={t.muted}>
|
|
71
|
+
{directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
|
|
72
|
+
</text>
|
|
73
|
+
}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<TextField active={nameActive} label="Workspace name" value={sessionName} />
|
|
77
|
+
</Form>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
2
|
+
import { Form, TextField } from '../shared/form'
|
|
3
|
+
|
|
4
|
+
export function SessionNameModal({ title, value }: { title: string; value: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<Form title={title} keybindsModeId="modal.session-name" width={uiTokens.modalWidth.md}>
|
|
7
|
+
<TextField active value={value} />
|
|
8
|
+
</Form>
|
|
9
|
+
)
|
|
10
|
+
}
|
package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx}
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { SessionRecord } from '
|
|
1
|
+
import type { SessionRecord } from '../../../../state/types'
|
|
2
2
|
|
|
3
|
-
import { dispatchGlobal, runSideEffectGlobal } from '
|
|
4
|
-
import { filterSessions } from '
|
|
5
|
-
import { abbreviatePath } from '
|
|
6
|
-
import { orderSessionsForDisplay } from '
|
|
7
|
-
import { useTokens } from '
|
|
8
|
-
import { uiTokens } from '
|
|
9
|
-
import { Picker, type PickerItem } from '
|
|
3
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
4
|
+
import { filterSessions } from '../../../../state/selectors'
|
|
5
|
+
import { abbreviatePath } from '../../../path-format'
|
|
6
|
+
import { orderSessionsForDisplay } from '../../../session-ordering'
|
|
7
|
+
import { useTokens } from '../../../theme'
|
|
8
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
9
|
+
import { Picker, type PickerItem } from '../shared/picker'
|
|
10
10
|
|
|
11
11
|
interface SessionPickerModalProps {
|
|
12
12
|
sessions: SessionRecord[]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { useTokens } from '../../../theme'
|
|
6
|
+
import { InputField } from '../../primitives/input-field'
|
|
7
|
+
import { ListItem } from '../../primitives/list-item'
|
|
8
|
+
import { ModalShell } from './modal-shell'
|
|
9
|
+
|
|
10
|
+
interface FormProps {
|
|
11
|
+
children: ReactNode
|
|
12
|
+
footer?: ReactNode
|
|
13
|
+
keybindsModeId?: ModeId
|
|
14
|
+
listGap?: number
|
|
15
|
+
subtitle?: string
|
|
16
|
+
title: string
|
|
17
|
+
width: number | `${number}%`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FieldLabelProps {
|
|
21
|
+
active?: boolean
|
|
22
|
+
children?: ReactNode
|
|
23
|
+
description?: ReactNode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TextFieldProps {
|
|
27
|
+
active: boolean
|
|
28
|
+
cursorPos?: number
|
|
29
|
+
description?: ReactNode
|
|
30
|
+
label?: ReactNode
|
|
31
|
+
placeholder?: string
|
|
32
|
+
value: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type FormOptionContent = ReactNode | ((active: boolean) => ReactNode)
|
|
36
|
+
|
|
37
|
+
export interface FormOptionItem {
|
|
38
|
+
key: string
|
|
39
|
+
leading?: FormOptionContent
|
|
40
|
+
subtitle?: FormOptionContent
|
|
41
|
+
title: FormOptionContent
|
|
42
|
+
trailing?: FormOptionContent
|
|
43
|
+
onClick?: () => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface AutoCompleteProps {
|
|
47
|
+
active: boolean
|
|
48
|
+
cursorPos?: number
|
|
49
|
+
displayValue?: string
|
|
50
|
+
emptyState?: ReactNode
|
|
51
|
+
items: FormOptionItem[]
|
|
52
|
+
label: ReactNode
|
|
53
|
+
maxVisibleRows?: number
|
|
54
|
+
onHover?: (index: number) => void
|
|
55
|
+
placeholder?: string
|
|
56
|
+
selectedIndex: number
|
|
57
|
+
value: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function Form({
|
|
61
|
+
children,
|
|
62
|
+
footer,
|
|
63
|
+
keybindsModeId,
|
|
64
|
+
listGap,
|
|
65
|
+
subtitle,
|
|
66
|
+
title,
|
|
67
|
+
width,
|
|
68
|
+
}: FormProps) {
|
|
69
|
+
return (
|
|
70
|
+
<ModalShell
|
|
71
|
+
title={title}
|
|
72
|
+
subtitle={subtitle}
|
|
73
|
+
width={width}
|
|
74
|
+
keybindsModeId={keybindsModeId}
|
|
75
|
+
listGap={listGap}
|
|
76
|
+
footer={footer}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</ModalShell>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function FieldLabel({ active = false, children, description }: FieldLabelProps) {
|
|
84
|
+
const t = useTokens()
|
|
85
|
+
return (
|
|
86
|
+
<box flexDirection="column">
|
|
87
|
+
{children ? <text fg={active ? t.palette.ink : t.muted}>{children}</text> : null}
|
|
88
|
+
{description ? <text fg={t.muted}>{description}</text> : null}
|
|
89
|
+
</box>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function TextField({
|
|
94
|
+
active,
|
|
95
|
+
cursorPos,
|
|
96
|
+
description,
|
|
97
|
+
label,
|
|
98
|
+
placeholder,
|
|
99
|
+
value,
|
|
100
|
+
}: TextFieldProps) {
|
|
101
|
+
return (
|
|
102
|
+
<box flexDirection="column">
|
|
103
|
+
{label || description ? (
|
|
104
|
+
<FieldLabel active={active} description={description}>
|
|
105
|
+
{label}
|
|
106
|
+
</FieldLabel>
|
|
107
|
+
) : null}
|
|
108
|
+
<InputField active={active} value={value} cursorPos={cursorPos} placeholder={placeholder} />
|
|
109
|
+
</box>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveItemContent(content: FormOptionContent | undefined, active: boolean): ReactNode {
|
|
114
|
+
return typeof content === 'function' ? content(active) : content
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function AutoComplete({
|
|
118
|
+
active,
|
|
119
|
+
cursorPos,
|
|
120
|
+
displayValue,
|
|
121
|
+
emptyState,
|
|
122
|
+
items,
|
|
123
|
+
label,
|
|
124
|
+
maxVisibleRows = 8,
|
|
125
|
+
onHover,
|
|
126
|
+
placeholder,
|
|
127
|
+
selectedIndex,
|
|
128
|
+
value,
|
|
129
|
+
}: AutoCompleteProps) {
|
|
130
|
+
const scrollOffset = Math.max(0, selectedIndex - maxVisibleRows + 1)
|
|
131
|
+
const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisibleRows)
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<box flexDirection="column" gap={1}>
|
|
135
|
+
<TextField
|
|
136
|
+
active={active}
|
|
137
|
+
label={label}
|
|
138
|
+
value={active ? value : (displayValue ?? value)}
|
|
139
|
+
cursorPos={cursorPos}
|
|
140
|
+
placeholder={placeholder}
|
|
141
|
+
/>
|
|
142
|
+
<box flexDirection="column" height={maxVisibleRows}>
|
|
143
|
+
{items.length === 0
|
|
144
|
+
? (emptyState ?? null)
|
|
145
|
+
: visibleItems.map((item, index) => {
|
|
146
|
+
const optionIndex = scrollOffset + index
|
|
147
|
+
const optionActive = active && optionIndex === selectedIndex
|
|
148
|
+
return (
|
|
149
|
+
<ListItem
|
|
150
|
+
key={item.key}
|
|
151
|
+
active={optionActive}
|
|
152
|
+
leading={resolveItemContent(item.leading, optionActive)}
|
|
153
|
+
title={resolveItemContent(item.title, optionActive)}
|
|
154
|
+
subtitle={resolveItemContent(item.subtitle, optionActive)}
|
|
155
|
+
trailing={resolveItemContent(item.trailing, optionActive)}
|
|
156
|
+
onHover={onHover ? () => onHover(optionIndex) : undefined}
|
|
157
|
+
onClick={item.onClick}
|
|
158
|
+
/>
|
|
159
|
+
)
|
|
160
|
+
})}
|
|
161
|
+
</box>
|
|
162
|
+
</box>
|
|
163
|
+
)
|
|
164
|
+
}
|
package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
2
|
|
|
3
|
-
import { describeBindings } from '
|
|
4
|
-
import { useKeymap } from '
|
|
5
|
-
import { useTokens } from '
|
|
6
|
-
import { Surface } from '
|
|
3
|
+
import { describeBindings } from '../../../../input/keymap/describe-bindings'
|
|
4
|
+
import { useKeymap } from '../../../keymap-context'
|
|
5
|
+
import { useTokens } from '../../../theme'
|
|
6
|
+
import { Surface } from '../../primitives/surface'
|
|
7
7
|
|
|
8
8
|
const KEYS_COLUMN_WIDTH = 12
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
16
16
|
const t = useTokens()
|
|
17
17
|
const config = useKeymap()
|
|
18
18
|
const bindings = describeBindings(config, modeId, {
|
|
19
|
-
|
|
19
|
+
mergeAlternativesByDescription: true,
|
|
20
20
|
withDescriptionOnly: true,
|
|
21
21
|
})
|
|
22
22
|
|
|
@@ -24,10 +24,10 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
|
|
|
24
24
|
const entries = typeof limit === 'number' ? bindings.slice(0, limit) : bindings
|
|
25
25
|
|
|
26
26
|
return (
|
|
27
|
-
<box position="absolute" bottom={
|
|
27
|
+
<box position="absolute" bottom={0} right={0}>
|
|
28
28
|
<Surface tone="elevated" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
|
|
29
29
|
{entries.map((binding) => (
|
|
30
|
-
<box key={
|
|
30
|
+
<box key={binding.description ?? binding.keys} flexDirection="row">
|
|
31
31
|
<box width={KEYS_COLUMN_WIDTH}>
|
|
32
32
|
<text fg={t.accent}>{binding.keysDisplay}</text>
|
|
33
33
|
</box>
|
|
@@ -3,7 +3,7 @@ import type { ModeId } from '@brimveyn/aimux-config'
|
|
|
3
3
|
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
4
4
|
import { type ReactNode } from 'react'
|
|
5
5
|
|
|
6
|
-
import { useTokens, useTransparent } from '
|
|
6
|
+
import { useTokens, useTransparent } from '../../../theme'
|
|
7
7
|
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
8
8
|
|
|
9
9
|
interface ModalShellProps {
|
|
@@ -4,9 +4,9 @@ import type { ScrollBoxRenderable } from '@opentui/core'
|
|
|
4
4
|
import { useTerminalDimensions } from '@opentui/react'
|
|
5
5
|
import { type ReactNode, useLayoutEffect, useRef } from 'react'
|
|
6
6
|
|
|
7
|
-
import { useTokens } from '
|
|
8
|
-
import { BareInput } from '
|
|
9
|
-
import { ListItem } from '
|
|
7
|
+
import { useTokens } from '../../../theme'
|
|
8
|
+
import { BareInput } from '../../primitives/bare-input'
|
|
9
|
+
import { ListItem } from '../../primitives/list-item'
|
|
10
10
|
import { ModalShell } from './modal-shell'
|
|
11
11
|
|
|
12
12
|
export interface PickerItem {
|