@brimveyn/aimux 1.12.6 → 1.13.2
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 +3 -0
- package/package.json +4 -3
- package/src/app-runtime/auto-commit-driver.ts +8 -4
- package/src/app-runtime/auto-commit-ref.ts +1 -1
- package/src/app-runtime/backend-attach-runtime.ts +29 -13
- package/src/app-runtime/backend-runtime-events.ts +1 -1
- package/src/app-runtime/selection-scroll.ts +2 -2
- package/src/app-runtime/session-actions.ts +16 -4
- package/src/app-runtime/side-effects.ts +563 -85
- package/src/app-runtime/snippet-actions.ts +3 -3
- package/src/app-runtime/use-auto-commit-driver.ts +7 -4
- package/src/app-runtime/use-backend-runtime.ts +3 -3
- package/src/app-runtime/use-directory-search.ts +7 -5
- package/src/app-runtime/use-mouse-handlers.ts +11 -6
- package/src/app-runtime/use-pane-size-report.ts +1 -1
- package/src/app-runtime/use-renderer-bindings.ts +6 -5
- package/src/app.tsx +12 -7
- package/src/auto-commit/headless-commands.ts +4 -6
- package/src/daemon/daemon.ts +21 -11
- package/src/daemon/runtime-paths.ts +1 -1
- package/src/daemon/session-manager.ts +1 -1
- package/src/daemon/session-registry.ts +10 -3
- package/src/diff-parser/parse-patch-files.ts +18 -18
- package/src/git/command-queue.ts +19 -3
- package/src/git/divergence.ts +46 -0
- package/src/git/git-diff.ts +12 -5
- package/src/git/git-poller.ts +33 -11
- package/src/git/git-status.ts +14 -3
- package/src/git/move-worktree.ts +96 -0
- package/src/git/use-repo-discovery.ts +2 -1
- package/src/git/worktree-divergence-poller.ts +59 -0
- package/src/git/worktree.ts +99 -0
- package/src/index.tsx +1 -1
- package/src/input/keymap/describe-bindings.ts +27 -9
- package/src/input/keymap/key-chord.ts +4 -4
- package/src/input/keymap/key-format.ts +2 -2
- package/src/input/keymap/sequence-resolver.ts +1 -1
- package/src/input/keymap/trie.ts +1 -1
- package/src/input/modes/bridge.ts +7 -0
- package/src/input/modes/transitions.ts +2 -1
- package/src/input/modes/types.ts +11 -1
- package/src/input/raw-input-handler.ts +1 -1
- package/src/input/terminal-text-extraction.ts +9 -5
- package/src/integrations/claude-syntax-overlay.ts +8 -8
- package/src/integrations/claude-theme-sync.ts +12 -12
- package/src/ipc/protocol.ts +3 -3
- package/src/platform/clipboard.ts +4 -2
- package/src/platform/worktree-deps.ts +22 -0
- package/src/platform/worktree-paths.ts +67 -0
- package/src/profile-paths.ts +3 -3
- package/src/pty/assistant-status-detection-loop.ts +2 -2
- package/src/pty/assistant-status-detector.ts +15 -6
- package/src/pty/command-registry.ts +2 -1
- package/src/pty/pty-manager.ts +1 -1
- package/src/services/ai-usage/adapters/claude.ts +5 -5
- package/src/services/ai-usage/adapters/codex.ts +8 -8
- package/src/services/ai-usage/cache.ts +2 -2
- package/src/services/ai-usage/pace.ts +3 -6
- package/src/services/ai-usage/provider.ts +1 -1
- package/src/session-backend/bootstrap.ts +2 -2
- package/src/session-backend/local-session-backend.ts +11 -11
- package/src/session-backend/remote-session-backend.ts +60 -45
- package/src/session-backend/types.ts +2 -2
- package/src/snippets/expand-variables.ts +7 -7
- package/src/snippets/run-shell-var.ts +1 -1
- package/src/snippets/trigger-detector.ts +1 -1
- package/src/state/ai-usage-store.ts +1 -1
- package/src/state/git-tree.ts +8 -4
- package/src/state/layout-tree.ts +8 -5
- package/src/state/reducers/git-mode-state.ts +44 -14
- package/src/state/reducers/git-panel-state.ts +38 -2
- package/src/state/reducers/modal-state.ts +355 -21
- package/src/state/reducers/session-state.ts +60 -1
- package/src/state/reducers/tab-state.ts +151 -85
- package/src/state/selectors.ts +6 -4
- package/src/state/session-catalog.ts +20 -5
- package/src/state/session-persistence.ts +8 -4
- package/src/state/session-worktrees.ts +227 -0
- package/src/state/store.ts +1 -0
- package/src/state/toast-store.ts +95 -0
- package/src/state/types.ts +92 -13
- package/src/state/validation.ts +25 -2
- package/src/terminal-manager/manager-client.ts +15 -13
- package/src/ui/components/git/diff-renderer/fold-strip.tsx +32 -24
- package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
- package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
- package/src/ui/components/git/diff-renderer/split-view.tsx +39 -11
- package/src/ui/components/git/diff-renderer/stacked-view.tsx +29 -7
- package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +28 -11
- package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +10 -8
- package/src/ui/components/git/git-panel.tsx +129 -83
- package/src/ui/components/git/git-view.tsx +96 -28
- package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
- package/src/ui/components/git/image-diff/terminal-image-pane.tsx +14 -13
- package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
- package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
- package/src/ui/components/layout/session-bar.tsx +135 -117
- package/src/ui/components/layout/sidebar/sidebar.tsx +244 -82
- package/src/ui/components/layout/sidebar/tab-item.tsx +68 -41
- package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
- package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
- package/src/ui/components/layout/split-layout.tsx +27 -20
- package/src/ui/components/layout/terminal-pane.tsx +163 -120
- package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
- package/src/ui/components/modals/app/help-modal.tsx +29 -20
- package/src/ui/components/modals/git/git-commit-modal.tsx +17 -10
- package/src/ui/components/modals/sessions/create-session-modal.tsx +18 -10
- package/src/ui/components/modals/sessions/session-picker-modal.tsx +36 -28
- package/src/ui/components/modals/shared/form.tsx +6 -6
- package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
- package/src/ui/components/modals/shared/picker.tsx +71 -32
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +50 -29
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +209 -25
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +24 -15
- package/src/ui/components/modals/worktree/worktree-move-modal.tsx +111 -0
- package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +14 -9
- package/src/ui/components/overlays/context-menu/context-menu-box.tsx +20 -11
- package/src/ui/components/overlays/context-menu/context-menu-overlay.tsx +69 -34
- package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
- package/src/ui/components/overlays/toast/toast-position.ts +45 -0
- package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
- package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
- package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
- package/src/ui/components/primitives/input-field.tsx +1 -1
- package/src/ui/components/primitives/list-item.tsx +23 -7
- package/src/ui/filter-themes.ts +1 -1
- package/src/ui/root.tsx +121 -38
- package/src/ui/session-ordering.ts +2 -2
- package/src/ui/status-bar-model.ts +14 -7
- package/src/ui/terminal-graphics/capabilities.ts +1 -1
- package/src/ui/terminal-graphics/format-fallback.ts +7 -9
- package/src/ui/terminal-graphics/kitty.ts +2 -7
- package/src/update/version-check.ts +1 -1
|
@@ -114,7 +114,7 @@ export class LocalSessionBackend
|
|
|
114
114
|
rows: number
|
|
115
115
|
cwd?: string
|
|
116
116
|
}): void {
|
|
117
|
-
if (!this.currentSessionId) {
|
|
117
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
|
|
118
118
|
logDebug('backend.local.skipCreateWithoutSession', { tabId: options.tabId })
|
|
119
119
|
return
|
|
120
120
|
}
|
|
@@ -127,7 +127,7 @@ export class LocalSessionBackend
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
write(tabId: string, input: string): void {
|
|
130
|
-
if (!this.currentSessionId) {
|
|
130
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) {
|
|
131
131
|
logDebug('backend.local.skipWriteWithoutSession', { inputLength: input.length, tabId })
|
|
132
132
|
return
|
|
133
133
|
}
|
|
@@ -140,22 +140,22 @@ export class LocalSessionBackend
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
143
|
-
if (!this.currentSessionId) return
|
|
143
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
144
144
|
this.sessionManager.scroll(this.currentSessionId, tabId, deltaLines)
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
scrollViewportToBottom(tabId: string): void {
|
|
148
|
-
if (!this.currentSessionId) return
|
|
148
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
149
149
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
153
|
-
if (!this.currentSessionId) return
|
|
153
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
154
154
|
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
setActiveTab(tabId: string | null): void {
|
|
158
|
-
if (!this.currentSessionId) return
|
|
158
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
159
159
|
logDebug('backend.local.setActiveTab', { sessionId: this.currentSessionId, tabId })
|
|
160
160
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
161
161
|
}
|
|
@@ -166,7 +166,7 @@ export class LocalSessionBackend
|
|
|
166
166
|
intents?: Map<string, ScrollIntent>,
|
|
167
167
|
options?: { sync?: boolean }
|
|
168
168
|
): void {
|
|
169
|
-
if (!this.currentSessionId) return
|
|
169
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
170
170
|
this.sessionManager.resize(this.currentSessionId, cols, rows, intents, options)
|
|
171
171
|
}
|
|
172
172
|
|
|
@@ -177,25 +177,25 @@ export class LocalSessionBackend
|
|
|
177
177
|
intent?: ScrollIntent,
|
|
178
178
|
options?: { sync?: boolean }
|
|
179
179
|
): void {
|
|
180
|
-
if (!this.currentSessionId) return
|
|
180
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
181
181
|
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent, options)
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
disposeSession(tabId: string): void {
|
|
185
|
-
if (!this.currentSessionId) return
|
|
185
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
186
186
|
logDebug('backend.local.disposeSession', { sessionId: this.currentSessionId, tabId })
|
|
187
187
|
this.sessionManager.closeTab(this.currentSessionId, tabId)
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
disposeAll(): void {
|
|
191
|
-
if (!this.currentSessionId) return
|
|
191
|
+
if (!(this.currentSessionId != null && this.currentSessionId !== '')) return
|
|
192
192
|
logDebug('backend.local.disposeAll', { sessionId: this.currentSessionId })
|
|
193
193
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
destroy(keepSessions = true): void {
|
|
197
197
|
logDebug('backend.local.destroy', { keepSessions, sessionId: this.currentSessionId })
|
|
198
|
-
if (!keepSessions && this.currentSessionId) {
|
|
198
|
+
if (!keepSessions && this.currentSessionId != null && this.currentSessionId !== '') {
|
|
199
199
|
this.sessionManager.disposeSession(this.currentSessionId)
|
|
200
200
|
}
|
|
201
201
|
this.statusLoop.stop()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
|
-
import { connect, Socket } from 'node:net'
|
|
2
|
+
import { connect, type Socket } from 'node:net'
|
|
3
3
|
|
|
4
4
|
import type { AssistantId, ScrollIntent, WorkspaceSnapshotV1 } from '../state/types'
|
|
5
5
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
@@ -96,7 +96,7 @@ export class RemoteSessionBackend
|
|
|
96
96
|
return this.socket
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
private send(request: ClientRequest): Promise<ServerResponse> {
|
|
99
|
+
private async send(request: ClientRequest): Promise<ServerResponse> {
|
|
100
100
|
const socket = this.getConnectedSocket()
|
|
101
101
|
logDebug('backend.remote.send', { id: request.id, type: request.type })
|
|
102
102
|
return new Promise((resolve, reject) => {
|
|
@@ -139,11 +139,22 @@ export class RemoteSessionBackend
|
|
|
139
139
|
private reportCommandError(context: string, error: unknown, tabId?: string): void {
|
|
140
140
|
const message = error instanceof Error ? error.message : String(error)
|
|
141
141
|
logDebug('backend.remote.commandError', { context, error: message, tabId })
|
|
142
|
-
if (tabId) {
|
|
142
|
+
if (tabId != null && tabId !== '') {
|
|
143
143
|
this.emit('error', tabId, message)
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/** Fire-and-forget a command, reporting any failure via reportCommandError. */
|
|
148
|
+
private dispatchCommand(request: ClientRequest, context: string, tabId?: string): void {
|
|
149
|
+
void (async () => {
|
|
150
|
+
try {
|
|
151
|
+
await this.sendExpectOk(request)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.reportCommandError(context, error, tabId)
|
|
154
|
+
}
|
|
155
|
+
})()
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
private handleServerEvent(message: ServerEvent): void {
|
|
148
159
|
logDebug('backend.remote.event', { type: message.type })
|
|
149
160
|
switch (message.type) {
|
|
@@ -352,8 +363,10 @@ export class RemoteSessionBackend
|
|
|
352
363
|
return
|
|
353
364
|
}
|
|
354
365
|
|
|
355
|
-
|
|
356
|
-
(
|
|
366
|
+
this.dispatchCommand(
|
|
367
|
+
{ id: crypto.randomUUID(), payload: options, type: 'createTab' },
|
|
368
|
+
'createTab',
|
|
369
|
+
options.tabId
|
|
357
370
|
)
|
|
358
371
|
}
|
|
359
372
|
|
|
@@ -362,55 +375,54 @@ export class RemoteSessionBackend
|
|
|
362
375
|
logDebug('backend.remote.skipWriteBeforeAttach', { inputLength: input.length, tabId })
|
|
363
376
|
return
|
|
364
377
|
}
|
|
365
|
-
|
|
366
|
-
id: crypto.randomUUID(),
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
378
|
+
this.dispatchCommand(
|
|
379
|
+
{ id: crypto.randomUUID(), payload: { data: input, tabId }, type: 'write' },
|
|
380
|
+
'write',
|
|
381
|
+
tabId
|
|
382
|
+
)
|
|
370
383
|
}
|
|
371
384
|
|
|
372
385
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
373
386
|
if (!this.attached) {
|
|
374
387
|
return
|
|
375
388
|
}
|
|
376
|
-
|
|
377
|
-
id: crypto.randomUUID(),
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
389
|
+
this.dispatchCommand(
|
|
390
|
+
{ id: crypto.randomUUID(), payload: { deltaLines, tabId }, type: 'scroll' },
|
|
391
|
+
'scroll',
|
|
392
|
+
tabId
|
|
393
|
+
)
|
|
381
394
|
}
|
|
382
395
|
|
|
383
396
|
scrollViewportToBottom(tabId: string): void {
|
|
384
397
|
if (!this.attached) {
|
|
385
398
|
return
|
|
386
399
|
}
|
|
387
|
-
|
|
388
|
-
id: crypto.randomUUID(),
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
400
|
+
this.dispatchCommand(
|
|
401
|
+
{ id: crypto.randomUUID(), payload: { tabId }, type: 'scrollToBottom' },
|
|
402
|
+
'scrollToBottom',
|
|
403
|
+
tabId
|
|
404
|
+
)
|
|
392
405
|
}
|
|
393
406
|
|
|
394
407
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
395
408
|
if (!this.attached) {
|
|
396
409
|
return
|
|
397
410
|
}
|
|
398
|
-
|
|
399
|
-
id: crypto.randomUUID(),
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
411
|
+
this.dispatchCommand(
|
|
412
|
+
{ id: crypto.randomUUID(), payload: { intent, tabId }, type: 'reapplyScrollIntent' },
|
|
413
|
+
'reapplyScrollIntent',
|
|
414
|
+
tabId
|
|
415
|
+
)
|
|
403
416
|
}
|
|
404
417
|
|
|
405
418
|
setActiveTab(tabId: string | null): void {
|
|
406
419
|
if (!this.attached) {
|
|
407
420
|
return
|
|
408
421
|
}
|
|
409
|
-
|
|
410
|
-
id: crypto.randomUUID(),
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}).catch((error) => this.reportCommandError('setActiveTab', error))
|
|
422
|
+
this.dispatchCommand(
|
|
423
|
+
{ id: crypto.randomUUID(), payload: { tabId }, type: 'setActiveTab' },
|
|
424
|
+
'setActiveTab'
|
|
425
|
+
)
|
|
414
426
|
}
|
|
415
427
|
|
|
416
428
|
resizeAll(
|
|
@@ -425,11 +437,14 @@ export class RemoteSessionBackend
|
|
|
425
437
|
}
|
|
426
438
|
logDebug('backend.remote.resize', { cols, rows, sessionId: this.currentSessionId })
|
|
427
439
|
const intentsRecord = intents ? Object.fromEntries(intents.entries()) : undefined
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
440
|
+
this.dispatchCommand(
|
|
441
|
+
{
|
|
442
|
+
id: crypto.randomUUID(),
|
|
443
|
+
payload: { cols, intents: intentsRecord, rows },
|
|
444
|
+
type: 'resizeClient',
|
|
445
|
+
},
|
|
446
|
+
'resizeClient'
|
|
447
|
+
)
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
resizeTab(
|
|
@@ -442,19 +457,21 @@ export class RemoteSessionBackend
|
|
|
442
457
|
if (!this.attached) {
|
|
443
458
|
return
|
|
444
459
|
}
|
|
445
|
-
|
|
446
|
-
id: crypto.randomUUID(),
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
460
|
+
this.dispatchCommand(
|
|
461
|
+
{ id: crypto.randomUUID(), payload: { cols, intent, rows, tabId }, type: 'resizeTab' },
|
|
462
|
+
'resizeTab',
|
|
463
|
+
tabId
|
|
464
|
+
)
|
|
450
465
|
}
|
|
451
466
|
|
|
452
467
|
disposeSession(tabId: string): void {
|
|
453
468
|
if (!this.attached) {
|
|
454
469
|
return
|
|
455
470
|
}
|
|
456
|
-
|
|
457
|
-
(
|
|
471
|
+
this.dispatchCommand(
|
|
472
|
+
{ id: crypto.randomUUID(), payload: { tabId }, type: 'closeTab' },
|
|
473
|
+
'closeTab',
|
|
474
|
+
tabId
|
|
458
475
|
)
|
|
459
476
|
}
|
|
460
477
|
|
|
@@ -462,9 +479,7 @@ export class RemoteSessionBackend
|
|
|
462
479
|
if (!this.attached) {
|
|
463
480
|
return
|
|
464
481
|
}
|
|
465
|
-
|
|
466
|
-
(error) => this.reportCommandError('disposeAll', error)
|
|
467
|
-
)
|
|
482
|
+
this.dispatchCommand({ id: crypto.randomUUID(), payload: {}, type: 'disposeAll' }, 'disposeAll')
|
|
468
483
|
}
|
|
469
484
|
|
|
470
485
|
async destroy(keepSessions = true): Promise<void> {
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
WorkspaceSnapshotV1,
|
|
11
11
|
} from '../state/types'
|
|
12
12
|
|
|
13
|
-
export
|
|
13
|
+
export interface SessionBackendEvents {
|
|
14
14
|
render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
|
|
15
15
|
exit: [tabId: string, exitCode: number]
|
|
16
16
|
error: [tabId: string, message: string]
|
|
@@ -26,7 +26,7 @@ export interface BackendAttachResult {
|
|
|
26
26
|
* atomically with tab hydration to prevent an unknown-tab race on
|
|
27
27
|
* separate `sessionActivity` events.
|
|
28
28
|
*/
|
|
29
|
-
initialSessionStatuses:
|
|
29
|
+
initialSessionStatuses: { sessionId: string; status: SessionStatus }[]
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
|
@@ -34,16 +34,16 @@ const VAR_RE = /\{\{([^{}]+)\}\}/g
|
|
|
34
34
|
const TWO = (n: number): string => String(n).padStart(2, '0')
|
|
35
35
|
|
|
36
36
|
function formatDate(date: Date, format?: string): string {
|
|
37
|
-
if (!format) {
|
|
37
|
+
if (!(format != null && format !== '')) {
|
|
38
38
|
return `${date.getFullYear()}-${TWO(date.getMonth() + 1)}-${TWO(date.getDate())}`
|
|
39
39
|
}
|
|
40
40
|
return format
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
.
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
.
|
|
41
|
+
.replaceAll('YYYY', String(date.getFullYear()))
|
|
42
|
+
.replaceAll('MM', TWO(date.getMonth() + 1))
|
|
43
|
+
.replaceAll('DD', TWO(date.getDate()))
|
|
44
|
+
.replaceAll('HH', TWO(date.getHours()))
|
|
45
|
+
.replaceAll('mm', TWO(date.getMinutes()))
|
|
46
|
+
.replaceAll('ss', TWO(date.getSeconds()))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function resolveSyncVariable(name: string, ctx: SyncExpansionContext): string | null {
|
|
@@ -14,7 +14,7 @@ const MAX_TIMEOUT_MS = 30_000
|
|
|
14
14
|
*/
|
|
15
15
|
function buildShellArgv(cmd: string): string[] {
|
|
16
16
|
const userShell = process.env.SHELL
|
|
17
|
-
if (userShell && userShell.length > 0) {
|
|
17
|
+
if (userShell != null && userShell !== '' && userShell.length > 0) {
|
|
18
18
|
return [userShell, '-l', '-c', cmd]
|
|
19
19
|
}
|
|
20
20
|
return ['/bin/sh', '-c', cmd]
|
|
@@ -50,7 +50,7 @@ export function createTriggerDetector(opts: TriggerDetectorOptions): TriggerDete
|
|
|
50
50
|
function tryMatch(separator: string): TriggerMatch | null {
|
|
51
51
|
const triggerChar = opts.getTriggerChar()
|
|
52
52
|
for (const snippet of opts.getSnippets()) {
|
|
53
|
-
if (snippet.trigger && snippet.trigger === buffer) {
|
|
53
|
+
if (snippet.trigger != null && snippet.trigger !== '' && snippet.trigger === buffer) {
|
|
54
54
|
const triggerText = `${triggerChar}${buffer}${separator}`
|
|
55
55
|
reset()
|
|
56
56
|
return { snippet, triggerText }
|
|
@@ -23,7 +23,7 @@ export const aiUsageStore = createStore<AIUsageState>((set) => ({
|
|
|
23
23
|
const isFailure = Boolean(snap.error)
|
|
24
24
|
const hasPriorValue = prev && prev.percent !== null
|
|
25
25
|
const merged: UsageSnapshot =
|
|
26
|
-
isFailure && hasPriorValue && prev
|
|
26
|
+
isFailure && hasPriorValue === true && prev != null
|
|
27
27
|
? { ...prev, error: snap.error, lastUpdated: snap.lastUpdated, stale: true }
|
|
28
28
|
: snap
|
|
29
29
|
return { snapshots: { ...state.snapshots, [snap.tool]: merged } }
|
package/src/state/git-tree.ts
CHANGED
|
@@ -42,8 +42,12 @@ export interface GitTreeRows {
|
|
|
42
42
|
visibleRows: GitTreeRow[]
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export function gitFileKey(
|
|
46
|
-
|
|
45
|
+
export function gitFileKey(
|
|
46
|
+
file: Pick<GitFileEntry, 'path' | 'section'> & { repoPath?: string }
|
|
47
|
+
): string {
|
|
48
|
+
return file.repoPath != null && file.repoPath !== ''
|
|
49
|
+
? `${file.section}:${file.repoPath}:${file.path}`
|
|
50
|
+
: `${file.section}:${file.path}`
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
export function gitFolderKey(section: GitFileSection, folderPath: string): string {
|
|
@@ -85,7 +89,7 @@ export function getSelectedGitRow(
|
|
|
85
89
|
compact?: boolean
|
|
86
90
|
}
|
|
87
91
|
): GitTreeRow | null {
|
|
88
|
-
if (!options.selectedEntryKey) return null
|
|
92
|
+
if (!(options.selectedEntryKey != null && options.selectedEntryKey !== '')) return null
|
|
89
93
|
const { visibleRows } = buildGitTreeRows(
|
|
90
94
|
files,
|
|
91
95
|
options.collapsedFolders,
|
|
@@ -181,7 +185,7 @@ function buildSectionTree(files: GitFileEntry[], section: GitFileSection): GitTr
|
|
|
181
185
|
let cursor = root
|
|
182
186
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
183
187
|
const name = parts[i]
|
|
184
|
-
if (!name) continue
|
|
188
|
+
if (!(name != null && name !== '')) continue
|
|
185
189
|
const nextPath = cursor.path ? `${cursor.path}/${name}` : name
|
|
186
190
|
let child = cursor.folders.get(name)
|
|
187
191
|
if (!child) {
|
package/src/state/layout-tree.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export type SplitDirection = 'horizontal' | 'vertical'
|
|
2
2
|
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
export interface LayoutLeaf {
|
|
4
|
+
type: 'leaf'
|
|
5
|
+
tabId: string
|
|
6
|
+
}
|
|
7
|
+
export interface LayoutSplit {
|
|
5
8
|
type: 'split'
|
|
6
9
|
direction: SplitDirection
|
|
7
10
|
ratio: number
|
|
@@ -42,7 +45,7 @@ export function getTreeForTab(
|
|
|
42
45
|
tabId: string
|
|
43
46
|
): LayoutNode | null {
|
|
44
47
|
const groupId = tabGroupMap[tabId]
|
|
45
|
-
if (!groupId) return null
|
|
48
|
+
if (!(groupId != null && groupId !== '')) return null
|
|
46
49
|
return layoutTrees[groupId] ?? null
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -303,14 +306,14 @@ export function getAdjacentLeaf(
|
|
|
303
306
|
if (inFirst && goingToSecond) {
|
|
304
307
|
// Try to recurse in first child first
|
|
305
308
|
const deeper = getAdjacentLeaf(tree.first, fromTabId, direction)
|
|
306
|
-
if (deeper) return deeper
|
|
309
|
+
if (deeper != null && deeper !== '') return deeper
|
|
307
310
|
// Cross boundary: go to first leaf of second child
|
|
308
311
|
return firstLeafId(tree.second)
|
|
309
312
|
}
|
|
310
313
|
|
|
311
314
|
if (inSecond && !goingToSecond) {
|
|
312
315
|
const deeper = getAdjacentLeaf(tree.second, fromTabId, direction)
|
|
313
|
-
if (deeper) return deeper
|
|
316
|
+
if (deeper != null && deeper !== '') return deeper
|
|
314
317
|
return lastLeafId(tree.first)
|
|
315
318
|
}
|
|
316
319
|
}
|
|
@@ -50,6 +50,7 @@ export function emptyGitMode(): GitModeState {
|
|
|
50
50
|
loading: {},
|
|
51
51
|
parsedFiles: {},
|
|
52
52
|
pendingDeletePath: null,
|
|
53
|
+
reviewBase: false,
|
|
53
54
|
selectedEntryKey: null,
|
|
54
55
|
}
|
|
55
56
|
}
|
|
@@ -90,6 +91,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
90
91
|
highlights: {},
|
|
91
92
|
loading: {},
|
|
92
93
|
parsedFiles: {},
|
|
94
|
+
reviewBase: false,
|
|
93
95
|
},
|
|
94
96
|
}
|
|
95
97
|
}
|
|
@@ -102,7 +104,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
102
104
|
action.delta,
|
|
103
105
|
state.gitPane.treeCompaction
|
|
104
106
|
)
|
|
105
|
-
if (
|
|
107
|
+
if (next == null || next === '' || next === state.gitMode.selectedEntryKey) return state
|
|
106
108
|
return {
|
|
107
109
|
...state,
|
|
108
110
|
gitMode: {
|
|
@@ -121,7 +123,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
121
123
|
action.delta,
|
|
122
124
|
state.gitPane.treeCompaction
|
|
123
125
|
)
|
|
124
|
-
if (
|
|
126
|
+
if (next == null || next === '' || next === state.gitMode.selectedEntryKey) return state
|
|
125
127
|
return {
|
|
126
128
|
...state,
|
|
127
129
|
gitMode: {
|
|
@@ -140,7 +142,13 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
140
142
|
[action.key],
|
|
141
143
|
state.gitPane.treeCompaction
|
|
142
144
|
)
|
|
143
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
next == null ||
|
|
147
|
+
next === '' ||
|
|
148
|
+
next !== action.key ||
|
|
149
|
+
next === state.gitMode.selectedEntryKey
|
|
150
|
+
)
|
|
151
|
+
return state
|
|
144
152
|
return {
|
|
145
153
|
...state,
|
|
146
154
|
gitMode: {
|
|
@@ -199,7 +207,12 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
199
207
|
},
|
|
200
208
|
}
|
|
201
209
|
}
|
|
202
|
-
if (
|
|
210
|
+
if (
|
|
211
|
+
row.parentKey == null ||
|
|
212
|
+
row.parentKey === '' ||
|
|
213
|
+
row.parentKey === state.gitMode.selectedEntryKey
|
|
214
|
+
)
|
|
215
|
+
return state
|
|
203
216
|
return {
|
|
204
217
|
...state,
|
|
205
218
|
gitMode: {
|
|
@@ -318,7 +331,7 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
318
331
|
const k = `${action.key}|${action.themeId}`
|
|
319
332
|
const existing = state.gitMode.highlights[k]
|
|
320
333
|
const sameContent = existing && existing.hash === action.hash
|
|
321
|
-
if (sameContent) {
|
|
334
|
+
if (sameContent === true) {
|
|
322
335
|
const existingAdd = existing.add as unknown[]
|
|
323
336
|
const existingDel = existing.del as unknown[]
|
|
324
337
|
let changesAnything = false
|
|
@@ -344,8 +357,8 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
344
357
|
}
|
|
345
358
|
if (!changesAnything) return state
|
|
346
359
|
}
|
|
347
|
-
const nextAdd = sameContent ? ([...(existing.add as unknown[])] as unknown[]) : []
|
|
348
|
-
const nextDel = sameContent ? ([...(existing.del as unknown[])] as unknown[]) : []
|
|
360
|
+
const nextAdd = sameContent === true ? ([...(existing.add as unknown[])] as unknown[]) : []
|
|
361
|
+
const nextDel = sameContent === true ? ([...(existing.del as unknown[])] as unknown[]) : []
|
|
349
362
|
for (const patch of action.add) {
|
|
350
363
|
const tokens = patch.tokens as unknown[]
|
|
351
364
|
for (let i = 0; i < tokens.length; i++) nextAdd[patch.start + i] = tokens[i]
|
|
@@ -398,6 +411,26 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
398
411
|
const next = state.gitMode.diffView === 'split' ? 'stacked' : 'split'
|
|
399
412
|
return { ...state, gitMode: { ...state.gitMode, diffView: next } }
|
|
400
413
|
}
|
|
414
|
+
case 'git-mode-toggle-review-base': {
|
|
415
|
+
// Base review and history walking are mutually exclusive views; entering
|
|
416
|
+
// either resets headOffset and drops cached diffs (computed vs old ref).
|
|
417
|
+
const next = !state.gitMode.reviewBase
|
|
418
|
+
return {
|
|
419
|
+
...state,
|
|
420
|
+
gitMode: {
|
|
421
|
+
...state.gitMode,
|
|
422
|
+
actionMessage: null,
|
|
423
|
+
diffs: {},
|
|
424
|
+
folds: {},
|
|
425
|
+
headOffset: 0,
|
|
426
|
+
highlights: {},
|
|
427
|
+
loading: {},
|
|
428
|
+
parsedFiles: {},
|
|
429
|
+
pendingDeletePath: null,
|
|
430
|
+
reviewBase: next,
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
}
|
|
401
434
|
case 'git-mode-shift-head-offset': {
|
|
402
435
|
const next = Math.max(0, state.gitMode.headOffset + action.delta)
|
|
403
436
|
if (next === state.gitMode.headOffset) return state
|
|
@@ -485,13 +518,10 @@ export function reduceGitModeState(state: AppState, action: AppAction): AppState
|
|
|
485
518
|
const duplicateIdx = files.findIndex(
|
|
486
519
|
(f, i) => i !== idx && f.path === action.path && f.section === toSection
|
|
487
520
|
)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
i === idx ? ({ ...f, section: toSection } as GitFileEntry) : f
|
|
493
|
-
)
|
|
494
|
-
}
|
|
521
|
+
nextFiles =
|
|
522
|
+
duplicateIdx >= 0
|
|
523
|
+
? files.filter((_, i) => i !== idx)
|
|
524
|
+
: files.map((f, i) => (i === idx ? ({ ...f, section: toSection } as GitFileEntry) : f))
|
|
495
525
|
}
|
|
496
526
|
nextFiles = sortFilesBySection(nextFiles)
|
|
497
527
|
const movedCurrentKey = gitFileKey({ path: action.path, section: action.fromSection })
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AppAction,
|
|
3
|
+
AppState,
|
|
4
|
+
BranchDivergence,
|
|
5
|
+
GitFileEntry,
|
|
6
|
+
GitFileSection,
|
|
7
|
+
GitPanelState,
|
|
8
|
+
} from '../types'
|
|
2
9
|
|
|
3
10
|
import { clampGitPaneRatio } from '../git-pane-sizing'
|
|
4
11
|
import { reconcileSelectedGitEntryKey } from '../git-tree'
|
|
@@ -27,6 +34,20 @@ function clampRatio(value: number): number {
|
|
|
27
34
|
return clampGitPaneRatio(value)
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
function sameDivergence(
|
|
38
|
+
a: Record<string, BranchDivergence>,
|
|
39
|
+
b: Record<string, BranchDivergence>
|
|
40
|
+
): boolean {
|
|
41
|
+
const aKeys = Object.keys(a)
|
|
42
|
+
if (aKeys.length !== Object.keys(b).length) return false
|
|
43
|
+
for (const key of aKeys) {
|
|
44
|
+
const x = a[key]
|
|
45
|
+
const y = b[key]
|
|
46
|
+
if (x == null || y == null || x.ahead !== y.ahead || x.behind !== y.behind) return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
30
51
|
export function emptyGitPanel(): GitPanelState {
|
|
31
52
|
return {
|
|
32
53
|
ahead: 0,
|
|
@@ -188,6 +209,10 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
188
209
|
gitPanel: { ...prev, error: action.kind, files: [] },
|
|
189
210
|
}
|
|
190
211
|
}
|
|
212
|
+
case 'set-worktree-divergence': {
|
|
213
|
+
if (sameDivergence(state.worktreeDivergence, action.divergence)) return state
|
|
214
|
+
return { ...state, worktreeDivergence: action.divergence }
|
|
215
|
+
}
|
|
191
216
|
case 'git-panel-reset': {
|
|
192
217
|
const prev = state.gitPanel
|
|
193
218
|
if (
|
|
@@ -201,7 +226,18 @@ export function reduceGitPanelState(state: AppState, action: AppAction): AppStat
|
|
|
201
226
|
}
|
|
202
227
|
return {
|
|
203
228
|
...state,
|
|
204
|
-
|
|
229
|
+
// A reset means the underlying worktree/ref changed, so cached diffs are
|
|
230
|
+
// stale — drop them (keyed by section:path, they'd otherwise serve a
|
|
231
|
+
// previous worktree's diff for a same-named file after a move/switch).
|
|
232
|
+
gitMode: {
|
|
233
|
+
...state.gitMode,
|
|
234
|
+
diffs: {},
|
|
235
|
+
highlights: {},
|
|
236
|
+
loading: {},
|
|
237
|
+
parsedFiles: {},
|
|
238
|
+
pendingDeletePath: null,
|
|
239
|
+
selectedEntryKey: null,
|
|
240
|
+
},
|
|
205
241
|
gitPanel: emptyGitPanel(),
|
|
206
242
|
}
|
|
207
243
|
}
|