@brimveyn/aimux 1.20.3 → 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.
- package/package.json +2 -2
- package/src/app-runtime/side-effects.ts +3 -48
- package/src/app-runtime/split-drag-controller.ts +4 -8
- package/src/app-runtime/use-mouse-handlers.ts +27 -42
- package/src/app-runtime/use-terminal-resize.ts +11 -20
- package/src/app.tsx +9 -37
- package/src/config.ts +98 -8
- 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/index.tsx +7 -2
- package/src/platform/clipboard.ts +140 -4
- package/src/platform/open-url.ts +39 -0
- package/src/services/ai-usage/spawn.ts +3 -1
- package/src/state/bars.ts +75 -0
- package/src/state/git-pane-sizing.ts +0 -9
- package/src/state/pr-status-store.ts +39 -0
- package/src/state/reducers/git-panel-state.ts +0 -47
- package/src/state/reducers/ui-state.ts +83 -13
- package/src/state/session-persistence.ts +5 -7
- package/src/state/store.ts +28 -35
- package/src/state/types.ts +24 -19
- package/src/state/workspace-save.ts +5 -7
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +2 -1
- package/src/ui/components/git/pane/git-pane-header.tsx +105 -39
- package/src/ui/components/git/pane/git-pane-widget.tsx +30 -16
- 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/top-tab-bar.tsx +3 -2
- package/src/ui/root.tsx +25 -109
- package/src/ui/widgets/registry.tsx +18 -0
- package/src/ui/widgets/widget-context-menu.ts +69 -0
- package/src/ui/components/git/pane/git-pane-context-menu.ts +0 -26
- package/src/ui/components/layout/sidebar/sidebar.tsx +0 -163
|
@@ -105,58 +105,11 @@ function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
|
105
105
|
|
|
106
106
|
export function reduceGitPanelState(state: AppState, action: AppAction): AppState | null {
|
|
107
107
|
switch (action.type) {
|
|
108
|
-
case 'toggle-git-pane': {
|
|
109
|
-
const nextVisible = !state.gitPane.visible
|
|
110
|
-
const sidebarMustShow = state.gitPane.mode === 'embedded' && nextVisible
|
|
111
|
-
return {
|
|
112
|
-
...state,
|
|
113
|
-
gitPane: { ...state.gitPane, visible: nextVisible },
|
|
114
|
-
sidebar: sidebarMustShow ? { ...state.sidebar, visible: true } : state.sidebar,
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
case 'resize-git-pane': {
|
|
118
|
-
const target = state.gitPane.mode === 'pane' ? 'paneRatio' : 'embeddedRatio'
|
|
119
|
-
const nextRatio = clampRatio(state.gitPane[target] + action.delta)
|
|
120
|
-
if (nextRatio === state.gitPane[target]) return state
|
|
121
|
-
return { ...state, gitPane: { ...state.gitPane, [target]: nextRatio } }
|
|
122
|
-
}
|
|
123
|
-
case 'set-git-pane-ratio': {
|
|
124
|
-
const key = action.target === 'pane' ? 'paneRatio' : 'embeddedRatio'
|
|
125
|
-
const nextRatio = clampRatio(action.ratio)
|
|
126
|
-
if (nextRatio === state.gitPane[key]) return state
|
|
127
|
-
return { ...state, gitPane: { ...state.gitPane, [key]: nextRatio } }
|
|
128
|
-
}
|
|
129
108
|
case 'resize-git-diff-pane': {
|
|
130
109
|
const nextRatio = clampRatio(state.gitPane.diffModeRatio + action.delta)
|
|
131
110
|
if (nextRatio === state.gitPane.diffModeRatio) return state
|
|
132
111
|
return { ...state, gitPane: { ...state.gitPane, diffModeRatio: nextRatio } }
|
|
133
112
|
}
|
|
134
|
-
case 'set-git-pane-mode': {
|
|
135
|
-
if (state.gitPane.mode === action.mode) return state
|
|
136
|
-
const isEmbedded = action.mode === 'embedded'
|
|
137
|
-
const isValidEmbedded =
|
|
138
|
-
state.gitPane.position === 'top' || state.gitPane.position === 'bottom'
|
|
139
|
-
const isValidPane = state.gitPane.position === 'left' || state.gitPane.position === 'right'
|
|
140
|
-
let nextPosition: typeof state.gitPane.position
|
|
141
|
-
if (isEmbedded) {
|
|
142
|
-
nextPosition = isValidEmbedded ? state.gitPane.position : 'bottom'
|
|
143
|
-
} else {
|
|
144
|
-
nextPosition = isValidPane ? state.gitPane.position : 'left'
|
|
145
|
-
}
|
|
146
|
-
return {
|
|
147
|
-
...state,
|
|
148
|
-
gitPane: { ...state.gitPane, mode: action.mode, position: nextPosition },
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
case 'set-git-pane-position': {
|
|
152
|
-
const validForMode =
|
|
153
|
-
state.gitPane.mode === 'embedded'
|
|
154
|
-
? action.position === 'top' || action.position === 'bottom'
|
|
155
|
-
: action.position === 'left' || action.position === 'right'
|
|
156
|
-
if (!validForMode) return state
|
|
157
|
-
if (state.gitPane.position === action.position) return state
|
|
158
|
-
return { ...state, gitPane: { ...state.gitPane, position: action.position } }
|
|
159
|
-
}
|
|
160
113
|
case 'git-refresh-success': {
|
|
161
114
|
const prev = state.gitPanel
|
|
162
115
|
const next = action.payload
|
|
@@ -1,21 +1,91 @@
|
|
|
1
|
-
import type { AppAction, AppState } from '../types'
|
|
1
|
+
import type { AppAction, AppState, BarSide, BarState, BarWidget } from '../types'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
boundaryDeltaFromRatio,
|
|
5
|
+
clampBarWidth,
|
|
6
|
+
findWidgetBar,
|
|
7
|
+
shiftBoundary,
|
|
8
|
+
visibleWidgets,
|
|
9
|
+
} from '../bars'
|
|
10
|
+
|
|
11
|
+
function withBar(state: AppState, side: BarSide, next: BarState): AppState {
|
|
12
|
+
if (next === state.bars[side]) return state
|
|
13
|
+
return { ...state, bars: { ...state.bars, [side]: next } }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setBarWidth(state: AppState, side: BarSide, width: number): AppState {
|
|
17
|
+
const bar = state.bars[side]
|
|
18
|
+
const next = clampBarWidth(width)
|
|
19
|
+
if (next === bar.width) return state
|
|
20
|
+
return withBar(state, side, { ...bar, width: next })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Move `widgetId` to `side` at `index`. Covers both cross-bar moves and
|
|
25
|
+
* in-bar reordering — removing then re-inserting is the same operation.
|
|
26
|
+
*/
|
|
27
|
+
function moveWidget(state: AppState, widgetId: string, side: BarSide, index: number): AppState {
|
|
28
|
+
const from = findWidgetBar(state.bars, widgetId)
|
|
29
|
+
if (from === null) return state
|
|
30
|
+
const widget = state.bars[from].widgets.find((w) => w.id === widgetId)
|
|
31
|
+
if (!widget) return state
|
|
32
|
+
|
|
33
|
+
const source = state.bars[from].widgets.filter((w) => w.id !== widgetId)
|
|
34
|
+
const target: BarWidget[] = from === side ? source : [...state.bars[side].widgets]
|
|
35
|
+
const at = Math.max(0, Math.min(target.length, index))
|
|
36
|
+
target.splice(at, 0, widget)
|
|
37
|
+
|
|
38
|
+
const bars = { ...state.bars }
|
|
39
|
+
bars[from] = { ...state.bars[from], widgets: from === side ? target : source }
|
|
40
|
+
// Showing a widget in a hidden bar would silently swallow it.
|
|
41
|
+
bars[side] = { ...bars[side], visible: bars[side].visible || widget.visible, widgets: target }
|
|
42
|
+
return { ...state, bars }
|
|
43
|
+
}
|
|
2
44
|
|
|
3
45
|
export function reduceUIState(state: AppState, action: AppAction): AppState | null {
|
|
4
46
|
switch (action.type) {
|
|
5
|
-
case 'toggle-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
47
|
+
case 'toggle-bar': {
|
|
48
|
+
const bar = state.bars[action.side]
|
|
49
|
+
return withBar(state, action.side, { ...bar, visible: !bar.visible })
|
|
50
|
+
}
|
|
51
|
+
case 'resize-bar':
|
|
52
|
+
return setBarWidth(state, action.side, state.bars[action.side].width + action.delta)
|
|
53
|
+
case 'set-bar-width':
|
|
54
|
+
return setBarWidth(state, action.side, action.width)
|
|
55
|
+
case 'toggle-widget': {
|
|
56
|
+
const side = findWidgetBar(state.bars, action.widgetId)
|
|
57
|
+
if (side === null) return state
|
|
58
|
+
const bar = state.bars[side]
|
|
59
|
+
const widgets = bar.widgets.map((w) =>
|
|
60
|
+
w.id === action.widgetId ? { ...w, visible: !w.visible } : w
|
|
11
61
|
)
|
|
12
|
-
|
|
13
|
-
return { ...
|
|
62
|
+
const revealed = widgets.some((w) => w.id === action.widgetId && w.visible)
|
|
63
|
+
return withBar(state, side, { ...bar, visible: bar.visible || revealed, widgets })
|
|
64
|
+
}
|
|
65
|
+
case 'move-widget':
|
|
66
|
+
return moveWidget(state, action.widgetId, action.side, action.index)
|
|
67
|
+
case 'set-bar-boundary': {
|
|
68
|
+
const bar = state.bars[action.side]
|
|
69
|
+
const delta = boundaryDeltaFromRatio(bar, action.index, action.ratio)
|
|
70
|
+
const widgets = shiftBoundary(bar, action.index, delta)
|
|
71
|
+
if (widgets === bar.widgets) return state
|
|
72
|
+
return withBar(state, action.side, { ...bar, widgets })
|
|
14
73
|
}
|
|
15
|
-
case '
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
74
|
+
case 'resize-widget': {
|
|
75
|
+
// Keyboard resize: grow/shrink this widget against its neighbour. The
|
|
76
|
+
// widget below owns the boundary above it, so the sign flips there.
|
|
77
|
+
const side = findWidgetBar(state.bars, action.widgetId)
|
|
78
|
+
if (side === null) return state
|
|
79
|
+
const bar = state.bars[side]
|
|
80
|
+
const visible = visibleWidgets(bar)
|
|
81
|
+
const at = visible.findIndex((w) => w.id === action.widgetId)
|
|
82
|
+
if (at === -1) return state
|
|
83
|
+
const total = visible.reduce((sum, w) => sum + w.grow, 0)
|
|
84
|
+
const index = at > 0 ? at - 1 : at
|
|
85
|
+
const deltaGrow = action.delta * total * (at > 0 ? -1 : 1)
|
|
86
|
+
const widgets = shiftBoundary(bar, index, deltaGrow)
|
|
87
|
+
if (widgets === bar.widgets) return state
|
|
88
|
+
return withBar(state, side, { ...bar, widgets })
|
|
19
89
|
}
|
|
20
90
|
case 'set-focus-mode':
|
|
21
91
|
return { ...state, focusMode: action.focusMode }
|
|
@@ -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.
|
|
47
|
-
width: state.
|
|
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 } : {}),
|
package/src/state/store.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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: [],
|
package/src/state/types.ts
CHANGED
|
@@ -211,15 +211,26 @@ export interface TabSession {
|
|
|
211
211
|
autoRenameStatus?: 'eligible' | 'attempted'
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
export
|
|
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
|
-
|
|
218
|
-
|
|
229
|
+
/** Ordered top → bottom. */
|
|
230
|
+
widgets: BarWidget[]
|
|
219
231
|
}
|
|
220
232
|
|
|
221
|
-
export type
|
|
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
|
-
|
|
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-
|
|
807
|
-
| { type: 'resize-
|
|
808
|
-
| { type: 'set-
|
|
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.
|
|
39
|
-
width: state.
|
|
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.
|
|
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 {
|
|
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 ? (
|