@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,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>
|
|
@@ -8,21 +8,26 @@ import { memo, type ReactNode, useCallback, useMemo, useRef, useState } from 're
|
|
|
8
8
|
|
|
9
9
|
import type { FocusMode, TabSession } from '../../../state/types'
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { useWorkspaceDivergencePolling } from '../../../git/workspace-divergence-poller'
|
|
12
12
|
import { useAppStore } from '../../../state/app-store'
|
|
13
13
|
import { getBarWidth } from '../../../state/bars'
|
|
14
14
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
15
|
-
import {
|
|
15
|
+
import { filterTabsForActiveWorkspace } from '../../../state/project-workspaces'
|
|
16
16
|
import { buildTabEntries, type GroupEntry, type TabEntry } from '../../../state/tab-entries'
|
|
17
|
-
import {
|
|
17
|
+
import { useScrollActiveIntoView } from '../../hooks/use-scroll-active-into-view'
|
|
18
|
+
import { moveIdToIdPosition } from '../../project-ordering'
|
|
18
19
|
import { useTheme } from '../../theme'
|
|
19
20
|
import { FlashLabelBadge } from '../flash/flash-label-badge'
|
|
20
21
|
import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
|
|
21
22
|
import { TabItem } from './sidebar/tab-item'
|
|
22
|
-
import { useTopTabBarAutoScroll } from './sidebar/use-top-tab-bar-auto-scroll'
|
|
23
23
|
|
|
24
24
|
interface TopTabBarProps {
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A full-screen view has replaced the pane tree (git mode, settings). The strip
|
|
27
|
+
* shows regardless of the user's own preference — it is the only orientation
|
|
28
|
+
* left — but it shows as a strip to read, not to act on.
|
|
29
|
+
*/
|
|
30
|
+
panesReplaced?: boolean
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const ROW_CONTENT_OPTIONS = {
|
|
@@ -192,34 +197,34 @@ function entryTabIds(entry: TabEntry): string[] {
|
|
|
192
197
|
return entry.kind === 'single' ? [entry.tab.id] : entry.tabs.map((tab) => tab.id)
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
export function TopTabBar({
|
|
200
|
+
export function TopTabBar({ panesReplaced = false }: TopTabBarProps) {
|
|
196
201
|
const t = useTheme()
|
|
197
202
|
const headerBg = t.backgroundPanel
|
|
198
203
|
const tabs = useAppStore((s) => s.tabs)
|
|
199
204
|
const activeTabId = useAppStore((s) => s.activeTabId)
|
|
200
|
-
const bar = useAppStore((s) => s.
|
|
205
|
+
const bar = useAppStore((s) => s.projectBar)
|
|
201
206
|
const leftBarVisible = useAppStore((s) => getBarWidth(s.bars.left) > 0)
|
|
202
|
-
const
|
|
203
|
-
const
|
|
207
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
208
|
+
const projects = useAppStore((s) => s.projects)
|
|
204
209
|
const focusMode: FocusMode = useAppStore((s) => s.focusMode)
|
|
205
210
|
const layoutTrees = useAppStore((s) => s.layoutTrees)
|
|
206
211
|
const tabGroupMap = useAppStore((s) => s.tabGroupMap)
|
|
207
212
|
|
|
208
|
-
// Sidebar now also shows
|
|
213
|
+
// Sidebar now also shows workspace chips with divergence — poll whenever
|
|
209
214
|
// either surface is visible.
|
|
210
|
-
|
|
215
|
+
useWorkspaceDivergencePolling(bar.visible || leftBarVisible || panesReplaced)
|
|
211
216
|
|
|
212
|
-
const
|
|
217
|
+
const currentProject = useMemo(
|
|
213
218
|
() =>
|
|
214
|
-
|
|
215
|
-
?
|
|
219
|
+
currentProjectId != null && currentProjectId !== ''
|
|
220
|
+
? projects.find((s) => s.id === currentProjectId)
|
|
216
221
|
: undefined,
|
|
217
|
-
[
|
|
222
|
+
[currentProjectId, projects]
|
|
218
223
|
)
|
|
219
224
|
|
|
220
225
|
const visibleTabs = useMemo(
|
|
221
|
-
() =>
|
|
222
|
-
[tabs,
|
|
226
|
+
() => filterTabsForActiveWorkspace(tabs, currentProject),
|
|
227
|
+
[tabs, currentProject]
|
|
223
228
|
)
|
|
224
229
|
|
|
225
230
|
const entries = useMemo(
|
|
@@ -239,11 +244,11 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
239
244
|
}, [entries, activeTabId])
|
|
240
245
|
|
|
241
246
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null)
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
useScrollActiveIntoView({
|
|
248
|
+
activeId: activeEntryId,
|
|
244
249
|
idPrefix: 'top-tab-',
|
|
245
250
|
scrollRef,
|
|
246
|
-
visible: bar.visible ||
|
|
251
|
+
visible: bar.visible || panesReplaced,
|
|
247
252
|
})
|
|
248
253
|
|
|
249
254
|
const handleEntryActivate = useCallback(
|
|
@@ -260,7 +265,7 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
260
265
|
)
|
|
261
266
|
|
|
262
267
|
// --- Drag-and-drop reorder of the tab strip ---------------------------------
|
|
263
|
-
// Mirrors the
|
|
268
|
+
// Mirrors the project-list drag, but on the horizontal axis: entries are
|
|
264
269
|
// laid out left-to-right inside a scrollX box, so hit-testing is on x/width.
|
|
265
270
|
const [draggingId, setDraggingId] = useState<string | null>(null)
|
|
266
271
|
const [dragOrder, setDragOrder] = useState<string[] | null>(null)
|
|
@@ -344,13 +349,13 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
344
349
|
|
|
345
350
|
const handleNewTab = useCallback((e: OtuiMouseEvent) => {
|
|
346
351
|
e.stopPropagation()
|
|
347
|
-
|
|
352
|
+
runSideEffectGlobal({ type: 'open-new-tab' })
|
|
348
353
|
}, [])
|
|
349
354
|
|
|
350
|
-
if (!bar.visible && !
|
|
351
|
-
// Keep the bar visible even with zero entries — when the active
|
|
355
|
+
if (!bar.visible && !panesReplaced) return null
|
|
356
|
+
// Keep the bar visible even with zero entries — when the active workspace
|
|
352
357
|
// has no tabs, the lone "+" affordance is what tells the user "you're in
|
|
353
|
-
// an empty
|
|
358
|
+
// an empty workspace, click here to start one".
|
|
354
359
|
|
|
355
360
|
const isFocused = focusMode === 'terminal-input' || focusMode === 'navigation'
|
|
356
361
|
|
|
@@ -426,17 +431,22 @@ export function TopTabBar({ forceVisible = false }: TopTabBarProps) {
|
|
|
426
431
|
</TopTabCell>
|
|
427
432
|
)
|
|
428
433
|
})}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
434
|
+
{/* Not while a full-screen view is up: a `+` that quietly starts a tab
|
|
435
|
+
and drops you out of the screen you were reading is noise, and on the
|
|
436
|
+
settings screen it is not even about anything on it. */}
|
|
437
|
+
{panesReplaced ? null : (
|
|
438
|
+
<box
|
|
439
|
+
flexDirection="row"
|
|
440
|
+
flexShrink={0}
|
|
441
|
+
paddingLeft={1}
|
|
442
|
+
paddingRight={1}
|
|
443
|
+
onMouseDown={handleNewTab}
|
|
444
|
+
>
|
|
445
|
+
<text fg={t.textMuted} selectable={false}>
|
|
446
|
+
+
|
|
447
|
+
</text>
|
|
448
|
+
</box>
|
|
449
|
+
)}
|
|
440
450
|
</scrollbox>
|
|
441
451
|
</box>
|
|
442
452
|
)
|
|
@@ -89,10 +89,10 @@ export function GitCommitModal({
|
|
|
89
89
|
const bodyActive = activeField === 'body'
|
|
90
90
|
const isConfirm = stage === 'confirm'
|
|
91
91
|
const isGenerating = stage === 'generating'
|
|
92
|
-
const
|
|
92
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
93
93
|
const bgKind = useAppStore((s) => {
|
|
94
|
-
const id = s.
|
|
95
|
-
return id != null && id !== '' ? s.autoCommit.
|
|
94
|
+
const id = s.currentProjectId
|
|
95
|
+
return id != null && id !== '' ? s.autoCommit.byProject[id]?.kind : undefined
|
|
96
96
|
})
|
|
97
97
|
const isBgGenerating = bgKind === 'generating'
|
|
98
98
|
const showBgSpinner = isBgGenerating && !isConfirm && !isGenerating
|
|
@@ -103,19 +103,19 @@ export function GitCommitModal({
|
|
|
103
103
|
|
|
104
104
|
const onAutoCommitClick = useCallback((): void => {
|
|
105
105
|
const hasTitle = title.trim().length > 0
|
|
106
|
-
if (hasTitle || !(
|
|
106
|
+
if (hasTitle || !(currentProjectId != null && currentProjectId !== '')) {
|
|
107
107
|
dispatchGlobal({ type: 'git-commit-enter-confirm' })
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
110
|
if (bgKind === 'ready') {
|
|
111
|
-
dispatchGlobal({
|
|
111
|
+
dispatchGlobal({ projectId: currentProjectId, type: 'git-commit-use-background-suggestion' })
|
|
112
112
|
return
|
|
113
113
|
}
|
|
114
|
-
dispatchGlobal({
|
|
114
|
+
dispatchGlobal({ projectId: currentProjectId, type: 'git-commit-enter-generating' })
|
|
115
115
|
if (bgKind !== 'generating') {
|
|
116
|
-
runSideEffectGlobal({
|
|
116
|
+
runSideEffectGlobal({ projectId: currentProjectId, type: 'generate-auto-commit-now' })
|
|
117
117
|
}
|
|
118
|
-
}, [bgKind,
|
|
118
|
+
}, [bgKind, currentProjectId, title])
|
|
119
119
|
|
|
120
120
|
const handleAutoCommitMouseDown = useCallback(
|
|
121
121
|
(event: OtuiMouseEvent) => {
|