@brimveyn/aimux 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -176
- package/package.json +7 -3
- package/src/app-runtime/use-terminal-resize.ts +112 -32
- package/src/app.tsx +20 -3
- package/src/config.ts +53 -17
- package/src/daemon/session-manager.ts +11 -4
- package/src/daemon/session-registry.ts +15 -4
- package/src/index.tsx +6 -1
- package/src/input/keymap/build-handlers.ts +1 -0
- package/src/input/keymap/help-entries.ts +44 -0
- package/src/input/keymap/keymap-ref.ts +11 -0
- package/src/input/keymap/sequence-resolver.ts +35 -2
- package/src/input/keymap/trie.ts +1 -0
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/handlers/shared.ts +0 -16
- package/src/input/modes/transitions.ts +4 -4
- package/src/input/modes/types.ts +1 -1
- package/src/platform/daemon-control.ts +0 -8
- package/src/pty/pty-manager.ts +127 -11
- package/src/restart-terminal-manager.ts +44 -0
- package/src/session-backend/local-session-backend.ts +15 -4
- package/src/session-backend/remote-session-backend.ts +13 -2
- package/src/session-backend/types.ts +13 -2
- package/src/state/reducers/git-panel-state.ts +35 -8
- package/src/state/reducers/modal-state.ts +48 -3
- package/src/state/selectors.ts +1 -5
- package/src/state/session-persistence.ts +1 -20
- package/src/state/store.ts +38 -5
- package/src/state/types.ts +27 -13
- package/src/state/validation.ts +0 -2
- package/src/state/workspace-save.ts +6 -2
- package/src/ui/components/create-session-modal.tsx +5 -3
- package/src/ui/components/git-commit-modal.tsx +1 -3
- package/src/ui/components/git-pane-widget.tsx +46 -0
- package/src/ui/components/git-panel.tsx +72 -25
- package/src/ui/components/help-modal.tsx +156 -42
- package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
- package/src/ui/components/modal-shell.tsx +15 -3
- package/src/ui/components/new-tab-modal.tsx +9 -10
- package/src/ui/components/pending-chord-overlay.tsx +2 -1
- package/src/ui/components/session-name-modal.tsx +1 -3
- package/src/ui/components/session-picker-modal.tsx +1 -3
- package/src/ui/components/sidebar.tsx +24 -50
- package/src/ui/components/snippet-editor-modal.tsx +1 -3
- package/src/ui/components/snippet-picker-modal.tsx +1 -3
- package/src/ui/components/status-bar.tsx +0 -4
- package/src/ui/components/terminal-pane.tsx +17 -12
- package/src/ui/components/theme-picker-modal.tsx +6 -3
- package/src/ui/components/update-available-modal.tsx +7 -4
- package/src/ui/keymap-context.ts +1 -6
- package/src/ui/root.tsx +29 -1
- package/src/ui/status-bar-model.ts +0 -5
- package/src/ui/directory-search.ts +0 -1
package/src/pty/pty-manager.ts
CHANGED
|
@@ -79,8 +79,98 @@ function getTerminalModes(emulator: XTerm, alternateScrollMode: boolean): Termin
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function envInt(name: string, fallback: number): number {
|
|
83
|
+
const raw = process.env[name]
|
|
84
|
+
if (raw === undefined) return fallback
|
|
85
|
+
const parsed = Number(raw)
|
|
86
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const RENDER_COALESCE_MS = 16
|
|
90
|
+
const DATA_DEBOUNCE_MS = envInt('AIMUX_RENDER_DEBOUNCE_MS', 32)
|
|
91
|
+
const BURST_MAX_MS = envInt('AIMUX_RENDER_BURST_MS', 500)
|
|
92
|
+
|
|
82
93
|
export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
83
94
|
private sessions = new Map<string, SessionHandle>()
|
|
95
|
+
private pendingFlushes = new Map<string, ReturnType<typeof setTimeout>>()
|
|
96
|
+
private pendingBurstCaps = new Map<string, ReturnType<typeof setTimeout>>()
|
|
97
|
+
|
|
98
|
+
private clearTimers(tabId: string): void {
|
|
99
|
+
const flush = this.pendingFlushes.get(tabId)
|
|
100
|
+
if (flush) {
|
|
101
|
+
clearTimeout(flush)
|
|
102
|
+
this.pendingFlushes.delete(tabId)
|
|
103
|
+
}
|
|
104
|
+
const burst = this.pendingBurstCaps.get(tabId)
|
|
105
|
+
if (burst) {
|
|
106
|
+
clearTimeout(burst)
|
|
107
|
+
this.pendingBurstCaps.delete(tabId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private scheduleRender(session: SessionHandle): void {
|
|
112
|
+
if (this.pendingFlushes.has(session.tabId)) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
this.pendingFlushes.delete(session.tabId)
|
|
117
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const burst = this.pendingBurstCaps.get(session.tabId)
|
|
121
|
+
if (burst) {
|
|
122
|
+
clearTimeout(burst)
|
|
123
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
124
|
+
}
|
|
125
|
+
this.emitRenderIfChanged(session)
|
|
126
|
+
}, RENDER_COALESCE_MS)
|
|
127
|
+
this.pendingFlushes.set(session.tabId, timer)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private scheduleDataRender(session: SessionHandle): void {
|
|
131
|
+
const existingFlush = this.pendingFlushes.get(session.tabId)
|
|
132
|
+
if (existingFlush) {
|
|
133
|
+
clearTimeout(existingFlush)
|
|
134
|
+
}
|
|
135
|
+
const flushTimer = setTimeout(() => {
|
|
136
|
+
this.pendingFlushes.delete(session.tabId)
|
|
137
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (session.pendingWrites > 0) {
|
|
141
|
+
this.scheduleDataRender(session)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const burst = this.pendingBurstCaps.get(session.tabId)
|
|
145
|
+
if (burst) {
|
|
146
|
+
clearTimeout(burst)
|
|
147
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
148
|
+
}
|
|
149
|
+
this.emitRenderIfChanged(session)
|
|
150
|
+
}, DATA_DEBOUNCE_MS)
|
|
151
|
+
this.pendingFlushes.set(session.tabId, flushTimer)
|
|
152
|
+
|
|
153
|
+
if (!this.pendingBurstCaps.has(session.tabId)) {
|
|
154
|
+
const burstTimer = setTimeout(() => {
|
|
155
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
156
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const flush = this.pendingFlushes.get(session.tabId)
|
|
160
|
+
if (flush) {
|
|
161
|
+
clearTimeout(flush)
|
|
162
|
+
this.pendingFlushes.delete(session.tabId)
|
|
163
|
+
}
|
|
164
|
+
this.emitRenderIfChanged(session)
|
|
165
|
+
}, BURST_MAX_MS)
|
|
166
|
+
this.pendingBurstCaps.set(session.tabId, burstTimer)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private flushRenderNow(session: SessionHandle): void {
|
|
171
|
+
this.clearTimers(session.tabId)
|
|
172
|
+
this.emitRenderIfChanged(session)
|
|
173
|
+
}
|
|
84
174
|
|
|
85
175
|
private emitRenderIfChanged(session: SessionHandle): void {
|
|
86
176
|
const nextSnapshot = snapshotTerminal(session.emulator, session.cursorVisible)
|
|
@@ -116,7 +206,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
116
206
|
}
|
|
117
207
|
|
|
118
208
|
this.sessions.delete(session.tabId)
|
|
119
|
-
this.
|
|
209
|
+
this.flushRenderNow(session)
|
|
120
210
|
session.emulator.dispose()
|
|
121
211
|
logDebug('ptyManager.finalize', { exitCode, tabId: session.tabId })
|
|
122
212
|
this.emit('exit', session.tabId, exitCode)
|
|
@@ -188,9 +278,10 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
188
278
|
session.cursorVisible = trackedModes.cursorVisible
|
|
189
279
|
session.pendingModeSequence = trackedModes.pendingSequence
|
|
190
280
|
session.pendingWrites += 1
|
|
281
|
+
this.scheduleDataRender(session)
|
|
191
282
|
emulator.write(data, () => {
|
|
192
283
|
session.pendingWrites -= 1
|
|
193
|
-
this.
|
|
284
|
+
this.scheduleDataRender(session)
|
|
194
285
|
|
|
195
286
|
if (session.pendingWrites === 0 && session.pendingExitCode !== null) {
|
|
196
287
|
this.finalizeSession(session, session.pendingExitCode)
|
|
@@ -215,7 +306,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
215
306
|
|
|
216
307
|
this.sessions.set(options.tabId, session)
|
|
217
308
|
logDebug('ptyManager.create.success', { tabId: options.tabId })
|
|
218
|
-
this.
|
|
309
|
+
this.scheduleRender(session)
|
|
219
310
|
} catch (error) {
|
|
220
311
|
const message = error instanceof Error ? error.message : String(error)
|
|
221
312
|
logDebug('ptyManager.create.error', { error: message, tabId: options.tabId })
|
|
@@ -224,7 +315,9 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
224
315
|
}
|
|
225
316
|
|
|
226
317
|
write(tabId: string, input: string): void {
|
|
227
|
-
this.sessions.get(tabId)
|
|
318
|
+
const session = this.sessions.get(tabId)
|
|
319
|
+
if (!session) return
|
|
320
|
+
session.pty.write(input)
|
|
228
321
|
}
|
|
229
322
|
|
|
230
323
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
@@ -234,7 +327,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
234
327
|
}
|
|
235
328
|
|
|
236
329
|
session.emulator.scrollLines(deltaLines)
|
|
237
|
-
this.
|
|
330
|
+
this.scheduleRender(session)
|
|
238
331
|
}
|
|
239
332
|
|
|
240
333
|
scrollViewportToBottom(tabId: string): void {
|
|
@@ -244,7 +337,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
244
337
|
}
|
|
245
338
|
|
|
246
339
|
session.emulator.scrollToBottom()
|
|
247
|
-
this.
|
|
340
|
+
this.scheduleRender(session)
|
|
248
341
|
}
|
|
249
342
|
|
|
250
343
|
private applyScrollIntent(session: SessionHandle, intent: ScrollIntent | undefined): void {
|
|
@@ -262,7 +355,12 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
262
355
|
session.emulator.scrollToLine(Math.max(0, intent.absoluteLine))
|
|
263
356
|
}
|
|
264
357
|
|
|
265
|
-
resizeAll(
|
|
358
|
+
resizeAll(
|
|
359
|
+
cols: number,
|
|
360
|
+
rows: number,
|
|
361
|
+
intents?: Map<string, ScrollIntent>,
|
|
362
|
+
options?: { sync?: boolean }
|
|
363
|
+
): void {
|
|
266
364
|
const safeCols = Math.max(20, cols)
|
|
267
365
|
const safeRows = Math.max(8, rows)
|
|
268
366
|
|
|
@@ -270,11 +368,21 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
270
368
|
session.pty.resize(safeCols, safeRows)
|
|
271
369
|
session.emulator.resize(safeCols, safeRows)
|
|
272
370
|
this.applyScrollIntent(session, intents?.get(session.tabId))
|
|
273
|
-
|
|
371
|
+
if (options?.sync) {
|
|
372
|
+
this.flushRenderNow(session)
|
|
373
|
+
} else {
|
|
374
|
+
this.scheduleRender(session)
|
|
375
|
+
}
|
|
274
376
|
}
|
|
275
377
|
}
|
|
276
378
|
|
|
277
|
-
resizeSession(
|
|
379
|
+
resizeSession(
|
|
380
|
+
tabId: string,
|
|
381
|
+
cols: number,
|
|
382
|
+
rows: number,
|
|
383
|
+
intent?: ScrollIntent,
|
|
384
|
+
options?: { sync?: boolean }
|
|
385
|
+
): void {
|
|
278
386
|
const session = this.sessions.get(tabId)
|
|
279
387
|
if (!session) {
|
|
280
388
|
return
|
|
@@ -284,7 +392,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
284
392
|
session.pty.resize(safeCols, safeRows)
|
|
285
393
|
session.emulator.resize(safeCols, safeRows)
|
|
286
394
|
this.applyScrollIntent(session, intent)
|
|
287
|
-
|
|
395
|
+
if (options?.sync) {
|
|
396
|
+
this.flushRenderNow(session)
|
|
397
|
+
} else {
|
|
398
|
+
this.scheduleRender(session)
|
|
399
|
+
}
|
|
288
400
|
}
|
|
289
401
|
|
|
290
402
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
@@ -293,7 +405,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
293
405
|
return
|
|
294
406
|
}
|
|
295
407
|
this.applyScrollIntent(session, intent)
|
|
296
|
-
this.
|
|
408
|
+
this.scheduleRender(session)
|
|
297
409
|
}
|
|
298
410
|
|
|
299
411
|
disposeSession(tabId: string): void {
|
|
@@ -302,12 +414,16 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
302
414
|
return
|
|
303
415
|
}
|
|
304
416
|
|
|
417
|
+
this.clearTimers(tabId)
|
|
305
418
|
this.sessions.delete(tabId)
|
|
306
419
|
session.pty.kill()
|
|
307
420
|
session.emulator.dispose()
|
|
308
421
|
}
|
|
309
422
|
|
|
310
423
|
disposeAll(): void {
|
|
424
|
+
for (const tabId of this.sessions.keys()) {
|
|
425
|
+
this.clearTimers(tabId)
|
|
426
|
+
}
|
|
311
427
|
for (const session of this.sessions.values()) {
|
|
312
428
|
session.pty.kill()
|
|
313
429
|
session.emulator.dispose()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTerminalManagerSocketPath,
|
|
3
|
+
removeTerminalManagerSocketIfExists,
|
|
4
|
+
} from './daemon/runtime-paths'
|
|
5
|
+
import {
|
|
6
|
+
findIpcDaemonPid,
|
|
7
|
+
findTerminalManagerPid,
|
|
8
|
+
killProcess,
|
|
9
|
+
spawnDetachedTerminalManager,
|
|
10
|
+
} from './platform/daemon-control'
|
|
11
|
+
|
|
12
|
+
export async function runRestartTerminalManager(): Promise<number> {
|
|
13
|
+
const socketPath = getTerminalManagerSocketPath()
|
|
14
|
+
|
|
15
|
+
const daemonPid = await findIpcDaemonPid()
|
|
16
|
+
if (daemonPid !== null) {
|
|
17
|
+
process.stdout.write(`Stopping IPC daemon (pid ${daemonPid})...\n`)
|
|
18
|
+
await killProcess(daemonPid)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const managerPid = await findTerminalManagerPid()
|
|
22
|
+
if (managerPid !== null) {
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
`WARNING: killing terminal-manager (pid ${managerPid}) will kill live sessions.\n`
|
|
25
|
+
)
|
|
26
|
+
await killProcess(managerPid)
|
|
27
|
+
process.stdout.write('Terminal-manager stopped.\n')
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write('No running terminal-manager found.\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
removeTerminalManagerSocketIfExists()
|
|
33
|
+
|
|
34
|
+
process.stdout.write('Starting terminal-manager...\n')
|
|
35
|
+
const ok = await spawnDetachedTerminalManager()
|
|
36
|
+
|
|
37
|
+
if (ok) {
|
|
38
|
+
process.stdout.write(`Terminal-manager started on ${socketPath}.\n`)
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stderr.write('Failed to start terminal-manager.\n')
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
@@ -151,18 +151,29 @@ export class LocalSessionBackend
|
|
|
151
151
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
resizeAll(
|
|
154
|
+
resizeAll(
|
|
155
|
+
cols: number,
|
|
156
|
+
rows: number,
|
|
157
|
+
intents?: Map<string, ScrollIntent>,
|
|
158
|
+
options?: { sync?: boolean }
|
|
159
|
+
): void {
|
|
155
160
|
if (!this.currentSessionId) {
|
|
156
161
|
return
|
|
157
162
|
}
|
|
158
|
-
this.sessionManager.resize(this.currentSessionId, cols, rows, intents)
|
|
163
|
+
this.sessionManager.resize(this.currentSessionId, cols, rows, intents, options)
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
resizeTab(
|
|
166
|
+
resizeTab(
|
|
167
|
+
tabId: string,
|
|
168
|
+
cols: number,
|
|
169
|
+
rows: number,
|
|
170
|
+
intent?: ScrollIntent,
|
|
171
|
+
options?: { sync?: boolean }
|
|
172
|
+
): void {
|
|
162
173
|
if (!this.currentSessionId) {
|
|
163
174
|
return
|
|
164
175
|
}
|
|
165
|
-
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent)
|
|
176
|
+
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent, options)
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
disposeSession(tabId: string): void {
|
|
@@ -398,7 +398,12 @@ export class RemoteSessionBackend
|
|
|
398
398
|
}).catch((error) => this.reportCommandError('setActiveTab', error))
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
resizeAll(
|
|
401
|
+
resizeAll(
|
|
402
|
+
cols: number,
|
|
403
|
+
rows: number,
|
|
404
|
+
intents?: Map<string, ScrollIntent>,
|
|
405
|
+
_options?: { sync?: boolean }
|
|
406
|
+
): void {
|
|
402
407
|
if (!this.attached) {
|
|
403
408
|
logDebug('backend.remote.skipResizeBeforeAttach', { cols, rows })
|
|
404
409
|
return
|
|
@@ -412,7 +417,13 @@ export class RemoteSessionBackend
|
|
|
412
417
|
}).catch((error) => this.reportCommandError('resizeClient', error))
|
|
413
418
|
}
|
|
414
419
|
|
|
415
|
-
resizeTab(
|
|
420
|
+
resizeTab(
|
|
421
|
+
tabId: string,
|
|
422
|
+
cols: number,
|
|
423
|
+
rows: number,
|
|
424
|
+
intent?: ScrollIntent,
|
|
425
|
+
_options?: { sync?: boolean }
|
|
426
|
+
): void {
|
|
416
427
|
if (!this.attached) {
|
|
417
428
|
return
|
|
418
429
|
}
|
|
@@ -42,8 +42,19 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
|
|
42
42
|
scrollViewportToBottom(tabId: string): void
|
|
43
43
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void
|
|
44
44
|
setActiveTab(tabId: string | null): void
|
|
45
|
-
resizeAll(
|
|
46
|
-
|
|
45
|
+
resizeAll(
|
|
46
|
+
cols: number,
|
|
47
|
+
rows: number,
|
|
48
|
+
intents?: Map<string, ScrollIntent>,
|
|
49
|
+
options?: { sync?: boolean }
|
|
50
|
+
): void
|
|
51
|
+
resizeTab(
|
|
52
|
+
tabId: string,
|
|
53
|
+
cols: number,
|
|
54
|
+
rows: number,
|
|
55
|
+
intent?: ScrollIntent,
|
|
56
|
+
options?: { sync?: boolean }
|
|
57
|
+
): void
|
|
47
58
|
disposeSession(tabId: string): void
|
|
48
59
|
disposeAll(): void
|
|
49
60
|
destroy(keepSessions?: boolean): Promise<void> | void
|
|
@@ -49,18 +49,45 @@ function sameFiles(a: GitFileEntry[], b: GitFileEntry[]): boolean {
|
|
|
49
49
|
|
|
50
50
|
export function reduceGitPanelState(state: AppState, action: AppAction): AppState | null {
|
|
51
51
|
switch (action.type) {
|
|
52
|
-
case 'toggle-git-
|
|
53
|
-
const
|
|
54
|
-
const
|
|
52
|
+
case 'toggle-git-pane': {
|
|
53
|
+
const nextVisible = !state.gitPane.visible
|
|
54
|
+
const sidebarMustShow = state.gitPane.mode === 'embedded' && nextVisible
|
|
55
55
|
return {
|
|
56
56
|
...state,
|
|
57
|
-
|
|
57
|
+
gitPane: { ...state.gitPane, visible: nextVisible },
|
|
58
|
+
sidebar: sidebarMustShow ? { ...state.sidebar, visible: true } : state.sidebar,
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
case 'resize-git-
|
|
61
|
-
const nextRatio = clampRatio(state.
|
|
62
|
-
if (nextRatio === state.
|
|
63
|
-
return { ...state,
|
|
61
|
+
case 'resize-git-pane': {
|
|
62
|
+
const nextRatio = clampRatio(state.gitPane.ratio + action.delta)
|
|
63
|
+
if (nextRatio === state.gitPane.ratio) return state
|
|
64
|
+
return { ...state, gitPane: { ...state.gitPane, ratio: nextRatio } }
|
|
65
|
+
}
|
|
66
|
+
case 'set-git-pane-mode': {
|
|
67
|
+
if (state.gitPane.mode === action.mode) return state
|
|
68
|
+
const isEmbedded = action.mode === 'embedded'
|
|
69
|
+
const isValidEmbedded =
|
|
70
|
+
state.gitPane.position === 'top' || state.gitPane.position === 'bottom'
|
|
71
|
+
const isValidPane = state.gitPane.position === 'left' || state.gitPane.position === 'right'
|
|
72
|
+
let nextPosition: typeof state.gitPane.position
|
|
73
|
+
if (isEmbedded) {
|
|
74
|
+
nextPosition = isValidEmbedded ? state.gitPane.position : 'bottom'
|
|
75
|
+
} else {
|
|
76
|
+
nextPosition = isValidPane ? state.gitPane.position : 'left'
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...state,
|
|
80
|
+
gitPane: { ...state.gitPane, mode: action.mode, position: nextPosition },
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
case 'set-git-pane-position': {
|
|
84
|
+
const validForMode =
|
|
85
|
+
state.gitPane.mode === 'embedded'
|
|
86
|
+
? action.position === 'top' || action.position === 'bottom'
|
|
87
|
+
: action.position === 'left' || action.position === 'right'
|
|
88
|
+
if (!validForMode) return state
|
|
89
|
+
if (state.gitPane.position === action.position) return state
|
|
90
|
+
return { ...state, gitPane: { ...state.gitPane, position: action.position } }
|
|
64
91
|
}
|
|
65
92
|
case 'git-refresh-success': {
|
|
66
93
|
const prev = state.gitPanel
|
|
@@ -2,6 +2,8 @@ import { basename } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import type { AppAction, AppState } from '../types'
|
|
4
4
|
|
|
5
|
+
import { collectHelpEntries } from '../../input/keymap/help-entries'
|
|
6
|
+
import { getActiveKeymap } from '../../input/keymap/keymap-ref'
|
|
5
7
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
6
8
|
import { THEME_IDS } from '../../ui/themes'
|
|
7
9
|
import { filterSessions, filterSnippets } from '../selectors'
|
|
@@ -38,18 +40,22 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
38
40
|
type: 'new-tab',
|
|
39
41
|
},
|
|
40
42
|
}
|
|
41
|
-
case 'open-help-modal':
|
|
43
|
+
case 'open-help-modal': {
|
|
44
|
+
const keymap = getActiveKeymap()
|
|
45
|
+
const entryCount = keymap ? collectHelpEntries(keymap).length : 0
|
|
42
46
|
return {
|
|
43
47
|
...state,
|
|
44
48
|
focusMode: 'modal',
|
|
45
49
|
modal: {
|
|
46
50
|
cursorPos: 0,
|
|
47
51
|
editBuffer: null,
|
|
52
|
+
entryCount,
|
|
48
53
|
selectedIndex: 0,
|
|
49
54
|
sessionTargetId: null,
|
|
50
55
|
type: 'help',
|
|
51
56
|
},
|
|
52
57
|
}
|
|
58
|
+
}
|
|
53
59
|
case 'open-split-picker':
|
|
54
60
|
return {
|
|
55
61
|
...state,
|
|
@@ -200,12 +206,45 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
200
206
|
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
201
207
|
}
|
|
202
208
|
}
|
|
209
|
+
case 'set-help-entry-count': {
|
|
210
|
+
if (state.modal.type !== 'help') return state
|
|
211
|
+
if (state.modal.entryCount === action.count) {
|
|
212
|
+
// Still clamp selectedIndex in case the count shrank below it.
|
|
213
|
+
const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
|
|
214
|
+
if (clamped === state.modal.selectedIndex) return state
|
|
215
|
+
return { ...state, modal: { ...state.modal, selectedIndex: clamped } }
|
|
216
|
+
}
|
|
217
|
+
const clamped = Math.min(state.modal.selectedIndex, Math.max(0, action.count - 1))
|
|
218
|
+
return {
|
|
219
|
+
...state,
|
|
220
|
+
modal: { ...state.modal, entryCount: action.count, selectedIndex: clamped },
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
case 'begin-help-filter': {
|
|
224
|
+
if (state.modal.type !== 'help') {
|
|
225
|
+
return state
|
|
226
|
+
}
|
|
227
|
+
const buf = state.modal.editBuffer ?? ''
|
|
228
|
+
return {
|
|
229
|
+
...state,
|
|
230
|
+
focusMode: 'command-edit',
|
|
231
|
+
modal: { ...state.modal, cursorPos: buf.length, editBuffer: buf },
|
|
232
|
+
}
|
|
233
|
+
}
|
|
203
234
|
case 'close-modal': {
|
|
204
235
|
const nextFocus: AppState['focusMode'] =
|
|
205
236
|
state.modal.type === 'git-commit' ? 'git' : 'navigation'
|
|
206
237
|
return { ...state, focusMode: nextFocus, modal: emptyModal() }
|
|
207
238
|
}
|
|
208
239
|
case 'move-modal-selection': {
|
|
240
|
+
if (state.modal.type === 'help') {
|
|
241
|
+
const count = state.modal.entryCount
|
|
242
|
+
if (count <= 0) return state
|
|
243
|
+
const raw = state.modal.selectedIndex + action.delta
|
|
244
|
+
const nextIndex = ((raw % count) + count) % count
|
|
245
|
+
if (nextIndex === state.modal.selectedIndex) return state
|
|
246
|
+
return { ...state, modal: { ...state.modal, selectedIndex: nextIndex } }
|
|
247
|
+
}
|
|
209
248
|
if (
|
|
210
249
|
state.modal.type !== 'new-tab' &&
|
|
211
250
|
state.modal.type !== 'session-picker' &&
|
|
@@ -293,7 +332,9 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
293
332
|
nextCursor = cursor + action.char.length
|
|
294
333
|
}
|
|
295
334
|
const resetIndex =
|
|
296
|
-
state.modal.type === 'session-picker' ||
|
|
335
|
+
state.modal.type === 'session-picker' ||
|
|
336
|
+
state.modal.type === 'snippet-picker' ||
|
|
337
|
+
state.modal.type === 'help'
|
|
297
338
|
? 0
|
|
298
339
|
: state.modal.selectedIndex
|
|
299
340
|
return {
|
|
@@ -360,7 +401,11 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
360
401
|
if (state.modal.type === 'create-session' || state.modal.type === 'snippet-editor') {
|
|
361
402
|
return { ...state, focusMode: 'navigation', modal: emptyModal() }
|
|
362
403
|
}
|
|
363
|
-
if (
|
|
404
|
+
if (
|
|
405
|
+
state.modal.type === 'session-picker' ||
|
|
406
|
+
state.modal.type === 'snippet-picker' ||
|
|
407
|
+
state.modal.type === 'help'
|
|
408
|
+
) {
|
|
364
409
|
return {
|
|
365
410
|
...state,
|
|
366
411
|
focusMode: 'modal',
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionRecord, SnippetRecord } from './types'
|
|
2
2
|
|
|
3
3
|
export function filterSessions(sessions: SessionRecord[], filter: string | null): SessionRecord[] {
|
|
4
4
|
if (!filter) {
|
|
@@ -24,7 +24,3 @@ export function filterSnippets(snippets: SnippetRecord[], filter: string | null)
|
|
|
24
24
|
snippet.name.toLowerCase().includes(lower) || snippet.content.toLowerCase().includes(lower)
|
|
25
25
|
)
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
export function getActiveTab(state: AppState): TabSession | undefined {
|
|
29
|
-
return state.activeTabId ? state.tabs.find((tab) => tab.id === state.activeTabId) : undefined
|
|
30
|
-
}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
allLeafIds,
|
|
3
|
-
createGroupId,
|
|
4
|
-
createLeaf,
|
|
5
|
-
type LayoutNode,
|
|
6
|
-
pruneLayoutTree,
|
|
7
|
-
} from './layout-tree'
|
|
1
|
+
import { allLeafIds, createGroupId, type LayoutNode, pruneLayoutTree } from './layout-tree'
|
|
8
2
|
import {
|
|
9
3
|
type AppState,
|
|
10
4
|
DEFAULT_SCROLL_INTENT,
|
|
@@ -83,19 +77,6 @@ export function restoreTabsFromWorkspace(snapshot: WorkspaceSnapshotV1 | undefin
|
|
|
83
77
|
}))
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
export function restoreLayoutTree(
|
|
87
|
-
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
88
|
-
tabs: TabSession[]
|
|
89
|
-
): LayoutNode | null {
|
|
90
|
-
if (snapshot?.layoutTree) {
|
|
91
|
-
const validTabIds = new Set(tabs.map((t) => t.id))
|
|
92
|
-
const pruned = pruneLayoutTree(snapshot.layoutTree, validTabIds)
|
|
93
|
-
if (pruned) return pruned
|
|
94
|
-
}
|
|
95
|
-
// Fallback: single leaf for the first tab
|
|
96
|
-
return tabs[0] ? createLeaf(tabs[0].id) : null
|
|
97
|
-
}
|
|
98
|
-
|
|
99
80
|
export function restoreLayoutTrees(
|
|
100
81
|
snapshot: WorkspaceSnapshotV1 | undefined,
|
|
101
82
|
tabs: TabSession[]
|
package/src/state/store.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AppAction,
|
|
3
|
+
AppState,
|
|
4
|
+
GitPaneMode,
|
|
5
|
+
GitPanePosition,
|
|
6
|
+
GitPaneState,
|
|
7
|
+
SessionBarPosition,
|
|
8
|
+
SessionRecord,
|
|
9
|
+
SnippetRecord,
|
|
10
|
+
} from './types'
|
|
2
11
|
|
|
3
12
|
import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
|
|
4
13
|
import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
|
|
@@ -15,12 +24,27 @@ const DEFAULT_TERMINAL_COLS = 80
|
|
|
15
24
|
const DEFAULT_TERMINAL_ROWS = 24
|
|
16
25
|
|
|
17
26
|
export interface InitialStateOverrides {
|
|
18
|
-
|
|
19
|
-
gitPanelRatio?: number
|
|
27
|
+
gitPane?: Partial<GitPaneState>
|
|
20
28
|
sessionBarVisible?: boolean
|
|
21
29
|
sessionBarPosition?: SessionBarPosition
|
|
22
30
|
}
|
|
23
31
|
|
|
32
|
+
const DEFAULT_GIT_PANE: GitPaneState = {
|
|
33
|
+
diffCount: { enabled: true },
|
|
34
|
+
mode: 'embedded',
|
|
35
|
+
path: { enabled: true },
|
|
36
|
+
position: 'bottom',
|
|
37
|
+
ratio: 0.5,
|
|
38
|
+
visible: true,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveGitPanePosition(mode: GitPaneMode, position: GitPanePosition): GitPanePosition {
|
|
42
|
+
if (mode === 'embedded') {
|
|
43
|
+
return position === 'top' || position === 'bottom' ? position : 'bottom'
|
|
44
|
+
}
|
|
45
|
+
return position === 'left' || position === 'right' ? position : 'left'
|
|
46
|
+
}
|
|
47
|
+
|
|
24
48
|
export function createInitialState(
|
|
25
49
|
customCommands: Record<string, string> = {},
|
|
26
50
|
sessions: SessionRecord[] = [],
|
|
@@ -28,12 +52,23 @@ export function createInitialState(
|
|
|
28
52
|
showSessionPicker = false,
|
|
29
53
|
overrides: InitialStateOverrides = {}
|
|
30
54
|
): AppState {
|
|
55
|
+
const gitPaneMode = overrides.gitPane?.mode ?? DEFAULT_GIT_PANE.mode
|
|
56
|
+
const gitPanePosition = resolveGitPanePosition(
|
|
57
|
+
gitPaneMode,
|
|
58
|
+
overrides.gitPane?.position ?? DEFAULT_GIT_PANE.position
|
|
59
|
+
)
|
|
31
60
|
return {
|
|
32
61
|
activeTabId: null,
|
|
33
62
|
currentSessionId: null,
|
|
34
63
|
customCommands,
|
|
35
64
|
focusMode: showSessionPicker ? 'modal' : 'navigation',
|
|
36
65
|
gitMode: emptyGitMode(),
|
|
66
|
+
gitPane: {
|
|
67
|
+
...DEFAULT_GIT_PANE,
|
|
68
|
+
...overrides.gitPane,
|
|
69
|
+
mode: gitPaneMode,
|
|
70
|
+
position: gitPanePosition,
|
|
71
|
+
},
|
|
37
72
|
gitPanel: emptyGitPanel(),
|
|
38
73
|
layout: {
|
|
39
74
|
terminalCols: DEFAULT_TERMINAL_COLS,
|
|
@@ -51,8 +86,6 @@ export function createInitialState(
|
|
|
51
86
|
sessions,
|
|
52
87
|
sessionsBusy: {},
|
|
53
88
|
sidebar: {
|
|
54
|
-
gitPanelRatio: overrides.gitPanelRatio ?? 0.5,
|
|
55
|
-
gitPanelVisible: overrides.gitPanelVisible ?? true,
|
|
56
89
|
maxWidth: DEFAULT_SIDEBAR_MAX_WIDTH,
|
|
57
90
|
minWidth: DEFAULT_SIDEBAR_MIN_WIDTH,
|
|
58
91
|
visible: true,
|