@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.
Files changed (233) hide show
  1. package/README.md +27 -25
  2. package/package.json +2 -2
  3. package/src/app-runtime/auto-commit-driver.ts +22 -27
  4. package/src/app-runtime/backend-attach-runtime.ts +43 -36
  5. package/src/app-runtime/backend-runtime-events.ts +107 -88
  6. package/src/app-runtime/editor-actions.ts +201 -0
  7. package/src/app-runtime/git-actions.ts +197 -0
  8. package/src/app-runtime/measure-ref.ts +19 -0
  9. package/src/app-runtime/navigation-actions.ts +213 -0
  10. package/src/app-runtime/project-actions.ts +188 -0
  11. package/src/app-runtime/prompt-injection.ts +123 -0
  12. package/src/app-runtime/selection.ts +29 -0
  13. package/src/app-runtime/settings-actions.ts +156 -0
  14. package/src/app-runtime/setup-actions.ts +237 -0
  15. package/src/app-runtime/side-effect-context.ts +33 -0
  16. package/src/app-runtime/side-effects.ts +305 -1589
  17. package/src/app-runtime/snippet-actions.ts +3 -2
  18. package/src/app-runtime/split-drag-controller.ts +4 -8
  19. package/src/app-runtime/tab-actions.ts +218 -0
  20. package/src/app-runtime/tab-runtime-timeouts.ts +1 -1
  21. package/src/app-runtime/use-auto-commit-driver.ts +14 -13
  22. package/src/app-runtime/use-backend-runtime.ts +12 -11
  23. package/src/app-runtime/use-directory-search.ts +5 -4
  24. package/src/app-runtime/use-mouse-handlers.ts +28 -42
  25. package/src/app-runtime/{use-workspace-autosave.ts → use-project-autosave.ts} +4 -4
  26. package/src/app-runtime/use-renderer-bindings.ts +2 -1
  27. package/src/app-runtime/use-setup-runner.ts +96 -0
  28. package/src/app-runtime/use-terminal-resize.ts +28 -33
  29. package/src/app-runtime/workspace-actions.ts +366 -0
  30. package/src/app-runtime/workspace-activity.ts +138 -0
  31. package/src/app-runtime/workspace-naming.ts +90 -0
  32. package/src/app.tsx +102 -85
  33. package/src/assets/sounds/bell.wav +0 -0
  34. package/src/assets/sounds/ding.wav +0 -0
  35. package/src/assets/sounds/plane.wav +0 -0
  36. package/src/cli/client/daemon-client.ts +2 -2
  37. package/src/cli/client/project-resolver.ts +112 -0
  38. package/src/cli/commands/project/close.ts +32 -0
  39. package/src/cli/commands/project/create.ts +93 -0
  40. package/src/cli/commands/project/list.ts +25 -0
  41. package/src/cli/commands/project/show.ts +32 -0
  42. package/src/cli/commands/{workspace → project}/switch.ts +19 -19
  43. package/src/cli/commands/tab/await.ts +2 -2
  44. package/src/cli/commands/tab/close.ts +3 -3
  45. package/src/cli/commands/tab/create.ts +74 -73
  46. package/src/cli/commands/tab/focus.ts +3 -3
  47. package/src/cli/commands/tab/list.ts +6 -6
  48. package/src/cli/commands/tab/run.ts +2 -2
  49. package/src/cli/commands/tab/send.ts +2 -2
  50. package/src/cli/commands/tab/snapshot.ts +2 -2
  51. package/src/cli/commands/tab/tail.ts +2 -2
  52. package/src/cli/commands/tab/wait.ts +2 -2
  53. package/src/cli/commands/worker/await.ts +3 -3
  54. package/src/cli/commands/worker/doctor.ts +30 -30
  55. package/src/cli/commands/worker/list.ts +15 -15
  56. package/src/cli/commands/worker/prompt.ts +3 -3
  57. package/src/cli/commands/worker/run.ts +23 -23
  58. package/src/cli/commands/worker/shared.ts +53 -59
  59. package/src/cli/commands/worker/stop.ts +29 -29
  60. package/src/cli/commands/worker/submit.ts +3 -3
  61. package/src/cli/commands/{worktree → workspace}/create-core.ts +26 -26
  62. package/src/cli/commands/workspace/create.ts +30 -68
  63. package/src/cli/commands/workspace/list.ts +52 -9
  64. package/src/cli/commands/workspace/remove.ts +81 -0
  65. package/src/cli/completion/plan.ts +2 -2
  66. package/src/cli/completion/sources.ts +18 -18
  67. package/src/cli/context.ts +13 -13
  68. package/src/cli/flags.ts +30 -10
  69. package/src/cli/index.ts +26 -16
  70. package/src/cli/output.ts +1 -1
  71. package/src/cli/registry.ts +12 -12
  72. package/src/config/loader.ts +16 -5
  73. package/src/config.ts +149 -101
  74. package/src/daemon/catalog-writer.ts +67 -67
  75. package/src/daemon/daemon.ts +205 -201
  76. package/src/daemon/session-manager.ts +39 -39
  77. package/src/daemon/session-registry.ts +14 -14
  78. package/src/git/divergence.ts +33 -2
  79. package/src/git/git-poller.ts +1 -1
  80. package/src/git/git-status.ts +2 -2
  81. package/src/git/{move-worktree.ts → move-workspace.ts} +5 -5
  82. package/src/git/pr-merge.ts +64 -0
  83. package/src/git/pr-status-poller.ts +57 -0
  84. package/src/git/pr-status.ts +227 -0
  85. package/src/git/repo-discovery.ts +1 -1
  86. package/src/git/use-repo-discovery.ts +1 -1
  87. package/src/git/workspace-branch-poller.ts +54 -0
  88. package/src/git/workspace-divergence-poller.ts +65 -0
  89. package/src/git/worktree.ts +29 -0
  90. package/src/index.tsx +11 -6
  91. package/src/input/keymap/help-entries.ts +7 -5
  92. package/src/input/modes/bridge.ts +13 -12
  93. package/src/input/modes/handlers/shared.ts +1 -1
  94. package/src/input/modes/transitions.ts +34 -21
  95. package/src/input/modes/types.ts +48 -31
  96. package/src/ipc/manager-protocol.ts +44 -44
  97. package/src/ipc/protocol.ts +192 -166
  98. package/src/platform/open-url.ts +39 -0
  99. package/src/platform/play-sound.ts +184 -0
  100. package/src/platform/project-search.ts +8 -8
  101. package/src/platform/worktree-paths.ts +8 -6
  102. package/src/pty/assistant-question-extractor.ts +1 -1
  103. package/src/pty/assistant-status-detection-loop.ts +79 -55
  104. package/src/pty/assistant-status-detector.ts +40 -15
  105. package/src/pty/command-registry.ts +81 -1
  106. package/src/restart-terminal-manager.ts +1 -1
  107. package/src/services/ai-usage/provider.ts +9 -2
  108. package/src/services/ai-usage/spawn.ts +3 -1
  109. package/src/session-backend/bootstrap.ts +4 -4
  110. package/src/session-backend/local-session-backend.ts +78 -75
  111. package/src/session-backend/remote-session-backend.ts +64 -52
  112. package/src/session-backend/types.ts +43 -32
  113. package/src/settings/live.ts +90 -0
  114. package/src/settings/search.ts +43 -0
  115. package/src/settings/sections/about.ts +49 -0
  116. package/src/settings/sections/appearance.ts +54 -0
  117. package/src/settings/sections/automation.ts +105 -0
  118. package/src/settings/sections/commands.ts +72 -0
  119. package/src/settings/sections/editor.ts +56 -0
  120. package/src/settings/sections/experimental.ts +57 -0
  121. package/src/settings/sections/git.ts +112 -0
  122. package/src/settings/sections/index.ts +95 -0
  123. package/src/settings/sections/integrations.ts +23 -0
  124. package/src/settings/sections/layout.ts +72 -0
  125. package/src/settings/sections/notifications.ts +99 -0
  126. package/src/settings/sections/setup.ts +54 -0
  127. package/src/settings/sections/status-bar.ts +63 -0
  128. package/src/settings/settings-store.ts +207 -0
  129. package/src/settings/types.ts +108 -0
  130. package/src/snippets/run-shell-var.ts +48 -22
  131. package/src/state/actions.ts +342 -0
  132. package/src/state/app-store.ts +2 -1
  133. package/src/state/bars.ts +75 -0
  134. package/src/state/dispatch-ref.ts +1 -1
  135. package/src/state/git-pane-sizing.ts +0 -9
  136. package/src/state/layout-resize.ts +2 -2
  137. package/src/state/pr-status-store.ts +39 -0
  138. package/src/state/project-catalog.ts +275 -0
  139. package/src/state/project-data.ts +143 -0
  140. package/src/state/{session-persistence.ts → project-persistence.ts} +77 -73
  141. package/src/state/project-save.ts +46 -0
  142. package/src/state/project-workspaces.ts +392 -0
  143. package/src/state/reducers/auto-commit-state.ts +11 -10
  144. package/src/state/reducers/git-commit-modal-state.ts +122 -0
  145. package/src/state/reducers/git-mode-state.ts +1 -1
  146. package/src/state/reducers/git-panel-state.ts +10 -53
  147. package/src/state/reducers/modal-state.ts +290 -681
  148. package/src/state/reducers/multi-repo-state.ts +3 -6
  149. package/src/state/reducers/project-state.ts +312 -0
  150. package/src/state/reducers/settings-state.ts +80 -0
  151. package/src/state/reducers/tab-state.ts +107 -60
  152. package/src/state/reducers/ui-state.ts +86 -15
  153. package/src/state/selectors.ts +49 -37
  154. package/src/state/store.ts +83 -57
  155. package/src/state/types.ts +261 -417
  156. package/src/state/validation.ts +14 -12
  157. package/src/terminal-manager/manager-client.ts +32 -32
  158. package/src/terminal-manager/terminal-manager.ts +24 -24
  159. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
  160. package/src/ui/components/git/git-panel.tsx +2 -2
  161. package/src/ui/components/git/git-view.tsx +12 -12
  162. package/src/ui/components/git/image-diff/image-diff-view.tsx +1 -1
  163. package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
  164. package/src/ui/components/git/pane/git-pane-widget.tsx +37 -23
  165. package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
  166. package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
  167. package/src/ui/components/layout/bar.tsx +191 -0
  168. package/src/ui/components/layout/sidebar/project-list.tsx +444 -0
  169. package/src/ui/components/layout/sidebar/tab-item.tsx +27 -12
  170. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +1 -1
  171. package/src/ui/components/layout/sidebar/workspace-row.tsx +253 -0
  172. package/src/ui/components/layout/status-bar.tsx +6 -2
  173. package/src/ui/components/layout/terminal-pane.tsx +34 -29
  174. package/src/ui/components/layout/top-tab-bar.tsx +48 -37
  175. package/src/ui/components/modals/git/git-commit-modal.tsx +8 -8
  176. package/src/ui/components/modals/{sessions/create-session-modal.tsx → projects/create-project-modal.tsx} +12 -12
  177. package/src/ui/components/modals/{sessions/session-name-modal.tsx → projects/project-name-modal.tsx} +2 -2
  178. package/src/ui/components/modals/{sessions/session-picker-modal.tsx → projects/project-picker-modal.tsx} +33 -35
  179. package/src/ui/components/modals/settings/settings-search-modal.tsx +70 -0
  180. package/src/ui/components/modals/shared/form.tsx +34 -22
  181. package/src/ui/components/modals/shared/picker.tsx +10 -10
  182. package/src/ui/components/modals/shared/{worktree-delete-confirm.tsx → workspace-delete-confirm.tsx} +9 -9
  183. package/src/ui/components/modals/tabs/new-tab-modal.tsx +30 -324
  184. package/src/ui/components/modals/workspace/create-workspace-modal.tsx +101 -0
  185. package/src/ui/components/modals/{worktree/worktree-move-confirm-modal.tsx → workspace/workspace-move-confirm-modal.tsx} +5 -5
  186. package/src/ui/components/modals/{worktree/worktree-move-modal.tsx → workspace/workspace-move-modal.tsx} +29 -27
  187. package/src/ui/components/primitives/input-field.tsx +7 -3
  188. package/src/ui/components/primitives/surface.tsx +3 -0
  189. package/src/ui/components/settings/row-value.tsx +74 -0
  190. package/src/ui/components/settings/settings-row.tsx +78 -0
  191. package/src/ui/components/settings/settings-view.tsx +169 -0
  192. package/src/ui/components/setup/setup-widget.tsx +152 -0
  193. package/src/ui/flash/build-labels.ts +27 -27
  194. package/src/ui/hooks/use-activity-sprite.ts +73 -0
  195. package/src/ui/hooks/use-scroll-active-into-view.ts +26 -0
  196. package/src/ui/{session-ordering.ts → project-ordering.ts} +4 -4
  197. package/src/ui/root.tsx +164 -230
  198. package/src/ui/status-bar-model.ts +48 -34
  199. package/src/ui/terminal-graphics/kitty.ts +28 -2
  200. package/src/ui/terminal-graphics/sprites/done@330/0.png +0 -0
  201. package/src/ui/terminal-graphics/sprites/done@330/1.png +0 -0
  202. package/src/ui/terminal-graphics/sprites/idle@1000/0.png +0 -0
  203. package/src/ui/terminal-graphics/sprites/idle@1000/1.png +0 -0
  204. package/src/ui/terminal-graphics/sprites/waiting@170/0.png +0 -0
  205. package/src/ui/terminal-graphics/sprites/waiting@170/1.png +0 -0
  206. package/src/ui/terminal-graphics/sprites/working@150/0.png +0 -0
  207. package/src/ui/terminal-graphics/sprites/working@150/1.png +0 -0
  208. package/src/ui/terminal-graphics/sprites/working@150/2.png +0 -0
  209. package/src/ui/terminal-graphics/sprites/working@150/3.png +0 -0
  210. package/src/ui/terminal-graphics/sprites.ts +246 -0
  211. package/src/ui/truncate.ts +7 -0
  212. package/src/ui/widgets/registry.tsx +21 -0
  213. package/src/ui/widgets/widget-context-menu.ts +85 -0
  214. package/src/update.ts +2 -2
  215. package/src/app-runtime/session-actions.ts +0 -187
  216. package/src/cli/client/workspace-resolver.ts +0 -110
  217. package/src/cli/commands/workspace/close.ts +0 -32
  218. package/src/cli/commands/workspace/show.ts +0 -32
  219. package/src/cli/commands/worktree/create.ts +0 -55
  220. package/src/cli/commands/worktree/list.ts +0 -68
  221. package/src/cli/commands/worktree/remove.ts +0 -81
  222. package/src/git/worktree-branch-poller.ts +0 -54
  223. package/src/git/worktree-divergence-poller.ts +0 -59
  224. package/src/state/reducers/session-state.ts +0 -262
  225. package/src/state/session-catalog.ts +0 -143
  226. package/src/state/session-worktrees.ts +0 -260
  227. package/src/state/workspace-save.ts +0 -45
  228. package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
  229. package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
  230. package/src/ui/components/layout/sidebar/use-top-tab-bar-auto-scroll.ts +0 -30
  231. package/src/ui/components/layout/sidebar/workspace-list.tsx +0 -418
  232. package/src/ui/components/layout/sidebar/worktree-row.tsx +0 -149
  233. /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 { useTheme } from '../../../theme'
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
- if (gitPanel.error !== null) return null
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
- const showToggle = gitPanel.files.length > 0
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
- return (
50
- <box flexDirection="column" flexShrink={0} paddingBottom={1}>
51
- <box flexDirection="row" justifyContent="space-between">
52
- <box flexDirection="row" flexShrink={1} overflow="hidden">
53
- <text selectable={false} fg={t.textMuted} wrapMode="none">
54
- {'\u{e702}'}
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 selectable={false} fg={branchIsResolved ? t.text : t.textMuted} wrapMode="none">
60
- {branchIsResolved ? <strong>{branchLabel}</strong> : branchLabel}
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
- <box flexDirection="row" flexShrink={0} gap={2}>
64
- {showTracking ? (
65
- <box flexDirection="row" gap={1}>
66
- {showAhead ? (
67
- <text selectable={false} fg={t.textMuted} wrapMode="none">
68
- {`↑${ahead}`}
69
- </text>
70
- ) : null}
71
- {showBehind ? (
72
- <text selectable={false} fg={t.textMuted} wrapMode="none">
73
- {`↓${behind}`}
74
- </text>
75
- ) : null}
76
- </box>
77
- ) : null}
78
- {showToggle ? (
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={fileListMode === 'tree' ? t.primary : t.textMuted}
125
+ fg={tab === 'files' ? t.text : t.textMuted}
126
+ bg={tab === 'files' ? activeTabBg : undefined}
83
127
  wrapMode="none"
84
128
  >
85
- tree
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={fileListMode === 'flat' ? t.primary : t.textMuted}
140
+ fg={tab === 'checks' ? t.text : t.textMuted}
141
+ bg={tab === 'checks' ? activeTabBg : undefined}
93
142
  wrapMode="none"
94
143
  >
95
- flat
144
+ checks
96
145
  </text>
97
146
  </box>
98
- ) : null}
147
+ </box>
148
+ {trackingAndToggle}
99
149
  </box>
100
- </box>
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 { getSessionProjectPath } from '../../../../state/session-worktrees'
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({ pollingEnabled }: GitPaneWidgetProps) {
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 currentSessionId = useAppStore((s) => s.currentSessionId)
24
- const sessions = useAppStore((s) => s.sessions)
25
- const currentSession =
26
- currentSessionId != null && currentSessionId !== ''
27
- ? sessions.find((s) => s.id === currentSessionId)
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 = getSessionProjectPath(currentSession)
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
- <GitPanel
50
- collapsedFolders={gitMode.collapsedFolders}
51
- compact={treeCompaction}
52
- diffCountConfig={diffCountConfig}
53
- fileListMode={gitFileListMode}
54
- gitPanel={display}
55
- pathConfig={pathConfig}
56
- projectPath={projectPath}
57
- selectedEntryKey={gitMode.selectedEntryKey}
58
- showFileListToggle={false}
59
- showRemoteTracking={false}
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
+ })