@brimveyn/aimux 1.12.2 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +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 +18 -2
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/pty-write.ts +52 -0
- 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 +598 -102
- package/src/app-runtime/snippet-actions.ts +25 -11
- 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 +143 -8
- package/src/app.tsx +25 -9
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +3 -3
- 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 +1 -1
- 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 +13 -1
- package/src/input/raw-input-handler.ts +34 -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 +18 -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 +8 -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 +3 -3
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- 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 +385 -24
- 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/snippet-catalog.ts +65 -4
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +108 -16
- package/src/state/validation.ts +47 -3
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
- 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 +6 -4
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
- package/src/ui/components/git/git-panel.tsx +17 -10
- package/src/ui/components/git/git-view.tsx +79 -19
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
- 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 +3 -3
- package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
- package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
- 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 +1 -1
- package/src/ui/components/layout/terminal-pane.tsx +43 -40
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +4 -2
- package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
- package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
- package/src/ui/components/modals/shared/form.tsx +4 -5
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +28 -5
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
- 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 +3 -3
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +65 -14
- 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,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>
|
|
@@ -40,11 +40,11 @@ export function ListItem({
|
|
|
40
40
|
</>
|
|
41
41
|
)}
|
|
42
42
|
{leading}
|
|
43
|
-
{leading ? <text> </text> : null}
|
|
43
|
+
{leading != null ? <text> </text> : null}
|
|
44
44
|
<box flexGrow={isRow ? 0 : 1}>{title}</box>
|
|
45
|
-
{trailing ? <box>{trailing}</box> : null}
|
|
45
|
+
{trailing != null ? <box>{trailing}</box> : null}
|
|
46
46
|
</box>
|
|
47
|
-
{subtitle ? <box paddingLeft={2}>{subtitle}</box> : null}
|
|
47
|
+
{subtitle != null ? <box paddingLeft={2}>{subtitle}</box> : null}
|
|
48
48
|
</box>
|
|
49
49
|
)
|
|
50
50
|
return (
|
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
|
@@ -28,9 +28,11 @@ import { SnippetEditorModal } from './components/modals/snippets/snippet-editor-
|
|
|
28
28
|
import { SnippetPickerModal } from './components/modals/snippets/snippet-picker-modal'
|
|
29
29
|
import { NewTabModal } from './components/modals/tabs/new-tab-modal'
|
|
30
30
|
import { ThemePickerModal } from './components/modals/themes/theme-picker-modal'
|
|
31
|
+
import { WorktreeMoveModal } from './components/modals/worktree/worktree-move-modal'
|
|
31
32
|
import { ContextMenuBox } from './components/overlays/context-menu/context-menu-box'
|
|
32
33
|
import { ContextMenuOverlay } from './components/overlays/context-menu/context-menu-overlay'
|
|
33
34
|
import { PendingChordOverlay } from './components/overlays/pending-chord-overlay'
|
|
35
|
+
import { ToastViewport } from './components/overlays/toast/toast-viewport'
|
|
34
36
|
import { useTheme } from './theme'
|
|
35
37
|
|
|
36
38
|
function getCreateSessionFields(modal: ModalState) {
|
|
@@ -53,19 +55,14 @@ function getCreateSessionFields(modal: ModalState) {
|
|
|
53
55
|
|
|
54
56
|
function getSnippetEditorFields(modal: ModalState) {
|
|
55
57
|
if (modal.type !== 'snippet-editor') {
|
|
56
|
-
return { snippetContent: '', snippetName: '' }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (modal.activeField === 'name') {
|
|
60
|
-
return {
|
|
61
|
-
snippetContent: modal.contentBuffer,
|
|
62
|
-
snippetName: modal.editBuffer ?? '',
|
|
63
|
-
}
|
|
58
|
+
return { snippetContent: '', snippetName: '', snippetTrigger: '' }
|
|
64
59
|
}
|
|
65
60
|
|
|
61
|
+
const editValue = modal.editBuffer ?? ''
|
|
66
62
|
return {
|
|
67
|
-
snippetContent: modal.
|
|
68
|
-
snippetName: modal.
|
|
63
|
+
snippetContent: modal.activeField === 'content' ? editValue : modal.contentBuffer,
|
|
64
|
+
snippetName: modal.activeField === 'name' ? editValue : modal.nameBuffer,
|
|
65
|
+
snippetTrigger: modal.activeField === 'trigger' ? editValue : modal.triggerBuffer,
|
|
69
66
|
}
|
|
70
67
|
}
|
|
71
68
|
|
|
@@ -79,10 +76,11 @@ function renderModal(
|
|
|
79
76
|
snippets: SnippetRecord[]
|
|
80
77
|
themeId: ThemeId
|
|
81
78
|
createSessionFields: { directoryQuery: string; sessionName: string }
|
|
82
|
-
snippetEditorFields: { snippetName: string; snippetContent: string }
|
|
79
|
+
snippetEditorFields: { snippetName: string; snippetTrigger: string; snippetContent: string }
|
|
83
80
|
focusMode: FocusMode
|
|
84
81
|
activeAssistant?: string
|
|
85
82
|
autoCommitModel?: string
|
|
83
|
+
worktreeDivergence: Record<string, { ahead: number; behind: number }>
|
|
86
84
|
}
|
|
87
85
|
) {
|
|
88
86
|
switch (modal.type) {
|
|
@@ -94,8 +92,24 @@ function renderModal(
|
|
|
94
92
|
customCommands={options.customCommands}
|
|
95
93
|
filter={modal.editBuffer}
|
|
96
94
|
cursorPos={modal.cursorPos}
|
|
95
|
+
currentSessionId={options.currentSessionId}
|
|
97
96
|
editingCommand={modal.type === 'new-tab' ? modal.editingCommand : null}
|
|
98
97
|
editBuffer={modal.editBuffer ?? ''}
|
|
98
|
+
activeField={modal.type === 'new-tab' ? modal.activeField : 'assistant'}
|
|
99
|
+
branchError={modal.type === 'new-tab' ? modal.branchError : null}
|
|
100
|
+
branchName={modal.type === 'new-tab' ? modal.branchName : ''}
|
|
101
|
+
createWorktree={modal.type === 'new-tab' ? modal.createWorktree : false}
|
|
102
|
+
selectedAssistantId={modal.type === 'new-tab' ? modal.selectedAssistantId : null}
|
|
103
|
+
step={modal.type === 'new-tab' ? modal.step : 'assistant'}
|
|
104
|
+
worktreeDeleteConfirmId={modal.type === 'new-tab' ? modal.worktreeDeleteConfirmId : null}
|
|
105
|
+
worktreeDeleteMessage={modal.type === 'new-tab' ? modal.worktreeDeleteMessage : null}
|
|
106
|
+
worktrees={
|
|
107
|
+
options.currentSessionId != null && options.currentSessionId !== ''
|
|
108
|
+
? (options.sessions.find((session) => session.id === options.currentSessionId)
|
|
109
|
+
?.worktrees ?? [])
|
|
110
|
+
: []
|
|
111
|
+
}
|
|
112
|
+
worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
|
|
99
113
|
/>
|
|
100
114
|
)
|
|
101
115
|
case 'session-picker':
|
|
@@ -112,7 +126,11 @@ function renderModal(
|
|
|
112
126
|
case 'session-name':
|
|
113
127
|
return (
|
|
114
128
|
<SessionNameModal
|
|
115
|
-
title={
|
|
129
|
+
title={
|
|
130
|
+
modal.sessionTargetId != null && modal.sessionTargetId !== ''
|
|
131
|
+
? 'Rename workspace'
|
|
132
|
+
: 'Create workspace'
|
|
133
|
+
}
|
|
116
134
|
value={modal.editBuffer ?? ''}
|
|
117
135
|
/>
|
|
118
136
|
)
|
|
@@ -136,6 +154,7 @@ function renderModal(
|
|
|
136
154
|
selectedIndex={modal.selectedIndex}
|
|
137
155
|
filter={modal.editBuffer}
|
|
138
156
|
cursorPos={modal.cursorPos}
|
|
157
|
+
actionMessage={modal.actionMessage}
|
|
139
158
|
/>
|
|
140
159
|
)
|
|
141
160
|
case 'snippet-editor':
|
|
@@ -143,6 +162,7 @@ function renderModal(
|
|
|
143
162
|
<SnippetEditorModal
|
|
144
163
|
activeField={modal.activeField}
|
|
145
164
|
snippetName={options.snippetEditorFields.snippetName}
|
|
165
|
+
snippetTrigger={options.snippetEditorFields.snippetTrigger}
|
|
146
166
|
snippetContent={options.snippetEditorFields.snippetContent}
|
|
147
167
|
isEditing={modal.sessionTargetId !== null}
|
|
148
168
|
/>
|
|
@@ -164,6 +184,29 @@ function renderModal(
|
|
|
164
184
|
latestVersion={modal.latestVersion}
|
|
165
185
|
/>
|
|
166
186
|
)
|
|
187
|
+
case 'worktree-move': {
|
|
188
|
+
const session =
|
|
189
|
+
options.currentSessionId != null && options.currentSessionId !== ''
|
|
190
|
+
? options.sessions.find((entry) => entry.id === options.currentSessionId)
|
|
191
|
+
: undefined
|
|
192
|
+
const worktrees = session?.worktrees ?? []
|
|
193
|
+
const sourceId = modal.sourceWorktreeId
|
|
194
|
+
const source = worktrees.find((w) => w.id === sourceId)
|
|
195
|
+
const targets = worktrees.filter((w) => w.id !== sourceId)
|
|
196
|
+
const sourceLabel =
|
|
197
|
+
source?.branch != null && source.branch !== ''
|
|
198
|
+
? source.branch
|
|
199
|
+
: (source?.name ?? 'worktree')
|
|
200
|
+
return (
|
|
201
|
+
<WorktreeMoveModal
|
|
202
|
+
deleteSource={modal.type === 'worktree-move' ? modal.deleteSource : false}
|
|
203
|
+
divergence={options.worktreeDivergence}
|
|
204
|
+
selectedIndex={modal.selectedIndex}
|
|
205
|
+
sourceLabel={sourceLabel}
|
|
206
|
+
targets={targets}
|
|
207
|
+
/>
|
|
208
|
+
)
|
|
209
|
+
}
|
|
167
210
|
case 'help':
|
|
168
211
|
return (
|
|
169
212
|
<HelpModal
|
|
@@ -269,6 +312,7 @@ export function RootView({
|
|
|
269
312
|
const customCommands = useAppStore((s) => s.customCommands)
|
|
270
313
|
const sessions = useAppStore((s) => s.sessions)
|
|
271
314
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
315
|
+
const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
|
|
272
316
|
const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
|
|
273
317
|
const gitPaneMode = useAppStore((s) => s.gitPane.mode)
|
|
274
318
|
const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
|
|
@@ -287,7 +331,10 @@ export function RootView({
|
|
|
287
331
|
: undefined
|
|
288
332
|
|
|
289
333
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
290
|
-
const activeTree =
|
|
334
|
+
const activeTree =
|
|
335
|
+
activeTabId != null && activeTabId !== ''
|
|
336
|
+
? getTreeForTab(layoutTrees, tabGroupMap, activeTabId)
|
|
337
|
+
: null
|
|
291
338
|
const createSessionFields = getCreateSessionFields(modal)
|
|
292
339
|
const snippetEditorFields = getSnippetEditorFields(modal)
|
|
293
340
|
const splitChrome = PANE_BORDER * 2
|
|
@@ -313,7 +360,9 @@ export function RootView({
|
|
|
313
360
|
snippetEditorFields,
|
|
314
361
|
snippets,
|
|
315
362
|
themeId,
|
|
363
|
+
worktreeDivergence,
|
|
316
364
|
})}
|
|
365
|
+
<ToastViewport />
|
|
317
366
|
</box>
|
|
318
367
|
)
|
|
319
368
|
}
|
|
@@ -325,7 +374,7 @@ export function RootView({
|
|
|
325
374
|
height="100%"
|
|
326
375
|
backgroundColor={editorBg}
|
|
327
376
|
onMouseDrag={(event) => {
|
|
328
|
-
if (onSeparatorDrag?.(event)) {
|
|
377
|
+
if (onSeparatorDrag?.(event) === true) {
|
|
329
378
|
event.preventDefault()
|
|
330
379
|
event.stopPropagation()
|
|
331
380
|
}
|
|
@@ -427,7 +476,9 @@ export function RootView({
|
|
|
427
476
|
snippetEditorFields,
|
|
428
477
|
snippets,
|
|
429
478
|
themeId,
|
|
479
|
+
worktreeDivergence,
|
|
430
480
|
})}
|
|
481
|
+
<ToastViewport />
|
|
431
482
|
</box>
|
|
432
483
|
)
|
|
433
484
|
}
|
|
@@ -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
|
|
@@ -3,6 +3,7 @@ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
|
|
|
3
3
|
import type { AppState, TabSession } from '../state/types'
|
|
4
4
|
|
|
5
5
|
import { describeBindings } from '../input/keymap/describe-bindings'
|
|
6
|
+
import { getSessionProjectPath } from '../state/session-worktrees'
|
|
6
7
|
import { buildHintText } from './keymap-context'
|
|
7
8
|
import { abbreviatePath } from './path-format'
|
|
8
9
|
|
|
@@ -58,13 +59,16 @@ export function getStatusBarModel(
|
|
|
58
59
|
activeTab: TabSession | undefined,
|
|
59
60
|
config: ResolvedKeymapConfig
|
|
60
61
|
): StatusBarModel {
|
|
61
|
-
const currentSession =
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const currentSession =
|
|
63
|
+
state.currentSessionId != null && state.currentSessionId !== ''
|
|
64
|
+
? state.sessions.find((session) => session.id === state.currentSessionId)
|
|
65
|
+
: undefined
|
|
64
66
|
const sessionName = currentSession?.name ?? 'no workspace'
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
const sessionPath = getSessionProjectPath(currentSession)
|
|
68
|
+
const sessionLabel =
|
|
69
|
+
sessionPath != null && sessionPath !== ''
|
|
70
|
+
? `${sessionName} (${abbreviatePath(sessionPath)})`
|
|
71
|
+
: sessionName
|
|
68
72
|
|
|
69
73
|
// \u{f0b1} = nf-fa-briefcase (session icon)
|
|
70
74
|
const sessionIcon = '\u{f0b1}'
|
|
@@ -87,9 +91,10 @@ export function getStatusBarModel(
|
|
|
87
91
|
case 'git': {
|
|
88
92
|
const headOffset = state.gitMode.headOffset
|
|
89
93
|
const offsetTag = headOffset > 0 ? ` HEAD~${headOffset}` : ''
|
|
94
|
+
const reviewTag = state.gitMode.reviewBase ? ' vs base' : ''
|
|
90
95
|
return {
|
|
91
96
|
help: helpHintForMode(config, 'git-mode'),
|
|
92
|
-
left: `${sessionIcon} ${sessionLabel}${offsetTag}`,
|
|
97
|
+
left: `${sessionIcon} ${sessionLabel}${offsetTag}${reviewTag}`,
|
|
93
98
|
right: hintForGitMode(config, headOffset),
|
|
94
99
|
}
|
|
95
100
|
}
|
|
@@ -127,6 +132,8 @@ function deriveModalModeId(modalType: AppState['modal']['type']): ModeId | null
|
|
|
127
132
|
return 'modal.theme-picker.filtering'
|
|
128
133
|
case 'update-available':
|
|
129
134
|
return 'modal.update-available'
|
|
135
|
+
case 'worktree-move':
|
|
136
|
+
return 'modal.worktree-move'
|
|
130
137
|
default:
|
|
131
138
|
return null
|
|
132
139
|
}
|
|
@@ -11,7 +11,7 @@ let cached: GraphicsProtocol | null = null
|
|
|
11
11
|
export function detectGraphicsProtocol(renderer: CliRenderer): GraphicsProtocol {
|
|
12
12
|
if (cached !== null) return cached
|
|
13
13
|
const caps = (renderer as RendererWithCapabilities).capabilities
|
|
14
|
-
if (caps?.kitty_graphics) {
|
|
14
|
+
if (caps?.kitty_graphics === true) {
|
|
15
15
|
cached = 'kitty'
|
|
16
16
|
return cached
|
|
17
17
|
}
|
|
@@ -24,10 +24,8 @@ async function tryConverter(
|
|
|
24
24
|
stdout: 'pipe',
|
|
25
25
|
})
|
|
26
26
|
const writer = proc.stdin
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await writer.end()
|
|
30
|
-
}
|
|
27
|
+
void writer.write(bytes)
|
|
28
|
+
await writer.end()
|
|
31
29
|
const timer = setTimeout(() => proc.kill(), timeoutMs)
|
|
32
30
|
const [out, code] = await Promise.all([new Response(proc.stdout).bytes(), proc.exited])
|
|
33
31
|
clearTimeout(timer)
|
|
@@ -85,11 +83,11 @@ export async function convertToPng(bytes: Uint8Array, mime: string): Promise<Con
|
|
|
85
83
|
const png = await renderSvgToPng(bytes)
|
|
86
84
|
result = png ? { kind: 'ok', png } : { kind: 'error', reason: 'failed to render SVG' }
|
|
87
85
|
} else {
|
|
88
|
-
const attempts:
|
|
89
|
-
() => tryConverter(['magick', '-', 'png:-'], bytes, 1500),
|
|
90
|
-
() => tryConverter(['convert', '-', 'png:-'], bytes, 1500),
|
|
91
|
-
() => trySips(bytes),
|
|
92
|
-
() =>
|
|
86
|
+
const attempts: (() => Promise<Uint8Array | null>)[] = [
|
|
87
|
+
async () => tryConverter(['magick', '-', 'png:-'], bytes, 1500),
|
|
88
|
+
async () => tryConverter(['convert', '-', 'png:-'], bytes, 1500),
|
|
89
|
+
async () => trySips(bytes),
|
|
90
|
+
async () =>
|
|
93
91
|
tryConverter(
|
|
94
92
|
[
|
|
95
93
|
'ffmpeg',
|
|
@@ -53,13 +53,8 @@ export function uploadPngEscape(pngBytes: Uint8Array, id: number): string {
|
|
|
53
53
|
for (let i = 0; i < chunks.length; i++) {
|
|
54
54
|
const isLast = i === chunks.length - 1
|
|
55
55
|
const m = isLast ? 0 : 1
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// a=t (transmit), t=d (direct), f=100 (PNG), q=2 (quiet).
|
|
59
|
-
header = `q=2,a=t,t=d,f=100,i=${id},m=${m}`
|
|
60
|
-
} else {
|
|
61
|
-
header = `m=${m},q=2`
|
|
62
|
-
}
|
|
56
|
+
// a=t (transmit), t=d (direct), f=100 (PNG), q=2 (quiet).
|
|
57
|
+
const header = i === 0 ? `q=2,a=t,t=d,f=100,i=${id},m=${m}` : `m=${m},q=2`
|
|
63
58
|
parts.push(`${ESC}_G${header};${chunks[i]}${ST}`)
|
|
64
59
|
}
|
|
65
60
|
return wrapForTmux(parts.join(''))
|
|
@@ -9,7 +9,7 @@ export async function getCurrentPackageVersion(): Promise<string> {
|
|
|
9
9
|
|
|
10
10
|
export async function fetchLatestNpmVersion(packageName: string): Promise<string | null> {
|
|
11
11
|
const debugOverride = process.env.AIMUX_DEBUG_UPDATE_LATEST
|
|
12
|
-
if (debugOverride) {
|
|
12
|
+
if (debugOverride != null && debugOverride !== '') {
|
|
13
13
|
return debugOverride
|
|
14
14
|
}
|
|
15
15
|
|