@brimveyn/aimux 1.21.0 → 1.22.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 +27 -25
- package/package.json +2 -2
- package/src/app-runtime/auto-commit-driver.ts +22 -27
- package/src/app-runtime/backend-attach-runtime.ts +43 -36
- package/src/app-runtime/backend-runtime-events.ts +107 -88
- package/src/app-runtime/editor-actions.ts +201 -0
- package/src/app-runtime/git-actions.ts +197 -0
- package/src/app-runtime/measure-ref.ts +19 -0
- package/src/app-runtime/navigation-actions.ts +213 -0
- package/src/app-runtime/project-actions.ts +188 -0
- package/src/app-runtime/prompt-injection.ts +123 -0
- package/src/app-runtime/selection.ts +29 -0
- package/src/app-runtime/settings-actions.ts +156 -0
- package/src/app-runtime/setup-actions.ts +237 -0
- package/src/app-runtime/side-effect-context.ts +33 -0
- package/src/app-runtime/side-effects.ts +302 -1541
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/tab-actions.ts +218 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
- package/src/app-runtime/use-auto-commit-driver.ts +14 -13
- package/src/app-runtime/use-backend-runtime.ts +12 -11
- package/src/app-runtime/use-directory-search.ts +5 -4
- package/src/app-runtime/use-mouse-handlers.ts +2 -1
- package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
- package/src/app-runtime/use-renderer-bindings.ts +2 -1
- package/src/app-runtime/use-setup-runner.ts +96 -0
- package/src/app-runtime/use-terminal-resize.ts +18 -14
- package/src/app-runtime/workspace-actions.ts +366 -0
- package/src/app-runtime/workspace-activity.ts +138 -0
- package/src/app-runtime/workspace-naming.ts +90 -0
- package/src/app.tsx +93 -48
- package/src/assets/sounds/bell.wav +0 -0
- package/src/assets/sounds/ding.wav +0 -0
- package/src/assets/sounds/plane.wav +0 -0
- package/src/cli/client/daemon-client.ts +2 -2
- package/src/cli/client/project-resolver.ts +112 -0
- package/src/cli/commands/project/close.ts +32 -0
- package/src/cli/commands/project/create.ts +93 -0
- package/src/cli/commands/project/list.ts +25 -0
- package/src/cli/commands/project/show.ts +32 -0
- package/src/cli/commands/{workspace → project}/switch.ts +19 -19
- package/src/cli/commands/tab/await.ts +2 -2
- package/src/cli/commands/tab/close.ts +3 -3
- package/src/cli/commands/tab/create.ts +74 -73
- package/src/cli/commands/tab/focus.ts +3 -3
- package/src/cli/commands/tab/list.ts +6 -6
- package/src/cli/commands/tab/run.ts +2 -2
- package/src/cli/commands/tab/send.ts +2 -2
- package/src/cli/commands/tab/snapshot.ts +2 -2
- package/src/cli/commands/tab/tail.ts +2 -2
- package/src/cli/commands/tab/wait.ts +2 -2
- package/src/cli/commands/worker/await.ts +3 -3
- package/src/cli/commands/worker/doctor.ts +30 -30
- package/src/cli/commands/worker/list.ts +15 -15
- package/src/cli/commands/worker/prompt.ts +3 -3
- package/src/cli/commands/worker/run.ts +23 -23
- package/src/cli/commands/worker/shared.ts +53 -59
- package/src/cli/commands/worker/stop.ts +29 -29
- package/src/cli/commands/worker/submit.ts +3 -3
- package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
- package/src/cli/commands/workspace/create.ts +30 -68
- package/src/cli/commands/workspace/list.ts +52 -9
- package/src/cli/commands/workspace/remove.ts +81 -0
- package/src/cli/completion/plan.ts +2 -2
- package/src/cli/completion/sources.ts +18 -18
- package/src/cli/context.ts +13 -13
- package/src/cli/flags.ts +30 -10
- package/src/cli/index.ts +26 -16
- package/src/cli/output.ts +1 -1
- package/src/cli/registry.ts +12 -12
- package/src/config/loader.ts +16 -5
- package/src/config.ts +70 -112
- package/src/daemon/catalog-writer.ts +67 -67
- package/src/daemon/daemon.ts +205 -201
- package/src/daemon/session-manager.ts +39 -39
- package/src/daemon/session-registry.ts +14 -14
- package/src/git/divergence.ts +33 -2
- package/src/git/git-poller.ts +1 -1
- package/src/git/git-status.ts +2 -2
- package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
- package/src/git/pr-status.ts +1 -1
- package/src/git/repo-discovery.ts +1 -1
- package/src/git/use-repo-discovery.ts +1 -1
- package/src/git/workspace-branch-poller.ts +54 -0
- package/src/git/workspace-divergence-poller.ts +65 -0
- package/src/git/worktree.ts +29 -0
- package/src/index.tsx +4 -4
- package/src/input/keymap/help-entries.ts +7 -5
- package/src/input/modes/bridge.ts +13 -12
- package/src/input/modes/handlers/shared.ts +1 -1
- package/src/input/modes/transitions.ts +34 -21
- package/src/input/modes/types.ts +48 -31
- package/src/ipc/manager-protocol.ts +44 -44
- package/src/ipc/protocol.ts +192 -166
- package/src/platform/play-sound.ts +184 -0
- package/src/platform/project-search.ts +8 -8
- package/src/platform/worktree-paths.ts +8 -6
- package/src/pty/assistant-question-extractor.ts +1 -1
- package/src/pty/assistant-status-detection-loop.ts +79 -55
- package/src/pty/assistant-status-detector.ts +40 -15
- package/src/pty/command-registry.ts +81 -1
- package/src/restart-terminal-manager.ts +1 -1
- package/src/services/ai-usage/provider.ts +9 -2
- package/src/services/ai-usage/spawn.ts +19 -6
- package/src/session-backend/bootstrap.ts +4 -4
- package/src/session-backend/local-session-backend.ts +78 -75
- package/src/session-backend/remote-session-backend.ts +64 -52
- package/src/session-backend/types.ts +43 -32
- package/src/settings/live.ts +90 -0
- package/src/settings/search.ts +43 -0
- package/src/settings/sections/about.ts +49 -0
- package/src/settings/sections/appearance.ts +54 -0
- package/src/settings/sections/automation.ts +105 -0
- package/src/settings/sections/commands.ts +72 -0
- package/src/settings/sections/editor.ts +56 -0
- package/src/settings/sections/experimental.ts +57 -0
- package/src/settings/sections/git.ts +112 -0
- package/src/settings/sections/index.ts +95 -0
- package/src/settings/sections/integrations.ts +23 -0
- package/src/settings/sections/layout.ts +72 -0
- package/src/settings/sections/notifications.ts +99 -0
- package/src/settings/sections/setup.ts +54 -0
- package/src/settings/sections/status-bar.ts +63 -0
- package/src/settings/settings-store.ts +207 -0
- package/src/settings/types.ts +108 -0
- package/src/snippets/run-shell-var.ts +48 -22
- package/src/state/actions.ts +342 -0
- package/src/state/app-store.ts +2 -1
- package/src/state/bars.ts +1 -1
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/layout-resize.ts +2 -2
- package/src/state/project-catalog.ts +275 -0
- package/src/state/project-data.ts +143 -0
- package/src/state/{session-persistence.ts → project-persistence.ts} +74 -68
- package/src/state/project-save.ts +46 -0
- package/src/state/project-workspaces.ts +392 -0
- package/src/state/reducers/auto-commit-state.ts +11 -10
- package/src/state/reducers/git-commit-modal-state.ts +122 -0
- package/src/state/reducers/git-mode-state.ts +1 -1
- package/src/state/reducers/git-panel-state.ts +10 -6
- package/src/state/reducers/modal-state.ts +290 -681
- package/src/state/reducers/multi-repo-state.ts +3 -6
- package/src/state/reducers/project-state.ts +312 -0
- package/src/state/reducers/settings-state.ts +80 -0
- package/src/state/reducers/tab-state.ts +107 -60
- package/src/state/reducers/ui-state.ts +4 -3
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +57 -24
- package/src/state/types.ts +244 -405
- package/src/state/validation.ts +14 -12
- package/src/terminal-manager/manager-client.ts +32 -32
- package/src/terminal-manager/terminal-manager.ts +24 -24
- package/src/ui/components/git/git-panel.tsx +2 -2
- package/src/ui/components/git/git-view.tsx +12 -12
- package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
- package/src/ui/components/git/pane/git-pane-widget.tsx +7 -7
- package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
- package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
- package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
- package/src/ui/components/layout/status-bar.tsx +6 -2
- package/src/ui/components/layout/terminal-pane.tsx +34 -29
- package/src/ui/components/layout/top-tab-bar.tsx +46 -36
- package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
- package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
- package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
- package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
- package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
- package/src/ui/components/modals/shared/form.tsx +34 -22
- package/src/ui/components/modals/shared/picker.tsx +10 -10
- package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
- package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
- package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
- package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
- package/src/ui/components/primitives/input-field.tsx +7 -3
- package/src/ui/components/primitives/surface.tsx +3 -0
- package/src/ui/components/settings/row-value.tsx +74 -0
- package/src/ui/components/settings/settings-row.tsx +78 -0
- package/src/ui/components/settings/settings-view.tsx +169 -0
- package/src/ui/components/setup/setup-widget.tsx +152 -0
- package/src/ui/flash/build-labels.ts +27 -27
- package/src/ui/hooks/use-activity-sprite.ts +73 -0
- package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
- package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
- package/src/ui/root.tsx +139 -121
- package/src/ui/status-bar-model.ts +48 -34
- package/src/ui/terminal-graphics/kitty.ts +28 -2
- package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
- package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
- package/src/ui/terminal-graphics/sprites.ts +246 -0
- package/src/ui/truncate.ts +7 -0
- package/src/ui/widgets/registry.tsx +6 -3
- package/src/ui/widgets/widget-context-menu.ts +26 -10
- package/src/update.ts +2 -2
- package/src/app-runtime/session-actions.ts +0 -187
- package/src/cli/client/workspace-resolver.ts +0 -110
- package/src/cli/commands/workspace/close.ts +0 -32
- package/src/cli/commands/workspace/show.ts +0 -32
- package/src/cli/commands/worktree/create.ts +0 -55
- package/src/cli/commands/worktree/list.ts +0 -68
- package/src/cli/commands/worktree/remove.ts +0 -81
- package/src/git/worktree-branch-poller.ts +0 -54
- package/src/git/worktree-divergence-poller.ts +0 -59
- package/src/state/reducers/session-state.ts +0 -262
- package/src/state/session-catalog.ts +0 -143
- package/src/state/session-worktrees.ts +0 -260
- package/src/state/workspace-save.ts +0 -43
- package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
- package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
- package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
- /package/src/ui/{components/git/image-diff → terminal-graphics}/dimensions.ts +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BoxRenderable,
|
|
3
|
+
MouseEvent as OtuiMouseEvent,
|
|
4
|
+
ScrollBoxRenderable,
|
|
5
|
+
} from '@opentui/core'
|
|
6
|
+
|
|
7
|
+
import { memo, type ReactNode, useCallback, useMemo, useRef, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
import type { ProjectRecord, ProjectStatus } from '../../../../state/types'
|
|
10
|
+
|
|
11
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
12
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
13
|
+
import { getPrimaryWorkspace } from '../../../../state/project-workspaces'
|
|
14
|
+
// eslint-disable-next-line no-duplicate-imports
|
|
15
|
+
import { IDLE_PROJECT_STATUS } from '../../../../state/types'
|
|
16
|
+
import { moveIdToIdPosition, orderProjectsForDisplay } from '../../../project-ordering'
|
|
17
|
+
import { useBaseTheme, useTheme } from '../../../theme'
|
|
18
|
+
import { truncate } from '../../../truncate'
|
|
19
|
+
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
20
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
21
|
+
import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
|
|
22
|
+
import { WorkspaceRow } from './workspace-row'
|
|
23
|
+
|
|
24
|
+
interface ProjectListProps {
|
|
25
|
+
contentWidth: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const COLUMN_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 0 }
|
|
29
|
+
|
|
30
|
+
const RULE = '─'
|
|
31
|
+
const HEADER_TITLE = 'Projects'
|
|
32
|
+
/**
|
|
33
|
+
* U+2699, not the nerd-font gear: its Emoji_Presentation is No, so a conforming
|
|
34
|
+
* terminal draws it text-style in one cell and no font has to be installed for
|
|
35
|
+
* the one button that opens the settings.
|
|
36
|
+
*/
|
|
37
|
+
const SETTINGS_GLYPH = '⚙'
|
|
38
|
+
|
|
39
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
40
|
+
if (a.length !== b.length) return false
|
|
41
|
+
for (let i = 0; i < a.length; i++) {
|
|
42
|
+
if (a[i] !== b[i]) return false
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function ProjectList({ contentWidth }: ProjectListProps) {
|
|
48
|
+
const t = useTheme()
|
|
49
|
+
const projects = useAppStore((s) => s.projects)
|
|
50
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
51
|
+
const statusMap = useAppStore((s) => s.projectStatuses)
|
|
52
|
+
|
|
53
|
+
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
54
|
+
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
55
|
+
const lastSwapWithRef = useRef<string | null>(null)
|
|
56
|
+
const rowRefs = useRef(new Map<string, BoxRenderable>())
|
|
57
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
58
|
+
|
|
59
|
+
const ordered = useMemo(() => orderProjectsForDisplay(projects), [projects])
|
|
60
|
+
const baselineOrder = useMemo(() => ordered.map((s) => s.id), [ordered])
|
|
61
|
+
|
|
62
|
+
const currentProject = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
currentProjectId != null && currentProjectId !== ''
|
|
65
|
+
? projects.find((s) => s.id === currentProjectId)
|
|
66
|
+
: undefined,
|
|
67
|
+
[currentProjectId, projects]
|
|
68
|
+
)
|
|
69
|
+
// The cursor is always on a workspace row now, so there is one id to scroll
|
|
70
|
+
// to instead of two — otherwise it visually "disappears" off-screen when a
|
|
71
|
+
// key press crosses a project boundary.
|
|
72
|
+
const rawActiveWorkspaceId = currentProject?.activeWorkspaceId
|
|
73
|
+
const activeWorkspaceId =
|
|
74
|
+
rawActiveWorkspaceId != null && rawActiveWorkspaceId !== ''
|
|
75
|
+
? rawActiveWorkspaceId
|
|
76
|
+
: getPrimaryWorkspace(currentProject?.workspaces)?.id
|
|
77
|
+
const activeRowId =
|
|
78
|
+
activeWorkspaceId != null && activeWorkspaceId !== '' ? `sidebar-wt-${activeWorkspaceId}` : null
|
|
79
|
+
|
|
80
|
+
useSidebarAutoScroll({
|
|
81
|
+
activeRowId,
|
|
82
|
+
scrollRef,
|
|
83
|
+
visible: true,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const setRowRef = useCallback((id: string, ref: BoxRenderable | null): void => {
|
|
87
|
+
if (ref) rowRefs.current.set(id, ref)
|
|
88
|
+
else rowRefs.current.delete(id)
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
const findRowAtY = useCallback((y: number): string | null => {
|
|
92
|
+
for (const [id, ref] of rowRefs.current) {
|
|
93
|
+
if (y >= ref.y && y < ref.y + ref.height) return id
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
const handleRowDragStart = useCallback(
|
|
99
|
+
(id: string) => {
|
|
100
|
+
setDraggingId(id)
|
|
101
|
+
setDragOrder(baselineOrder)
|
|
102
|
+
lastSwapWithRef.current = null
|
|
103
|
+
},
|
|
104
|
+
[baselineOrder]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const handleRowDrag = useCallback(
|
|
108
|
+
(event: OtuiMouseEvent) => {
|
|
109
|
+
if (!(draggingId != null && draggingId !== '')) return
|
|
110
|
+
const hit = findRowAtY(event.y)
|
|
111
|
+
if (hit === null) {
|
|
112
|
+
lastSwapWithRef.current = null
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
if (hit === draggingId) {
|
|
116
|
+
lastSwapWithRef.current = null
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (hit === lastSwapWithRef.current) return
|
|
120
|
+
setDragOrder((prev) => (prev ? moveIdToIdPosition(prev, draggingId, hit) : prev))
|
|
121
|
+
lastSwapWithRef.current = hit
|
|
122
|
+
},
|
|
123
|
+
[draggingId, findRowAtY]
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const commitDrop = useCallback(() => {
|
|
127
|
+
const source = draggingId
|
|
128
|
+
const finalOrder = dragOrder
|
|
129
|
+
setDraggingId(null)
|
|
130
|
+
setDragOrder(null)
|
|
131
|
+
lastSwapWithRef.current = null
|
|
132
|
+
|
|
133
|
+
if (source == null || source === '' || !finalOrder) return
|
|
134
|
+
|
|
135
|
+
const changed = !arraysEqual(finalOrder, baselineOrder)
|
|
136
|
+
if (changed) {
|
|
137
|
+
dispatchGlobal({ orderedIds: finalOrder, type: 'reorder-projects' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const idx = baselineOrder.indexOf(source)
|
|
142
|
+
if (idx >= 0) {
|
|
143
|
+
// The heading stands for the project itself, not for whichever workspace
|
|
144
|
+
// was last active in it, so clicking it lands on the checkout — the one
|
|
145
|
+
// workspace every project is guaranteed to have.
|
|
146
|
+
const sourceProject = ordered.find((s) => s.id === source)
|
|
147
|
+
const sourceWorkspaces = sourceProject?.workspaces ?? []
|
|
148
|
+
const sourcePrimaryId = getPrimaryWorkspace(sourceWorkspaces)?.id
|
|
149
|
+
runSideEffectGlobal({
|
|
150
|
+
index: idx + 1,
|
|
151
|
+
type: 'switch-project-by-index',
|
|
152
|
+
workspaceId: sourcePrimaryId,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}, [baselineOrder, dragOrder, draggingId, ordered])
|
|
156
|
+
|
|
157
|
+
const cancelDrag = useCallback(() => {
|
|
158
|
+
setDraggingId(null)
|
|
159
|
+
setDragOrder(null)
|
|
160
|
+
lastSwapWithRef.current = null
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
const handleNewProject = useCallback((e: OtuiMouseEvent) => {
|
|
164
|
+
e.stopPropagation()
|
|
165
|
+
e.preventDefault()
|
|
166
|
+
dispatchGlobal({ returnToProjectPicker: false, type: 'open-create-project-modal' })
|
|
167
|
+
}, [])
|
|
168
|
+
|
|
169
|
+
// The settings screen's only mouse-reachable way in. It lives here because this
|
|
170
|
+
// header is on screen whenever the left bar is, and because the `+` beside it
|
|
171
|
+
// already taught the same gesture.
|
|
172
|
+
const handleOpenSettings = useCallback((e: OtuiMouseEvent) => {
|
|
173
|
+
e.stopPropagation()
|
|
174
|
+
e.preventDefault()
|
|
175
|
+
dispatchGlobal({ type: 'enter-settings' })
|
|
176
|
+
}, [])
|
|
177
|
+
|
|
178
|
+
const visibleProjects =
|
|
179
|
+
dragOrder !== null
|
|
180
|
+
? dragOrder
|
|
181
|
+
.map((id) => ordered.find((s) => s.id === id))
|
|
182
|
+
.filter((s): s is ProjectRecord => !!s)
|
|
183
|
+
: ordered
|
|
184
|
+
|
|
185
|
+
const rule = RULE.repeat(Math.max(1, contentWidth))
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1} overflow="hidden">
|
|
189
|
+
<box flexShrink={0}>
|
|
190
|
+
<text fg={t.border} selectable={false} wrapMode="none">
|
|
191
|
+
{rule}
|
|
192
|
+
</text>
|
|
193
|
+
</box>
|
|
194
|
+
<box flexDirection="row" flexShrink={0} paddingLeft={1} paddingRight={1}>
|
|
195
|
+
<text fg={t.text} selectable={false} wrapMode="none">
|
|
196
|
+
{HEADER_TITLE}
|
|
197
|
+
</text>
|
|
198
|
+
<box flexGrow={1} flexShrink={1} />
|
|
199
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none" onMouseDown={handleNewProject}>
|
|
200
|
+
+
|
|
201
|
+
</text>
|
|
202
|
+
</box>
|
|
203
|
+
<scrollbox
|
|
204
|
+
ref={scrollRef}
|
|
205
|
+
scrollY
|
|
206
|
+
flexGrow={1}
|
|
207
|
+
flexShrink={1}
|
|
208
|
+
contentOptions={COLUMN_CONTENT_OPTIONS}
|
|
209
|
+
>
|
|
210
|
+
{(() => {
|
|
211
|
+
// Build a single flat list — each project's heading followed by every
|
|
212
|
+
// one of its workspaces. One map, one React keypath per visible row;
|
|
213
|
+
// transitions are a single atomic reconciliation.
|
|
214
|
+
const rows: ReactNode[] = []
|
|
215
|
+
for (const project of visibleProjects) {
|
|
216
|
+
const projectIndex = baselineOrder.indexOf(project.id) + 1
|
|
217
|
+
const isCurrentProject = project.id === currentProjectId
|
|
218
|
+
const workspaces = project.workspaces ?? []
|
|
219
|
+
// Every workspace gets a row, the checkout included. Folding it into
|
|
220
|
+
// the project row made one row mean two things — a project you
|
|
221
|
+
// switch to and a workspace you run tabs in — and left the checkout
|
|
222
|
+
// the only workspace with no branch and no churn on screen.
|
|
223
|
+
const activeWorkspaceId =
|
|
224
|
+
project.activeWorkspaceId != null && project.activeWorkspaceId !== ''
|
|
225
|
+
? project.activeWorkspaceId
|
|
226
|
+
: getPrimaryWorkspace(workspaces)?.id
|
|
227
|
+
rows.push(
|
|
228
|
+
<ProjectRow
|
|
229
|
+
key={`ws:${project.id}`}
|
|
230
|
+
project={project}
|
|
231
|
+
inCurrentGroup={isCurrentProject}
|
|
232
|
+
projectIndex={projectIndex}
|
|
233
|
+
status={statusMap[project.id] ?? IDLE_PROJECT_STATUS}
|
|
234
|
+
dragging={draggingId === project.id}
|
|
235
|
+
contentWidth={contentWidth}
|
|
236
|
+
// Every project gets a blank line above it, which doubles as
|
|
237
|
+
// the gap under the header — one rule instead of a spacer row
|
|
238
|
+
// that would shift the list every time the header changes.
|
|
239
|
+
marginTop={1}
|
|
240
|
+
setRowRef={setRowRef}
|
|
241
|
+
onDragStart={handleRowDragStart}
|
|
242
|
+
onDrag={handleRowDrag}
|
|
243
|
+
onDrop={commitDrop}
|
|
244
|
+
onDragCancel={cancelDrag}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
247
|
+
for (const workspace of workspaces) {
|
|
248
|
+
rows.push(
|
|
249
|
+
<WorkspaceRow
|
|
250
|
+
key={`wt:${workspace.id}`}
|
|
251
|
+
project={project}
|
|
252
|
+
workspace={workspace}
|
|
253
|
+
projectIndex={projectIndex}
|
|
254
|
+
isActiveItem={isCurrentProject && workspace.id === activeWorkspaceId}
|
|
255
|
+
inCurrentGroup={isCurrentProject}
|
|
256
|
+
contentWidth={contentWidth}
|
|
257
|
+
/>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return rows
|
|
262
|
+
})()}
|
|
263
|
+
</scrollbox>
|
|
264
|
+
<box flexShrink={0}>
|
|
265
|
+
<text fg={t.border} selectable={false} wrapMode="none">
|
|
266
|
+
{rule}
|
|
267
|
+
</text>
|
|
268
|
+
</box>
|
|
269
|
+
<box flexDirection="row" flexShrink={0} paddingLeft={1} paddingRight={1}>
|
|
270
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none" onMouseDown={handleOpenSettings}>
|
|
271
|
+
{`${SETTINGS_GLYPH} Settings`}
|
|
272
|
+
</text>
|
|
273
|
+
</box>
|
|
274
|
+
</box>
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
interface ProjectRowProps {
|
|
279
|
+
project: ProjectRecord
|
|
280
|
+
/**
|
|
281
|
+
* True when this row belongs to the current project (selection scope).
|
|
282
|
+
*
|
|
283
|
+
* There is deliberately no `isActiveItem`: the cursor lives on workspace
|
|
284
|
+
* rows, and this row is the heading they sit under.
|
|
285
|
+
*/
|
|
286
|
+
inCurrentGroup: boolean
|
|
287
|
+
/** 1-based index in the visible order, so the "+" can switch projects first. */
|
|
288
|
+
projectIndex: number
|
|
289
|
+
status: ProjectStatus
|
|
290
|
+
dragging: boolean
|
|
291
|
+
contentWidth: number
|
|
292
|
+
/** Vertical spacing above this row — used to separate project blocks. */
|
|
293
|
+
marginTop: number
|
|
294
|
+
setRowRef: (id: string, ref: BoxRenderable | null) => void
|
|
295
|
+
onDragStart: (id: string) => void
|
|
296
|
+
onDrag: (event: OtuiMouseEvent) => void
|
|
297
|
+
onDrop: () => void
|
|
298
|
+
onDragCancel: () => void
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const ProjectRow = memo(function ProjectRow({
|
|
302
|
+
contentWidth,
|
|
303
|
+
dragging,
|
|
304
|
+
inCurrentGroup,
|
|
305
|
+
marginTop,
|
|
306
|
+
onDrag,
|
|
307
|
+
onDragCancel,
|
|
308
|
+
onDragStart,
|
|
309
|
+
onDrop,
|
|
310
|
+
project,
|
|
311
|
+
projectIndex,
|
|
312
|
+
setRowRef,
|
|
313
|
+
status,
|
|
314
|
+
}: ProjectRowProps) {
|
|
315
|
+
const t = useTheme()
|
|
316
|
+
// Selection highlight must stay opaque in transparent mode — otherwise the
|
|
317
|
+
// cursor row visually disappears against the see-through chrome.
|
|
318
|
+
const base = useBaseTheme()
|
|
319
|
+
// State belongs on the workspace rows: they say *which* one is working or
|
|
320
|
+
// waiting, and this heading can only say "somewhere below". So the heading
|
|
321
|
+
// speaks only when none of its workspaces can — a tab whose workspace this
|
|
322
|
+
// client doesn't know, which today means a daemon still running a pre-v18
|
|
323
|
+
// protocol. Without that fallback the sidebar would go silent instead of
|
|
324
|
+
// degrading.
|
|
325
|
+
const workspacesSpeak = useAppStore((s) =>
|
|
326
|
+
(project.workspaces ?? []).some((workspace) => {
|
|
327
|
+
const activity = s.workspaceActivity[workspace.id]
|
|
328
|
+
return activity !== undefined && (activity.working || activity.waiting || activity.done)
|
|
329
|
+
})
|
|
330
|
+
)
|
|
331
|
+
const showWaiting = status.waiting && !workspacesSpeak
|
|
332
|
+
// Only the drag highlight is "selected"-strength here. A heading that lights
|
|
333
|
+
// up like a cursor row is what made the project look like a workspace.
|
|
334
|
+
let bgColor: string | undefined
|
|
335
|
+
if (dragging) {
|
|
336
|
+
bgColor = base.backgroundElement
|
|
337
|
+
} else if (inCurrentGroup) {
|
|
338
|
+
bgColor = base.backgroundPanel
|
|
339
|
+
}
|
|
340
|
+
const waitingColor = t.warning
|
|
341
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
342
|
+
|
|
343
|
+
const handleRef = useCallback(
|
|
344
|
+
(r: BoxRenderable | null) => setRowRef(project.id, r),
|
|
345
|
+
[setRowRef, project.id]
|
|
346
|
+
)
|
|
347
|
+
const handleMouseDown = useCallback(
|
|
348
|
+
(e: OtuiMouseEvent) => {
|
|
349
|
+
e.preventDefault()
|
|
350
|
+
e.stopPropagation()
|
|
351
|
+
onDragStart(project.id)
|
|
352
|
+
},
|
|
353
|
+
[onDragStart, project.id]
|
|
354
|
+
)
|
|
355
|
+
const handleMouseUp = useCallback(
|
|
356
|
+
(e: OtuiMouseEvent) => {
|
|
357
|
+
e.preventDefault()
|
|
358
|
+
onDrop()
|
|
359
|
+
},
|
|
360
|
+
[onDrop]
|
|
361
|
+
)
|
|
362
|
+
const handleNewWorkspace = useCallback(
|
|
363
|
+
(e: OtuiMouseEvent) => {
|
|
364
|
+
e.preventDefault()
|
|
365
|
+
e.stopPropagation()
|
|
366
|
+
// The modal always targets the current project, so make this one current
|
|
367
|
+
// first. Both run through the store synchronously, so the branch load
|
|
368
|
+
// below already sees the switch.
|
|
369
|
+
if (project.id !== currentProjectId) {
|
|
370
|
+
runSideEffectGlobal({ index: projectIndex, type: 'switch-project-by-index' })
|
|
371
|
+
}
|
|
372
|
+
dispatchGlobal({ type: 'open-create-workspace-modal' })
|
|
373
|
+
runSideEffectGlobal({ type: 'load-create-workspace-base-branches' })
|
|
374
|
+
},
|
|
375
|
+
[currentProjectId, project.id, projectIndex]
|
|
376
|
+
)
|
|
377
|
+
const rightClickMenu = useMemo<[string, () => void][]>(
|
|
378
|
+
() => [
|
|
379
|
+
[
|
|
380
|
+
'Rename project',
|
|
381
|
+
() =>
|
|
382
|
+
dispatchGlobal({
|
|
383
|
+
initialName: project.name,
|
|
384
|
+
projectTargetId: project.id,
|
|
385
|
+
returnToProjectPicker: false,
|
|
386
|
+
type: 'open-project-name-modal',
|
|
387
|
+
}),
|
|
388
|
+
],
|
|
389
|
+
[
|
|
390
|
+
'Delete project',
|
|
391
|
+
() => runSideEffectGlobal({ projectId: project.id, type: 'delete-project' }),
|
|
392
|
+
],
|
|
393
|
+
],
|
|
394
|
+
[project.id, project.name]
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
// Neutral muted marker, and a question when a workspace of this project needs
|
|
398
|
+
// an answer. No progress indicator: a heading can only say "somewhere below",
|
|
399
|
+
// and the row that actually knows is one line down. Keeping a glyph in the slot
|
|
400
|
+
// avoids name shifts when the question comes and goes — which is also why the
|
|
401
|
+
// gap after it is part of the glyph.
|
|
402
|
+
let leadingGlyph = '• '
|
|
403
|
+
let leadingColor = t.textMuted
|
|
404
|
+
if (showWaiting) {
|
|
405
|
+
leadingGlyph = '? '
|
|
406
|
+
leadingColor = waitingColor
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// No branch line: the repo checkout is not somewhere aimux works, so naming
|
|
410
|
+
// it under every project only ever read as "you are on main".
|
|
411
|
+
const nameLabel = truncate(project.name, Math.max(0, contentWidth - 6))
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<ContextMenuBox
|
|
415
|
+
ref={handleRef}
|
|
416
|
+
id={`sidebar-ws-${project.id}`}
|
|
417
|
+
flexDirection="column"
|
|
418
|
+
flexShrink={0}
|
|
419
|
+
marginTop={marginTop}
|
|
420
|
+
paddingLeft={1}
|
|
421
|
+
paddingRight={1}
|
|
422
|
+
backgroundColor={bgColor}
|
|
423
|
+
rightClickMenu={rightClickMenu}
|
|
424
|
+
onMouseDown={handleMouseDown}
|
|
425
|
+
onMouseDrag={onDrag}
|
|
426
|
+
onMouseUp={handleMouseUp}
|
|
427
|
+
onMouseDragEnd={onDragCancel}
|
|
428
|
+
>
|
|
429
|
+
<box flexDirection="row" alignItems="center">
|
|
430
|
+
<text fg={leadingColor} selectable={false} wrapMode="none">
|
|
431
|
+
{leadingGlyph}
|
|
432
|
+
</text>
|
|
433
|
+
<FlashLabelBadge rowKey={`ws:${project.id}`} />
|
|
434
|
+
<text fg={t.text} selectable={false} wrapMode="none">
|
|
435
|
+
{nameLabel}
|
|
436
|
+
</text>
|
|
437
|
+
<box flexGrow={1} flexShrink={1} />
|
|
438
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none" onMouseDown={handleNewWorkspace}>
|
|
439
|
+
+
|
|
440
|
+
</text>
|
|
441
|
+
</box>
|
|
442
|
+
</ContextMenuBox>
|
|
443
|
+
)
|
|
444
|
+
})
|
|
@@ -16,8 +16,8 @@ interface TabItemProps {
|
|
|
16
16
|
active: boolean
|
|
17
17
|
focused: boolean
|
|
18
18
|
inLayout?: boolean
|
|
19
|
-
/** When set, this tab's
|
|
20
|
-
|
|
19
|
+
/** When set, this tab's workspace can be moved — adds a "Move workspace" entry. */
|
|
20
|
+
moveWorkspaceId?: string
|
|
21
21
|
/** 1-based position in the visible tab list — rendered as `[N]` and matches Leader+N. */
|
|
22
22
|
indexLabel?: string
|
|
23
23
|
/** Render the close × unconditionally instead of only on hover. */
|
|
@@ -57,11 +57,11 @@ function getIndicatorColor(active: boolean, focused: boolean, inLayout: boolean)
|
|
|
57
57
|
|
|
58
58
|
function BusyGlyph() {
|
|
59
59
|
const t = useTheme()
|
|
60
|
-
const
|
|
60
|
+
const spinner = useBusySpinner()
|
|
61
61
|
return (
|
|
62
62
|
<text fg={t.primary} selectable={false}>
|
|
63
63
|
{' '}
|
|
64
|
-
{
|
|
64
|
+
{spinner}
|
|
65
65
|
</text>
|
|
66
66
|
)
|
|
67
67
|
}
|
|
@@ -78,6 +78,8 @@ function WaitingGlyph() {
|
|
|
78
78
|
|
|
79
79
|
function ActivityGlyph({ tab }: { tab: TabSession }) {
|
|
80
80
|
const t = useTheme()
|
|
81
|
+
// No sprite here on purpose: a tab is one line, and a sprite placed at a
|
|
82
|
+
// second size would give a cell two images it could resolve to.
|
|
81
83
|
if (tab.status === 'error') {
|
|
82
84
|
return (
|
|
83
85
|
<text fg={t.error} selectable={false}>
|
|
@@ -104,7 +106,10 @@ function ActivityGlyph({ tab }: { tab: TabSession }) {
|
|
|
104
106
|
return <WaitingGlyph />
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
|
|
109
|
+
// The tab that rang. There is deliberately no glyph for a plain idle tab:
|
|
110
|
+
// "not busy" is the resting state of every tab you are not watching, and a
|
|
111
|
+
// dot that is always on cannot also mean "read this one".
|
|
112
|
+
if (tab.unseen === true) {
|
|
108
113
|
return (
|
|
109
114
|
<text fg={t.success} selectable={false}>
|
|
110
115
|
{' '}
|
|
@@ -113,6 +118,16 @@ function ActivityGlyph({ tab }: { tab: TabSession }) {
|
|
|
113
118
|
)
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
if (tab.activity === 'idle') {
|
|
122
|
+
// Blank, but the same width as every other state — a tab must not resize
|
|
123
|
+
// when its agent stops.
|
|
124
|
+
return (
|
|
125
|
+
<text fg={t.textMuted} selectable={false}>
|
|
126
|
+
{' '}
|
|
127
|
+
</text>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
116
131
|
return (
|
|
117
132
|
<text fg={getStatusColor(tab.status)} selectable={false}>
|
|
118
133
|
{' '}
|
|
@@ -128,7 +143,7 @@ export function TabItem({
|
|
|
128
143
|
id,
|
|
129
144
|
indexLabel,
|
|
130
145
|
inLayout,
|
|
131
|
-
|
|
146
|
+
moveWorkspaceId,
|
|
132
147
|
tab,
|
|
133
148
|
}: TabItemProps) {
|
|
134
149
|
const t = useTheme()
|
|
@@ -167,22 +182,22 @@ export function TabItem({
|
|
|
167
182
|
dispatchGlobal({ delta: 1, type: 'reorder-active-tab' })
|
|
168
183
|
},
|
|
169
184
|
],
|
|
170
|
-
...(
|
|
185
|
+
...(moveWorkspaceId != null && moveWorkspaceId !== ''
|
|
171
186
|
? [
|
|
172
187
|
[
|
|
173
|
-
'Move
|
|
188
|
+
'Move workspace',
|
|
174
189
|
() => {
|
|
175
190
|
dispatchGlobal({
|
|
176
|
-
|
|
177
|
-
type: 'open-
|
|
191
|
+
sourceWorkspaceId: moveWorkspaceId,
|
|
192
|
+
type: 'open-workspace-move-modal',
|
|
178
193
|
})
|
|
179
|
-
runSideEffectGlobal({ type: 'load-
|
|
194
|
+
runSideEffectGlobal({ type: 'load-workspace-move-stats' })
|
|
180
195
|
},
|
|
181
196
|
] as [string, () => void],
|
|
182
197
|
]
|
|
183
198
|
: []),
|
|
184
199
|
],
|
|
185
|
-
[
|
|
200
|
+
[moveWorkspaceId, tab.id]
|
|
186
201
|
)
|
|
187
202
|
const handleMouseOver = useCallback(() => setHovered(true), [])
|
|
188
203
|
const handleMouseOut = useCallback(() => setHovered(false), [])
|
|
@@ -5,7 +5,7 @@ import { useEffect } from 'react'
|
|
|
5
5
|
interface Options {
|
|
6
6
|
scrollRef: React.RefObject<ScrollBoxRenderable | null>
|
|
7
7
|
visible: boolean
|
|
8
|
-
/** Full id of the active row to bring into view (
|
|
8
|
+
/** Full id of the active row to bring into view (project OR workspace). */
|
|
9
9
|
activeRowId: string | null
|
|
10
10
|
}
|
|
11
11
|
|