@brimveyn/aimux 1.20.4 → 1.21.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 (34) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +3 -48
  3. package/src/app-runtime/split-drag-controller.ts +4 -8
  4. package/src/app-runtime/use-mouse-handlers.ts +27 -42
  5. package/src/app-runtime/use-terminal-resize.ts +11 -20
  6. package/src/app.tsx +9 -37
  7. package/src/config.ts +98 -8
  8. package/src/git/pr-merge.ts +64 -0
  9. package/src/git/pr-status-poller.ts +57 -0
  10. package/src/git/pr-status.ts +227 -0
  11. package/src/index.tsx +7 -2
  12. package/src/platform/open-url.ts +39 -0
  13. package/src/services/ai-usage/spawn.ts +3 -1
  14. package/src/state/bars.ts +75 -0
  15. package/src/state/git-pane-sizing.ts +0 -9
  16. package/src/state/pr-status-store.ts +39 -0
  17. package/src/state/reducers/git-panel-state.ts +0 -47
  18. package/src/state/reducers/ui-state.ts +83 -13
  19. package/src/state/session-persistence.ts +5 -7
  20. package/src/state/store.ts +28 -35
  21. package/src/state/types.ts +24 -19
  22. package/src/state/workspace-save.ts +5 -7
  23. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
  24. package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
  25. package/src/ui/components/git/pane/git-pane-widget.tsx +30 -16
  26. package/src/ui/components/git/pane/pr-checks-panel.tsx +197 -0
  27. package/src/ui/components/git/pane/pr-state-row.tsx +103 -0
  28. package/src/ui/components/layout/bar.tsx +191 -0
  29. package/src/ui/components/layout/top-tab-bar.tsx +3 -2
  30. package/src/ui/root.tsx +25 -109
  31. package/src/ui/widgets/registry.tsx +18 -0
  32. package/src/ui/widgets/widget-context-menu.ts +69 -0
  33. package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
  34. package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
@@ -42,9 +42,11 @@ export function serializeWorkspace(state: AppState): WorkspaceSnapshotV1 {
42
42
  layoutTree: Object.values(state.layoutTrees)[0] ?? undefined,
43
43
  layoutTrees: Object.keys(state.layoutTrees).length > 0 ? state.layoutTrees : undefined,
44
44
  savedAt: new Date().toISOString(),
45
+ // Mirror of the left bar. `isWorkspaceSnapshotV1` requires this key —
46
+ // dropping it invalidates every entry in the session catalog.
45
47
  sidebar: {
46
- visible: state.sidebar.visible,
47
- width: state.sidebar.width,
48
+ visible: state.bars.left.visible,
49
+ width: state.bars.left.width,
48
50
  },
49
51
  tabGroupMap: Object.keys(state.tabGroupMap).length > 0 ? state.tabGroupMap : undefined,
50
52
  tabs: state.tabs.map((tab) => ({
@@ -277,10 +279,7 @@ export function restoreWorkspaceState(
277
279
  state: AppState,
278
280
  workspaceSnapshot: WorkspaceSnapshotV1 | undefined,
279
281
  options: RestoreOptions = {}
280
- ): Pick<
281
- AppState,
282
- 'tabs' | 'activeTabId' | 'focusMode' | 'sidebar' | 'layoutTrees' | 'tabGroupMap'
283
- > &
282
+ ): Pick<AppState, 'tabs' | 'activeTabId' | 'focusMode' | 'layoutTrees' | 'tabGroupMap'> &
284
283
  Partial<Pick<AppState, 'lastActiveTabByWorktree'>> {
285
284
  const tabs = restoreTabsFromWorkspace(workspaceSnapshot, options)
286
285
  const activeTabId =
@@ -305,7 +304,6 @@ export function restoreWorkspaceState(
305
304
  activeTabId,
306
305
  focusMode: 'navigation',
307
306
  layoutTrees,
308
- sidebar: state.sidebar,
309
307
  tabGroupMap,
310
308
  tabs: orderedTabs,
311
309
  ...(persistedLastActiveTab ? { lastActiveTabByWorktree: persistedLastActiveTab } : {}),
@@ -1,5 +1,6 @@
1
1
  import type { WorktreeTemplate } from '../config'
2
2
 
3
+ import { clampBarWidth, KNOWN_WIDGET_IDS } from './bars'
3
4
  import { reduceAutoCommit } from './reducers/auto-commit-state'
4
5
  import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
5
6
  import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
@@ -12,26 +13,22 @@ import { filterSnippets } from './selectors'
12
13
  import {
13
14
  type AppAction,
14
15
  type AppState,
16
+ type BarsState,
15
17
  EMPTY_AUTO_COMMIT_STATE,
16
18
  EMPTY_MULTI_REPO_STATE,
17
19
  type GitModeState,
18
- type GitPaneMode,
19
- type GitPanePosition,
20
20
  type GitPaneState,
21
21
  type SessionRecord,
22
22
  type SnippetRecord,
23
23
  } from './types'
24
24
 
25
- const DEFAULT_SIDEBAR_WIDTH = 28
26
- const DEFAULT_SIDEBAR_MIN_WIDTH = 18
27
- const DEFAULT_SIDEBAR_MAX_WIDTH = 42
28
25
  const DEFAULT_TERMINAL_COLS = 80
29
26
  const DEFAULT_TERMINAL_ROWS = 24
30
27
 
31
28
  export interface InitialStateOverrides {
32
29
  gitMode?: Partial<GitModeState>
33
30
  gitPane?: Partial<GitPaneState>
34
- sidebar?: Pick<AppState['sidebar'], 'visible' | 'width'>
31
+ bars?: BarsState
35
32
  sessionBarVisible?: boolean
36
33
  worktreeTemplates?: WorktreeTemplate[]
37
34
  }
@@ -39,26 +36,37 @@ export interface InitialStateOverrides {
39
36
  const DEFAULT_GIT_PANE: GitPaneState = {
40
37
  diffCount: { enabled: true },
41
38
  diffModeRatio: 0.35,
42
- embeddedRatio: 0.5,
43
39
  fileListMode: 'tree',
44
- mode: 'embedded',
45
- paneRatio: 0.5,
46
40
  path: { enabled: true },
47
- position: 'bottom',
48
41
  prefetchRadius: 5,
49
42
  treeCompaction: true,
50
- visible: true,
51
43
  }
52
44
 
53
- function resolveGitPanePosition(mode: GitPaneMode, position: GitPanePosition): GitPanePosition {
54
- if (mode === 'embedded') {
55
- return position === 'top' || position === 'bottom' ? position : 'bottom'
56
- }
57
- return position === 'left' || position === 'right' ? position : 'left'
45
+ export const DEFAULT_BARS: BarsState = {
46
+ left: {
47
+ visible: true,
48
+ widgets: [
49
+ { grow: 50, id: 'workspaces', visible: true },
50
+ { grow: 50, id: 'git', visible: true },
51
+ ],
52
+ width: 28,
53
+ },
54
+ right: { visible: false, widgets: [], width: 40 },
58
55
  }
59
56
 
60
- function clampSidebarWidth(width: number): number {
61
- return Math.min(DEFAULT_SIDEBAR_MAX_WIDTH, Math.max(DEFAULT_SIDEBAR_MIN_WIDTH, width))
57
+ /**
58
+ * Drop widget ids this build cannot render (config written by a newer or
59
+ * patched version) and normalise widths — the only place unknown ids can enter.
60
+ */
61
+ function sanitizeBars(bars: BarsState): BarsState {
62
+ const sanitizeBar = (bar: BarsState[keyof BarsState]): BarsState[keyof BarsState] => ({
63
+ ...bar,
64
+ widgets: bar.widgets
65
+ .filter((widget) => (KNOWN_WIDGET_IDS as readonly string[]).includes(widget.id))
66
+ .map((widget) => ({ ...widget, grow: Math.max(1, Math.round(widget.grow)) })),
67
+ width: clampBarWidth(bar.width),
68
+ })
69
+ return { left: sanitizeBar(bars.left), right: sanitizeBar(bars.right) }
62
70
  }
63
71
 
64
72
  export function createInitialState(
@@ -68,24 +76,15 @@ export function createInitialState(
68
76
  showSessionPicker = false,
69
77
  overrides: InitialStateOverrides = {}
70
78
  ): AppState {
71
- const gitPaneMode = overrides.gitPane?.mode ?? DEFAULT_GIT_PANE.mode
72
- const gitPanePosition = resolveGitPanePosition(
73
- gitPaneMode,
74
- overrides.gitPane?.position ?? DEFAULT_GIT_PANE.position
75
- )
76
79
  return {
77
80
  activeTabId: null,
78
81
  autoCommit: EMPTY_AUTO_COMMIT_STATE,
82
+ bars: sanitizeBars(overrides.bars ?? DEFAULT_BARS),
79
83
  currentSessionId: null,
80
84
  customCommands,
81
85
  focusMode: showSessionPicker ? 'command-edit' : 'navigation',
82
86
  gitMode: { ...emptyGitMode(), ...overrides.gitMode },
83
- gitPane: {
84
- ...DEFAULT_GIT_PANE,
85
- ...overrides.gitPane,
86
- mode: gitPaneMode,
87
- position: gitPanePosition,
88
- },
87
+ gitPane: { ...DEFAULT_GIT_PANE, ...overrides.gitPane },
89
88
  gitPanel: emptyGitPanel(),
90
89
  lastActiveTabByWorktree: {},
91
90
  layout: {
@@ -109,12 +108,6 @@ export function createInitialState(
109
108
  },
110
109
  sessions,
111
110
  sessionStatuses: {},
112
- sidebar: {
113
- maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
114
- minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
115
- visible: overrides.sidebar?.visible ?? true,
116
- width: clampSidebarWidth(overrides.sidebar?.width ?? DEFAULT_SIDEBAR_WIDTH),
117
- },
118
111
  snippets,
119
112
  tabGroupMap: {},
120
113
  tabs: [],
@@ -211,15 +211,26 @@ export interface TabSession {
211
211
  autoRenameStatus?: 'eligible' | 'attempted'
212
212
  }
213
213
 
214
- export interface SidebarState {
214
+ export type BarSide = 'left' | 'right'
215
+
216
+ /**
217
+ * One widget slot in a bar. `grow` is the flex weight opentui consumes
218
+ * directly; hidden widgets keep their weight but are excluded from the layout.
219
+ */
220
+ export interface BarWidget {
221
+ id: string
222
+ grow: number
223
+ visible: boolean
224
+ }
225
+
226
+ export interface BarState {
215
227
  visible: boolean
216
228
  width: number
217
- minWidth: number
218
- maxWidth: number
229
+ /** Ordered top → bottom. */
230
+ widgets: BarWidget[]
219
231
  }
220
232
 
221
- export type GitPaneMode = 'embedded' | 'pane'
222
- export type GitPanePosition = 'top' | 'bottom' | 'left' | 'right'
233
+ export type BarsState = Record<BarSide, BarState>
223
234
 
224
235
  export type GitPanePathConfig =
225
236
  | { enabled: false }
@@ -230,11 +241,6 @@ export interface GitPaneDiffCountConfig {
230
241
  }
231
242
 
232
243
  export interface GitPaneState {
233
- visible: boolean
234
- mode: GitPaneMode
235
- position: GitPanePosition
236
- paneRatio: number
237
- embeddedRatio: number
238
244
  diffModeRatio: number
239
245
  fileListMode: GitFileListMode
240
246
  treeCompaction: boolean
@@ -611,7 +617,7 @@ export interface AppState {
611
617
  sessionBar: SessionBarState
612
618
  snippets: SnippetRecord[]
613
619
  focusMode: FocusMode
614
- sidebar: SidebarState
620
+ bars: BarsState
615
621
  gitPane: GitPaneState
616
622
  modal: ModalState
617
623
  layout: LayoutState
@@ -803,17 +809,16 @@ export type LayoutAction =
803
809
 
804
810
  // -- UI actions --
805
811
  export type UIAction =
806
- | { type: 'toggle-sidebar' }
807
- | { type: 'resize-sidebar'; delta: number }
808
- | { type: 'set-sidebar-width'; width: number }
812
+ | { type: 'toggle-bar'; side: BarSide }
813
+ | { type: 'resize-bar'; side: BarSide; delta: number }
814
+ | { type: 'set-bar-width'; side: BarSide; width: number }
815
+ | { type: 'toggle-widget'; widgetId: string }
816
+ | { type: 'move-widget'; widgetId: string; side: BarSide; index: number }
817
+ | { type: 'set-bar-boundary'; side: BarSide; index: number; ratio: number }
818
+ | { type: 'resize-widget'; widgetId: string; delta: number }
809
819
  | { type: 'set-focus-mode'; focusMode: FocusMode }
810
820
  | { type: 'set-terminal-size'; cols: number; rows: number }
811
- | { type: 'toggle-git-pane' }
812
- | { type: 'resize-git-pane'; delta: number }
813
- | { type: 'set-git-pane-ratio'; target: 'pane' | 'embedded'; ratio: number }
814
821
  | { type: 'resize-git-diff-pane'; delta: number }
815
- | { type: 'set-git-pane-mode'; mode: GitPaneMode }
816
- | { type: 'set-git-pane-position'; position: GitPanePosition }
817
822
  | { type: 'set-pending-chords'; chords: string[] | null }
818
823
  | { type: 'toggle-session-bar' }
819
824
 
@@ -23,20 +23,18 @@ export function buildSessionsWithCurrentSnapshot(
23
23
  export function saveCurrentWorkspace(state: AppState): void {
24
24
  saveConfig({
25
25
  ...loadConfig(),
26
+ bars: state.bars,
26
27
  customCommands: state.customCommands,
27
28
  gitPane: {
28
29
  diffModeRatio: state.gitPane.diffModeRatio,
29
- embeddedRatio: state.gitPane.embeddedRatio,
30
30
  fileListMode: state.gitPane.fileListMode,
31
- mode: state.gitPane.mode,
32
- paneRatio: state.gitPane.paneRatio,
33
- position: state.gitPane.position,
34
- visible: state.gitPane.visible,
35
31
  },
36
32
  sessionBarVisible: state.sessionBar.visible,
33
+ // Legacy mirror of the left bar: lets an older build (and the workspace
34
+ // snapshot schema) still find a sidebar after a downgrade.
37
35
  sidebar: {
38
- visible: state.sidebar.visible,
39
- width: state.sidebar.width,
36
+ visible: state.bars.left.visible,
37
+ width: state.bars.left.width,
40
38
  },
41
39
  })
42
40
  saveSessionCatalog(
@@ -7,6 +7,7 @@ import type { FoldState } from '../../../../state/types'
7
7
  import type { ThemeId } from '../../../themes'
8
8
 
9
9
  import { useAppStore } from '../../../../state/app-store'
10
+ import { getBarWidth } from '../../../../state/bars'
10
11
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
11
12
  import { useTheme } from '../../../theme'
12
13
  import { buildDiffSegments, firstChangeSegmentOffset, gutterWidth } from './build-rows'
@@ -51,7 +52,7 @@ export const PierreDiff = forwardRef<PierreDiffHandle, Props>(function PierreDif
51
52
  const highlights: DiffHighlights = preparation.highlights
52
53
 
53
54
  const terminalCols = useAppStore((s) => s.layout.terminalCols)
54
- const sidebarWidth = useAppStore((s) => s.sidebar.width)
55
+ const sidebarWidth = useAppStore((s) => getBarWidth(s.bars.left))
55
56
  const contentWidth = useMemo(() => {
56
57
  if (!file) return 0
57
58
  const gw = gutterWidth(file)
@@ -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,19 +1,25 @@
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
9
  import { getSessionProjectPath } from '../../../../state/session-worktrees'
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)
@@ -28,8 +34,12 @@ export const GitPaneWidget = memo(function GitPaneWidget({ pollingEnabled }: Git
28
34
  : undefined
29
35
  const projectPath = getSessionProjectPath(currentSession)
30
36
 
37
+ const [tab, setTab] = useState<GitPaneTab>('files')
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
  })