@brimveyn/aimux 1.20.4 → 1.22.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 +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 +305 -1589
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/split-drag-controller.ts +4 -8
- 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 +28 -42
- 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 +28 -33
- 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 +102 -85
- 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 +149 -101
- 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-merge.ts +64 -0
- package/src/git/pr-status-poller.ts +57 -0
- package/src/git/pr-status.ts +227 -0
- 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 +11 -6
- 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/open-url.ts +39 -0
- 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 +3 -1
- 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 +75 -0
- package/src/state/dispatch-ref.ts +1 -1
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/layout-resize.ts +2 -2
- package/src/state/pr-status-store.ts +39 -0
- 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} +77 -73
- 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 -53
- 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 +86 -15
- package/src/state/selectors.ts +49 -37
- package/src/state/store.ts +83 -57
- package/src/state/types.ts +261 -417
- 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/diff-renderer/pierre-diff.tsx +2 -1
- 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-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
- package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
- package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
- package/src/ui/components/layout/bar.tsx +191 -0
- 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 +48 -37
- 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 +164 -230
- 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 +21 -0
- package/src/ui/widgets/widget-context-menu.ts +85 -0
- 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 -45
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
- 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
|
@@ -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
|
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { type MouseEvent as OtuiMouseEvent, TextAttributes } from '@opentui/core'
|
|
2
|
+
import { memo, useCallback, useMemo } from 'react'
|
|
3
|
+
|
|
4
|
+
import type { ProjectRecord, WorkspaceActivity, WorkspaceRecord } from '../../../../state/types'
|
|
5
|
+
import type { SpriteState } from '../../../terminal-graphics/sprites'
|
|
6
|
+
|
|
7
|
+
import { useAppStore } from '../../../../state/app-store'
|
|
8
|
+
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
9
|
+
import { formatDiffStat } from '../../../../state/project-workspaces'
|
|
10
|
+
// eslint-disable-next-line no-duplicate-imports
|
|
11
|
+
import { IDLE_WORKSPACE_ACTIVITY } from '../../../../state/types'
|
|
12
|
+
import { useActivitySprite } from '../../../hooks/use-activity-sprite'
|
|
13
|
+
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
14
|
+
// eslint-disable-next-line no-duplicate-imports
|
|
15
|
+
import { SPRITE_COLS } from '../../../terminal-graphics/sprites'
|
|
16
|
+
import { useBaseTheme, useTheme } from '../../../theme'
|
|
17
|
+
import { truncate } from '../../../truncate'
|
|
18
|
+
import { FlashLabelBadge } from '../../flash/flash-label-badge'
|
|
19
|
+
import { ContextMenuBox } from '../../overlays/context-menu/context-menu-box'
|
|
20
|
+
|
|
21
|
+
/** Same priority as the glyphs below, so a sprite says what the glyph would. */
|
|
22
|
+
function spriteStateFor(activity: WorkspaceActivity): SpriteState {
|
|
23
|
+
if (activity.waiting) return 'waiting'
|
|
24
|
+
if (activity.working) return 'working'
|
|
25
|
+
if (activity.done) return 'done'
|
|
26
|
+
return 'idle'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface WorkspaceRowProps {
|
|
30
|
+
project: ProjectRecord
|
|
31
|
+
workspace: WorkspaceRecord
|
|
32
|
+
/** 1-based index of the project in the visible order (for switch-project-by-index). */
|
|
33
|
+
projectIndex: number
|
|
34
|
+
/** True when this row is the active cursor item. */
|
|
35
|
+
isActiveItem: boolean
|
|
36
|
+
/** True when this row's project is the current project (selection scope). */
|
|
37
|
+
inCurrentGroup: boolean
|
|
38
|
+
contentWidth: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const WorkspaceRow = memo(function WorkspaceRow({
|
|
42
|
+
contentWidth,
|
|
43
|
+
inCurrentGroup,
|
|
44
|
+
isActiveItem,
|
|
45
|
+
project,
|
|
46
|
+
projectIndex,
|
|
47
|
+
workspace,
|
|
48
|
+
}: WorkspaceRowProps) {
|
|
49
|
+
const t = useTheme()
|
|
50
|
+
// Selection highlight must stay opaque in transparent mode.
|
|
51
|
+
const base = useBaseTheme()
|
|
52
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
53
|
+
const isCurrentProject = project.id === currentProjectId
|
|
54
|
+
const divergence = useAppStore((s) => s.workspaceDivergence[workspace.id])
|
|
55
|
+
const activity = useAppStore((s) => s.workspaceActivity[workspace.id]) ?? IDLE_WORKSPACE_ACTIVITY
|
|
56
|
+
// A sprite is placed across the two lines of the row, so only a row that has
|
|
57
|
+
// both — the branch line under the name — can carry one. A branchless checkout
|
|
58
|
+
// keeps the text marker rather than getting a second placement geometry of its
|
|
59
|
+
// own: one shape is what keeps a cell from resolving to the wrong image.
|
|
60
|
+
const hasBranch = workspace.branch != null && workspace.branch !== ''
|
|
61
|
+
const sprite = useActivitySprite(hasBranch ? spriteStateFor(activity) : null)
|
|
62
|
+
// Only the working case animates, so the timer is off for every other row —
|
|
63
|
+
// and off entirely when a sprite is drawing this row instead.
|
|
64
|
+
const spinner = useBusySpinner(activity.working && sprite === null)
|
|
65
|
+
|
|
66
|
+
const handleMouseDown = useCallback(
|
|
67
|
+
(event: OtuiMouseEvent) => {
|
|
68
|
+
if (event.button !== 0) return
|
|
69
|
+
event.preventDefault()
|
|
70
|
+
event.stopPropagation()
|
|
71
|
+
if (isCurrentProject) {
|
|
72
|
+
dispatchGlobal({
|
|
73
|
+
projectId: project.id,
|
|
74
|
+
type: 'set-active-workspace',
|
|
75
|
+
workspaceId: workspace.id,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
// Cross-project click: send the workspace id along with the switch so
|
|
80
|
+
// the side effect can apply it atomically. Splitting it into a separate
|
|
81
|
+
// dispatch+switch leaves a window where subscribers/re-renders can
|
|
82
|
+
// re-assert the target project's last-persisted workspace.
|
|
83
|
+
runSideEffectGlobal({
|
|
84
|
+
index: projectIndex,
|
|
85
|
+
type: 'switch-project-by-index',
|
|
86
|
+
workspaceId: workspace.id,
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
[isCurrentProject, project.id, projectIndex, workspace.id]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const rightClickMenu = useMemo<[string, () => void][] | undefined>(() => {
|
|
93
|
+
const entries: [string, () => void][] = [
|
|
94
|
+
[
|
|
95
|
+
'Rename',
|
|
96
|
+
() =>
|
|
97
|
+
dispatchGlobal({
|
|
98
|
+
initialName: workspace.name,
|
|
99
|
+
projectId: project.id,
|
|
100
|
+
type: 'open-rename-workspace-modal',
|
|
101
|
+
workspaceId: workspace.id,
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
]
|
|
105
|
+
if (workspace.source !== 'primary') {
|
|
106
|
+
entries.push([
|
|
107
|
+
'Remove workspace',
|
|
108
|
+
() =>
|
|
109
|
+
// Always confirm first. Confirming routes through the full delete side
|
|
110
|
+
// effect (closes the workspace's tabs, disposes their PTYs, prunes the
|
|
111
|
+
// snapshot, removes the git worktree). closeTabs (not force) cleans up
|
|
112
|
+
// the tabs while keeping the non-force `git worktree remove`, so
|
|
113
|
+
// uncommitted work in a temp workspace is still protected — a dirty
|
|
114
|
+
// workspace re-prompts for an explicit force-delete.
|
|
115
|
+
dispatchGlobal({
|
|
116
|
+
closeTabs: true,
|
|
117
|
+
force: false,
|
|
118
|
+
projectId: project.id,
|
|
119
|
+
reason: 'Its assistant tabs will be closed and the worktree removed.',
|
|
120
|
+
type: 'open-workspace-delete-confirm',
|
|
121
|
+
workspaceId: workspace.id,
|
|
122
|
+
workspaceLabel: workspace.branch ?? workspace.name,
|
|
123
|
+
}),
|
|
124
|
+
])
|
|
125
|
+
}
|
|
126
|
+
return entries
|
|
127
|
+
}, [project.id, workspace.branch, workspace.id, workspace.name, workspace.source])
|
|
128
|
+
|
|
129
|
+
let bgColor: string | undefined
|
|
130
|
+
if (isActiveItem) {
|
|
131
|
+
bgColor = base.backgroundElement
|
|
132
|
+
} else if (inCurrentGroup) {
|
|
133
|
+
bgColor = base.backgroundPanel
|
|
134
|
+
}
|
|
135
|
+
// The cursor: a full-height accent bar down the left of the row, both lines
|
|
136
|
+
// of it. The background alone is one step of grey and gets lost among rows
|
|
137
|
+
// that carry colour of their own; a bar the height of the row is found
|
|
138
|
+
// without reading anything.
|
|
139
|
+
const cursorGlyph = isActiveItem ? '▌' : ' '
|
|
140
|
+
|
|
141
|
+
// The marker slot: two cells of text, or the wider box a sprite needs. The
|
|
142
|
+
// bundled set covers every state, so with sprites on, every two-line row uses
|
|
143
|
+
// the same width whatever its state — only a branchless row keeps the narrow
|
|
144
|
+
// slot, and its name sits one cell left of its siblings'. Accepted: the
|
|
145
|
+
// alternative is a second placement geometry, which is what mis-sized every
|
|
146
|
+
// sprite in the first place.
|
|
147
|
+
const markerCells = sprite === null ? 2 : SPRITE_COLS
|
|
148
|
+
|
|
149
|
+
const { added, removed } = formatDiffStat(divergence)
|
|
150
|
+
const statWidth = added.length + removed.length + (added !== '' && removed !== '' ? 1 : 0)
|
|
151
|
+
const nameLabel = truncate(
|
|
152
|
+
workspace.name,
|
|
153
|
+
Math.max(0, contentWidth - 2 - markerCells - (statWidth > 0 ? statWidth + 1 : 0))
|
|
154
|
+
)
|
|
155
|
+
// Second line, hanging under the name, and where the branch glyph lives — it
|
|
156
|
+
// labels the branch, not the workspace. Only workspaces that own a branch get
|
|
157
|
+
// one — an external checkout without one would otherwise be a row of a
|
|
158
|
+
// different height for no information.
|
|
159
|
+
// The glyph starts where the name above it starts, so the branch hangs one
|
|
160
|
+
// glyph-width in from it — the row reads as a title with a labelled line
|
|
161
|
+
// under it, not as two columns.
|
|
162
|
+
const branchLabel = !hasBranch
|
|
163
|
+
? null
|
|
164
|
+
: truncate(workspace.branch ?? '', Math.max(0, contentWidth - 4 - markerCells))
|
|
165
|
+
|
|
166
|
+
// Same vocabulary as the project heading one level up, in the same priority:
|
|
167
|
+
// a question outranks work in progress, which outranks "it finished and you
|
|
168
|
+
// haven't looked". A space when there is nothing to say — the row keeps its
|
|
169
|
+
// width either way, so nothing shifts.
|
|
170
|
+
// Every text marker is two cells, the glyph and the gap after it, so nothing
|
|
171
|
+
// shifts when an agent starts or stops. The sprite is the one exception and
|
|
172
|
+
// carries its own width — see markerCells above.
|
|
173
|
+
let statusGlyph = ' '
|
|
174
|
+
let statusColor = t.textMuted
|
|
175
|
+
if (sprite) {
|
|
176
|
+
statusGlyph = sprite.glyphs[0] ?? ' '
|
|
177
|
+
statusColor = sprite.fg
|
|
178
|
+
} else if (activity.waiting) {
|
|
179
|
+
statusGlyph = '? '
|
|
180
|
+
statusColor = t.warning
|
|
181
|
+
} else if (activity.working) {
|
|
182
|
+
statusGlyph = `${spinner} `
|
|
183
|
+
statusColor = t.primary
|
|
184
|
+
} else if (activity.done) {
|
|
185
|
+
statusGlyph = '● '
|
|
186
|
+
statusColor = t.success
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<ContextMenuBox
|
|
191
|
+
id={`sidebar-wt-${workspace.id}`}
|
|
192
|
+
flexDirection="column"
|
|
193
|
+
flexShrink={0}
|
|
194
|
+
paddingRight={1}
|
|
195
|
+
backgroundColor={bgColor}
|
|
196
|
+
rightClickMenu={rightClickMenu}
|
|
197
|
+
onMouseDown={handleMouseDown}
|
|
198
|
+
>
|
|
199
|
+
<box flexDirection="row" alignItems="center">
|
|
200
|
+
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
201
|
+
{cursorGlyph}
|
|
202
|
+
</text>
|
|
203
|
+
<text fg={statusColor} selectable={false} wrapMode="none">
|
|
204
|
+
{statusGlyph}
|
|
205
|
+
</text>
|
|
206
|
+
<FlashLabelBadge rowKey={`wt:${workspace.id}`} />
|
|
207
|
+
{/* Full strength only under the cursor. Every name at full strength
|
|
208
|
+
reads as a wall of white; every name muted leaves the selected row
|
|
209
|
+
with nothing but a shade of grey to say so. */}
|
|
210
|
+
<text
|
|
211
|
+
fg={isActiveItem ? t.text : t.textMuted}
|
|
212
|
+
attributes={isActiveItem ? TextAttributes.BOLD : undefined}
|
|
213
|
+
selectable={false}
|
|
214
|
+
wrapMode="none"
|
|
215
|
+
>
|
|
216
|
+
{nameLabel}
|
|
217
|
+
</text>
|
|
218
|
+
<box flexGrow={1} flexShrink={1} />
|
|
219
|
+
{added !== '' ? (
|
|
220
|
+
<text fg={t.success} selectable={false} wrapMode="none">
|
|
221
|
+
{added}
|
|
222
|
+
</text>
|
|
223
|
+
) : null}
|
|
224
|
+
{added !== '' && removed !== '' ? (
|
|
225
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
226
|
+
{' '}
|
|
227
|
+
</text>
|
|
228
|
+
) : null}
|
|
229
|
+
{removed !== '' ? (
|
|
230
|
+
<text fg={t.error} selectable={false} wrapMode="none">
|
|
231
|
+
{removed}
|
|
232
|
+
</text>
|
|
233
|
+
) : null}
|
|
234
|
+
</box>
|
|
235
|
+
{branchLabel == null ? null : (
|
|
236
|
+
<box flexDirection="row" alignItems="center">
|
|
237
|
+
<text fg={t.primary} selectable={false} wrapMode="none">
|
|
238
|
+
{cursorGlyph}
|
|
239
|
+
</text>
|
|
240
|
+
{/* The marker slot again, so a sprite tall enough to want both lines
|
|
241
|
+
gets its bottom half. Two cells either way, which is what the
|
|
242
|
+
branch glyph has always hung off. */}
|
|
243
|
+
<text fg={statusColor} selectable={false} wrapMode="none">
|
|
244
|
+
{sprite?.glyphs[1] ?? ' '}
|
|
245
|
+
</text>
|
|
246
|
+
<text fg={t.textMuted} selectable={false} wrapMode="none">
|
|
247
|
+
{'\u{e702}'} {branchLabel}
|
|
248
|
+
</text>
|
|
249
|
+
</box>
|
|
250
|
+
)}
|
|
251
|
+
</ContextMenuBox>
|
|
252
|
+
)
|
|
253
|
+
})
|
|
@@ -44,6 +44,8 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
|
|
|
44
44
|
return 'EDIT'
|
|
45
45
|
case 'git':
|
|
46
46
|
return 'GIT'
|
|
47
|
+
case 'settings':
|
|
48
|
+
return 'SET'
|
|
47
49
|
case 'navigation':
|
|
48
50
|
default:
|
|
49
51
|
return 'NORMAL'
|
|
@@ -67,6 +69,8 @@ function getModeColor(focusMode: AppState['focusMode'], t: ResolvedTuiTheme): st
|
|
|
67
69
|
return t.warning
|
|
68
70
|
case 'git':
|
|
69
71
|
return t.success
|
|
72
|
+
case 'settings':
|
|
73
|
+
return t.secondary
|
|
70
74
|
case 'navigation':
|
|
71
75
|
default:
|
|
72
76
|
return t.text
|
|
@@ -128,7 +132,7 @@ export function StatusBar() {
|
|
|
128
132
|
const tileX = t.backgroundElement
|
|
129
133
|
const tileY = modeColor
|
|
130
134
|
|
|
131
|
-
const hasB = model.
|
|
135
|
+
const hasB = model.projectSegments.length > 0
|
|
132
136
|
const hasX = aiEnabled
|
|
133
137
|
|
|
134
138
|
return (
|
|
@@ -161,7 +165,7 @@ export function StatusBar() {
|
|
|
161
165
|
flexShrink={1}
|
|
162
166
|
overflow="hidden"
|
|
163
167
|
>
|
|
164
|
-
<Segments segments={model.
|
|
168
|
+
<Segments segments={model.projectSegments} t={t} />
|
|
165
169
|
</box>
|
|
166
170
|
<Separator glyph={glyphs.right} bg={tileFiller} fg={tileB} />
|
|
167
171
|
</>
|
|
@@ -48,15 +48,15 @@ function getTitle(
|
|
|
48
48
|
tab: TabSession | undefined,
|
|
49
49
|
isActive: boolean,
|
|
50
50
|
focusMode: TerminalPaneProps['focusMode'],
|
|
51
|
-
emptyContext: {
|
|
51
|
+
emptyContext: { projectName: string; workspaceName: string }
|
|
52
52
|
): string {
|
|
53
53
|
if (!tab) {
|
|
54
|
-
const {
|
|
55
|
-
if (
|
|
56
|
-
if (
|
|
57
|
-
return `${
|
|
54
|
+
const { projectName, workspaceName } = emptyContext
|
|
55
|
+
if (projectName === '' && workspaceName === '') return 'No active project'
|
|
56
|
+
if (workspaceName === '' || workspaceName === projectName) {
|
|
57
|
+
return `${projectName} · no tabs`
|
|
58
58
|
}
|
|
59
|
-
return `${
|
|
59
|
+
return `${projectName} / ${workspaceName} · no tabs`
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
if (isActive && focusMode === 'terminal-input') {
|
|
@@ -115,7 +115,7 @@ function renderSpan(span: TerminalSpan, key: string, softCursor: boolean): React
|
|
|
115
115
|
)
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
interface TerminalViewportProps {
|
|
118
|
+
export interface TerminalViewportProps {
|
|
119
119
|
viewport: TerminalSnapshot | undefined
|
|
120
120
|
buffer: string
|
|
121
121
|
softCursor: boolean
|
|
@@ -179,7 +179,7 @@ function useHardwareCursor(
|
|
|
179
179
|
// Cursor state only reaches the terminal with the next rendered frame
|
|
180
180
|
// (same reason opentui's editor calls requestRender() in focus/blur).
|
|
181
181
|
// Without it, a hide issued while the UI is static — e.g. switching to
|
|
182
|
-
// a
|
|
182
|
+
// a project with no live PTY — never flushes and the host cursor
|
|
183
183
|
// stays stranded at its old position.
|
|
184
184
|
renderer.requestRender()
|
|
185
185
|
// React runs all cleanups in a commit before all effects, so when focus
|
|
@@ -215,7 +215,7 @@ const pinTerminalScroll = (node: TextRenderable | null): void => {
|
|
|
215
215
|
if (node.scrollY !== 0) node.scrollY = 0
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
const TerminalViewport = memo(function TerminalViewport({
|
|
218
|
+
export const TerminalViewport = memo(function TerminalViewport({
|
|
219
219
|
buffer,
|
|
220
220
|
softCursor,
|
|
221
221
|
viewport,
|
|
@@ -239,7 +239,7 @@ const TerminalViewport = memo(function TerminalViewport({
|
|
|
239
239
|
|
|
240
240
|
return (
|
|
241
241
|
<text ref={pinTerminalScroll} fg={t.text} wrapMode="none">
|
|
242
|
-
{buffer.length > 0 ? buffer : 'Waiting for
|
|
242
|
+
{buffer.length > 0 ? buffer : 'Waiting for project output...'}
|
|
243
243
|
</text>
|
|
244
244
|
)
|
|
245
245
|
})
|
|
@@ -279,24 +279,24 @@ export function TerminalPane({
|
|
|
279
279
|
paneIsActive && focusMode === 'terminal-input' && tab?.status === 'running'
|
|
280
280
|
)
|
|
281
281
|
// These are only used when this pane is rendered without a tab (the
|
|
282
|
-
// top-level pane on a
|
|
282
|
+
// top-level pane on a workspace with zero tabs). Selectors return plain
|
|
283
283
|
// strings so re-renders are cheap and bounded to actual name changes.
|
|
284
|
-
const
|
|
285
|
-
const id = s.
|
|
284
|
+
const emptyProjectName = useAppStore((s) => {
|
|
285
|
+
const id = s.currentProjectId
|
|
286
286
|
if (id == null || id === '') return ''
|
|
287
|
-
return s.
|
|
287
|
+
return s.projects.find((sess) => sess.id === id)?.name ?? ''
|
|
288
288
|
})
|
|
289
|
-
const
|
|
290
|
-
const id = s.
|
|
289
|
+
const emptyWorkspaceName = useAppStore((s) => {
|
|
290
|
+
const id = s.currentProjectId
|
|
291
291
|
if (id == null || id === '') return ''
|
|
292
|
-
const
|
|
293
|
-
if (!
|
|
292
|
+
const project = s.projects.find((sess) => sess.id === id)
|
|
293
|
+
if (!project) return ''
|
|
294
294
|
const activeId =
|
|
295
|
-
|
|
296
|
-
?
|
|
297
|
-
:
|
|
295
|
+
project.activeWorkspaceId != null && project.activeWorkspaceId !== ''
|
|
296
|
+
? project.activeWorkspaceId
|
|
297
|
+
: project.workspaces?.[0]?.id
|
|
298
298
|
if (activeId == null || activeId === '') return ''
|
|
299
|
-
return
|
|
299
|
+
return project.workspaces?.find((w) => w.id === activeId)?.name ?? ''
|
|
300
300
|
})
|
|
301
301
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
302
302
|
const canUseLocalScrollback = focusMode === 'terminal-input' && !!tab && localScrollbackEnabled
|
|
@@ -510,7 +510,9 @@ export function TerminalPane({
|
|
|
510
510
|
)
|
|
511
511
|
const handleNoTabMouseDown = useCallback((event: OtuiMouseEvent) => {
|
|
512
512
|
event.stopPropagation()
|
|
513
|
-
|
|
513
|
+
// Opens the picker, or the create-workspace modal when there is no
|
|
514
|
+
// workspace to put a tab in — the effect owns that rule.
|
|
515
|
+
runSideEffectGlobal({ type: 'open-new-tab' })
|
|
514
516
|
}, [])
|
|
515
517
|
const handleContentMouseDown = useCallback(
|
|
516
518
|
(e: OtuiMouseEvent) => {
|
|
@@ -525,8 +527,8 @@ export function TerminalPane({
|
|
|
525
527
|
border
|
|
526
528
|
borderColor={getBorderColor(paneIsActive, focusMode)}
|
|
527
529
|
title={getTitle(tab, paneIsActive, focusMode, {
|
|
530
|
+
projectName: emptyProjectName,
|
|
528
531
|
workspaceName: emptyWorkspaceName,
|
|
529
|
-
worktreeName: emptyWorktreeName,
|
|
530
532
|
})}
|
|
531
533
|
padding={0}
|
|
532
534
|
flexDirection="column"
|
|
@@ -542,19 +544,22 @@ export function TerminalPane({
|
|
|
542
544
|
<box flexGrow={1} justifyContent="center" alignItems="center" flexDirection="column">
|
|
543
545
|
<text fg={t.textMuted}>· · ·</text>
|
|
544
546
|
<text fg={t.textMuted}> </text>
|
|
545
|
-
{
|
|
547
|
+
{emptyProjectName !== '' ? (
|
|
546
548
|
<box flexDirection="row">
|
|
547
|
-
<text fg={t.text}>{
|
|
548
|
-
{
|
|
549
|
+
<text fg={t.text}>{emptyProjectName}</text>
|
|
550
|
+
{emptyWorkspaceName !== '' && emptyWorkspaceName !== emptyProjectName ? (
|
|
549
551
|
<>
|
|
550
552
|
<text fg={t.textMuted}> / </text>
|
|
551
|
-
<text fg={t.text}>{
|
|
553
|
+
<text fg={t.text}>{emptyWorkspaceName}</text>
|
|
552
554
|
</>
|
|
553
555
|
) : null}
|
|
554
556
|
</box>
|
|
555
557
|
) : null}
|
|
556
558
|
<text fg={t.textMuted}>has no tabs</text>
|
|
557
559
|
<text fg={t.textMuted}> </text>
|
|
560
|
+
{/* Keep this at a fixed row count: adding a line shifts everything
|
|
561
|
+
under a centred column, and the vacated cells are not repainted
|
|
562
|
+
— the old text bleeds through the new spaces. */}
|
|
558
563
|
<box flexDirection="row">
|
|
559
564
|
<text fg={t.textMuted}>Press </text>
|
|
560
565
|
<text fg={t.primary}>Ctrl+n</text>
|
|
@@ -598,7 +603,7 @@ export function TerminalPane({
|
|
|
598
603
|
// re-introducing the shifted-content / dead-row bug.
|
|
599
604
|
<box position="absolute" bottom={0} left={0} backgroundColor={editorBg}>
|
|
600
605
|
{tab?.status === 'disconnected' ? (
|
|
601
|
-
<text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this
|
|
606
|
+
<text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this project.</text>
|
|
602
607
|
) : null}
|
|
603
608
|
{tab?.errorMessage != null && tab?.errorMessage !== '' ? (
|
|
604
609
|
<text fg={t.error}>{tab.errorMessage}</text>
|