@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
|
@@ -4,32 +4,51 @@ import type { GitPanelState } from '../../../../state/types'
|
|
|
4
4
|
|
|
5
5
|
import { useAppStore } from '../../../../state/app-store'
|
|
6
6
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
7
|
-
import {
|
|
7
|
+
import { selectPrRowVisible, usePrStatusStore } from '../../../../state/pr-status-store'
|
|
8
|
+
import { useTheme, useTransparent } from '../../../theme'
|
|
9
|
+
import { PrStateRow } from './pr-state-row'
|
|
10
|
+
|
|
11
|
+
export type GitPaneTab = 'files' | 'checks'
|
|
8
12
|
|
|
9
13
|
interface GitPaneHeaderProps {
|
|
10
14
|
gitPanel: GitPanelState
|
|
11
15
|
projectPath: string | undefined
|
|
12
16
|
headOffset?: number
|
|
13
17
|
baseLabel?: string
|
|
18
|
+
/** Only the bar widget switches tabs; full-screen git mode omits these. */
|
|
19
|
+
tab?: GitPaneTab
|
|
20
|
+
onTabChange?: (tab: GitPaneTab) => void
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export const GitPaneHeader = memo(function GitPaneHeader({
|
|
17
24
|
baseLabel,
|
|
18
25
|
gitPanel,
|
|
19
26
|
headOffset = 0,
|
|
27
|
+
onTabChange,
|
|
20
28
|
projectPath,
|
|
29
|
+
tab,
|
|
21
30
|
}: GitPaneHeaderProps) {
|
|
22
31
|
const t = useTheme()
|
|
32
|
+
// Transparent mode drops every painted background, so the active tab falls
|
|
33
|
+
// back to colour alone rather than punching a hole in the terminal image.
|
|
34
|
+
const transparent = useTransparent()
|
|
35
|
+
const activeTabBg = transparent ? undefined : t.backgroundElement
|
|
36
|
+
const prRowVisible = usePrStatusStore(selectPrRowVisible)
|
|
23
37
|
const fileListMode = useAppStore((s) => s.gitPane.fileListMode)
|
|
24
38
|
const nextFileListMode = fileListMode === 'tree' ? 'flat' : 'tree'
|
|
25
39
|
const toggleListMode = useCallback(() => {
|
|
26
40
|
dispatchGlobal({ type: 'git-mode-toggle-file-list-mode' })
|
|
27
41
|
runSideEffectGlobal({ mode: nextFileListMode, type: 'persist-git-file-list-mode' })
|
|
28
42
|
}, [nextFileListMode])
|
|
43
|
+
const showFiles = useCallback(() => onTabChange?.('files'), [onTabChange])
|
|
44
|
+
const showChecks = useCallback(() => onTabChange?.('checks'), [onTabChange])
|
|
29
45
|
|
|
46
|
+
const hasTabs = tab !== undefined && onTabChange !== undefined
|
|
30
47
|
const hasProject = projectPath != null && projectPath !== ''
|
|
31
48
|
if (!hasProject) return null
|
|
32
|
-
|
|
49
|
+
// A git error must not hide the tab row, otherwise there's no way back to
|
|
50
|
+
// `files` (and the PR checks are still worth showing outside a repo).
|
|
51
|
+
if (gitPanel.error !== null && !hasTabs) return null
|
|
33
52
|
|
|
34
53
|
const branch = gitPanel.branch
|
|
35
54
|
const branchLabel = branch != null && branch !== '' ? branch : 'detached'
|
|
@@ -41,63 +60,110 @@ export const GitPaneHeader = memo(function GitPaneHeader({
|
|
|
41
60
|
const showBehind = behind > 0
|
|
42
61
|
const showTracking = showAhead || showBehind
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
// The PR row already carries the branch identity (and the checks tab spells
|
|
64
|
+
// out `base ← head`), so the branch row is only worth a line without a PR.
|
|
65
|
+
const showPrRow = hasTabs && prRowVisible
|
|
66
|
+
const showBranchRow = !showPrRow && gitPanel.error === null
|
|
67
|
+
const showToggle = tab !== 'checks' && gitPanel.files.length > 0
|
|
45
68
|
const showHistorical = headOffset > 0
|
|
46
69
|
const showReviewBase = baseLabel != null && baseLabel !== ''
|
|
47
70
|
const showScope = showHistorical || showReviewBase
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
const trackingAndToggle = (
|
|
72
|
+
<box flexDirection="row" flexShrink={0} gap={2}>
|
|
73
|
+
{showTracking && tab !== 'checks' ? (
|
|
74
|
+
<box flexDirection="row" gap={1}>
|
|
75
|
+
{showAhead ? (
|
|
76
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
77
|
+
{`↑${ahead}`}
|
|
78
|
+
</text>
|
|
79
|
+
) : null}
|
|
80
|
+
{showBehind ? (
|
|
81
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
82
|
+
{`↓${behind}`}
|
|
83
|
+
</text>
|
|
84
|
+
) : null}
|
|
85
|
+
</box>
|
|
86
|
+
) : null}
|
|
87
|
+
{showToggle ? (
|
|
88
|
+
<box flexDirection="row" gap={1} paddingLeft={1} onMouseDown={toggleListMode}>
|
|
89
|
+
<text
|
|
90
|
+
selectable={false}
|
|
91
|
+
fg={fileListMode === 'tree' ? t.primary : t.textMuted}
|
|
92
|
+
wrapMode="none"
|
|
93
|
+
>
|
|
94
|
+
tree
|
|
55
95
|
</text>
|
|
56
96
|
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
57
|
-
|
|
97
|
+
|
|
|
58
98
|
</text>
|
|
59
|
-
<text
|
|
60
|
-
{
|
|
99
|
+
<text
|
|
100
|
+
selectable={false}
|
|
101
|
+
fg={fileListMode === 'flat' ? t.primary : t.textMuted}
|
|
102
|
+
wrapMode="none"
|
|
103
|
+
>
|
|
104
|
+
flat
|
|
61
105
|
</text>
|
|
62
106
|
</box>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
<box flexDirection="row" gap={1} paddingLeft={1} onMouseDown={toggleListMode}>
|
|
107
|
+
) : null}
|
|
108
|
+
</box>
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<box flexDirection="column" flexShrink={0} paddingBottom={1}>
|
|
113
|
+
{showPrRow && hasProject ? <PrStateRow projectPath={projectPath} /> : null}
|
|
114
|
+
{hasTabs ? (
|
|
115
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
116
|
+
<box flexDirection="row" flexShrink={0} gap={1}>
|
|
117
|
+
<box
|
|
118
|
+
paddingLeft={1}
|
|
119
|
+
paddingRight={1}
|
|
120
|
+
backgroundColor={tab === 'files' ? activeTabBg : undefined}
|
|
121
|
+
onMouseDown={showFiles}
|
|
122
|
+
>
|
|
80
123
|
<text
|
|
81
124
|
selectable={false}
|
|
82
|
-
fg={
|
|
125
|
+
fg={tab === 'files' ? t.text : t.textMuted}
|
|
126
|
+
bg={tab === 'files' ? activeTabBg : undefined}
|
|
83
127
|
wrapMode="none"
|
|
84
128
|
>
|
|
85
|
-
|
|
86
|
-
</text>
|
|
87
|
-
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
88
|
-
|
|
|
129
|
+
files
|
|
89
130
|
</text>
|
|
131
|
+
</box>
|
|
132
|
+
<box
|
|
133
|
+
paddingLeft={1}
|
|
134
|
+
paddingRight={1}
|
|
135
|
+
backgroundColor={tab === 'checks' ? activeTabBg : undefined}
|
|
136
|
+
onMouseDown={showChecks}
|
|
137
|
+
>
|
|
90
138
|
<text
|
|
91
139
|
selectable={false}
|
|
92
|
-
fg={
|
|
140
|
+
fg={tab === 'checks' ? t.text : t.textMuted}
|
|
141
|
+
bg={tab === 'checks' ? activeTabBg : undefined}
|
|
93
142
|
wrapMode="none"
|
|
94
143
|
>
|
|
95
|
-
|
|
144
|
+
checks
|
|
96
145
|
</text>
|
|
97
146
|
</box>
|
|
98
|
-
|
|
147
|
+
</box>
|
|
148
|
+
{trackingAndToggle}
|
|
99
149
|
</box>
|
|
100
|
-
|
|
150
|
+
) : null}
|
|
151
|
+
{showBranchRow ? (
|
|
152
|
+
<box flexDirection="row" justifyContent="space-between">
|
|
153
|
+
<box flexDirection="row" flexShrink={1} overflow="hidden">
|
|
154
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
155
|
+
{'\u{e702}'}
|
|
156
|
+
</text>
|
|
157
|
+
<text selectable={false} fg={t.textMuted} wrapMode="none">
|
|
158
|
+
{' '}
|
|
159
|
+
</text>
|
|
160
|
+
<text selectable={false} fg={branchIsResolved ? t.text : t.textMuted} wrapMode="none">
|
|
161
|
+
{branchIsResolved ? <strong>{branchLabel}</strong> : branchLabel}
|
|
162
|
+
</text>
|
|
163
|
+
</box>
|
|
164
|
+
{hasTabs ? null : trackingAndToggle}
|
|
165
|
+
</box>
|
|
166
|
+
) : null}
|
|
101
167
|
{showScope ? (
|
|
102
168
|
<box flexDirection="row" gap={2}>
|
|
103
169
|
{showHistorical ? (
|
|
@@ -1,35 +1,45 @@
|
|
|
1
|
-
import { memo, useRef } from 'react'
|
|
1
|
+
import { memo, useRef, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import type { GitPanelState } from '../../../../state/types'
|
|
4
4
|
|
|
5
5
|
import { useGitPanelPolling } from '../../../../git/git-poller'
|
|
6
|
+
import { usePrStatusPolling } from '../../../../git/pr-status-poller'
|
|
6
7
|
import { useRepoDiscovery } from '../../../../git/use-repo-discovery'
|
|
7
8
|
import { useAppStore } from '../../../../state/app-store'
|
|
8
|
-
import {
|
|
9
|
+
import { getActiveWorkspacePath } from '../../../../state/project-workspaces'
|
|
9
10
|
import { GitPanel } from '../git-panel'
|
|
10
|
-
import { GitPaneHeader } from './git-pane-header'
|
|
11
|
+
import { GitPaneHeader, type GitPaneTab } from './git-pane-header'
|
|
12
|
+
import { PrChecksPanel } from './pr-checks-panel'
|
|
11
13
|
|
|
12
14
|
interface GitPaneWidgetProps {
|
|
13
15
|
pollingEnabled: boolean
|
|
16
|
+
contentWidth: number
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export const GitPaneWidget = memo(function GitPaneWidget({
|
|
19
|
+
export const GitPaneWidget = memo(function GitPaneWidget({
|
|
20
|
+
contentWidth,
|
|
21
|
+
pollingEnabled,
|
|
22
|
+
}: GitPaneWidgetProps) {
|
|
17
23
|
const gitPanel = useAppStore((s) => s.gitPanel)
|
|
18
24
|
const gitMode = useAppStore((s) => s.gitMode)
|
|
19
25
|
const gitFileListMode = useAppStore((s) => s.gitPane.fileListMode)
|
|
20
26
|
const treeCompaction = useAppStore((s) => s.gitPane.treeCompaction)
|
|
21
27
|
const pathConfig = useAppStore((s) => s.gitPane.path)
|
|
22
28
|
const diffCountConfig = useAppStore((s) => s.gitPane.diffCount)
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
?
|
|
29
|
+
const currentProjectId = useAppStore((s) => s.currentProjectId)
|
|
30
|
+
const projects = useAppStore((s) => s.projects)
|
|
31
|
+
const currentProject =
|
|
32
|
+
currentProjectId != null && currentProjectId !== ''
|
|
33
|
+
? projects.find((s) => s.id === currentProjectId)
|
|
28
34
|
: undefined
|
|
29
|
-
const projectPath =
|
|
35
|
+
const projectPath = getActiveWorkspacePath(currentProject)
|
|
36
|
+
|
|
37
|
+
const [tab, setTab] = useState<GitPaneTab>('files')
|
|
30
38
|
|
|
31
39
|
useRepoDiscovery(projectPath)
|
|
32
40
|
useGitPanelPolling({ enabled: pollingEnabled, headOffset: 0, projectPath })
|
|
41
|
+
// The PR state row sits above the tabs, so this has to run on both of them.
|
|
42
|
+
usePrStatusPolling({ enabled: pollingEnabled, projectPath })
|
|
33
43
|
|
|
34
44
|
const lastGoodRef = useRef<GitPanelState | null>(null)
|
|
35
45
|
const prevProjectPathRef = useRef(projectPath)
|
|
@@ -45,19 +55,23 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
|
|
|
45
55
|
|
|
46
56
|
return (
|
|
47
57
|
<box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
|
|
48
|
-
<GitPaneHeader gitPanel={display} projectPath={projectPath} />
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
<GitPaneHeader gitPanel={display} onTabChange={setTab} projectPath={projectPath} tab={tab} />
|
|
59
|
+
{tab === 'checks' ? (
|
|
60
|
+
<PrChecksPanel contentWidth={contentWidth} />
|
|
61
|
+
) : (
|
|
62
|
+
<GitPanel
|
|
63
|
+
collapsedFolders={gitMode.collapsedFolders}
|
|
64
|
+
compact={treeCompaction}
|
|
65
|
+
diffCountConfig={diffCountConfig}
|
|
66
|
+
fileListMode={gitFileListMode}
|
|
67
|
+
gitPanel={display}
|
|
68
|
+
pathConfig={pathConfig}
|
|
69
|
+
projectPath={projectPath}
|
|
70
|
+
selectedEntryKey={gitMode.selectedEntryKey}
|
|
71
|
+
showFileListToggle={false}
|
|
72
|
+
showRemoteTracking={false}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
61
75
|
</box>
|
|
62
76
|
)
|
|
63
77
|
})
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { memo, useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clampPrBody,
|
|
5
|
+
type PrCheck,
|
|
6
|
+
type PrCheckState,
|
|
7
|
+
type PrStatusResult,
|
|
8
|
+
} from '../../../../git/pr-status'
|
|
9
|
+
import { openUrl } from '../../../../platform/open-url'
|
|
10
|
+
import { usePrStatusStore } from '../../../../state/pr-status-store'
|
|
11
|
+
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
12
|
+
import { type ResolvedTuiTheme, useTheme, useTransparent } from '../../../theme'
|
|
13
|
+
|
|
14
|
+
/** Below this the workflow column crowds out the check name. */
|
|
15
|
+
const WORKFLOW_MIN_WIDTH = 34
|
|
16
|
+
|
|
17
|
+
const HIDDEN_SCROLLBAR_OPTIONS = { visible: false }
|
|
18
|
+
/** gap 1 separates title / body / checks; the rows inside stay tight. */
|
|
19
|
+
const AIRY_CONTENT_OPTIONS = { flexDirection: 'column' as const, gap: 1 }
|
|
20
|
+
|
|
21
|
+
const STATE_GLYPH: Record<Exclude<PrCheckState, 'pending'>, string> = {
|
|
22
|
+
cancel: '⊘',
|
|
23
|
+
fail: '✗',
|
|
24
|
+
pass: '✓',
|
|
25
|
+
skipping: '○',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stateColor(state: PrCheckState, t: ResolvedTuiTheme): string {
|
|
29
|
+
if (state === 'pass') return t.success
|
|
30
|
+
if (state === 'fail') return t.error
|
|
31
|
+
if (state === 'pending') return t.warning
|
|
32
|
+
return t.textMuted
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatDuration(ms: number | null): string {
|
|
36
|
+
if (ms === null) return '—'
|
|
37
|
+
const seconds = Math.round(ms / 1000)
|
|
38
|
+
if (seconds < 60) return `${seconds}s`
|
|
39
|
+
const minutes = Math.floor(seconds / 60)
|
|
40
|
+
return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function placeholder(
|
|
44
|
+
result: PrStatusResult | null,
|
|
45
|
+
t: ResolvedTuiTheme
|
|
46
|
+
): { label: string; color: string } | null {
|
|
47
|
+
if (result === null) return { color: t.textMuted, label: '…' }
|
|
48
|
+
if (result.kind === 'no-gh') return { color: t.textMuted, label: 'gh CLI not found' }
|
|
49
|
+
if (result.kind === 'no-pr') return { color: t.textMuted, label: 'No pull request' }
|
|
50
|
+
if (result.kind === 'error') return { color: t.error, label: result.message }
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CheckRow = memo(function CheckRow({
|
|
55
|
+
bg,
|
|
56
|
+
check,
|
|
57
|
+
showWorkflow,
|
|
58
|
+
spinner,
|
|
59
|
+
}: {
|
|
60
|
+
check: PrCheck
|
|
61
|
+
showWorkflow: boolean
|
|
62
|
+
spinner: string
|
|
63
|
+
bg: string | undefined
|
|
64
|
+
}) {
|
|
65
|
+
const t = useTheme()
|
|
66
|
+
const onOpen = useCallback(() => {
|
|
67
|
+
openUrl(check.url)
|
|
68
|
+
}, [check.url])
|
|
69
|
+
const glyph = check.state === 'pending' ? spinner : STATE_GLYPH[check.state]
|
|
70
|
+
return (
|
|
71
|
+
<box flexDirection="row" gap={1} onMouseDown={onOpen}>
|
|
72
|
+
<box width={1} flexShrink={0}>
|
|
73
|
+
<text selectable={false} fg={stateColor(check.state, t)} bg={bg}>
|
|
74
|
+
{glyph}
|
|
75
|
+
</text>
|
|
76
|
+
</box>
|
|
77
|
+
<box flexGrow={1} overflow="hidden">
|
|
78
|
+
<text selectable={false} fg={t.text} bg={bg} wrapMode="none">
|
|
79
|
+
{check.name}
|
|
80
|
+
</text>
|
|
81
|
+
</box>
|
|
82
|
+
{showWorkflow && check.workflow !== '' ? (
|
|
83
|
+
<box flexShrink={0}>
|
|
84
|
+
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
85
|
+
{check.workflow}
|
|
86
|
+
</text>
|
|
87
|
+
</box>
|
|
88
|
+
) : null}
|
|
89
|
+
<box flexShrink={0}>
|
|
90
|
+
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
91
|
+
{formatDuration(check.durationMs)}
|
|
92
|
+
</text>
|
|
93
|
+
</box>
|
|
94
|
+
</box>
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
export const PrChecksPanel = memo(function PrChecksPanel({
|
|
99
|
+
contentWidth,
|
|
100
|
+
}: {
|
|
101
|
+
contentWidth: number
|
|
102
|
+
}) {
|
|
103
|
+
const t = useTheme()
|
|
104
|
+
// A tone apart from the PR state row above, so the two zones read as
|
|
105
|
+
// separate bands. Transparent mode paints neither.
|
|
106
|
+
const transparent = useTransparent()
|
|
107
|
+
const bg = transparent ? undefined : t.backgroundPanel
|
|
108
|
+
const result = usePrStatusStore((s) => s.result)
|
|
109
|
+
const stale = usePrStatusStore((s) => s.stale)
|
|
110
|
+
const [expanded, setExpanded] = useState(false)
|
|
111
|
+
const toggleBody = useCallback(() => setExpanded((prev) => !prev), [])
|
|
112
|
+
|
|
113
|
+
const checks = result?.kind === 'ok' ? result.checks : []
|
|
114
|
+
const spinner = useBusySpinner(checks.some((c) => c.state === 'pending'))
|
|
115
|
+
|
|
116
|
+
const status = placeholder(result, t)
|
|
117
|
+
if (status !== null || result?.kind !== 'ok') {
|
|
118
|
+
return (
|
|
119
|
+
<box
|
|
120
|
+
flexGrow={1}
|
|
121
|
+
flexDirection="column"
|
|
122
|
+
alignItems="center"
|
|
123
|
+
backgroundColor={bg}
|
|
124
|
+
paddingTop={1}
|
|
125
|
+
>
|
|
126
|
+
<text selectable={false} fg={status?.color ?? t.textMuted} bg={bg}>
|
|
127
|
+
{status?.label ?? '…'}
|
|
128
|
+
</text>
|
|
129
|
+
</box>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pr = result.pr
|
|
134
|
+
// The padding below costs a column on each side.
|
|
135
|
+
const innerWidth = contentWidth - 2
|
|
136
|
+
const clamped = clampPrBody(pr.body)
|
|
137
|
+
const body = expanded ? pr.body.trimEnd() : clamped.text
|
|
138
|
+
// The body is raw markdown on purpose: rendering it would cost a parser and
|
|
139
|
+
// the source is what a PR author actually wrote.
|
|
140
|
+
return (
|
|
141
|
+
<box
|
|
142
|
+
flexDirection="column"
|
|
143
|
+
flexGrow={1}
|
|
144
|
+
flexShrink={1}
|
|
145
|
+
flexBasis={0}
|
|
146
|
+
overflow="hidden"
|
|
147
|
+
backgroundColor={bg}
|
|
148
|
+
padding={1}
|
|
149
|
+
>
|
|
150
|
+
{/* No viewportCulling here — the wrapped body is one tall child, which
|
|
151
|
+
culling measures badly, and the content is a description plus a
|
|
152
|
+
handful of rows either way. */}
|
|
153
|
+
<scrollbox
|
|
154
|
+
flexGrow={1}
|
|
155
|
+
scrollY
|
|
156
|
+
scrollbarOptions={HIDDEN_SCROLLBAR_OPTIONS}
|
|
157
|
+
contentOptions={AIRY_CONTENT_OPTIONS}
|
|
158
|
+
>
|
|
159
|
+
<text selectable={false} fg={stale ? t.textMuted : t.text} bg={bg}>
|
|
160
|
+
<strong>{pr.title}</strong>
|
|
161
|
+
</text>
|
|
162
|
+
{body !== '' ? (
|
|
163
|
+
<box flexDirection="column">
|
|
164
|
+
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
165
|
+
{body}
|
|
166
|
+
</text>
|
|
167
|
+
{clamped.truncated ? (
|
|
168
|
+
<text selectable={false} fg={t.primary} bg={bg} onMouseDown={toggleBody}>
|
|
169
|
+
{expanded ? '▴ less' : '▾ more'}
|
|
170
|
+
</text>
|
|
171
|
+
) : null}
|
|
172
|
+
</box>
|
|
173
|
+
) : null}
|
|
174
|
+
<box flexDirection="column">
|
|
175
|
+
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
176
|
+
Checks
|
|
177
|
+
</text>
|
|
178
|
+
{checks.length === 0 ? (
|
|
179
|
+
<text selectable={false} fg={t.textMuted} bg={bg}>
|
|
180
|
+
No checks
|
|
181
|
+
</text>
|
|
182
|
+
) : (
|
|
183
|
+
checks.map((check) => (
|
|
184
|
+
<CheckRow
|
|
185
|
+
key={`${check.workflow}/${check.name}`}
|
|
186
|
+
bg={bg}
|
|
187
|
+
check={check}
|
|
188
|
+
showWorkflow={innerWidth >= WORKFLOW_MIN_WIDTH}
|
|
189
|
+
spinner={spinner}
|
|
190
|
+
/>
|
|
191
|
+
))
|
|
192
|
+
)}
|
|
193
|
+
</box>
|
|
194
|
+
</scrollbox>
|
|
195
|
+
</box>
|
|
196
|
+
)
|
|
197
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { memo, useCallback, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { approveAndMergePr } from '../../../../git/pr-merge'
|
|
4
|
+
import { type PrActionState, prActionState } from '../../../../git/pr-status'
|
|
5
|
+
import { refreshPrStatus } from '../../../../git/pr-status-poller'
|
|
6
|
+
import { openUrl } from '../../../../platform/open-url'
|
|
7
|
+
import { usePrStatusStore } from '../../../../state/pr-status-store'
|
|
8
|
+
import { toast } from '../../../../state/toast-store'
|
|
9
|
+
import { useBusySpinner } from '../../../hooks/use-busy-spinner'
|
|
10
|
+
import { type ResolvedTuiTheme, useTheme, useTransparent } from '../../../theme'
|
|
11
|
+
|
|
12
|
+
function toneColor(tone: PrActionState['tone'], t: ResolvedTuiTheme): string {
|
|
13
|
+
if (tone === 'ok') return t.success
|
|
14
|
+
if (tone === 'blocked') return t.error
|
|
15
|
+
return t.textMuted
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PrStateRow = memo(function PrStateRow({ projectPath }: { projectPath: string }) {
|
|
19
|
+
const t = useTheme()
|
|
20
|
+
// Transparent mode drops every painted background rather than punching a hole
|
|
21
|
+
// in whatever the terminal is showing behind aimux.
|
|
22
|
+
const transparent = useTransparent()
|
|
23
|
+
const bg = transparent ? undefined : t.backgroundElement
|
|
24
|
+
const result = usePrStatusStore((s) => s.result)
|
|
25
|
+
const [confirming, setConfirming] = useState(false)
|
|
26
|
+
const [merging, setMerging] = useState(false)
|
|
27
|
+
const spinner = useBusySpinner(merging)
|
|
28
|
+
|
|
29
|
+
const pr = result?.kind === 'ok' ? result.pr : null
|
|
30
|
+
const prUrl = pr?.url ?? ''
|
|
31
|
+
|
|
32
|
+
const openPr = useCallback(() => openUrl(prUrl), [prUrl])
|
|
33
|
+
const askConfirm = useCallback(() => setConfirming(true), [])
|
|
34
|
+
const cancel = useCallback(() => setConfirming(false), [])
|
|
35
|
+
const confirm = useCallback(() => {
|
|
36
|
+
setConfirming(false)
|
|
37
|
+
setMerging(true)
|
|
38
|
+
void (async () => {
|
|
39
|
+
const merged = await approveAndMergePr(projectPath)
|
|
40
|
+
setMerging(false)
|
|
41
|
+
if (merged.ok) toast.success('Pull request merged')
|
|
42
|
+
else toast.error(merged.message)
|
|
43
|
+
await refreshPrStatus(projectPath)
|
|
44
|
+
})()
|
|
45
|
+
}, [projectPath])
|
|
46
|
+
|
|
47
|
+
// First fetch still in flight: hold the band empty so the tabs below don't
|
|
48
|
+
// jump a row once the PR lands.
|
|
49
|
+
if (result === null) {
|
|
50
|
+
return (
|
|
51
|
+
<box backgroundColor={bg} paddingLeft={1} paddingRight={1}>
|
|
52
|
+
<text selectable={false} bg={bg} wrapMode="none">
|
|
53
|
+
{' '}
|
|
54
|
+
</text>
|
|
55
|
+
</box>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
if (result.kind !== 'ok' || pr === null) return null
|
|
59
|
+
const status = prActionState(pr, result.checks)
|
|
60
|
+
let label = status.label
|
|
61
|
+
if (confirming) label = 'Merge this PR?'
|
|
62
|
+
if (merging) label = `${spinner} merging…`
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<box flexDirection="row" gap={1} backgroundColor={bg} paddingLeft={1} paddingRight={1}>
|
|
66
|
+
<box flexDirection="row" flexShrink={0} gap={1} onMouseDown={openPr}>
|
|
67
|
+
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none">
|
|
68
|
+
#{pr.number}
|
|
69
|
+
</text>
|
|
70
|
+
<text selectable={false} fg={t.primary} bg={bg} wrapMode="none">
|
|
71
|
+
↗
|
|
72
|
+
</text>
|
|
73
|
+
</box>
|
|
74
|
+
<box flexGrow={1} flexShrink={1} overflow="hidden">
|
|
75
|
+
<text
|
|
76
|
+
selectable={false}
|
|
77
|
+
fg={merging || confirming ? t.warning : toneColor(status.tone, t)}
|
|
78
|
+
bg={bg}
|
|
79
|
+
wrapMode="none"
|
|
80
|
+
>
|
|
81
|
+
{label}
|
|
82
|
+
</text>
|
|
83
|
+
</box>
|
|
84
|
+
{confirming ? (
|
|
85
|
+
<box flexDirection="row" flexShrink={0} gap={1}>
|
|
86
|
+
<text selectable={false} fg={t.success} bg={bg} wrapMode="none" onMouseDown={confirm}>
|
|
87
|
+
<strong>yes</strong>
|
|
88
|
+
</text>
|
|
89
|
+
<text selectable={false} fg={t.textMuted} bg={bg} wrapMode="none" onMouseDown={cancel}>
|
|
90
|
+
no
|
|
91
|
+
</text>
|
|
92
|
+
</box>
|
|
93
|
+
) : null}
|
|
94
|
+
{status.action === 'merge' && !confirming && !merging ? (
|
|
95
|
+
<box flexShrink={0}>
|
|
96
|
+
<text selectable={false} fg={t.primary} bg={bg} wrapMode="none" onMouseDown={askConfirm}>
|
|
97
|
+
<strong>Merge</strong>
|
|
98
|
+
</text>
|
|
99
|
+
</box>
|
|
100
|
+
) : null}
|
|
101
|
+
</box>
|
|
102
|
+
)
|
|
103
|
+
})
|