@brimveyn/aimux 1.12.6 → 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/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +538 -83
- 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 +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 +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 +3 -3
- 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 +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-picker-modal.tsx +11 -3
- 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 +57 -3
- 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
|
@@ -62,7 +62,11 @@ export function CreateSessionModal({
|
|
|
62
62
|
label="Search projects"
|
|
63
63
|
placeholder="Type a project name..."
|
|
64
64
|
value={directoryQuery}
|
|
65
|
-
displayValue={
|
|
65
|
+
displayValue={
|
|
66
|
+
pendingProjectPath != null && pendingProjectPath !== ''
|
|
67
|
+
? abbreviatePath(pendingProjectPath)
|
|
68
|
+
: directoryQuery
|
|
69
|
+
}
|
|
66
70
|
items={items}
|
|
67
71
|
selectedIndex={selectedIndex}
|
|
68
72
|
maxVisibleRows={VISIBLE_ROWS}
|
|
@@ -47,7 +47,7 @@ export function SessionPickerModal({
|
|
|
47
47
|
const ordered = orderSessionsForDisplay(sessions)
|
|
48
48
|
const baselineOrder = ordered.map((s) => s.id)
|
|
49
49
|
const filtered = filterSessions(ordered, filter)
|
|
50
|
-
const hasFilter = !!filter
|
|
50
|
+
const hasFilter = !!(filter != null && filter !== '')
|
|
51
51
|
|
|
52
52
|
const sessionItems: PickerItem[] = filtered.map((session, index) => {
|
|
53
53
|
const active = index === selectedIndex
|
|
@@ -56,9 +56,10 @@ export function SessionPickerModal({
|
|
|
56
56
|
key: session.id,
|
|
57
57
|
onClick: () => runSideEffectGlobal({ type: 'confirm-selected-session' }),
|
|
58
58
|
onDelete: () => runSideEffectGlobal({ type: 'delete-selected-session' }),
|
|
59
|
-
subtitle:
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
subtitle:
|
|
60
|
+
session.projectPath != null && session.projectPath !== '' ? (
|
|
61
|
+
<text fg={t.textMuted}>{abbreviatePath(session.projectPath)}</text>
|
|
62
|
+
) : undefined,
|
|
62
63
|
title: (
|
|
63
64
|
<text fg={active ? t.text : t.textMuted}>
|
|
64
65
|
{formatSessionLine(session, currentSessionId, currentTabCount, displayIndex)}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
-
|
|
3
|
-
import { type ReactNode } from 'react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
4
3
|
|
|
5
4
|
import { useTheme } from '../../../theme'
|
|
6
5
|
import { InputField } from '../../primitives/input-field'
|
|
@@ -84,8 +83,8 @@ export function FieldLabel({ active = false, children, description }: FieldLabel
|
|
|
84
83
|
const t = useTheme()
|
|
85
84
|
return (
|
|
86
85
|
<box flexDirection="column">
|
|
87
|
-
{children ? <text fg={active ? t.text : t.textMuted}>{children}</text> : null}
|
|
88
|
-
{description ? <text fg={t.textMuted}>{description}</text> : null}
|
|
86
|
+
{children != null ? <text fg={active ? t.text : t.textMuted}>{children}</text> : null}
|
|
87
|
+
{description != null ? <text fg={t.textMuted}>{description}</text> : null}
|
|
89
88
|
</box>
|
|
90
89
|
)
|
|
91
90
|
}
|
|
@@ -100,7 +99,7 @@ export function TextField({
|
|
|
100
99
|
}: TextFieldProps) {
|
|
101
100
|
return (
|
|
102
101
|
<box flexDirection="column">
|
|
103
|
-
{label || description ? (
|
|
102
|
+
{label != null || description != null ? (
|
|
104
103
|
<FieldLabel active={active} description={description}>
|
|
105
104
|
{label}
|
|
106
105
|
</FieldLabel>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModeId } from '@brimveyn/aimux-config'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
2
3
|
|
|
3
4
|
import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
|
|
4
|
-
import { type ReactNode } from 'react'
|
|
5
5
|
|
|
6
6
|
import { useTheme, useTransparent } from '../../../theme'
|
|
7
7
|
import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
|
|
@@ -81,10 +81,10 @@ export function ModalShell({
|
|
|
81
81
|
<box width="100%" flexDirection="column" gap={listGap}>
|
|
82
82
|
<box flexDirection="column">
|
|
83
83
|
<text fg={t.text}>{title}</text>
|
|
84
|
-
{subtitle ? <text fg={t.textMuted}>{subtitle}</text> : null}
|
|
84
|
+
{subtitle != null && subtitle !== '' ? <text fg={t.textMuted}>{subtitle}</text> : null}
|
|
85
85
|
</box>
|
|
86
86
|
{children}
|
|
87
|
-
{footer ? <box>{footer}</box> : null}
|
|
87
|
+
{footer != null ? <box>{footer}</box> : null}
|
|
88
88
|
</box>
|
|
89
89
|
</box>
|
|
90
90
|
{keybindsModeId ? <ModalKeybindsOverlay modeId={keybindsModeId} /> : null}
|
|
@@ -18,6 +18,7 @@ export interface PickerItem {
|
|
|
18
18
|
onClick?: () => void
|
|
19
19
|
onEdit?: () => void
|
|
20
20
|
onDelete?: () => void
|
|
21
|
+
onWorktree?: () => void
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
interface PickerProps {
|
|
@@ -38,7 +39,15 @@ interface PickerProps {
|
|
|
38
39
|
const VIEWPORT_HEIGHT_RATIO = 0.6
|
|
39
40
|
const MODAL_CHROME_ROWS = 6
|
|
40
41
|
|
|
41
|
-
function PickerItemCtas({
|
|
42
|
+
function PickerItemCtas({
|
|
43
|
+
onDelete,
|
|
44
|
+
onEdit,
|
|
45
|
+
onWorktree,
|
|
46
|
+
}: {
|
|
47
|
+
onEdit?: () => void
|
|
48
|
+
onDelete?: () => void
|
|
49
|
+
onWorktree?: () => void
|
|
50
|
+
}) {
|
|
42
51
|
const t = useTheme()
|
|
43
52
|
return (
|
|
44
53
|
<box flexDirection="row" gap={1}>
|
|
@@ -52,6 +61,16 @@ function PickerItemCtas({ onDelete, onEdit }: { onEdit?: () => void; onDelete?:
|
|
|
52
61
|
<text fg={t.primary}>[edit]</text>
|
|
53
62
|
</box>
|
|
54
63
|
) : null}
|
|
64
|
+
{onWorktree ? (
|
|
65
|
+
<box
|
|
66
|
+
onMouseDown={(event) => {
|
|
67
|
+
event.stopPropagation()
|
|
68
|
+
onWorktree()
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<text fg={t.warning}>[WT]</text>
|
|
72
|
+
</box>
|
|
73
|
+
) : null}
|
|
55
74
|
{onDelete ? (
|
|
56
75
|
<box
|
|
57
76
|
onMouseDown={(event) => {
|
|
@@ -105,7 +124,7 @@ export function Picker({
|
|
|
105
124
|
return
|
|
106
125
|
}
|
|
107
126
|
const target = items[selectedIndex]?.key
|
|
108
|
-
if (!target) return
|
|
127
|
+
if (!(target != null && target !== '')) return
|
|
109
128
|
isScrollingRef.current = true
|
|
110
129
|
scrollboxRef.current.scrollChildIntoView(target)
|
|
111
130
|
resetScrollTimer()
|
|
@@ -137,7 +156,7 @@ export function Picker({
|
|
|
137
156
|
for (let index = 0; index < items.length; index++) {
|
|
138
157
|
const item = items[index]
|
|
139
158
|
if (!item) continue
|
|
140
|
-
if (item.group && item.group !== prevGroup) {
|
|
159
|
+
if (item.group != null && item.group !== '' && item.group !== prevGroup) {
|
|
141
160
|
nodes.push(
|
|
142
161
|
<box key={`group::${item.group}`} paddingLeft={1} paddingTop={index === 0 ? 0 : 1}>
|
|
143
162
|
<text fg={t.textMuted} wrapMode="none">
|
|
@@ -151,8 +170,12 @@ export function Picker({
|
|
|
151
170
|
const capturedIndex = index
|
|
152
171
|
const trailing =
|
|
153
172
|
item.trailing ??
|
|
154
|
-
(active && (item.onEdit || item.onDelete) ? (
|
|
155
|
-
<PickerItemCtas
|
|
173
|
+
(active && (item.onEdit || item.onDelete || item.onWorktree) ? (
|
|
174
|
+
<PickerItemCtas
|
|
175
|
+
onEdit={item.onEdit}
|
|
176
|
+
onDelete={item.onDelete}
|
|
177
|
+
onWorktree={item.onWorktree}
|
|
178
|
+
/>
|
|
156
179
|
) : null)
|
|
157
180
|
nodes.push(
|
|
158
181
|
<ListItem
|
|
@@ -52,7 +52,9 @@ export function SnippetPickerModal({
|
|
|
52
52
|
<text fg={active ? t.text : t.textMuted}>
|
|
53
53
|
<strong>{snippet.name}</strong>
|
|
54
54
|
</text>
|
|
55
|
-
{snippet.trigger
|
|
55
|
+
{snippet.trigger != null && snippet.trigger !== '' ? (
|
|
56
|
+
<text fg={t.textMuted}>{` :${snippet.trigger}`}</text>
|
|
57
|
+
) : null}
|
|
56
58
|
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
57
59
|
</box>
|
|
58
60
|
),
|
|
@@ -71,10 +73,16 @@ export function SnippetPickerModal({
|
|
|
71
73
|
selectedIndex={selectedIndex}
|
|
72
74
|
emptyState={
|
|
73
75
|
<text fg={t.textMuted}>
|
|
74
|
-
{filter
|
|
76
|
+
{filter != null && filter !== ''
|
|
77
|
+
? 'No matching snippets.'
|
|
78
|
+
: 'No snippets yet. Press n to create one.'}
|
|
75
79
|
</text>
|
|
76
80
|
}
|
|
77
|
-
footer={
|
|
81
|
+
footer={
|
|
82
|
+
actionMessage != null && actionMessage !== '' ? (
|
|
83
|
+
<text fg={t.error}>{actionMessage}</text>
|
|
84
|
+
) : undefined
|
|
85
|
+
}
|
|
78
86
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
79
87
|
/>
|
|
80
88
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AssistantId } from '../../../../state/types'
|
|
1
|
+
import type { AssistantId, WorktreeRecord } from '../../../../state/types'
|
|
2
2
|
|
|
3
3
|
import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
|
|
4
4
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
@@ -13,17 +13,59 @@ interface NewTabModalProps {
|
|
|
13
13
|
customCommands: Record<string, string>
|
|
14
14
|
filter: string | null
|
|
15
15
|
cursorPos?: number
|
|
16
|
+
currentSessionId: string | null
|
|
16
17
|
editingCommand: AssistantId | null
|
|
17
18
|
editBuffer: string
|
|
19
|
+
activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
|
|
20
|
+
branchError: string | null
|
|
21
|
+
branchName: string
|
|
22
|
+
createWorktree: boolean
|
|
23
|
+
selectedAssistantId: AssistantId | null
|
|
24
|
+
step: 'assistant' | 'worktree' | 'worktree-create'
|
|
25
|
+
worktreeDeleteConfirmId: string | null
|
|
26
|
+
worktreeDeleteMessage: string | null
|
|
27
|
+
worktrees: WorktreeRecord[]
|
|
28
|
+
worktreeName: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getDeleteBlockedReason({
|
|
32
|
+
currentSessionId,
|
|
33
|
+
worktree,
|
|
34
|
+
worktrees,
|
|
35
|
+
}: {
|
|
36
|
+
currentSessionId: string | null
|
|
37
|
+
worktree: WorktreeRecord | undefined
|
|
38
|
+
worktrees: WorktreeRecord[]
|
|
39
|
+
}): string | null {
|
|
40
|
+
if (!worktree) return null
|
|
41
|
+
if (!(currentSessionId != null && currentSessionId !== ''))
|
|
42
|
+
return 'Cannot delete: no active session.'
|
|
43
|
+
if (worktrees.length <= 1) return 'Cannot delete: at least one worktree must remain.'
|
|
44
|
+
if (worktree.source === 'primary') return 'Cannot delete: root worktree is required.'
|
|
45
|
+
if (worktree.source === 'aimux-temp' && !worktree.createdByAimux) {
|
|
46
|
+
return 'Cannot delete: this temp worktree was not created by Aimux.'
|
|
47
|
+
}
|
|
48
|
+
return null
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
export function NewTabModal({
|
|
52
|
+
activeField,
|
|
53
|
+
branchError,
|
|
54
|
+
branchName,
|
|
55
|
+
createWorktree,
|
|
56
|
+
currentSessionId,
|
|
21
57
|
cursorPos,
|
|
22
58
|
customCommands,
|
|
23
59
|
editBuffer,
|
|
24
60
|
editingCommand,
|
|
25
61
|
filter,
|
|
62
|
+
selectedAssistantId,
|
|
26
63
|
selectedIndex,
|
|
64
|
+
step,
|
|
65
|
+
worktreeDeleteConfirmId,
|
|
66
|
+
worktreeDeleteMessage,
|
|
67
|
+
worktreeName,
|
|
68
|
+
worktrees,
|
|
27
69
|
}: NewTabModalProps) {
|
|
28
70
|
const t = useTheme()
|
|
29
71
|
|
|
@@ -51,17 +93,137 @@ export function NewTabModal({
|
|
|
51
93
|
const options = getAllAssistantOptions(customCommands)
|
|
52
94
|
const filtered = filterAssistants(options, filter)
|
|
53
95
|
|
|
96
|
+
if (step === 'worktree-create') {
|
|
97
|
+
const selectedAssistant =
|
|
98
|
+
options.find((option) => option.id === selectedAssistantId) ?? options[0]
|
|
99
|
+
return (
|
|
100
|
+
<Form
|
|
101
|
+
title={`New worktree: ${selectedAssistant?.label ?? 'assistant'}`}
|
|
102
|
+
keybindsModeId="modal.new-tab.command-edit"
|
|
103
|
+
width={uiTokens.modalWidth.xl}
|
|
104
|
+
>
|
|
105
|
+
<box flexDirection="column" gap={1}>
|
|
106
|
+
<TextField
|
|
107
|
+
active={activeField === 'worktree-name'}
|
|
108
|
+
label="Worktree name"
|
|
109
|
+
value={worktreeName}
|
|
110
|
+
cursorPos={activeField === 'worktree-name' ? cursorPos : undefined}
|
|
111
|
+
placeholder="my-feature"
|
|
112
|
+
/>
|
|
113
|
+
<box flexDirection="column">
|
|
114
|
+
<TextField
|
|
115
|
+
active={activeField === 'branch-name'}
|
|
116
|
+
label="Branch name"
|
|
117
|
+
value={branchName}
|
|
118
|
+
cursorPos={activeField === 'branch-name' ? cursorPos : undefined}
|
|
119
|
+
placeholder="aimux/my-feature"
|
|
120
|
+
/>
|
|
121
|
+
{branchError != null && branchError !== '' ? (
|
|
122
|
+
<text fg={t.error}>{branchError}</text>
|
|
123
|
+
) : null}
|
|
124
|
+
</box>
|
|
125
|
+
<text fg={t.textMuted}>Step 3/3: configure new worktree</text>
|
|
126
|
+
</box>
|
|
127
|
+
</Form>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (step === 'worktree') {
|
|
132
|
+
const selectedAssistant =
|
|
133
|
+
options.find((option) => option.id === selectedAssistantId) ??
|
|
134
|
+
filtered[selectedIndex] ??
|
|
135
|
+
options[0]
|
|
136
|
+
const selectedWorktree = worktrees[selectedIndex]
|
|
137
|
+
const deleteBlockedReason = getDeleteBlockedReason({
|
|
138
|
+
currentSessionId,
|
|
139
|
+
worktree: selectedWorktree,
|
|
140
|
+
worktrees,
|
|
141
|
+
})
|
|
142
|
+
const worktreeItems: PickerItem[] = [
|
|
143
|
+
...worktrees.map((worktree, index) => {
|
|
144
|
+
const active = index === selectedIndex
|
|
145
|
+
const label = worktree.branch ?? worktree.name
|
|
146
|
+
const blockedReason = getDeleteBlockedReason({ currentSessionId, worktree, worktrees })
|
|
147
|
+
const canDelete = blockedReason == null || blockedReason === ''
|
|
148
|
+
return {
|
|
149
|
+
key: worktree.id,
|
|
150
|
+
onClick: () => {
|
|
151
|
+
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
152
|
+
runSideEffectGlobal({ type: 'launch-selected-assistant' })
|
|
153
|
+
},
|
|
154
|
+
onDelete:
|
|
155
|
+
active && canDelete && currentSessionId != null && currentSessionId !== ''
|
|
156
|
+
? () => {
|
|
157
|
+
dispatchGlobal({ index, type: 'set-modal-selection-index' })
|
|
158
|
+
dispatchGlobal({ message: null, type: 'set-new-tab-worktree-delete-state' })
|
|
159
|
+
runSideEffectGlobal({
|
|
160
|
+
force: worktreeDeleteConfirmId === worktree.id,
|
|
161
|
+
sessionId: currentSessionId,
|
|
162
|
+
type: 'delete-worktree',
|
|
163
|
+
worktreeId: worktree.id,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
: undefined,
|
|
167
|
+
subtitle: (
|
|
168
|
+
<box flexDirection="column">
|
|
169
|
+
<text fg={t.textMuted}>{worktree.path}</text>
|
|
170
|
+
<text fg={t.textMuted}>{worktree.source}</text>
|
|
171
|
+
</box>
|
|
172
|
+
),
|
|
173
|
+
title: <text fg={active ? t.text : t.textMuted}>{label}</text>,
|
|
174
|
+
}
|
|
175
|
+
}),
|
|
176
|
+
{
|
|
177
|
+
key: '__create-worktree__',
|
|
178
|
+
onClick: () => dispatchGlobal({ type: 'enter-new-tab-worktree-create' }),
|
|
179
|
+
subtitle: <text fg={t.textMuted}>Create an Aimux temp worktree</text>,
|
|
180
|
+
title: <text fg={createWorktree ? t.text : t.textMuted}>Create new worktree</text>,
|
|
181
|
+
},
|
|
182
|
+
]
|
|
183
|
+
return (
|
|
184
|
+
<Picker
|
|
185
|
+
title={`New assistant: ${selectedAssistant?.label ?? 'assistant'}`}
|
|
186
|
+
keybindsModeId="modal.new-tab.command-edit"
|
|
187
|
+
width={uiTokens.modalWidth.md}
|
|
188
|
+
gap={1}
|
|
189
|
+
filter={null}
|
|
190
|
+
items={worktreeItems}
|
|
191
|
+
selectedIndex={selectedIndex}
|
|
192
|
+
emptyState={<text fg={t.textMuted}>No worktrees.</text>}
|
|
193
|
+
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
194
|
+
footer={
|
|
195
|
+
<box flexDirection="column">
|
|
196
|
+
{(worktreeDeleteMessage != null && worktreeDeleteMessage !== '') ||
|
|
197
|
+
(deleteBlockedReason != null && deleteBlockedReason !== '') ? (
|
|
198
|
+
<text
|
|
199
|
+
fg={
|
|
200
|
+
worktreeDeleteMessage != null && worktreeDeleteMessage !== ''
|
|
201
|
+
? t.error
|
|
202
|
+
: t.textMuted
|
|
203
|
+
}
|
|
204
|
+
>
|
|
205
|
+
{worktreeDeleteMessage ?? deleteBlockedReason}
|
|
206
|
+
</text>
|
|
207
|
+
) : null}
|
|
208
|
+
<text fg={t.textMuted}>Step 2/2: choose worktree</text>
|
|
209
|
+
<text fg={t.textMuted}>Enter launches, Ctrl+d deletes selected worktree</text>
|
|
210
|
+
</box>
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
54
216
|
const items: PickerItem[] = filtered.map((option, index) => {
|
|
55
217
|
const active = index === selectedIndex
|
|
56
218
|
const customCmd = customCommands[option.id]
|
|
57
219
|
return {
|
|
58
220
|
key: option.id,
|
|
59
|
-
onClick: () =>
|
|
221
|
+
onClick: () => dispatchGlobal({ assistantId: option.id, type: 'select-new-tab-assistant' }),
|
|
60
222
|
onEdit: () => dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
61
223
|
subtitle: (
|
|
62
224
|
<box flexDirection="column">
|
|
63
225
|
<text fg={t.textMuted}>{option.description}</text>
|
|
64
|
-
{customCmd ? <text fg={t.primary}>{customCmd}</text> : null}
|
|
226
|
+
{customCmd != null && customCmd !== '' ? <text fg={t.primary}>{customCmd}</text> : null}
|
|
65
227
|
</box>
|
|
66
228
|
),
|
|
67
229
|
title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
|
|
@@ -70,16 +232,22 @@ export function NewTabModal({
|
|
|
70
232
|
|
|
71
233
|
return (
|
|
72
234
|
<Picker
|
|
73
|
-
title="New assistant
|
|
235
|
+
title="New assistant: choose assistant"
|
|
74
236
|
keybindsModeId="modal.new-tab.command-edit"
|
|
75
237
|
width={uiTokens.modalWidth.md}
|
|
76
238
|
gap={1}
|
|
77
239
|
filter={filter}
|
|
78
|
-
cursorPos={cursorPos}
|
|
240
|
+
cursorPos={activeField === 'assistant' ? cursorPos : undefined}
|
|
79
241
|
items={items}
|
|
80
242
|
selectedIndex={selectedIndex}
|
|
81
243
|
emptyState={<text fg={t.textMuted}>No matching assistants.</text>}
|
|
82
244
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
245
|
+
footer={
|
|
246
|
+
<box flexDirection="column">
|
|
247
|
+
<text fg={t.textMuted}>Step 1/2: choose assistant</text>
|
|
248
|
+
<text fg={t.textMuted}>Enter continues to worktree selection</text>
|
|
249
|
+
</box>
|
|
250
|
+
}
|
|
83
251
|
/>
|
|
84
252
|
)
|
|
85
253
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { WorktreeRecord } from '../../../../state/types'
|
|
2
|
+
|
|
3
|
+
import { dispatchGlobal } from '../../../../state/dispatch-ref'
|
|
4
|
+
import { useTheme } from '../../../theme'
|
|
5
|
+
import { uiTokens } from '../../../ui-tokens'
|
|
6
|
+
import { ListItem } from '../../primitives/list-item'
|
|
7
|
+
import { ModalShell } from '../shared/modal-shell'
|
|
8
|
+
|
|
9
|
+
interface WorktreeMoveModalProps {
|
|
10
|
+
deleteSource: boolean
|
|
11
|
+
divergence: Record<string, { ahead: number; behind: number }>
|
|
12
|
+
selectedIndex: number
|
|
13
|
+
sourceLabel: string
|
|
14
|
+
targets: WorktreeRecord[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const MOVE_HINTS: [key: string, label: string][] = [
|
|
18
|
+
['↵', 'move into selected target'],
|
|
19
|
+
['d', 'toggle delete source'],
|
|
20
|
+
['j/k', 'change target'],
|
|
21
|
+
['esc', 'cancel'],
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function formatDivergence(divergence: { ahead: number; behind: number } | undefined): string {
|
|
25
|
+
if (divergence == null) return ''
|
|
26
|
+
const parts: string[] = []
|
|
27
|
+
if (divergence.ahead > 0) parts.push(`↑${divergence.ahead}`)
|
|
28
|
+
if (divergence.behind > 0) parts.push(`↓${divergence.behind}`)
|
|
29
|
+
return parts.join(' ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function WorktreeMoveModal({
|
|
33
|
+
deleteSource,
|
|
34
|
+
divergence,
|
|
35
|
+
selectedIndex,
|
|
36
|
+
sourceLabel,
|
|
37
|
+
targets,
|
|
38
|
+
}: WorktreeMoveModalProps) {
|
|
39
|
+
const t = useTheme()
|
|
40
|
+
return (
|
|
41
|
+
<ModalShell
|
|
42
|
+
title={`Move ${sourceLabel} →`}
|
|
43
|
+
keybindsModeId="modal.worktree-move"
|
|
44
|
+
width={uiTokens.modalWidth.md}
|
|
45
|
+
footer={
|
|
46
|
+
<box flexDirection="column" gap={1} marginTop={1}>
|
|
47
|
+
<box flexDirection="row" gap={1}>
|
|
48
|
+
<text fg={t.textMuted}>delete source after move</text>
|
|
49
|
+
<text fg={deleteSource ? t.warning : t.text}>{deleteSource ? 'on' : 'off'}</text>
|
|
50
|
+
</box>
|
|
51
|
+
<box flexDirection="column" gap={0}>
|
|
52
|
+
{MOVE_HINTS.map(([key, label]) => (
|
|
53
|
+
<box key={key} flexDirection="row" gap={2}>
|
|
54
|
+
<box width={4} flexShrink={0}>
|
|
55
|
+
<text fg={t.primary}>{key}</text>
|
|
56
|
+
</box>
|
|
57
|
+
<text fg={t.textMuted}>{label}</text>
|
|
58
|
+
</box>
|
|
59
|
+
))}
|
|
60
|
+
</box>
|
|
61
|
+
</box>
|
|
62
|
+
}
|
|
63
|
+
>
|
|
64
|
+
<box flexDirection="column" marginTop={1}>
|
|
65
|
+
{targets.length === 0 ? (
|
|
66
|
+
<text fg={t.textMuted}>No other worktree to move into.</text>
|
|
67
|
+
) : (
|
|
68
|
+
targets.map((worktree, index) => {
|
|
69
|
+
const active = index === selectedIndex
|
|
70
|
+
const label =
|
|
71
|
+
worktree.branch != null && worktree.branch !== '' ? worktree.branch : worktree.name
|
|
72
|
+
const ahead = formatDivergence(divergence[worktree.id])
|
|
73
|
+
return (
|
|
74
|
+
<ListItem
|
|
75
|
+
key={worktree.id}
|
|
76
|
+
id={worktree.id}
|
|
77
|
+
active={active}
|
|
78
|
+
onHover={() => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
79
|
+
onClick={() => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
80
|
+
title={
|
|
81
|
+
<text fg={active ? t.text : t.textMuted} wrapMode="none">
|
|
82
|
+
{label}
|
|
83
|
+
{worktree.source === 'primary' ? ' (primary)' : ''}
|
|
84
|
+
{ahead !== '' ? ` ${ahead}` : ''}
|
|
85
|
+
</text>
|
|
86
|
+
}
|
|
87
|
+
/>
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
)}
|
|
91
|
+
</box>
|
|
92
|
+
</ModalShell>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -34,7 +34,7 @@ function formatResetIn(snap: {
|
|
|
34
34
|
resetAt: string | null
|
|
35
35
|
timeRemaining: string | null
|
|
36
36
|
}): string | null {
|
|
37
|
-
if (snap.resetAt) {
|
|
37
|
+
if (snap.resetAt != null && snap.resetAt !== '') {
|
|
38
38
|
const diffMs = new Date(snap.resetAt).getTime() - Date.now()
|
|
39
39
|
if (diffMs > 0) {
|
|
40
40
|
const totalMin = Math.round(diffMs / 60_000)
|
|
@@ -85,7 +85,7 @@ export function AIUsageIndicator() {
|
|
|
85
85
|
if (!snap) return null
|
|
86
86
|
const icon = TOOL_ICON[tool]
|
|
87
87
|
|
|
88
|
-
if (snap.error && !snap.stale) {
|
|
88
|
+
if (snap.error != null && snap.error !== '' && !(snap.stale === true)) {
|
|
89
89
|
return (
|
|
90
90
|
<box
|
|
91
91
|
key={tool}
|
|
@@ -134,7 +134,7 @@ export function AIUsageIndicator() {
|
|
|
134
134
|
<text fg={t.text} selectable={false}>
|
|
135
135
|
{` ${pctText}`}
|
|
136
136
|
</text>
|
|
137
|
-
{reset ? (
|
|
137
|
+
{reset != null && reset !== '' ? (
|
|
138
138
|
<text fg={t.textMuted} selectable={false}>
|
|
139
139
|
{` · ${reset}`}
|
|
140
140
|
</text>
|
|
@@ -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
|
+
}
|