@brimveyn/aimux 1.12.2 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +3 -0
  2. package/package.json +4 -3
  3. package/src/app-runtime/auto-commit-driver.ts +8 -4
  4. package/src/app-runtime/auto-commit-ref.ts +1 -1
  5. package/src/app-runtime/backend-attach-runtime.ts +18 -2
  6. package/src/app-runtime/backend-runtime-events.ts +1 -1
  7. package/src/app-runtime/pty-write.ts +52 -0
  8. package/src/app-runtime/selection-scroll.ts +2 -2
  9. package/src/app-runtime/session-actions.ts +16 -4
  10. package/src/app-runtime/side-effects.ts +598 -102
  11. package/src/app-runtime/snippet-actions.ts +25 -11
  12. package/src/app-runtime/use-auto-commit-driver.ts +7 -4
  13. package/src/app-runtime/use-backend-runtime.ts +3 -3
  14. package/src/app-runtime/use-directory-search.ts +7 -5
  15. package/src/app-runtime/use-mouse-handlers.ts +11 -6
  16. package/src/app-runtime/use-pane-size-report.ts +1 -1
  17. package/src/app-runtime/use-renderer-bindings.ts +143 -8
  18. package/src/app.tsx +25 -9
  19. package/src/auto-commit/headless-commands.ts +4 -6
  20. package/src/daemon/daemon.ts +3 -3
  21. package/src/daemon/runtime-paths.ts +1 -1
  22. package/src/daemon/session-manager.ts +1 -1
  23. package/src/daemon/session-registry.ts +10 -3
  24. package/src/diff-parser/parse-patch-files.ts +18 -18
  25. package/src/git/command-queue.ts +1 -1
  26. package/src/git/divergence.ts +46 -0
  27. package/src/git/git-diff.ts +12 -5
  28. package/src/git/git-poller.ts +33 -11
  29. package/src/git/git-status.ts +14 -3
  30. package/src/git/move-worktree.ts +96 -0
  31. package/src/git/use-repo-discovery.ts +2 -1
  32. package/src/git/worktree-divergence-poller.ts +59 -0
  33. package/src/git/worktree.ts +99 -0
  34. package/src/index.tsx +1 -1
  35. package/src/input/keymap/describe-bindings.ts +27 -9
  36. package/src/input/keymap/key-chord.ts +4 -4
  37. package/src/input/keymap/key-format.ts +2 -2
  38. package/src/input/keymap/sequence-resolver.ts +1 -1
  39. package/src/input/keymap/trie.ts +1 -1
  40. package/src/input/modes/bridge.ts +7 -0
  41. package/src/input/modes/transitions.ts +2 -1
  42. package/src/input/modes/types.ts +13 -1
  43. package/src/input/raw-input-handler.ts +34 -1
  44. package/src/input/terminal-text-extraction.ts +9 -5
  45. package/src/integrations/claude-syntax-overlay.ts +8 -8
  46. package/src/integrations/claude-theme-sync.ts +12 -12
  47. package/src/ipc/protocol.ts +3 -3
  48. package/src/platform/clipboard.ts +18 -2
  49. package/src/platform/worktree-deps.ts +22 -0
  50. package/src/platform/worktree-paths.ts +67 -0
  51. package/src/profile-paths.ts +3 -3
  52. package/src/pty/assistant-status-detection-loop.ts +2 -2
  53. package/src/pty/assistant-status-detector.ts +15 -6
  54. package/src/pty/command-registry.ts +8 -1
  55. package/src/pty/pty-manager.ts +1 -1
  56. package/src/services/ai-usage/adapters/claude.ts +5 -5
  57. package/src/services/ai-usage/adapters/codex.ts +8 -8
  58. package/src/services/ai-usage/cache.ts +2 -2
  59. package/src/services/ai-usage/pace.ts +3 -6
  60. package/src/services/ai-usage/provider.ts +1 -1
  61. package/src/session-backend/bootstrap.ts +2 -2
  62. package/src/session-backend/local-session-backend.ts +11 -11
  63. package/src/session-backend/remote-session-backend.ts +3 -3
  64. package/src/session-backend/types.ts +2 -2
  65. package/src/snippets/expand-variables.ts +112 -0
  66. package/src/snippets/run-shell-var.ts +93 -0
  67. package/src/snippets/trigger-detector.ts +105 -0
  68. package/src/state/ai-usage-store.ts +1 -1
  69. package/src/state/git-tree.ts +8 -4
  70. package/src/state/layout-tree.ts +8 -5
  71. package/src/state/reducers/git-mode-state.ts +44 -14
  72. package/src/state/reducers/git-panel-state.ts +38 -2
  73. package/src/state/reducers/modal-state.ts +385 -24
  74. package/src/state/reducers/session-state.ts +60 -1
  75. package/src/state/reducers/tab-state.ts +151 -85
  76. package/src/state/selectors.ts +6 -4
  77. package/src/state/session-catalog.ts +20 -5
  78. package/src/state/session-persistence.ts +8 -4
  79. package/src/state/session-worktrees.ts +227 -0
  80. package/src/state/snippet-catalog.ts +65 -4
  81. package/src/state/store.ts +1 -0
  82. package/src/state/toast-store.ts +95 -0
  83. package/src/state/types.ts +108 -16
  84. package/src/state/validation.ts +47 -3
  85. package/src/terminal-manager/manager-client.ts +15 -13
  86. package/src/ui/components/git/diff-renderer/fold-strip.tsx +2 -1
  87. package/src/ui/components/git/diff-renderer/pierre-diff.tsx +1 -1
  88. package/src/ui/components/git/diff-renderer/prepare-diff.ts +2 -2
  89. package/src/ui/components/git/diff-renderer/split-view.tsx +6 -4
  90. package/src/ui/components/git/diff-renderer/stacked-view.tsx +6 -4
  91. package/src/ui/components/git/diff-renderer/use-diff-prefetch.ts +19 -6
  92. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +2 -2
  93. package/src/ui/components/git/git-panel.tsx +17 -10
  94. package/src/ui/components/git/git-view.tsx +79 -19
  95. package/src/ui/components/git/image-diff/image-diff-view.tsx +2 -2
  96. package/src/ui/components/git/image-diff/terminal-image-pane.tsx +5 -4
  97. package/src/ui/components/git/pane/git-pane-context-menu.ts +2 -2
  98. package/src/ui/components/git/pane/git-pane-widget.tsx +6 -4
  99. package/src/ui/components/layout/session-bar.tsx +3 -3
  100. package/src/ui/components/layout/sidebar/sidebar.tsx +162 -36
  101. package/src/ui/components/layout/sidebar/tab-item.tsx +17 -1
  102. package/src/ui/components/layout/sidebar/use-sidebar-auto-scroll.ts +2 -2
  103. package/src/ui/components/layout/sidebar/use-sidebar-branch.ts +1 -1
  104. package/src/ui/components/layout/split-layout.tsx +1 -1
  105. package/src/ui/components/layout/terminal-pane.tsx +43 -40
  106. package/src/ui/components/modals/app/ai-usage-modal.tsx +8 -5
  107. package/src/ui/components/modals/app/help-modal.tsx +4 -2
  108. package/src/ui/components/modals/git/git-commit-modal.tsx +2 -2
  109. package/src/ui/components/modals/sessions/create-session-modal.tsx +5 -1
  110. package/src/ui/components/modals/sessions/session-picker-modal.tsx +5 -4
  111. package/src/ui/components/modals/shared/form.tsx +4 -5
  112. package/src/ui/components/modals/shared/modal-shell.tsx +3 -3
  113. package/src/ui/components/modals/shared/picker.tsx +28 -5
  114. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
  115. package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +25 -6
  116. package/src/ui/components/modals/tabs/new-tab-modal.tsx +173 -5
  117. package/src/ui/components/modals/worktree/worktree-move-modal.tsx +94 -0
  118. package/src/ui/components/overlays/ai-usage/ai-usage-indicator.tsx +3 -3
  119. package/src/ui/components/overlays/toast/toast-item.tsx +88 -0
  120. package/src/ui/components/overlays/toast/toast-position.ts +45 -0
  121. package/src/ui/components/overlays/toast/toast-stack.tsx +30 -0
  122. package/src/ui/components/overlays/toast/toast-viewport.tsx +24 -0
  123. package/src/ui/components/overlays/toast/use-toast-animation.ts +64 -0
  124. package/src/ui/components/primitives/input-field.tsx +1 -1
  125. package/src/ui/components/primitives/list-item.tsx +3 -3
  126. package/src/ui/filter-themes.ts +1 -1
  127. package/src/ui/root.tsx +65 -14
  128. package/src/ui/session-ordering.ts +2 -2
  129. package/src/ui/status-bar-model.ts +14 -7
  130. package/src/ui/terminal-graphics/capabilities.ts +1 -1
  131. package/src/ui/terminal-graphics/format-fallback.ts +7 -9
  132. package/src/ui/terminal-graphics/kitty.ts +2 -7
  133. package/src/update/version-check.ts +1 -1
@@ -1,7 +1,9 @@
1
- import type { ModeId } from '@brimveyn/aimux-config'
1
+ import type { ModeId, SnippetVar } from '@brimveyn/aimux-config'
2
2
  import type { ThemedToken } from 'shiki'
3
3
 
4
- export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
4
+ import type { LayoutNode, SplitDirection } from './layout-tree'
5
+
6
+ export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
5
7
 
6
8
  export type AssistantId = BuiltinAssistantId | (string & {})
7
9
 
@@ -43,6 +45,7 @@ export type ModalType =
43
45
  | 'git-commit'
44
46
  | 'update-available'
45
47
  | 'ai-usage'
48
+ | 'worktree-move'
46
49
  | null
47
50
 
48
51
  export interface TerminalSpan {
@@ -97,6 +100,7 @@ export interface PersistedTabSnapshot {
97
100
  scrollIntent?: ScrollIntent
98
101
  errorMessage?: string
99
102
  exitCode?: number
103
+ worktreeId?: string
100
104
  }
101
105
 
102
106
  export interface WorkspaceSnapshotV1 {
@@ -108,11 +112,28 @@ export interface WorkspaceSnapshotV1 {
108
112
  width: number
109
113
  }
110
114
  tabs: PersistedTabSnapshot[]
111
- layoutTree?: import('./layout-tree').LayoutNode
112
- layoutTrees?: Record<string, import('./layout-tree').LayoutNode>
115
+ layoutTree?: LayoutNode
116
+ layoutTrees?: Record<string, LayoutNode>
113
117
  tabGroupMap?: Record<string, string>
114
118
  }
115
119
 
120
+ export type WorktreeSource = 'primary' | 'aimux-temp' | 'external'
121
+
122
+ export interface WorktreeRecord {
123
+ id: string
124
+ name: string
125
+ path: string
126
+ repoRoot: string
127
+ branch?: string
128
+ baseRef?: string
129
+ commitSha?: string
130
+ source: WorktreeSource
131
+ createdByAimux: boolean
132
+ color?: string
133
+ createdAt: string
134
+ updatedAt: string
135
+ }
136
+
116
137
  export interface SessionRecord {
117
138
  id: string
118
139
  name: string
@@ -122,6 +143,8 @@ export interface SessionRecord {
122
143
  lastOpenedAt: string
123
144
  order?: number
124
145
  workspaceSnapshot?: WorkspaceSnapshotV1
146
+ worktrees?: WorktreeRecord[]
147
+ activeWorktreeId?: string
125
148
  }
126
149
 
127
150
  export type SessionBarPosition = 'top' | 'bottom'
@@ -144,6 +167,7 @@ export interface TabSession {
144
167
  command: string
145
168
  errorMessage?: string
146
169
  exitCode?: number
170
+ worktreeId?: string
147
171
  }
148
172
 
149
173
  export interface SidebarState {
@@ -160,7 +184,9 @@ export type GitPanePathConfig =
160
184
  | { enabled: false }
161
185
  | { enabled: true; pathFn?: (path: string) => string }
162
186
 
163
- export type GitPaneDiffCountConfig = { enabled: boolean }
187
+ export interface GitPaneDiffCountConfig {
188
+ enabled: boolean
189
+ }
164
190
 
165
191
  export interface GitPaneState {
166
192
  visible: boolean
@@ -271,6 +297,8 @@ export interface GitModeState {
271
297
  folds: Record<string, Record<string, FoldState>>
272
298
  /** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
273
299
  headOffset: number
300
+ /** When true, diff the active worktree's working tree against its fork point. */
301
+ reviewBase: boolean
274
302
  }
275
303
 
276
304
  interface ModalBase {
@@ -289,6 +317,16 @@ export interface ModalClosed extends ModalBase {
289
317
  export interface ModalNewTab extends ModalBase {
290
318
  type: 'new-tab'
291
319
  editingCommand: AssistantId | null
320
+ activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
321
+ branchError: string | null
322
+ branchName: string
323
+ createWorktree: boolean
324
+ selectedAssistantId: AssistantId | null
325
+ step: 'assistant' | 'worktree' | 'worktree-create'
326
+ targetWorktreeIndex: number
327
+ worktreeDeleteConfirmId: string | null
328
+ worktreeDeleteMessage: string | null
329
+ worktreeName: string
292
330
  }
293
331
 
294
332
  export interface ModalSessionPicker extends ModalBase {
@@ -306,6 +344,11 @@ export interface ModalRenameTab extends ModalBase {
306
344
 
307
345
  export interface ModalSnippetPicker extends ModalBase {
308
346
  type: 'snippet-picker'
347
+ /**
348
+ * Transient status line shown at the bottom of the picker (e.g. error from
349
+ * an open-in-editor attempt). Cleared automatically when the modal closes.
350
+ */
351
+ actionMessage?: string | null
309
352
  }
310
353
 
311
354
  export interface ModalThemePicker extends ModalBase {
@@ -321,7 +364,7 @@ export interface ModalHelp extends ModalBase {
321
364
 
322
365
  export interface ModalSplitPicker extends ModalBase {
323
366
  type: 'split-picker'
324
- splitDirection: import('./layout-tree').SplitDirection
367
+ splitDirection: SplitDirection
325
368
  }
326
369
 
327
370
  export interface ModalGitCommit extends ModalBase {
@@ -342,7 +385,12 @@ export interface ModalCreateSession extends ModalBase {
342
385
 
343
386
  export interface ModalSnippetEditor extends ModalBase {
344
387
  type: 'snippet-editor'
345
- activeField: 'name' | 'content'
388
+ activeField: 'name' | 'trigger' | 'content'
389
+ /** Persisted value of the name field when it is not the active editor. */
390
+ nameBuffer: string
391
+ /** Persisted value of the trigger field when it is not the active editor. */
392
+ triggerBuffer: string
393
+ /** Persisted value of the content field when it is not the active editor. */
346
394
  contentBuffer: string
347
395
  }
348
396
 
@@ -356,6 +404,13 @@ export interface ModalAIUsage extends ModalBase {
356
404
  type: 'ai-usage'
357
405
  }
358
406
 
407
+ export interface ModalWorktreeMove extends ModalBase {
408
+ type: 'worktree-move'
409
+ /** The worktree being moved (may differ from the active one, e.g. a tab menu). */
410
+ sourceWorktreeId: string
411
+ deleteSource: boolean
412
+ }
413
+
359
414
  export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
360
415
 
361
416
  export interface DirectoryResult {
@@ -378,6 +433,7 @@ export type ModalState =
378
433
  | ModalGitCommit
379
434
  | ModalUpdateAvailable
380
435
  | ModalAIUsage
436
+ | ModalWorktreeMove
381
437
 
382
438
  export interface LayoutState {
383
439
  terminalCols: number
@@ -388,6 +444,8 @@ export interface SnippetRecord {
388
444
  id: string
389
445
  name: string
390
446
  content: string
447
+ trigger?: string
448
+ vars?: Record<string, SnippetVar>
391
449
  }
392
450
 
393
451
  export interface DiscoveredRepo {
@@ -411,7 +469,7 @@ export const EMPTY_MULTI_REPO_STATE: MultiRepoState = { prefixes: {}, repos: []
411
469
  export interface AppState {
412
470
  tabs: TabSession[]
413
471
  activeTabId: string | null
414
- layoutTrees: Record<string, import('./layout-tree').LayoutNode>
472
+ layoutTrees: Record<string, LayoutNode>
415
473
  tabGroupMap: Record<string, string>
416
474
  sessions: SessionRecord[]
417
475
  currentSessionId: string | null
@@ -428,6 +486,11 @@ export interface AppState {
428
486
  gitMode: GitModeState
429
487
  autoCommit: AutoCommitState
430
488
  multiRepo: MultiRepoState
489
+ /**
490
+ * Commits each worktree's branch is ahead/behind the ref it forked from,
491
+ * keyed by worktree id. Ephemeral (polled); not persisted to the catalog.
492
+ */
493
+ worktreeDivergence: Record<string, BranchDivergence>
431
494
  /** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
432
495
  pendingChords: string[] | null
433
496
  }
@@ -436,9 +499,18 @@ export interface AppState {
436
499
  export type ModalAction =
437
500
  | { type: 'move-modal-cursor'; delta?: number; to?: 'home' | 'end' }
438
501
  | { type: 'open-new-tab-modal' }
502
+ | { type: 'set-new-tab-branch-error'; message: string | null }
503
+ | {
504
+ type: 'set-new-tab-worktree-delete-state'
505
+ confirmWorktreeId?: string | null
506
+ message: string | null
507
+ }
508
+ | { type: 'enter-new-tab-worktree-create' }
509
+ | { type: 'select-new-tab-assistant'; assistantId?: AssistantId }
510
+ | { type: 'toggle-new-tab-worktree'; assistantId?: AssistantId }
439
511
  | { type: 'open-edit-custom-command'; assistantId: AssistantId }
440
512
  | { type: 'open-help-modal'; scope?: ModeId }
441
- | { type: 'open-split-picker'; direction: import('./layout-tree').SplitDirection }
513
+ | { type: 'open-split-picker'; direction: SplitDirection }
442
514
  | { type: 'open-session-picker' }
443
515
  | {
444
516
  type: 'open-session-name-modal'
@@ -463,6 +535,8 @@ export type ModalAction =
463
535
  | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
464
536
  | { type: 'set-modal-selection-index'; index: number }
465
537
  | { type: 'open-ai-usage-modal' }
538
+ | { type: 'open-worktree-move-modal'; sourceWorktreeId: string }
539
+ | { type: 'toggle-worktree-move-delete' }
466
540
 
467
541
  // -- Session actions --
468
542
  export type SessionAction =
@@ -473,6 +547,15 @@ export type SessionAction =
473
547
  | { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
474
548
  | { type: 'reorder-sessions'; orderedIds: string[] }
475
549
  | { type: 'set-session-status'; sessionId: string; status: SessionStatus }
550
+ | { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
551
+ | { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
552
+ | { type: 'set-active-worktree'; sessionId: string; worktreeId: string }
553
+ | {
554
+ type: 'update-worktree-record'
555
+ sessionId: string
556
+ worktreeId: string
557
+ patch: Partial<WorktreeRecord>
558
+ }
476
559
 
477
560
  // -- Tab actions --
478
561
  export type TabAction =
@@ -481,8 +564,8 @@ export type TabAction =
481
564
  type: 'hydrate-workspace'
482
565
  tabs: TabSession[]
483
566
  activeTabId: string | null
484
- layoutTree?: import('./layout-tree').LayoutNode | null
485
- layoutTrees?: Record<string, import('./layout-tree').LayoutNode>
567
+ layoutTree?: LayoutNode | null
568
+ layoutTrees?: Record<string, LayoutNode>
486
569
  tabGroupMap?: Record<string, string>
487
570
  }
488
571
  | { type: 'close-tab'; tabId: string }
@@ -508,7 +591,7 @@ export type TabAction =
508
591
  export type LayoutAction =
509
592
  | {
510
593
  type: 'split-pane'
511
- direction: import('./layout-tree').SplitDirection
594
+ direction: SplitDirection
512
595
  newTab: TabSession
513
596
  }
514
597
  | { type: 'close-pane'; tabId: string }
@@ -520,13 +603,13 @@ export type LayoutAction =
520
603
  type: 'resize-pane'
521
604
  tabId: string
522
605
  delta: number
523
- axis?: import('./layout-tree').SplitDirection
606
+ axis?: SplitDirection
524
607
  }
525
608
  | {
526
609
  type: 'set-split-ratio'
527
610
  tabId: string
528
611
  ratio: number
529
- axis?: import('./layout-tree').SplitDirection
612
+ axis?: SplitDirection
530
613
  }
531
614
 
532
615
  // -- UI actions --
@@ -554,10 +637,17 @@ export interface GitRefreshPayload {
554
637
  files: GitFileEntry[]
555
638
  }
556
639
 
640
+ /** Commits a worktree branch is ahead/behind the ref it forked from. */
641
+ export interface BranchDivergence {
642
+ ahead: number
643
+ behind: number
644
+ }
645
+
557
646
  export type GitPanelAction =
558
647
  | { type: 'git-refresh-success'; payload: GitRefreshPayload }
559
648
  | { type: 'git-refresh-error'; kind: GitPanelError }
560
649
  | { type: 'git-panel-reset' }
650
+ | { type: 'set-worktree-divergence'; divergence: Record<string, BranchDivergence> }
561
651
 
562
652
  // -- Auto-commit state & actions --
563
653
  export type AutoCommitSuggestion =
@@ -635,15 +725,17 @@ export type GitModeAction =
635
725
  key: string
636
726
  hash: string
637
727
  themeId: string
638
- add: Array<{ start: number; tokens: unknown }>
639
- del: Array<{ start: number; tokens: unknown }>
728
+ add: { start: number; tokens: unknown }[]
729
+ del: { start: number; tokens: unknown }[]
640
730
  }
641
731
  | { type: 'git-mode-invalidate-diffs'; paths: string[] }
642
732
  | { type: 'git-mode-set-loading'; key: string; loading: boolean }
643
733
  | { type: 'git-mode-set-pending-delete'; path: string | null }
644
734
  | { type: 'git-mode-clear-diff-cache'; path: string }
645
735
  | { type: 'git-mode-set-message'; message: string | null }
736
+ | { type: 'snippet-picker-set-message'; message: string | null }
646
737
  | { type: 'git-mode-toggle-diff-view' }
738
+ | { type: 'git-mode-toggle-review-base' }
647
739
  | { type: 'git-mode-shift-head-offset'; delta: number }
648
740
  | { type: 'git-mode-set-head-offset'; offset: number }
649
741
  | {
@@ -5,6 +5,7 @@ import type {
5
5
  TerminalModeState,
6
6
  TerminalSnapshot,
7
7
  WorkspaceSnapshotV1,
8
+ WorktreeRecord,
8
9
  } from './types'
9
10
 
10
11
  function isObjectRecord(value: unknown): value is Record<string, unknown> {
@@ -105,6 +106,24 @@ function isStringRecord(value: unknown): boolean {
105
106
  return Object.values(value).every(isString)
106
107
  }
107
108
 
109
+ function isWorktreeRecord(value: unknown): value is WorktreeRecord {
110
+ return (
111
+ isObjectRecord(value) &&
112
+ isString(value.id) &&
113
+ isString(value.name) &&
114
+ isString(value.path) &&
115
+ isString(value.repoRoot) &&
116
+ (value.branch === undefined || isString(value.branch)) &&
117
+ (value.baseRef === undefined || isString(value.baseRef)) &&
118
+ (value.commitSha === undefined || isString(value.commitSha)) &&
119
+ (value.source === 'primary' || value.source === 'aimux-temp' || value.source === 'external') &&
120
+ isBoolean(value.createdByAimux) &&
121
+ (value.color === undefined || isString(value.color)) &&
122
+ isString(value.createdAt) &&
123
+ isString(value.updatedAt)
124
+ )
125
+ }
126
+
108
127
  export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapshotV1 {
109
128
  return (
110
129
  isObjectRecord(value) &&
@@ -132,7 +151,8 @@ export function isWorkspaceSnapshotV1(value: unknown): value is WorkspaceSnapsho
132
151
  (tab.viewport === undefined || isTerminalSnapshot(tab.viewport)) &&
133
152
  (tab.scrollIntent === undefined || isScrollIntent(tab.scrollIntent)) &&
134
153
  (tab.errorMessage === undefined || isString(tab.errorMessage)) &&
135
- (tab.exitCode === undefined || isFiniteNumber(tab.exitCode))
154
+ (tab.exitCode === undefined || isFiniteNumber(tab.exitCode)) &&
155
+ (tab.worktreeId === undefined || isString(tab.worktreeId))
136
156
  ) &&
137
157
  (value.layoutTree === undefined || isLayoutNode(value.layoutTree)) &&
138
158
  (value.layoutTrees === undefined || isLayoutTreesMap(value.layoutTrees)) &&
@@ -151,12 +171,36 @@ export function isSessionRecord(value: unknown): value is SessionRecord {
151
171
  isString(value.lastOpenedAt) &&
152
172
  (value.order === undefined ||
153
173
  (typeof value.order === 'number' && Number.isFinite(value.order))) &&
154
- (value.workspaceSnapshot === undefined || isWorkspaceSnapshotV1(value.workspaceSnapshot))
174
+ (value.workspaceSnapshot === undefined || isWorkspaceSnapshotV1(value.workspaceSnapshot)) &&
175
+ (value.worktrees === undefined ||
176
+ (Array.isArray(value.worktrees) && value.worktrees.every(isWorktreeRecord))) &&
177
+ (value.activeWorktreeId === undefined || isString(value.activeWorktreeId))
155
178
  )
156
179
  }
157
180
 
181
+ function isSnippetVar(value: unknown): boolean {
182
+ if (!isObjectRecord(value)) return false
183
+ if (!isString(value.sh)) return false
184
+ if (value.timeout !== undefined && !isFiniteNumber(value.timeout)) return false
185
+ if (value.trim !== undefined && !isBoolean(value.trim)) return false
186
+ return true
187
+ }
188
+
189
+ function isSnippetVarRecord(value: unknown): boolean {
190
+ if (!isObjectRecord(value)) return false
191
+ for (const entry of Object.values(value)) {
192
+ if (!isSnippetVar(entry)) return false
193
+ }
194
+ return true
195
+ }
196
+
158
197
  export function isSnippetRecord(value: unknown): value is SnippetRecord {
159
198
  return (
160
- isObjectRecord(value) && isString(value.id) && isString(value.name) && isString(value.content)
199
+ isObjectRecord(value) &&
200
+ isString(value.id) &&
201
+ isString(value.name) &&
202
+ isString(value.content) &&
203
+ (value.trigger === undefined || isString(value.trigger)) &&
204
+ (value.vars === undefined || isSnippetVarRecord(value.vars))
161
205
  )
162
206
  }
@@ -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 {
5
5
  ScrollIntent,
@@ -23,7 +23,7 @@ import {
23
23
  parseManagerMessage,
24
24
  } from '../ipc/manager-protocol'
25
25
 
26
- type ManagerClientEvents = {
26
+ interface ManagerClientEvents {
27
27
  render: [
28
28
  sessionId: string,
29
29
  tabId: string,
@@ -84,7 +84,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
84
84
  return this.socket
85
85
  }
86
86
 
87
- private send(request: ManagerRequest): Promise<ManagerResponse> {
87
+ private async send(request: ManagerRequest): Promise<ManagerResponse> {
88
88
  const socket = this.getConnectedSocket()
89
89
  logDebug('managerClient.send', { id: request.id, type: request.type })
90
90
  return new Promise((resolve, reject) => {
@@ -286,7 +286,9 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
286
286
  return response.payload
287
287
  }
288
288
 
289
- createTab(options: Extract<ManagerRequest, { type: 'createTab' }>['payload']): Promise<void> {
289
+ async createTab(
290
+ options: Extract<ManagerRequest, { type: 'createTab' }>['payload']
291
+ ): Promise<void> {
290
292
  logDebug('managerClient.createTab', {
291
293
  command: options.command,
292
294
  sessionId: options.sessionId,
@@ -296,7 +298,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
296
298
  return this.sendExpectOk({ id: crypto.randomUUID(), payload: options, type: 'createTab' })
297
299
  }
298
300
 
299
- write(sessionId: string, tabId: string, data: string): Promise<void> {
301
+ async write(sessionId: string, tabId: string, data: string): Promise<void> {
300
302
  return this.sendExpectOk({
301
303
  id: crypto.randomUUID(),
302
304
  payload: { data, sessionId, tabId },
@@ -304,7 +306,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
304
306
  })
305
307
  }
306
308
 
307
- resize(
309
+ async resize(
308
310
  sessionId: string,
309
311
  cols: number,
310
312
  rows: number,
@@ -317,7 +319,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
317
319
  })
318
320
  }
319
321
 
320
- resizeTab(
322
+ async resizeTab(
321
323
  sessionId: string,
322
324
  tabId: string,
323
325
  cols: number,
@@ -331,7 +333,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
331
333
  })
332
334
  }
333
335
 
334
- scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
336
+ async scroll(sessionId: string, tabId: string, deltaLines: number): Promise<void> {
335
337
  return this.sendExpectOk({
336
338
  id: crypto.randomUUID(),
337
339
  payload: { deltaLines, sessionId, tabId },
@@ -339,7 +341,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
339
341
  })
340
342
  }
341
343
 
342
- scrollToBottom(sessionId: string, tabId: string): Promise<void> {
344
+ async scrollToBottom(sessionId: string, tabId: string): Promise<void> {
343
345
  return this.sendExpectOk({
344
346
  id: crypto.randomUUID(),
345
347
  payload: { sessionId, tabId },
@@ -347,7 +349,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
347
349
  })
348
350
  }
349
351
 
350
- reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
352
+ async reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): Promise<void> {
351
353
  return this.sendExpectOk({
352
354
  id: crypto.randomUUID(),
353
355
  payload: { intent, sessionId, tabId },
@@ -355,7 +357,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
355
357
  })
356
358
  }
357
359
 
358
- setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
360
+ async setActiveTab(sessionId: string, tabId: string | null): Promise<void> {
359
361
  return this.sendExpectOk({
360
362
  id: crypto.randomUUID(),
361
363
  payload: { sessionId, tabId },
@@ -363,7 +365,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
363
365
  })
364
366
  }
365
367
 
366
- closeTab(sessionId: string, tabId: string): Promise<void> {
368
+ async closeTab(sessionId: string, tabId: string): Promise<void> {
367
369
  return this.sendExpectOk({
368
370
  id: crypto.randomUUID(),
369
371
  payload: { sessionId, tabId },
@@ -371,7 +373,7 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
371
373
  })
372
374
  }
373
375
 
374
- disposeSession(sessionId: string): Promise<void> {
376
+ async disposeSession(sessionId: string): Promise<void> {
375
377
  return this.sendExpectOk({
376
378
  id: crypto.randomUUID(),
377
379
  payload: { sessionId },
@@ -1,6 +1,7 @@
1
+ import type { FoldDispatch } from './pierre-diff'
2
+
1
3
  import { useTheme } from '../../../theme'
2
4
  import { FOLD_STEP, type FoldInfo } from './build-rows'
3
- import { type FoldDispatch } from './pierre-diff'
4
5
 
5
6
  interface Props {
6
7
  fold: FoldInfo
@@ -3,11 +3,11 @@ import type { ThemedToken } from 'shiki'
3
3
 
4
4
  import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'
5
5
 
6
+ import type { FoldState } from '../../../../state/types'
6
7
  import type { ThemeId } from '../../../themes'
7
8
 
8
9
  import { useAppStore } from '../../../../state/app-store'
9
10
  import { dispatchGlobal } from '../../../../state/dispatch-ref'
10
- import { type FoldState } from '../../../../state/types'
11
11
  import { useTheme } from '../../../theme'
12
12
  import { buildDiffSegments, firstChangeSegmentOffset, gutterWidth } from './build-rows'
13
13
  import { SplitView, type SplitViewHandle } from './split-view'
@@ -22,14 +22,14 @@ interface PrepareOptions {
22
22
  signal?: AbortSignal
23
23
  }
24
24
 
25
- function yieldToEventLoop(): Promise<void> {
25
+ async function yieldToEventLoop(): Promise<void> {
26
26
  return new Promise((resolve) => {
27
27
  setImmediate(resolve)
28
28
  })
29
29
  }
30
30
 
31
31
  function throwIfAborted(signal?: AbortSignal): void {
32
- if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
32
+ if (signal?.aborted === true) throw new DOMException('Aborted', 'AbortError')
33
33
  }
34
34
 
35
35
  // Lifts diff parsing off the synchronous render tree and returns a segmented
@@ -230,7 +230,9 @@ function HunkHeaderRow({ row }: { row: Extract<SplitRowOrHeader, { type: 'hunk-h
230
230
  return (
231
231
  <box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
232
232
  <text fg={t.text}>{row.spec}</text>
233
- {row.context ? <text fg={t.textMuted}> {row.context}</text> : null}
233
+ {row.context != null && row.context !== '' ? (
234
+ <text fg={t.textMuted}> {row.context}</text>
235
+ ) : null}
234
236
  </box>
235
237
  )
236
238
  }
@@ -285,9 +287,9 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
285
287
  {tokens.map((tok, i) => {
286
288
  const s = tokenToSpan(tok)
287
289
  let attributes = 0
288
- if (s.bold) attributes |= TextAttributes.BOLD
289
- if (s.italic) attributes |= TextAttributes.ITALIC
290
- if (s.underline) attributes |= TextAttributes.UNDERLINE
290
+ if (s.bold === true) attributes |= TextAttributes.BOLD
291
+ if (s.italic === true) attributes |= TextAttributes.ITALIC
292
+ if (s.underline === true) attributes |= TextAttributes.UNDERLINE
291
293
  return (
292
294
  <span key={i} fg={s.fg ?? t.text} attributes={attributes}>
293
295
  {s.text}
@@ -186,7 +186,9 @@ function UnifiedRowRender({
186
186
  return (
187
187
  <box flexDirection="row" backgroundColor={headerBg} paddingLeft={1} paddingRight={1}>
188
188
  <text fg={t.text}>{row.spec}</text>
189
- {row.context ? <text fg={t.textMuted}> {row.context}</text> : null}
189
+ {row.context != null && row.context !== '' ? (
190
+ <text fg={t.textMuted}> {row.context}</text>
191
+ ) : null}
190
192
  </box>
191
193
  )
192
194
  }
@@ -230,9 +232,9 @@ function LineContent({ content, tokens }: { content: string; tokens: ThemedToken
230
232
  {tokens.map((tok, i) => {
231
233
  const s = tokenToSpan(tok)
232
234
  let attributes = 0
233
- if (s.bold) attributes |= TextAttributes.BOLD
234
- if (s.italic) attributes |= TextAttributes.ITALIC
235
- if (s.underline) attributes |= TextAttributes.UNDERLINE
235
+ if (s.bold === true) attributes |= TextAttributes.BOLD
236
+ if (s.italic === true) attributes |= TextAttributes.ITALIC
237
+ if (s.underline === true) attributes |= TextAttributes.UNDERLINE
236
238
  return (
237
239
  <span key={i} fg={s.fg ?? t.text} attributes={attributes}>
238
240
  {s.text}
@@ -13,6 +13,7 @@ interface PrefetchOptions {
13
13
  projectPath: string | undefined
14
14
  themeId: string
15
15
  headOffset: number
16
+ compareRef?: string
16
17
  enabled: boolean
17
18
  }
18
19
 
@@ -94,15 +95,20 @@ export function useDiffPrefetch(
94
95
 
95
96
  const queueRef = useRef<PrefetchQueue | null>(null)
96
97
 
97
- const { enabled, headOffset, projectPath } = opts
98
+ const { compareRef, enabled, headOffset, projectPath } = opts
98
99
  const runTaskRef = useRef<(task: Task) => Promise<void>>(async () => {})
99
100
 
100
101
  // Keep a ref to the execute function so the queue keeps the latest closure
101
102
  // without needing to recreate the queue itself.
102
103
  runTaskRef.current = async (task: Task) => {
103
- if (!projectPath) return
104
+ if (!(projectPath != null && projectPath !== '')) return
104
105
  try {
105
- const diff = await fetchDiff(task.file.repoPath ?? projectPath, task.file, headOffset)
106
+ const diff = await fetchDiff(
107
+ task.file.repoPath ?? projectPath,
108
+ task.file,
109
+ headOffset,
110
+ compareRef
111
+ )
106
112
  if (task.controller.signal.aborted) return
107
113
  const hash = diffHash(diff.rawDiff)
108
114
  dispatchGlobal({ diff, hash, key: task.key, type: 'git-mode-set-diff' })
@@ -122,13 +128,19 @@ export function useDiffPrefetch(
122
128
  }
123
129
 
124
130
  if (!queueRef.current) {
125
- queueRef.current = new PrefetchQueue((task) => runTaskRef.current(task), MAX_CONCURRENCY)
131
+ queueRef.current = new PrefetchQueue(async (task) => runTaskRef.current(task), MAX_CONCURRENCY)
126
132
  }
127
133
 
128
134
  useEffect(() => {
129
135
  const queue = queueRef.current
130
136
  if (!queue) return
131
- if (!enabled || radius <= 0 || !projectPath || !selectedEntryKey) {
137
+ if (
138
+ !enabled ||
139
+ radius <= 0 ||
140
+ projectPath == null ||
141
+ projectPath === '' ||
142
+ !(selectedEntryKey != null && selectedEntryKey !== '')
143
+ ) {
132
144
  queue.cancelAll()
133
145
  return
134
146
  }
@@ -149,7 +161,7 @@ export function useDiffPrefetch(
149
161
  const key = row.key
150
162
  if (diffs[key]) continue
151
163
  if (parsed[key]) continue
152
- if (loading[key]) continue
164
+ if (loading[key] === true) continue
153
165
  tasks.push({
154
166
  controller: new AbortController(),
155
167
  distance: Math.abs(i - selectedIdx),
@@ -171,6 +183,7 @@ export function useDiffPrefetch(
171
183
  parsed,
172
184
  loading,
173
185
  headOffset,
186
+ compareRef,
174
187
  ])
175
188
 
176
189
  useEffect(() => {
@@ -163,11 +163,11 @@ export function useDiffPreparation(
163
163
  (segments: readonly DiffSegment[]) => {
164
164
  const file = parsed.file
165
165
  const filetype = parsed.filetype
166
- if (!file || !filetype) return
166
+ if (!file || !(filetype != null && filetype !== '')) return
167
167
  const owner = requestOwner
168
168
  const version = requestVersionRef.current
169
169
  const activeHash = cachedParsed?.hash ?? (localOwnerMatches ? localHash : null)
170
- if (!activeHash) return
170
+ if (!(activeHash != null && activeHash !== '')) return
171
171
  const next = segments.filter(
172
172
  (segment) =>
173
173
  (segment.kind === 'context' || segment.kind === 'change') &&