@brimveyn/aimux 1.1.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/LICENSE +21 -0
- package/README.md +206 -0
- package/package.json +79 -0
- package/src/app-runtime/backend-attach-runtime.ts +135 -0
- package/src/app-runtime/backend-runtime-events.ts +78 -0
- package/src/app-runtime/click-selection-resolver.ts +130 -0
- package/src/app-runtime/pty-write.ts +32 -0
- package/src/app-runtime/render-invalidation.ts +20 -0
- package/src/app-runtime/selection-clipboard.ts +69 -0
- package/src/app-runtime/selection-scroll.ts +143 -0
- package/src/app-runtime/session-actions.ts +170 -0
- package/src/app-runtime/side-effects.ts +434 -0
- package/src/app-runtime/snippet-actions.ts +90 -0
- package/src/app-runtime/split-drag-controller.ts +15 -0
- package/src/app-runtime/tab-runtime-timeouts.ts +86 -0
- package/src/app-runtime/terminal-mouse-adapter.ts +31 -0
- package/src/app-runtime/use-backend-runtime.ts +99 -0
- package/src/app-runtime/use-directory-search.ts +52 -0
- package/src/app-runtime/use-mouse-handlers.ts +183 -0
- package/src/app-runtime/use-renderer-bindings.ts +163 -0
- package/src/app-runtime/use-terminal-resize.ts +141 -0
- package/src/app-runtime/use-workspace-autosave.ts +39 -0
- package/src/app.tsx +247 -0
- package/src/config/loader.ts +36 -0
- package/src/config.ts +146 -0
- package/src/daemon/daemon.ts +236 -0
- package/src/daemon/runtime-paths.ts +69 -0
- package/src/daemon/session-manager.ts +109 -0
- package/src/daemon/session-registry.ts +207 -0
- package/src/debug/input-log.ts +29 -0
- package/src/doctor.ts +106 -0
- package/src/git/git-poller.ts +49 -0
- package/src/git/git-status.ts +183 -0
- package/src/index.tsx +56 -0
- package/src/input/keymap/build-handlers.ts +34 -0
- package/src/input/keymap/key-chord.ts +201 -0
- package/src/input/keymap/keymap-mode-handler.ts +88 -0
- package/src/input/keymap/sequence-resolver.ts +117 -0
- package/src/input/keymap/trie.ts +103 -0
- package/src/input/modes/bridge.ts +58 -0
- package/src/input/modes/handlers/index.ts +18 -0
- package/src/input/modes/handlers/shared.ts +70 -0
- package/src/input/modes/registry.ts +34 -0
- package/src/input/modes/transitions.ts +37 -0
- package/src/input/modes/types.ts +63 -0
- package/src/input/mouse-forwarding.ts +98 -0
- package/src/input/multi-click-detector.ts +55 -0
- package/src/input/paste.ts +12 -0
- package/src/input/raw-input-handler.ts +191 -0
- package/src/input/terminal-text-extraction.ts +78 -0
- package/src/ipc/protocol.ts +341 -0
- package/src/platform/clipboard.ts +13 -0
- package/src/platform/daemon-control.ts +62 -0
- package/src/platform/id.ts +7 -0
- package/src/platform/project-search.ts +75 -0
- package/src/pty/command-registry.ts +69 -0
- package/src/pty/pty-manager.ts +268 -0
- package/src/pty/terminal-snapshot.ts +210 -0
- package/src/restart-daemon.ts +28 -0
- package/src/session-backend/bootstrap.ts +107 -0
- package/src/session-backend/local-session-backend.ts +164 -0
- package/src/session-backend/remote-session-backend.ts +373 -0
- package/src/session-backend/types.ts +47 -0
- package/src/state/app-store.ts +19 -0
- package/src/state/layout-resize.ts +56 -0
- package/src/state/layout-tree.ts +323 -0
- package/src/state/reducers/git-panel-state.ts +102 -0
- package/src/state/reducers/modal-state.ts +358 -0
- package/src/state/reducers/session-state.ts +86 -0
- package/src/state/reducers/tab-state.ts +531 -0
- package/src/state/reducers/ui-state.ts +29 -0
- package/src/state/selectors.ts +30 -0
- package/src/state/session-catalog.ts +96 -0
- package/src/state/session-persistence.ts +210 -0
- package/src/state/snippet-catalog.ts +90 -0
- package/src/state/store.ts +93 -0
- package/src/state/terminal-modes.ts +11 -0
- package/src/state/types.ts +367 -0
- package/src/state/validation.ts +154 -0
- package/src/state/workspace-save.ts +33 -0
- package/src/ui/components/create-session-modal.tsx +100 -0
- package/src/ui/components/git-panel.tsx +200 -0
- package/src/ui/components/help-modal.tsx +79 -0
- package/src/ui/components/input-field.tsx +18 -0
- package/src/ui/components/list-item.tsx +30 -0
- package/src/ui/components/modal-filter-bar.tsx +18 -0
- package/src/ui/components/modal-shell.tsx +47 -0
- package/src/ui/components/new-tab-modal.tsx +52 -0
- package/src/ui/components/pending-chord-indicator.tsx +41 -0
- package/src/ui/components/session-name-modal.tsx +15 -0
- package/src/ui/components/session-picker-modal.tsx +93 -0
- package/src/ui/components/sidebar-group-metadata.ts +44 -0
- package/src/ui/components/sidebar-scroll.ts +33 -0
- package/src/ui/components/sidebar.tsx +225 -0
- package/src/ui/components/snippet-editor-modal.tsx +39 -0
- package/src/ui/components/snippet-picker-modal.tsx +56 -0
- package/src/ui/components/split-layout.tsx +201 -0
- package/src/ui/components/status-bar.tsx +60 -0
- package/src/ui/components/surface.tsx +63 -0
- package/src/ui/components/tab-item.tsx +118 -0
- package/src/ui/components/terminal-pane.tsx +215 -0
- package/src/ui/components/theme-picker-modal.tsx +35 -0
- package/src/ui/components/use-sidebar-auto-scroll.ts +63 -0
- package/src/ui/components/use-sidebar-branch.ts +36 -0
- package/src/ui/directory-search.ts +1 -0
- package/src/ui/git-branch.ts +11 -0
- package/src/ui/path-format.ts +6 -0
- package/src/ui/root.tsx +261 -0
- package/src/ui/status-bar-model.ts +87 -0
- package/src/ui/theme.ts +9 -0
- package/src/ui/themes.ts +255 -0
- package/src/ui/ui-tokens.ts +11 -0
- package/src/update.ts +62 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { WorkspaceSnapshotV1 } from './types'
|
|
2
|
+
|
|
3
|
+
import { computePaneRects, type LayoutNode, PANE_BORDER, type PaneRect } from './layout-tree'
|
|
4
|
+
|
|
5
|
+
export interface TerminalBounds {
|
|
6
|
+
x: number
|
|
7
|
+
y: number
|
|
8
|
+
cols: number
|
|
9
|
+
rows: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getSnapshotTrees(snapshot: WorkspaceSnapshotV1 | undefined): LayoutNode[] {
|
|
13
|
+
if (snapshot?.layoutTrees) {
|
|
14
|
+
return Object.values(snapshot.layoutTrees)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (snapshot?.layoutTree) {
|
|
18
|
+
return [snapshot.layoutTree]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createTerminalBounds(cols: number, rows: number): TerminalBounds {
|
|
25
|
+
const chrome = PANE_BORDER * 2
|
|
26
|
+
return {
|
|
27
|
+
cols: cols + chrome,
|
|
28
|
+
rows: rows + chrome,
|
|
29
|
+
x: 0,
|
|
30
|
+
y: 0,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function toTerminalContentSize(rect: PaneRect): { cols: number; rows: number } {
|
|
35
|
+
const chrome = PANE_BORDER * 2
|
|
36
|
+
return {
|
|
37
|
+
cols: Math.max(1, rect.cols - chrome),
|
|
38
|
+
rows: Math.max(1, rect.rows - chrome),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function forEachSplitPaneRect(
|
|
43
|
+
trees: LayoutNode[],
|
|
44
|
+
bounds: TerminalBounds,
|
|
45
|
+
callback: (tabId: string, rect: PaneRect) => void
|
|
46
|
+
): void {
|
|
47
|
+
for (const tree of trees) {
|
|
48
|
+
if (tree.type !== 'split') {
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [tabId, rect] of computePaneRects(tree, bounds)) {
|
|
53
|
+
callback(tabId, rect)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
export type SplitDirection = 'horizontal' | 'vertical'
|
|
2
|
+
|
|
3
|
+
export type LayoutLeaf = { type: 'leaf'; tabId: string }
|
|
4
|
+
export type LayoutSplit = {
|
|
5
|
+
type: 'split'
|
|
6
|
+
direction: SplitDirection
|
|
7
|
+
ratio: number
|
|
8
|
+
first: LayoutNode
|
|
9
|
+
second: LayoutNode
|
|
10
|
+
}
|
|
11
|
+
export type LayoutNode = LayoutLeaf | LayoutSplit
|
|
12
|
+
|
|
13
|
+
export interface PaneRect {
|
|
14
|
+
x: number
|
|
15
|
+
y: number
|
|
16
|
+
cols: number
|
|
17
|
+
rows: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Border thickness on each side of a split pane (used to compute content size from outer bounds). */
|
|
21
|
+
export const PANE_BORDER = 1
|
|
22
|
+
|
|
23
|
+
const MIN_RATIO = 0.15
|
|
24
|
+
const MAX_RATIO = 0.85
|
|
25
|
+
const RATIO_STEP = 0.05
|
|
26
|
+
|
|
27
|
+
let groupCounter = 0
|
|
28
|
+
export function createGroupId(): string {
|
|
29
|
+
return `group-${Date.now()}-${++groupCounter}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getGroupIdForTab(
|
|
33
|
+
tabGroupMap: Record<string, string>,
|
|
34
|
+
tabId: string
|
|
35
|
+
): string | null {
|
|
36
|
+
return tabGroupMap[tabId] ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getTreeForTab(
|
|
40
|
+
layoutTrees: Record<string, LayoutNode>,
|
|
41
|
+
tabGroupMap: Record<string, string>,
|
|
42
|
+
tabId: string
|
|
43
|
+
): LayoutNode | null {
|
|
44
|
+
const groupId = tabGroupMap[tabId]
|
|
45
|
+
if (!groupId) return null
|
|
46
|
+
return layoutTrees[groupId] ?? null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createLeaf(tabId: string): LayoutLeaf {
|
|
50
|
+
return { tabId, type: 'leaf' }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function splitNode(
|
|
54
|
+
tree: LayoutNode,
|
|
55
|
+
targetTabId: string,
|
|
56
|
+
direction: SplitDirection,
|
|
57
|
+
newTabId: string
|
|
58
|
+
): LayoutNode {
|
|
59
|
+
if (tree.type === 'leaf') {
|
|
60
|
+
if (tree.tabId === targetTabId) {
|
|
61
|
+
return {
|
|
62
|
+
direction,
|
|
63
|
+
first: tree,
|
|
64
|
+
ratio: 0.5,
|
|
65
|
+
second: createLeaf(newTabId),
|
|
66
|
+
type: 'split',
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return tree
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const newFirst = splitNode(tree.first, targetTabId, direction, newTabId)
|
|
73
|
+
if (newFirst !== tree.first) {
|
|
74
|
+
return { ...tree, first: newFirst }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const newSecond = splitNode(tree.second, targetTabId, direction, newTabId)
|
|
78
|
+
if (newSecond !== tree.second) {
|
|
79
|
+
return { ...tree, second: newSecond }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return tree
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function removeNode(tree: LayoutNode, tabId: string): LayoutNode | null {
|
|
86
|
+
if (tree.type === 'leaf') {
|
|
87
|
+
return tree.tabId === tabId ? null : tree
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const newFirst = removeNode(tree.first, tabId)
|
|
91
|
+
const newSecond = removeNode(tree.second, tabId)
|
|
92
|
+
|
|
93
|
+
if (newFirst === null && newSecond === null) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
if (newFirst === null) {
|
|
97
|
+
return newSecond
|
|
98
|
+
}
|
|
99
|
+
if (newSecond === null) {
|
|
100
|
+
return newFirst
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (newFirst !== tree.first || newSecond !== tree.second) {
|
|
104
|
+
return { ...tree, first: newFirst, second: newSecond }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return tree
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function findLeaf(tree: LayoutNode, tabId: string): boolean {
|
|
111
|
+
if (tree.type === 'leaf') {
|
|
112
|
+
return tree.tabId === tabId
|
|
113
|
+
}
|
|
114
|
+
return findLeaf(tree.first, tabId) || findLeaf(tree.second, tabId)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function allLeafIds(tree: LayoutNode): string[] {
|
|
118
|
+
if (tree.type === 'leaf') {
|
|
119
|
+
return [tree.tabId]
|
|
120
|
+
}
|
|
121
|
+
return [...allLeafIds(tree.first), ...allLeafIds(tree.second)]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function pruneLayoutTree(tree: LayoutNode, validTabIds: Set<string>): LayoutNode | null {
|
|
125
|
+
if (tree.type === 'leaf') {
|
|
126
|
+
return validTabIds.has(tree.tabId) ? tree : null
|
|
127
|
+
}
|
|
128
|
+
const first = pruneLayoutTree(tree.first, validTabIds)
|
|
129
|
+
const second = pruneLayoutTree(tree.second, validTabIds)
|
|
130
|
+
if (!first && !second) return null
|
|
131
|
+
if (!first) return second
|
|
132
|
+
if (!second) return first
|
|
133
|
+
if (first === tree.first && second === tree.second) return tree
|
|
134
|
+
return { ...tree, first, second }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function resizeSplit(
|
|
138
|
+
tree: LayoutNode,
|
|
139
|
+
targetTabId: string,
|
|
140
|
+
delta: number,
|
|
141
|
+
axis?: SplitDirection
|
|
142
|
+
): LayoutNode {
|
|
143
|
+
if (tree.type === 'leaf') {
|
|
144
|
+
return tree
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const inFirst = findLeaf(tree.first, targetTabId)
|
|
148
|
+
const inSecond = findLeaf(tree.second, targetTabId)
|
|
149
|
+
|
|
150
|
+
if (!inFirst && !inSecond) {
|
|
151
|
+
return tree
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// If axis is specified, only resize splits matching that axis
|
|
155
|
+
const canResizeThis = !axis || tree.direction === axis
|
|
156
|
+
|
|
157
|
+
if (inFirst || inSecond) {
|
|
158
|
+
// First try to recurse deeper
|
|
159
|
+
if (inFirst) {
|
|
160
|
+
const newFirst = resizeSplit(tree.first, targetTabId, delta, axis)
|
|
161
|
+
if (newFirst !== tree.first) return { ...tree, first: newFirst }
|
|
162
|
+
} else {
|
|
163
|
+
const newSecond = resizeSplit(tree.second, targetTabId, delta, axis)
|
|
164
|
+
if (newSecond !== tree.second) return { ...tree, second: newSecond }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Couldn't resize deeper — resize this split if axis matches
|
|
168
|
+
if (canResizeThis) {
|
|
169
|
+
const newRatio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, tree.ratio + delta * RATIO_STEP))
|
|
170
|
+
if (newRatio === tree.ratio) return tree
|
|
171
|
+
return { ...tree, ratio: newRatio }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return tree
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function setSplitRatio(
|
|
179
|
+
tree: LayoutNode,
|
|
180
|
+
targetTabId: string,
|
|
181
|
+
ratio: number,
|
|
182
|
+
axis?: SplitDirection
|
|
183
|
+
): LayoutNode {
|
|
184
|
+
if (tree.type === 'leaf') {
|
|
185
|
+
return tree
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const inFirst = findLeaf(tree.first, targetTabId)
|
|
189
|
+
const inSecond = findLeaf(tree.second, targetTabId)
|
|
190
|
+
|
|
191
|
+
if (!inFirst && !inSecond) {
|
|
192
|
+
return tree
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const canResizeThis = !axis || tree.direction === axis
|
|
196
|
+
|
|
197
|
+
if (inFirst || inSecond) {
|
|
198
|
+
if (inFirst) {
|
|
199
|
+
const newFirst = setSplitRatio(tree.first, targetTabId, ratio, axis)
|
|
200
|
+
if (newFirst !== tree.first) return { ...tree, first: newFirst }
|
|
201
|
+
} else {
|
|
202
|
+
const newSecond = setSplitRatio(tree.second, targetTabId, ratio, axis)
|
|
203
|
+
if (newSecond !== tree.second) return { ...tree, second: newSecond }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (canResizeThis) {
|
|
207
|
+
const newRatio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, ratio))
|
|
208
|
+
if (newRatio === tree.ratio) return tree
|
|
209
|
+
return { ...tree, ratio: newRatio }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return tree
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function computePaneRects(tree: LayoutNode, bounds: PaneRect): Map<string, PaneRect> {
|
|
217
|
+
const result = new Map<string, PaneRect>()
|
|
218
|
+
|
|
219
|
+
if (tree.type === 'leaf') {
|
|
220
|
+
result.set(tree.tabId, bounds)
|
|
221
|
+
return result
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (tree.direction === 'vertical') {
|
|
225
|
+
// Split left/right
|
|
226
|
+
const firstCols = Math.max(1, Math.floor(bounds.cols * tree.ratio))
|
|
227
|
+
const secondCols = Math.max(1, bounds.cols - firstCols - 1) // -1 for separator
|
|
228
|
+
|
|
229
|
+
const firstBounds: PaneRect = { cols: firstCols, rows: bounds.rows, x: bounds.x, y: bounds.y }
|
|
230
|
+
const secondBounds: PaneRect = {
|
|
231
|
+
cols: secondCols,
|
|
232
|
+
rows: bounds.rows,
|
|
233
|
+
x: bounds.x + firstCols + 1,
|
|
234
|
+
y: bounds.y,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (const [id, rect] of computePaneRects(tree.first, firstBounds)) {
|
|
238
|
+
result.set(id, rect)
|
|
239
|
+
}
|
|
240
|
+
for (const [id, rect] of computePaneRects(tree.second, secondBounds)) {
|
|
241
|
+
result.set(id, rect)
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Split top/bottom
|
|
245
|
+
const firstRows = Math.max(1, Math.floor(bounds.rows * tree.ratio))
|
|
246
|
+
const secondRows = Math.max(1, bounds.rows - firstRows - 1) // -1 for separator
|
|
247
|
+
|
|
248
|
+
const firstBounds: PaneRect = { cols: bounds.cols, rows: firstRows, x: bounds.x, y: bounds.y }
|
|
249
|
+
const secondBounds: PaneRect = {
|
|
250
|
+
cols: bounds.cols,
|
|
251
|
+
rows: secondRows,
|
|
252
|
+
x: bounds.x,
|
|
253
|
+
y: bounds.y + firstRows + 1,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const [id, rect] of computePaneRects(tree.first, firstBounds)) {
|
|
257
|
+
result.set(id, rect)
|
|
258
|
+
}
|
|
259
|
+
for (const [id, rect] of computePaneRects(tree.second, secondBounds)) {
|
|
260
|
+
result.set(id, rect)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return result
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
type NavDirection = 'left' | 'right' | 'up' | 'down'
|
|
268
|
+
|
|
269
|
+
function firstLeafId(node: LayoutNode): string {
|
|
270
|
+
if (node.type === 'leaf') return node.tabId
|
|
271
|
+
return firstLeafId(node.first)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function lastLeafId(node: LayoutNode): string {
|
|
275
|
+
if (node.type === 'leaf') return node.tabId
|
|
276
|
+
return lastLeafId(node.second)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getAdjacentLeaf(
|
|
280
|
+
tree: LayoutNode,
|
|
281
|
+
fromTabId: string,
|
|
282
|
+
direction: NavDirection
|
|
283
|
+
): string | null {
|
|
284
|
+
if (tree.type === 'leaf') {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const inFirst = findLeaf(tree.first, fromTabId)
|
|
289
|
+
const inSecond = findLeaf(tree.second, fromTabId)
|
|
290
|
+
|
|
291
|
+
if (!inFirst && !inSecond) {
|
|
292
|
+
return null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check if the split direction matches the navigation direction
|
|
296
|
+
const isMatchingDirection =
|
|
297
|
+
(tree.direction === 'vertical' && (direction === 'left' || direction === 'right')) ||
|
|
298
|
+
(tree.direction === 'horizontal' && (direction === 'up' || direction === 'down'))
|
|
299
|
+
|
|
300
|
+
if (isMatchingDirection) {
|
|
301
|
+
const goingToSecond = direction === 'right' || direction === 'down'
|
|
302
|
+
|
|
303
|
+
if (inFirst && goingToSecond) {
|
|
304
|
+
// Try to recurse in first child first
|
|
305
|
+
const deeper = getAdjacentLeaf(tree.first, fromTabId, direction)
|
|
306
|
+
if (deeper) return deeper
|
|
307
|
+
// Cross boundary: go to first leaf of second child
|
|
308
|
+
return firstLeafId(tree.second)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (inSecond && !goingToSecond) {
|
|
312
|
+
const deeper = getAdjacentLeaf(tree.second, fromTabId, direction)
|
|
313
|
+
if (deeper) return deeper
|
|
314
|
+
return lastLeafId(tree.first)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Direction doesn't match this split — recurse into the branch containing the tab
|
|
319
|
+
if (inFirst) {
|
|
320
|
+
return getAdjacentLeaf(tree.first, fromTabId, direction)
|
|
321
|
+
}
|
|
322
|
+
return getAdjacentLeaf(tree.second, fromTabId, direction)
|
|
323
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { AppAction, AppState, GitFileEntry, GitPanelState } from '../types'
|
|
2
|
+
|
|
3
|
+
export const GIT_PANEL_MIN_RATIO = 0.2
|
|
4
|
+
export const GIT_PANEL_MAX_RATIO = 0.8
|
|
5
|
+
|
|
6
|
+
function clampRatio(value: number): number {
|
|
7
|
+
return Math.max(GIT_PANEL_MIN_RATIO, Math.min(GIT_PANEL_MAX_RATIO, value))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function emptyGitPanel(): GitPanelState {
|
|
11
|
+
return {
|
|
12
|
+
ahead: 0,
|
|
13
|
+
behind: 0,
|
|
14
|
+
branch: null,
|
|
15
|
+
error: null,
|
|
16
|
+
files: [],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
21
|
+
if (a.length !== b.length) return false
|
|
22
|
+
for (let i = 0; i < a.length; i++) {
|
|
23
|
+
const x = a[i] as GitFileEntry
|
|
24
|
+
const y = b[i] as GitFileEntry
|
|
25
|
+
if (
|
|
26
|
+
x.path !== y.path ||
|
|
27
|
+
x.status !== y.status ||
|
|
28
|
+
x.section !== y.section ||
|
|
29
|
+
x.added !== y.added ||
|
|
30
|
+
x.removed !== y.removed ||
|
|
31
|
+
x.renamedFrom !== y.renamedFrom
|
|
32
|
+
) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function reduceGitPanelState(state: AppState, action: AppAction): AppState | null {
|
|
40
|
+
switch (action.type) {
|
|
41
|
+
case 'toggle-git-panel': {
|
|
42
|
+
const nextGitVisible = !state.sidebar.gitPanelVisible
|
|
43
|
+
const nextVisible = state.sidebar.visible || nextGitVisible
|
|
44
|
+
return {
|
|
45
|
+
...state,
|
|
46
|
+
sidebar: { ...state.sidebar, gitPanelVisible: nextGitVisible, visible: nextVisible },
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
case 'resize-git-panel': {
|
|
50
|
+
const nextRatio = clampRatio(state.sidebar.gitPanelRatio + action.delta)
|
|
51
|
+
if (nextRatio === state.sidebar.gitPanelRatio) return state
|
|
52
|
+
return { ...state, sidebar: { ...state.sidebar, gitPanelRatio: nextRatio } }
|
|
53
|
+
}
|
|
54
|
+
case 'git-refresh-success': {
|
|
55
|
+
const prev = state.gitPanel
|
|
56
|
+
const next = action.payload
|
|
57
|
+
if (
|
|
58
|
+
prev.branch === next.branch &&
|
|
59
|
+
prev.ahead === next.ahead &&
|
|
60
|
+
prev.behind === next.behind &&
|
|
61
|
+
prev.error === null &&
|
|
62
|
+
sameFiles(prev.files, next.files)
|
|
63
|
+
) {
|
|
64
|
+
return state
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...state,
|
|
68
|
+
gitPanel: {
|
|
69
|
+
...prev,
|
|
70
|
+
ahead: next.ahead,
|
|
71
|
+
behind: next.behind,
|
|
72
|
+
branch: next.branch,
|
|
73
|
+
error: null,
|
|
74
|
+
files: next.files,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
case 'git-refresh-error': {
|
|
79
|
+
const prev = state.gitPanel
|
|
80
|
+
if (prev.error === action.kind && prev.files.length === 0) return state
|
|
81
|
+
return {
|
|
82
|
+
...state,
|
|
83
|
+
gitPanel: { ...prev, error: action.kind, files: [] },
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
case 'git-panel-reset': {
|
|
87
|
+
const prev = state.gitPanel
|
|
88
|
+
if (
|
|
89
|
+
prev.branch === null &&
|
|
90
|
+
prev.error === null &&
|
|
91
|
+
prev.files.length === 0 &&
|
|
92
|
+
prev.ahead === 0 &&
|
|
93
|
+
prev.behind === 0
|
|
94
|
+
) {
|
|
95
|
+
return state
|
|
96
|
+
}
|
|
97
|
+
return { ...state, gitPanel: emptyGitPanel() }
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|