@brimveyn/aimux 1.12.6 → 1.13.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/README.md +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +19 -3
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { useTerminalDimensions } from '@opentui/react'
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
type Toast,
|
|
6
|
+
TOAST_CONFIG,
|
|
7
|
+
toastStore,
|
|
8
|
+
type ToastVariant,
|
|
9
|
+
} from '../../../../state/toast-store'
|
|
10
|
+
import { type ResolvedTuiTheme, useTheme } from '../../../theme'
|
|
11
|
+
import { slideOffsetStyle, type ToastAnchor } from './toast-position'
|
|
12
|
+
import { useToastAnimation } from './use-toast-animation'
|
|
13
|
+
|
|
14
|
+
// Cells the toast travels vertically for center anchors (compact, since the card
|
|
15
|
+
// is only a few rows tall); horizontal anchors travel the full card width.
|
|
16
|
+
const VERTICAL_SLIDE = 4
|
|
17
|
+
|
|
18
|
+
function variantStyle(variant: ToastVariant, t: ResolvedTuiTheme): { color: string; icon: string } {
|
|
19
|
+
switch (variant) {
|
|
20
|
+
case 'success':
|
|
21
|
+
return { color: t.success, icon: '✓' }
|
|
22
|
+
case 'warning':
|
|
23
|
+
return { color: t.warning, icon: '⚠' }
|
|
24
|
+
case 'error':
|
|
25
|
+
return { color: t.error, icon: '✗' }
|
|
26
|
+
case 'info':
|
|
27
|
+
return { color: t.primary, icon: '●' }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ToastItemProps {
|
|
32
|
+
toast: Toast
|
|
33
|
+
anchor: ToastAnchor
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function ToastItem({ anchor, toast }: ToastItemProps) {
|
|
37
|
+
const t = useTheme()
|
|
38
|
+
const dimensions = useTerminalDimensions()
|
|
39
|
+
const { away, requestExit } = useToastAnimation({
|
|
40
|
+
onExited: () => toastStore.getState().dismiss(toast.id),
|
|
41
|
+
reduceMotion: TOAST_CONFIG.reduceMotion,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (toast.durationMs <= 0) return
|
|
46
|
+
const timer = setTimeout(() => requestExit(), toast.durationMs)
|
|
47
|
+
return () => clearTimeout(timer)
|
|
48
|
+
}, [toast.durationMs, requestExit])
|
|
49
|
+
|
|
50
|
+
const width = Math.max(16, Math.min(TOAST_CONFIG.width, dimensions.width - 4))
|
|
51
|
+
const distance = anchor.horizontal === 'center' ? VERTICAL_SLIDE : width + 4
|
|
52
|
+
const margins = slideOffsetStyle(anchor, Math.round(away * distance))
|
|
53
|
+
const { color, icon } = variantStyle(toast.variant, t)
|
|
54
|
+
const hasTitle = toast.title != null && toast.title !== ''
|
|
55
|
+
const hasMessage = toast.message != null && toast.message !== ''
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<box flexShrink={0} {...margins}>
|
|
59
|
+
<box
|
|
60
|
+
border
|
|
61
|
+
borderColor={color}
|
|
62
|
+
backgroundColor={t.backgroundElement}
|
|
63
|
+
padding={1}
|
|
64
|
+
width={width}
|
|
65
|
+
>
|
|
66
|
+
{toast.content ?? (
|
|
67
|
+
<box flexDirection="row" gap={1}>
|
|
68
|
+
<text fg={color} selectable={false}>
|
|
69
|
+
{icon}
|
|
70
|
+
</text>
|
|
71
|
+
<box flexDirection="column" flexGrow={1}>
|
|
72
|
+
{hasTitle ? (
|
|
73
|
+
<text fg={t.text} selectable={false} wrapMode="word">
|
|
74
|
+
<strong>{toast.title}</strong>
|
|
75
|
+
</text>
|
|
76
|
+
) : null}
|
|
77
|
+
{hasMessage ? (
|
|
78
|
+
<text fg={t.textMuted} selectable={false} wrapMode="word">
|
|
79
|
+
{toast.message}
|
|
80
|
+
</text>
|
|
81
|
+
) : null}
|
|
82
|
+
</box>
|
|
83
|
+
</box>
|
|
84
|
+
)}
|
|
85
|
+
</box>
|
|
86
|
+
</box>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ToastPosition } from '../../../../state/toast-store'
|
|
2
|
+
|
|
3
|
+
export interface ToastAnchor {
|
|
4
|
+
vertical: 'top' | 'bottom'
|
|
5
|
+
horizontal: 'left' | 'center' | 'right'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseToastPosition(position: ToastPosition): ToastAnchor {
|
|
9
|
+
const [vertical, horizontal] = position.split('-') as [
|
|
10
|
+
ToastAnchor['vertical'],
|
|
11
|
+
ToastAnchor['horizontal'],
|
|
12
|
+
]
|
|
13
|
+
return { horizontal, vertical }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Absolute box props that pin a stack to its corner. Center spans the width and
|
|
17
|
+
// centers its children; left/right hug their edge. Margin keeps it off the edge.
|
|
18
|
+
export function stackContainerProps(anchor: ToastAnchor, margin: number) {
|
|
19
|
+
const vertical = anchor.vertical === 'top' ? { top: margin } : { bottom: margin }
|
|
20
|
+
if (anchor.horizontal === 'left') {
|
|
21
|
+
return { ...vertical, alignItems: 'flex-start' as const, left: margin }
|
|
22
|
+
}
|
|
23
|
+
if (anchor.horizontal === 'right') {
|
|
24
|
+
return { ...vertical, alignItems: 'flex-end' as const, right: margin }
|
|
25
|
+
}
|
|
26
|
+
return { ...vertical, alignItems: 'center' as const, left: margin, right: margin }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SlideMargins {
|
|
30
|
+
marginLeft?: number
|
|
31
|
+
marginRight?: number
|
|
32
|
+
marginTop?: number
|
|
33
|
+
marginBottom?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A negative margin on the entering edge pushes the toast that many cells off the
|
|
37
|
+
// nearest screen edge (the overflow is clipped by the screen buffer); animating it
|
|
38
|
+
// back to 0 slides the toast in. `offset` is cells off-screen (0 = resting). Used
|
|
39
|
+
// instead of opacity/transform, which terminals don't have.
|
|
40
|
+
export function slideOffsetStyle(anchor: ToastAnchor, offset: number): SlideMargins {
|
|
41
|
+
const off = -offset
|
|
42
|
+
if (anchor.horizontal === 'right') return { marginRight: off }
|
|
43
|
+
if (anchor.horizontal === 'left') return { marginLeft: off }
|
|
44
|
+
return anchor.vertical === 'top' ? { marginTop: off } : { marginBottom: off }
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Toast, TOAST_CONFIG, type ToastPosition } from '../../../../state/toast-store'
|
|
2
|
+
import { ToastItem } from './toast-item'
|
|
3
|
+
import { parseToastPosition, stackContainerProps } from './toast-position'
|
|
4
|
+
|
|
5
|
+
const EDGE_MARGIN = 1
|
|
6
|
+
|
|
7
|
+
interface ToastStackProps {
|
|
8
|
+
position: ToastPosition
|
|
9
|
+
toasts: Toast[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// One corner's column of toasts. Newest sits nearest the anchored edge; the stack
|
|
13
|
+
// is capped at maxVisible (older toasts drop off, though they usually auto-dismiss).
|
|
14
|
+
export function ToastStack({ position, toasts }: ToastStackProps) {
|
|
15
|
+
const anchor = parseToastPosition(position)
|
|
16
|
+
const recent = toasts.slice(-TOAST_CONFIG.maxVisible)
|
|
17
|
+
const ordered = anchor.vertical === 'top' ? [...recent].reverse() : recent
|
|
18
|
+
return (
|
|
19
|
+
<box
|
|
20
|
+
position="absolute"
|
|
21
|
+
flexDirection="column"
|
|
22
|
+
gap={TOAST_CONFIG.gap}
|
|
23
|
+
{...stackContainerProps(anchor, EDGE_MARGIN)}
|
|
24
|
+
>
|
|
25
|
+
{ordered.map((toast) => (
|
|
26
|
+
<ToastItem key={toast.id} toast={toast} anchor={anchor} />
|
|
27
|
+
))}
|
|
28
|
+
</box>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Toast, type ToastPosition, useToastStore } from '../../../../state/toast-store'
|
|
2
|
+
import { ToastStack } from './toast-stack'
|
|
3
|
+
|
|
4
|
+
// Mounted once at the root. Groups active toasts by position and renders one
|
|
5
|
+
// absolutely-positioned stack per occupied corner, on top of everything else.
|
|
6
|
+
export function ToastViewport() {
|
|
7
|
+
const toasts = useToastStore((state) => state.toasts)
|
|
8
|
+
if (toasts.length === 0) return null
|
|
9
|
+
|
|
10
|
+
const byPosition = new Map<ToastPosition, Toast[]>()
|
|
11
|
+
for (const entry of toasts) {
|
|
12
|
+
const group = byPosition.get(entry.position) ?? []
|
|
13
|
+
group.push(entry)
|
|
14
|
+
byPosition.set(entry.position, group)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
{[...byPosition].map(([position, group]) => (
|
|
20
|
+
<ToastStack key={position} position={position} toasts={group} />
|
|
21
|
+
))}
|
|
22
|
+
</>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
const STEP_MS = 28
|
|
4
|
+
// Normalized distance off-screen: 1 = fully hidden, 0 = resting. Eased so the
|
|
5
|
+
// toast decelerates in and accelerates out.
|
|
6
|
+
const ENTER_FRAMES = [1, 0.55, 0.3, 0.15, 0.05, 0]
|
|
7
|
+
const EXIT_FRAMES = [0.2, 0.45, 0.72, 1]
|
|
8
|
+
|
|
9
|
+
interface ToastAnimationOptions {
|
|
10
|
+
reduceMotion: boolean
|
|
11
|
+
onExited: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Drives a toast's slide via a frame loop (terminals have no opacity/transform,
|
|
16
|
+
* so motion is a stepped integer offset). Returns `away` in [0,1] — multiply by a
|
|
17
|
+
* pixel distance to get the cell offset — and `requestExit` to slide back out.
|
|
18
|
+
*/
|
|
19
|
+
export function useToastAnimation({ onExited, reduceMotion }: ToastAnimationOptions): {
|
|
20
|
+
away: number
|
|
21
|
+
requestExit: () => void
|
|
22
|
+
} {
|
|
23
|
+
const [away, setAway] = useState(reduceMotion ? 0 : 1)
|
|
24
|
+
const exitingRef = useRef(false)
|
|
25
|
+
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
26
|
+
const onExitedRef = useRef(onExited)
|
|
27
|
+
onExitedRef.current = onExited
|
|
28
|
+
|
|
29
|
+
const runFrames = useCallback((frames: number[], done?: () => void) => {
|
|
30
|
+
if (timerRef.current != null) clearInterval(timerRef.current)
|
|
31
|
+
let index = 0
|
|
32
|
+
setAway(frames[0] ?? 0)
|
|
33
|
+
timerRef.current = setInterval(() => {
|
|
34
|
+
index += 1
|
|
35
|
+
if (index >= frames.length) {
|
|
36
|
+
if (timerRef.current != null) clearInterval(timerRef.current)
|
|
37
|
+
timerRef.current = null
|
|
38
|
+
done?.()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
setAway(frames[index] ?? 0)
|
|
42
|
+
}, STEP_MS)
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (reduceMotion) return
|
|
47
|
+
runFrames(ENTER_FRAMES)
|
|
48
|
+
return () => {
|
|
49
|
+
if (timerRef.current != null) clearInterval(timerRef.current)
|
|
50
|
+
}
|
|
51
|
+
}, [reduceMotion, runFrames])
|
|
52
|
+
|
|
53
|
+
const requestExit = useCallback(() => {
|
|
54
|
+
if (exitingRef.current) return
|
|
55
|
+
exitingRef.current = true
|
|
56
|
+
if (reduceMotion) {
|
|
57
|
+
onExitedRef.current()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
runFrames(EXIT_FRAMES, () => onExitedRef.current())
|
|
61
|
+
}, [reduceMotion, runFrames])
|
|
62
|
+
|
|
63
|
+
return { away, requestExit }
|
|
64
|
+
}
|
|
@@ -12,7 +12,7 @@ export function InputField({ active, cursorPos, placeholder, value }: InputField
|
|
|
12
12
|
const t = useTheme()
|
|
13
13
|
const fg = active ? t.text : t.textMuted
|
|
14
14
|
if (!active) {
|
|
15
|
-
const showPlaceholder = !value && !!placeholder
|
|
15
|
+
const showPlaceholder = !value && !!(placeholder != null && placeholder !== '')
|
|
16
16
|
return (
|
|
17
17
|
<Surface tone="input" padding={1}>
|
|
18
18
|
<text fg={showPlaceholder ? t.textMuted : fg}>{showPlaceholder ? placeholder : value}</text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { memo, type ReactNode, useMemo } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useTheme } from '../../theme'
|
|
4
4
|
import { Surface } from './surface'
|
|
@@ -9,27 +9,43 @@ interface ListItemProps {
|
|
|
9
9
|
active: boolean
|
|
10
10
|
direction?: ListItemDirection
|
|
11
11
|
id?: string
|
|
12
|
+
index?: number
|
|
12
13
|
leading?: ReactNode
|
|
13
14
|
subtitle?: ReactNode
|
|
14
15
|
title: ReactNode
|
|
15
16
|
trailing?: ReactNode
|
|
16
17
|
onHover?: () => void
|
|
17
18
|
onClick?: () => void
|
|
19
|
+
onHoverIndex?: (index: number) => void
|
|
20
|
+
onClickIndex?: (index: number) => void
|
|
18
21
|
}
|
|
19
22
|
|
|
20
|
-
export function ListItem({
|
|
23
|
+
export const ListItem = memo(function ListItem({
|
|
21
24
|
active,
|
|
22
25
|
direction = 'column',
|
|
23
26
|
id,
|
|
27
|
+
index,
|
|
24
28
|
leading,
|
|
25
29
|
onClick,
|
|
30
|
+
onClickIndex,
|
|
26
31
|
onHover,
|
|
32
|
+
onHoverIndex,
|
|
27
33
|
subtitle,
|
|
28
34
|
title,
|
|
29
35
|
trailing,
|
|
30
36
|
}: ListItemProps) {
|
|
31
37
|
const t = useTheme()
|
|
32
38
|
const isRow = direction === 'row'
|
|
39
|
+
// Build per-item handlers from the stable index-based callbacks so callers can
|
|
40
|
+
// avoid creating fresh closures inside their list `.map()`.
|
|
41
|
+
const handleHover = useMemo<(() => void) | undefined>(() => {
|
|
42
|
+
if (onHoverIndex && index !== undefined) return () => onHoverIndex(index)
|
|
43
|
+
return onHover
|
|
44
|
+
}, [index, onHover, onHoverIndex])
|
|
45
|
+
const handleClick = useMemo<(() => void) | undefined>(() => {
|
|
46
|
+
if (onClickIndex && index !== undefined) return () => onClickIndex(index)
|
|
47
|
+
return onClick
|
|
48
|
+
}, [index, onClick, onClickIndex])
|
|
33
49
|
const inner = (
|
|
34
50
|
<box flexDirection="column">
|
|
35
51
|
<box flexDirection="row">
|
|
@@ -40,15 +56,15 @@ export function ListItem({
|
|
|
40
56
|
</>
|
|
41
57
|
)}
|
|
42
58
|
{leading}
|
|
43
|
-
{leading ? <text> </text> : null}
|
|
59
|
+
{leading != null ? <text> </text> : null}
|
|
44
60
|
<box flexGrow={isRow ? 0 : 1}>{title}</box>
|
|
45
|
-
{trailing ? <box>{trailing}</box> : null}
|
|
61
|
+
{trailing != null ? <box>{trailing}</box> : null}
|
|
46
62
|
</box>
|
|
47
|
-
{subtitle ? <box paddingLeft={2}>{subtitle}</box> : null}
|
|
63
|
+
{subtitle != null ? <box paddingLeft={2}>{subtitle}</box> : null}
|
|
48
64
|
</box>
|
|
49
65
|
)
|
|
50
66
|
return (
|
|
51
|
-
<box id={id} onMouseOver={
|
|
67
|
+
<box id={id} onMouseOver={handleHover} onMouseDown={handleClick}>
|
|
52
68
|
{active ? (
|
|
53
69
|
<Surface tone="selected" paddingLeft={isRow ? 2 : 1} paddingRight={isRow ? 2 : 1}>
|
|
54
70
|
{inner}
|
|
@@ -60,4 +76,4 @@ export function ListItem({
|
|
|
60
76
|
)}
|
|
61
77
|
</box>
|
|
62
78
|
)
|
|
63
|
-
}
|
|
79
|
+
})
|
package/src/ui/filter-themes.ts
CHANGED
|
@@ -8,7 +8,7 @@ export function themeDisplayName(id: ThemeId): string {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function filterThemeIds(filter: string | null): ThemeId[] {
|
|
11
|
-
if (!filter) return THEME_IDS
|
|
11
|
+
if (!(filter != null && filter !== '')) return THEME_IDS
|
|
12
12
|
const needle = filter.toLowerCase()
|
|
13
13
|
return THEME_IDS.filter((id) => {
|
|
14
14
|
return id.toLowerCase().includes(needle) || themeDisplayName(id).toLowerCase().includes(needle)
|
package/src/ui/root.tsx
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import type { MouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
+
import { useCallback, useMemo } from 'react'
|
|
4
|
+
|
|
3
5
|
import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
|
|
4
6
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
5
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
FocusMode,
|
|
9
|
+
ModalState,
|
|
10
|
+
SessionRecord,
|
|
11
|
+
SnippetRecord,
|
|
12
|
+
WorktreeRecord,
|
|
13
|
+
} from '../state/types'
|
|
6
14
|
import type { ThemeId } from './themes'
|
|
7
15
|
|
|
8
16
|
import { useAppStore } from '../state/app-store'
|
|
@@ -28,11 +36,15 @@ import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-
|
|
|
28
36
|
import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
|
|
29
37
|
import { NewTabModal } from './components/modals/tabs/new-tab-modal'
|
|
30
38
|
import { ThemePickerModal } from './components/modals/themes/theme-picker-modal'
|
|
39
|
+
import { WorktreeMoveModal } from './components/modals/worktree/worktree-move-modal'
|
|
31
40
|
import { ContextMenuBox } from './components/overlays/context-menu/context-menu-box'
|
|
32
41
|
import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
|
|
33
42
|
import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
|
|
43
|
+
import { ToastViewport } from './components/overlays/toast/toast-viewport'
|
|
34
44
|
import { useTheme } from './theme'
|
|
35
45
|
|
|
46
|
+
const EMPTY_WORKTREES: WorktreeRecord[] = []
|
|
47
|
+
|
|
36
48
|
function getCreateSessionFields(modal: ModalState) {
|
|
37
49
|
if (modal.type !== 'create-session') {
|
|
38
50
|
return { directoryQuery: '', sessionName: '' }
|
|
@@ -78,6 +90,7 @@ function renderModal(
|
|
|
78
90
|
focusMode: FocusMode
|
|
79
91
|
activeAssistant?: string
|
|
80
92
|
autoCommitModel?: string
|
|
93
|
+
worktreeDivergence: Record<string, { ahead: number; behind: number }>
|
|
81
94
|
}
|
|
82
95
|
) {
|
|
83
96
|
switch (modal.type) {
|
|
@@ -89,8 +102,24 @@ function renderModal(
|
|
|
89
102
|
customCommands={options.customCommands}
|
|
90
103
|
filter={modal.editBuffer}
|
|
91
104
|
cursorPos={modal.cursorPos}
|
|
105
|
+
currentSessionId={options.currentSessionId}
|
|
92
106
|
editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
|
|
93
107
|
editBuffer={modal.editBuffer ?? ''}
|
|
108
|
+
activeField={modal.type === 'new-tab' ? modal.activeField : 'assistant'}
|
|
109
|
+
branchError={modal.type === 'new-tab' ? modal.branchError : null}
|
|
110
|
+
branchName={modal.type === 'new-tab' ? modal.branchName : ''}
|
|
111
|
+
createWorktree={modal.type === 'new-tab' ? modal.createWorktree : false}
|
|
112
|
+
selectedAssistantId={modal.type === 'new-tab' ? modal.selectedAssistantId : null}
|
|
113
|
+
step={modal.type === 'new-tab' ? modal.step : 'assistant'}
|
|
114
|
+
worktreeDeleteConfirmId={modal.type === 'new-tab' ? modal.worktreeDeleteConfirmId : null}
|
|
115
|
+
worktreeDeleteMessage={modal.type === 'new-tab' ? modal.worktreeDeleteMessage : null}
|
|
116
|
+
worktrees={
|
|
117
|
+
options.currentSessionId != null && options.currentSessionId !== ''
|
|
118
|
+
? (options.sessions.find((session) => session.id === options.currentSessionId)
|
|
119
|
+
?.worktrees ?? EMPTY_WORKTREES)
|
|
120
|
+
: EMPTY_WORKTREES
|
|
121
|
+
}
|
|
122
|
+
worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
|
|
94
123
|
/>
|
|
95
124
|
)
|
|
96
125
|
case 'session-picker':
|
|
@@ -107,7 +136,11 @@ function renderModal(
|
|
|
107
136
|
case 'session-name':
|
|
108
137
|
return (
|
|
109
138
|
<SessionNameModal
|
|
110
|
-
title={
|
|
139
|
+
title={
|
|
140
|
+
modal.sessionTargetId != null && modal.sessionTargetId !== ''
|
|
141
|
+
? 'Rename workspace'
|
|
142
|
+
: 'Create workspace'
|
|
143
|
+
}
|
|
111
144
|
value={modal.editBuffer ?? ''}
|
|
112
145
|
/>
|
|
113
146
|
)
|
|
@@ -161,6 +194,21 @@ function renderModal(
|
|
|
161
194
|
latestVersion={modal.latestVersion}
|
|
162
195
|
/>
|
|
163
196
|
)
|
|
197
|
+
case 'worktree-move': {
|
|
198
|
+
const session =
|
|
199
|
+
options.currentSessionId != null && options.currentSessionId !== ''
|
|
200
|
+
? options.sessions.find((entry) => entry.id === options.currentSessionId)
|
|
201
|
+
: undefined
|
|
202
|
+
return (
|
|
203
|
+
<WorktreeMoveModal
|
|
204
|
+
deleteSource={modal.type === 'worktree-move' ? modal.deleteSource : false}
|
|
205
|
+
divergence={options.worktreeDivergence}
|
|
206
|
+
selectedIndex={modal.selectedIndex}
|
|
207
|
+
sourceWorktreeId={modal.sourceWorktreeId}
|
|
208
|
+
worktrees={session?.worktrees ?? EMPTY_WORKTREES}
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
164
212
|
case 'help':
|
|
165
213
|
return (
|
|
166
214
|
<HelpModal
|
|
@@ -266,6 +314,7 @@ export function RootView({
|
|
|
266
314
|
const customCommands = useAppStore((s) => s.customCommands)
|
|
267
315
|
const sessions = useAppStore((s) => s.sessions)
|
|
268
316
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
317
|
+
const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
|
|
269
318
|
const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
|
|
270
319
|
const gitPaneMode = useAppStore((s) => s.gitPane.mode)
|
|
271
320
|
const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
|
|
@@ -275,19 +324,66 @@ export function RootView({
|
|
|
275
324
|
const sidebarVisible = useAppStore((s) => s.sidebar.visible)
|
|
276
325
|
|
|
277
326
|
const gitPaneInPaneOnLeft = gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left'
|
|
327
|
+
const splitChrome = PANE_BORDER * 2
|
|
328
|
+
|
|
329
|
+
const handleSidebarEdgeResize = useCallback(
|
|
330
|
+
(event: MouseEvent): boolean => {
|
|
331
|
+
onSidebarResizeStart?.({ initialWidth: sidebarWidth, screenStart: event.x })
|
|
332
|
+
return true
|
|
333
|
+
},
|
|
334
|
+
[onSidebarResizeStart, sidebarWidth]
|
|
335
|
+
)
|
|
278
336
|
const handleTerminalLeftEdgeMouseDown =
|
|
279
337
|
sidebarVisible && !gitPaneInPaneOnLeft && onSidebarResizeStart
|
|
280
|
-
?
|
|
281
|
-
onSidebarResizeStart({ initialWidth: sidebarWidth, screenStart: event.x })
|
|
282
|
-
return true
|
|
283
|
-
}
|
|
338
|
+
? handleSidebarEdgeResize
|
|
284
339
|
: undefined
|
|
285
340
|
|
|
341
|
+
const handleRootMouseDrag = useCallback(
|
|
342
|
+
(event: MouseEvent) => {
|
|
343
|
+
if (onSeparatorDrag?.(event) === true) {
|
|
344
|
+
event.preventDefault()
|
|
345
|
+
event.stopPropagation()
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
[onSeparatorDrag]
|
|
349
|
+
)
|
|
350
|
+
const handleRootMouseUp = useCallback(
|
|
351
|
+
(event: MouseEvent) => {
|
|
352
|
+
// Catch releases that land outside any TerminalPane (sidebar, gap,
|
|
353
|
+
// status bar, …) so an in-flight multi-click drag — and its
|
|
354
|
+
// auto-scroll interval — is always finalised.
|
|
355
|
+
onTerminalMouseUp?.(event)
|
|
356
|
+
onSeparatorDragEnd?.()
|
|
357
|
+
},
|
|
358
|
+
[onSeparatorDragEnd, onTerminalMouseUp]
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
const splitContentOrigin = useMemo(
|
|
362
|
+
() => ({
|
|
363
|
+
cols: terminalCols + splitChrome,
|
|
364
|
+
rows: terminalRows + splitChrome,
|
|
365
|
+
x: contentOrigin.x - PANE_BORDER,
|
|
366
|
+
y: contentOrigin.y - PANE_BORDER,
|
|
367
|
+
}),
|
|
368
|
+
[contentOrigin, splitChrome, terminalCols, terminalRows]
|
|
369
|
+
)
|
|
370
|
+
const splitBounds = useMemo(
|
|
371
|
+
() => ({
|
|
372
|
+
cols: terminalCols + splitChrome,
|
|
373
|
+
rows: terminalRows + splitChrome,
|
|
374
|
+
x: 0,
|
|
375
|
+
y: 0,
|
|
376
|
+
}),
|
|
377
|
+
[splitChrome, terminalCols, terminalRows]
|
|
378
|
+
)
|
|
379
|
+
|
|
286
380
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
287
|
-
const activeTree =
|
|
381
|
+
const activeTree =
|
|
382
|
+
activeTabId != null && activeTabId !== ''
|
|
383
|
+
? getTreeForTab(layoutTrees, tabGroupMap, activeTabId)
|
|
384
|
+
: null
|
|
288
385
|
const createSessionFields = getCreateSessionFields(modal)
|
|
289
386
|
const snippetEditorFields = getSnippetEditorFields(modal)
|
|
290
|
-
const splitChrome = PANE_BORDER * 2
|
|
291
387
|
|
|
292
388
|
const inGitMode = focusMode === 'git' || modal.type === 'git-commit'
|
|
293
389
|
if (inGitMode) {
|
|
@@ -310,7 +406,9 @@ export function RootView({
|
|
|
310
406
|
snippetEditorFields,
|
|
311
407
|
snippets,
|
|
312
408
|
themeId,
|
|
409
|
+
worktreeDivergence,
|
|
313
410
|
})}
|
|
411
|
+
<ToastViewport />
|
|
314
412
|
</box>
|
|
315
413
|
)
|
|
316
414
|
}
|
|
@@ -321,19 +419,8 @@ export function RootView({
|
|
|
321
419
|
width="100%"
|
|
322
420
|
height="100%"
|
|
323
421
|
backgroundColor={editorBg}
|
|
324
|
-
onMouseDrag={
|
|
325
|
-
|
|
326
|
-
event.preventDefault()
|
|
327
|
-
event.stopPropagation()
|
|
328
|
-
}
|
|
329
|
-
}}
|
|
330
|
-
onMouseUp={(event) => {
|
|
331
|
-
// Catch releases that land outside any TerminalPane (sidebar, gap,
|
|
332
|
-
// status bar, …) so an in-flight multi-click drag — and its
|
|
333
|
-
// auto-scroll interval — is always finalised.
|
|
334
|
-
onTerminalMouseUp?.(event)
|
|
335
|
-
onSeparatorDragEnd?.()
|
|
336
|
-
}}
|
|
422
|
+
onMouseDrag={handleRootMouseDrag}
|
|
423
|
+
onMouseUp={handleRootMouseUp}
|
|
337
424
|
>
|
|
338
425
|
{sessionBarPosition === 'top' && <SessionBar />}
|
|
339
426
|
<box flexDirection="row" gap={0} padding={0} flexGrow={1}>
|
|
@@ -356,12 +443,7 @@ export function RootView({
|
|
|
356
443
|
tabs={tabs}
|
|
357
444
|
activeTabId={activeTabId}
|
|
358
445
|
focusMode={focusMode}
|
|
359
|
-
contentOrigin={
|
|
360
|
-
cols: terminalCols + splitChrome,
|
|
361
|
-
rows: terminalRows + splitChrome,
|
|
362
|
-
x: contentOrigin.x - PANE_BORDER,
|
|
363
|
-
y: contentOrigin.y - PANE_BORDER,
|
|
364
|
-
}}
|
|
446
|
+
contentOrigin={splitContentOrigin}
|
|
365
447
|
mouseForwardingEnabled={mouseForwardingEnabled}
|
|
366
448
|
localScrollbackEnabled={localScrollbackEnabled}
|
|
367
449
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
@@ -376,12 +458,7 @@ export function RootView({
|
|
|
376
458
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
377
459
|
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
378
460
|
onMeasure={onMeasure}
|
|
379
|
-
bounds={
|
|
380
|
-
cols: terminalCols + splitChrome,
|
|
381
|
-
rows: terminalRows + splitChrome,
|
|
382
|
-
x: 0,
|
|
383
|
-
y: 0,
|
|
384
|
-
}}
|
|
461
|
+
bounds={splitBounds}
|
|
385
462
|
/>
|
|
386
463
|
) : (
|
|
387
464
|
<TerminalPane
|
|
@@ -424,7 +501,9 @@ export function RootView({
|
|
|
424
501
|
snippetEditorFields,
|
|
425
502
|
snippets,
|
|
426
503
|
themeId,
|
|
504
|
+
worktreeDivergence,
|
|
427
505
|
})}
|
|
506
|
+
<ToastViewport />
|
|
428
507
|
</box>
|
|
429
508
|
)
|
|
430
509
|
}
|
|
@@ -455,16 +534,20 @@ function GitPaneInPaneMode({
|
|
|
455
534
|
dispatchGlobal({ position: nextPosition, type: 'set-git-pane-position' })
|
|
456
535
|
}
|
|
457
536
|
)
|
|
537
|
+
const handleResizeMouseDown = useCallback(
|
|
538
|
+
(event: MouseEvent) => {
|
|
539
|
+
event.preventDefault()
|
|
540
|
+
event.stopPropagation()
|
|
541
|
+
onGitPaneResizeStart?.({ initialWidth: width, screenStart: event.x, side: position })
|
|
542
|
+
},
|
|
543
|
+
[onGitPaneResizeStart, position, width]
|
|
544
|
+
)
|
|
458
545
|
const handle = (
|
|
459
546
|
<box
|
|
460
547
|
width={1}
|
|
461
548
|
flexShrink={0}
|
|
462
549
|
backgroundColor={tokens.border}
|
|
463
|
-
onMouseDown={
|
|
464
|
-
event.preventDefault()
|
|
465
|
-
event.stopPropagation()
|
|
466
|
-
onGitPaneResizeStart?.({ initialWidth: width, screenStart: event.x, side: position })
|
|
467
|
-
}}
|
|
550
|
+
onMouseDown={handleResizeMouseDown}
|
|
468
551
|
/>
|
|
469
552
|
)
|
|
470
553
|
return (
|
|
@@ -5,7 +5,7 @@ import type { SessionRecord } from '../state/types'
|
|
|
5
5
|
* with any missing `order` falling back to `createdAt` ascending.
|
|
6
6
|
*/
|
|
7
7
|
export function orderSessionsForDisplay(sessions: SessionRecord[]): SessionRecord[] {
|
|
8
|
-
return sessions.
|
|
8
|
+
return [...sessions].sort((a, b) => {
|
|
9
9
|
const ao = a.order ?? Number.MAX_SAFE_INTEGER
|
|
10
10
|
const bo = b.order ?? Number.MAX_SAFE_INTEGER
|
|
11
11
|
if (ao !== bo) return ao - bo
|
|
@@ -27,7 +27,7 @@ export function moveIdToIdPosition(
|
|
|
27
27
|
const from = ids.indexOf(moveId)
|
|
28
28
|
const to = ids.indexOf(intoPositionOfId)
|
|
29
29
|
if (from < 0 || to < 0) return ids
|
|
30
|
-
const next = ids
|
|
30
|
+
const next = [...ids]
|
|
31
31
|
next.splice(from, 1)
|
|
32
32
|
next.splice(to, 0, moveId)
|
|
33
33
|
return next
|