@brimveyn/aimux-config 0.10.9 → 0.11.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.
@@ -0,0 +1,1467 @@
1
+ // -----------------------------------------------------------------------------
2
+ // The aimux application state, mode and layout shapes.
3
+ //
4
+ // These are the SOURCE definitions, not a copy. They live in this package
5
+ // because everything a user's `aimux.config.ts` or a plugin can touch has to
6
+ // be importable without depending on the aimux binary — and because the
7
+ // previous arrangement, two hand-maintained copies "kept structurally
8
+ // identical", had already drifted on six types by the time anyone checked.
9
+ //
10
+ // `src/state/types.ts` re-exports from here.
11
+ // -----------------------------------------------------------------------------
12
+
13
+ import type { LayoutNode, SnippetVar, SplitDirection } from './types'
14
+
15
+ export type BuiltinAssistantId =
16
+ | 'claude'
17
+ | 'codex'
18
+ | 'opencode'
19
+ | 'grok'
20
+ | 'kimi'
21
+ | 'terminal'
22
+ | 'antigravity'
23
+
24
+ export type AssistantId = BuiltinAssistantId | (string & {})
25
+
26
+ export type TabStatus = 'starting' | 'running' | 'disconnected' | 'error'
27
+
28
+ /**
29
+ * Status values that may appear in legacy on-disk project snapshots but are
30
+ * not produced by the running app anymore. Filtered at restore time.
31
+ */
32
+ export type LegacyPersistedTabStatus = TabStatus | 'exited'
33
+
34
+ export type TabActivity = 'working' | 'waiting-input' | 'idle'
35
+
36
+ /**
37
+ * Classifies why a tab is blocked on user input. `permission` is a tool /
38
+ * command approval prompt; `question` is any other prompt the assistant is
39
+ * waiting on. Carried by the `tabQuestion` server event so an orchestrator can
40
+ * branch without re-scraping the screen.
41
+ */
42
+ export type QuestionKind = 'question' | 'permission'
43
+
44
+ /**
45
+ * Per-project status flags. Both can be true at once (e.g. one tab working,
46
+ * another waiting for user input) so we keep them as independent booleans
47
+ * rather than a single priority enum.
48
+ */
49
+ export interface ProjectStatus {
50
+ working: boolean
51
+ waiting: boolean
52
+ }
53
+
54
+ /**
55
+ * A workspace's status, as the sidebar draws it. `working`/`waiting` mirror
56
+ * `ProjectStatus` one level down; `done` is a latch — an assistant here
57
+ * finished a turn and nobody has looked yet — cleared when the workspace is
58
+ * entered or when it goes back to work.
59
+ */
60
+ export interface WorkspaceActivity {
61
+ working: boolean
62
+ waiting: boolean
63
+ done: boolean
64
+ }
65
+
66
+ export type FocusMode =
67
+ | 'navigation'
68
+ | 'terminal-input'
69
+ | 'modal'
70
+ | 'command-edit'
71
+ | 'git'
72
+ | 'settings'
73
+ | 'stats'
74
+ /**
75
+ * A plugin's full-screen view has replaced the pane tree. Which one is in
76
+ * `AppState.activePluginView` — one focus mode covers every plugin view, the
77
+ * way `modal` covers every modal, so adding a view is a registration rather
78
+ * than a change to this union.
79
+ */
80
+ | 'plugin-view'
81
+
82
+ export type ModalType =
83
+ | 'new-tab'
84
+ | 'project-picker'
85
+ | 'project-name'
86
+ | 'create-project'
87
+ | 'create-workspace'
88
+ | 'rename-tab'
89
+ | 'rename-workspace'
90
+ | 'snippet-picker'
91
+ | 'snippet-editor'
92
+ | 'theme-picker'
93
+ | 'help'
94
+ | 'split-picker'
95
+ | 'git-commit'
96
+ | 'update-available'
97
+ | 'quotas'
98
+ | 'workspace-move'
99
+ | 'workspace-move-confirm'
100
+ | 'workspace-delete-confirm'
101
+ | 'flash-jump'
102
+ | 'setting-text'
103
+ | 'settings-search'
104
+ /**
105
+ * Every plugin modal, the way `plugin-view` covers every plugin view. Which
106
+ * one is on `ModalPlugin.modalId`, so adding a modal is a registration
107
+ * rather than an arm on this union.
108
+ */
109
+ | 'plugin-modal'
110
+ | null
111
+
112
+ export interface TerminalSpan {
113
+ text: string
114
+ /** Hex color for RGB cells, or undefined for the "default" foreground. */
115
+ fg?: string
116
+ /** Hex color for RGB cells, or undefined for the "default" background. */
117
+ bg?: string
118
+ /** ANSI palette index (0-255) when the cell emitted an indexed color.
119
+ * Resolved client-side against the host terminal's queried palette so
120
+ * user themes (Ghostty, iTerm2, …) show through. Wins over `fg` if set. */
121
+ fgPalette?: number
122
+ /** ANSI palette index (0-255). Resolved client-side. Wins over `bg`. */
123
+ bgPalette?: number
124
+ bold?: boolean
125
+ italic?: boolean
126
+ underline?: boolean
127
+ cursor?: boolean
128
+ }
129
+
130
+ export interface TerminalLine {
131
+ spans: TerminalSpan[]
132
+ }
133
+
134
+ /** DECSCUSR cursor shape; 'default' restores the host terminal's configured cursor. */
135
+ export type TerminalCursorStyle = 'block' | 'underline' | 'bar' | 'default'
136
+
137
+ export interface TerminalSnapshot {
138
+ lines: TerminalLine[]
139
+ tailLines?: TerminalLine[]
140
+ viewportY: number
141
+ baseY: number
142
+ cursorVisible: boolean
143
+ cursorStyle?: TerminalCursorStyle
144
+ /** Blink flag from DECSCUSR; undefined means "host terminal's default". */
145
+ cursorBlink?: boolean
146
+ /** Cursor row relative to the rendered viewport; outside [0, rows) when
147
+ * the user scrolled the viewport away from the active screen. */
148
+ cursorRow?: number
149
+ cursorCol?: number
150
+ }
151
+
152
+ // The scroll position is owned end-to-end by the backend emulator; this type
153
+ // only describes the re-anchor target the backend computes for itself across a
154
+ // reflow. The frontend no longer derives, stores, or sends a scroll intent.
155
+ export type ScrollIntent = { kind: 'bottom' } | { absoluteLine: number; kind: 'anchor' }
156
+
157
+ export interface TerminalModeState {
158
+ mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any'
159
+ sendFocusMode: boolean
160
+ alternateScrollMode: boolean
161
+ isAlternateBuffer: boolean
162
+ bracketedPasteMode: boolean
163
+ }
164
+
165
+ export interface PersistedTabSnapshot {
166
+ id: string
167
+ assistant: AssistantId
168
+ title: string
169
+ command: string
170
+ /** Optional and additive: an older build ignores it and spawns fresh. */
171
+ sessionId?: string
172
+ status: Exclude<LegacyPersistedTabStatus, 'disconnected'>
173
+ buffer: string
174
+ viewport?: TerminalSnapshot
175
+ terminalModes: TerminalModeState
176
+ errorMessage?: string
177
+ exitCode?: number
178
+ workspaceId?: string
179
+ /** Stable project-scoped name assigned by the headless worker facade. */
180
+ workerName?: string
181
+ autoRenameStatus?: 'eligible' | 'attempted'
182
+ }
183
+
184
+ export interface ProjectSnapshotV1 {
185
+ version: 1
186
+ savedAt: string
187
+ activeTabId: string | null
188
+ sidebar: {
189
+ visible: boolean
190
+ width: number
191
+ }
192
+ tabs: PersistedTabSnapshot[]
193
+ layoutTree?: LayoutNode
194
+ layoutTrees?: Record<string, LayoutNode>
195
+ tabGroupMap?: Record<string, string>
196
+ /**
197
+ * Last viewed tab per workspace, keyed by workspace id. Optional and additive
198
+ * (no version bump): older builds ignore it, newer builds tolerate its
199
+ * absence. Written now so the data accrues, but restoring it at startup is
200
+ * gated off until a future change flips RESTORE_LAST_ACTIVE_TAB_BY_WORKSPACE.
201
+ */
202
+ lastActiveTabByWorkspace?: Record<string, string>
203
+ }
204
+
205
+ export type WorkspaceSource = 'primary' | 'aimux-temp' | 'external'
206
+
207
+ export interface WorkspaceRecord {
208
+ id: string
209
+ name: string
210
+ path: string
211
+ repoRoot: string
212
+ branch?: string
213
+ baseRef?: string
214
+ commitSha?: string
215
+ source: WorkspaceSource
216
+ createdByAimux: boolean
217
+ color?: string
218
+ createdAt: string
219
+ updatedAt: string
220
+ /** Last setup-script run for this workspace. Display only — the auto-run gate
221
+ * is "this session has not seen this workspace id before". */
222
+ setupRanAt?: string
223
+ setupExitCode?: number
224
+ }
225
+
226
+ export interface ProjectRecord {
227
+ id: string
228
+ name: string
229
+ projectPath?: string
230
+ createdAt: string
231
+ updatedAt: string
232
+ lastOpenedAt: string
233
+ order?: number
234
+ projectSnapshot?: ProjectSnapshotV1
235
+ workspaces?: WorkspaceRecord[]
236
+ activeWorkspaceId?: string
237
+ /**
238
+ * The branch new workspaces fork from, when the repo's own default is the
239
+ * wrong answer — a gitflow repo branches off `develop` while still declaring
240
+ * `main` as its default, and nothing about the repo says so. Per project
241
+ * because the convention is the team's, not the user's: three repos, three
242
+ * answers. Unset means the repo's default branch.
243
+ */
244
+ defaultBaseRef?: string
245
+ /**
246
+ * Folded in the sidebar: the project's workspace rows are hidden, bar the one
247
+ * the cursor is on. Persisted, because a fold you have to redo on every start
248
+ * is worse than no fold at all.
249
+ */
250
+ collapsed?: boolean
251
+ }
252
+
253
+ export interface ProjectBarState {
254
+ visible: boolean
255
+ }
256
+
257
+ export interface TabSession {
258
+ id: string
259
+ assistant: AssistantId
260
+ title: string
261
+ status: TabStatus
262
+ activity?: TabActivity
263
+ buffer: string
264
+ viewport?: TerminalSnapshot
265
+ terminalModes: TerminalModeState
266
+ command: string
267
+ /**
268
+ * Names the assistant conversation this tab owns, so a respawn reopens it
269
+ * instead of starting over. Minted when the tab is created and handed to the
270
+ * CLI at spawn — we pick the id rather than discover it afterwards, because
271
+ * discovery cannot tell two tabs sharing a cwd apart.
272
+ *
273
+ * Absent on tabs created before this existed, and on assistants with no
274
+ * session support; both spawn exactly as they always did.
275
+ */
276
+ sessionId?: string
277
+ /**
278
+ * Deliberately put to sleep: the PTY is gone, the frozen viewport stays, and
279
+ * focusing the tab resumes it. Distinct from the `disconnected` it rides on —
280
+ * that also means "restored from a snapshot after a restart" — and only used
281
+ * to tell the two apart in the sidebar glyph, the pane overlay, and the wake.
282
+ * Ephemeral — never persisted, never on the wire: a restart turns every tab
283
+ * disconnected anyway, and Ctrl+r resumes them just the same.
284
+ */
285
+ hibernated?: boolean
286
+ errorMessage?: string
287
+ exitCode?: number
288
+ workspaceId?: string
289
+ /**
290
+ * This tab is the one that rang: it finished a turn while you were looking
291
+ * elsewhere. Set alongside the workspace's own tick and cleared the moment
292
+ * the tab is opened or goes back to work, so the notification points at
293
+ * something rather than just happening. Ephemeral — never persisted, never
294
+ * on the wire.
295
+ */
296
+ unseen?: boolean
297
+ /** Stable project-scoped name assigned by the headless worker facade. */
298
+ workerName?: string
299
+ autoRenameStatus?: 'eligible' | 'attempted'
300
+ /**
301
+ * A real PTY tab that no chrome enumerates: absent from the top tab bar, from
302
+ * tab navigation, from `activeTabId` picks, and from the persisted snapshot.
303
+ * Rendered by whatever created it — today only the setup widget.
304
+ * `filterTabsForActiveWorkspace` is the single guard the UI paths share.
305
+ */
306
+ hidden?: boolean
307
+ /**
308
+ * Who owns this tab. Separate from `hidden` on purpose: promoting a setup tab
309
+ * into the main pane clears `hidden` but must not turn it back into an
310
+ * ordinary tab, or the PTY exit would close it and take the failure output
311
+ * with it — which is the one thing the promotion exists to read.
312
+ *
313
+ * ponytail: not on the ipc wire nor in the snapshot, so a promoted setup tab
314
+ * comes back from a restart as a plain tab running `bash …/setup.sh`. Add it
315
+ * to `TabSessionSummary` if that ever matters.
316
+ */
317
+ role?: 'setup'
318
+ }
319
+
320
+ export type BarSide = 'left' | 'right'
321
+
322
+ /**
323
+ * One widget slot in a bar. `grow` is the flex weight opentui consumes
324
+ * directly; hidden widgets keep their weight but are excluded from the layout.
325
+ */
326
+ export interface BarWidget {
327
+ id: string
328
+ grow: number
329
+ visible: boolean
330
+ /**
331
+ * Set when a plugin's manifest asked for this placement rather than the
332
+ * user. It is what lets an unload withdraw what the plugin put there without
333
+ * touching what the user arranged — and the moment the user moves or hides
334
+ * the widget the mark is dropped, because the placement has become theirs.
335
+ */
336
+ placedBy?: 'plugin'
337
+ }
338
+
339
+ export interface BarState {
340
+ visible: boolean
341
+ width: number
342
+ /** Ordered top → bottom. */
343
+ widgets: BarWidget[]
344
+ }
345
+
346
+ export type BarsState = Record<BarSide, BarState>
347
+
348
+ export type GitPanePathConfig =
349
+ | { enabled: false }
350
+ | { enabled: true; pathFn?: (path: string) => string }
351
+
352
+ export interface GitPaneDiffCountConfig {
353
+ enabled: boolean
354
+ }
355
+
356
+ export interface GitPaneState {
357
+ diffModeRatio: number
358
+ fileListMode: GitFileListMode
359
+ treeCompaction: boolean
360
+ path: GitPanePathConfig
361
+ diffCount: GitPaneDiffCountConfig
362
+ /** Prefetch this many neighbours around the selection. 0 disables prefetch. */
363
+ prefetchRadius: number
364
+ }
365
+
366
+ export type GitFileStatus = 'M' | 'A' | 'D' | 'R' | 'C' | 'U' | '?'
367
+
368
+ export type GitFileSection = 'staged' | 'unstaged' | 'untracked' | 'historical'
369
+
370
+ export interface GitFileEntry {
371
+ path: string
372
+ renamedFrom?: string
373
+ section: GitFileSection
374
+ status: GitFileStatus
375
+ added: number | null
376
+ removed: number | null
377
+ /** Absolute path to the originating git repo. Set when the entry comes from a sub-repo. */
378
+ repoPath?: string
379
+ }
380
+
381
+ export type GitPanelError = 'not-a-repo' | 'unknown'
382
+
383
+ export interface GitPanelState {
384
+ branch: string | null
385
+ ahead: number
386
+ behind: number
387
+ files: GitFileEntry[]
388
+ error: GitPanelError | null
389
+ }
390
+
391
+ export type DiffFileStatus =
392
+ | 'modified'
393
+ | 'new'
394
+ | 'deleted'
395
+ | 'binary'
396
+ | 'renamed'
397
+ | 'image'
398
+ | 'too-large'
399
+
400
+ export interface DiffData {
401
+ path: string
402
+ status: DiffFileStatus
403
+ oldPath?: string
404
+ rawDiff: string
405
+ binarySizeBefore?: number
406
+ binarySizeAfter?: number
407
+ errorMessage?: string
408
+ imageBytesBefore?: Uint8Array
409
+ imageBytesAfter?: Uint8Array
410
+ imageMime?: string
411
+ imageFormatLabel?: string
412
+ }
413
+
414
+ export type GitDiffView = 'split' | 'stacked'
415
+ export type GitFileListMode = 'tree' | 'flat'
416
+
417
+ export interface FoldState {
418
+ top: number
419
+ bottom: number
420
+ }
421
+
422
+ export interface ParsedDiffEntry {
423
+ hash: string
424
+ // Stored as unknown at the state boundary and narrowed at read sites.
425
+ file: unknown
426
+ }
427
+
428
+ export interface HighlightsEntry {
429
+ hash: string
430
+ themeId: string
431
+ // See ParsedDiffEntry note — narrowed via getHighlights().
432
+ add: unknown
433
+ del: unknown
434
+ }
435
+
436
+ export interface GitModeState {
437
+ selectedEntryKey: string | null
438
+ collapsedFolders: Record<string, true>
439
+ diffs: Record<string, DiffData>
440
+ /** Parsed diff keyed by fileKey. Invalidated on file change or head offset shift. */
441
+ parsedFiles: Record<string, ParsedDiffEntry>
442
+ /** Tokenised highlights keyed by `${fileKey}|${themeId}`. */
443
+ highlights: Record<string, HighlightsEntry>
444
+ loading: Record<string, boolean>
445
+ pendingDeletePath: string | null
446
+ actionMessage: string | null
447
+ diffView: GitDiffView
448
+ folds: Record<string, Record<string, FoldState>>
449
+ /** Working-tree-vs-HEAD~N offset. 0 = working tree vs HEAD (default). */
450
+ headOffset: number
451
+ /** When true, diff the active workspace's working tree against its fork point. */
452
+ reviewBase: boolean
453
+ }
454
+
455
+ interface ModalBase {
456
+ selectedIndex: number
457
+ editBuffer: string | null
458
+ projectTargetId: string | null
459
+ cursorPos?: number
460
+ /**
461
+ * Where the focus goes when this modal closes, when it is not back to the
462
+ * panes. Set by whoever opened it, because that is who knows what is behind it
463
+ * — the settings screen opens five different modals, and none of them should
464
+ * have to grow a case in the reducer to find their way home.
465
+ */
466
+ returnTo?: FocusMode
467
+ }
468
+
469
+ export interface ModalClosed extends ModalBase {
470
+ type: null
471
+ editBuffer: null
472
+ projectTargetId: null
473
+ }
474
+
475
+ /**
476
+ * Picking an assistant for a new tab, and nothing else. The tab lands in the
477
+ * project's active workspace; creating a workspace is `create-workspace`'s job.
478
+ */
479
+ export interface ModalNewTab extends ModalBase {
480
+ type: 'new-tab'
481
+ editingCommand: AssistantId | null
482
+ /**
483
+ * Set when `create-workspace` chained into this picker. The prompt the user
484
+ * typed there is sent to the assistant and drives the workspace's real name,
485
+ * and the tab is pinned to the freshly created workspace rather than the
486
+ * project's active one. Living on the modal means `close-modal` clears it, so
487
+ * escaping the picker cannot leak a stale prompt into a later tab.
488
+ */
489
+ pendingWorkspace?: PendingWorkspaceLaunch
490
+ /**
491
+ * A prompt to hand the picked assistant, with none of `pendingWorkspace`'s
492
+ * other behaviour: no workspace pinning, no rename. Used by "Ask an agent" in
493
+ * the setup widget.
494
+ */
495
+ pendingPrompt?: string
496
+ }
497
+
498
+ export interface PendingWorkspaceLaunch {
499
+ projectId: string
500
+ workspaceId: string
501
+ prompt: string
502
+ }
503
+
504
+ export interface ModalProjectPicker extends ModalBase {
505
+ type: 'project-picker'
506
+ }
507
+
508
+ export interface ModalProjectName extends ModalBase {
509
+ type: 'project-name'
510
+ returnToProjectPicker: boolean
511
+ }
512
+
513
+ export interface ModalRenameTab extends ModalBase {
514
+ type: 'rename-tab'
515
+ }
516
+ /**
517
+ * One text field over a settings row. Carries the row it belongs to so confirming
518
+ * writes back to the right one, and closing returns to the settings screen rather
519
+ * than to the panes.
520
+ */
521
+ /** Fuzzy-ish search across every setting, from inside the settings screen. */
522
+ export interface ModalSettingsSearch extends ModalBase {
523
+ type: 'settings-search'
524
+ }
525
+ export interface ModalSettingText extends ModalBase {
526
+ type: 'setting-text'
527
+ settingId: string
528
+ settingLabel: string
529
+ }
530
+
531
+ export interface ModalRenameWorkspace extends ModalBase {
532
+ type: 'rename-workspace'
533
+ workspaceProjectId: string
534
+ }
535
+
536
+ export interface ModalSnippetPicker extends ModalBase {
537
+ type: 'snippet-picker'
538
+ /**
539
+ * Transient status line shown at the bottom of the picker (e.g. error from
540
+ * an open-in-editor attempt). Cleared automatically when the modal closes.
541
+ */
542
+ actionMessage?: string | null
543
+ }
544
+
545
+ export interface ModalThemePicker extends ModalBase {
546
+ type: 'theme-picker'
547
+ entryCount: number
548
+ }
549
+
550
+ export interface ModalHelp extends ModalBase {
551
+ type: 'help'
552
+ entryCount: number
553
+ scope: ModeId | null
554
+ }
555
+
556
+ export interface ModalSplitPicker extends ModalBase {
557
+ type: 'split-picker'
558
+ splitDirection: SplitDirection
559
+ }
560
+
561
+ export interface ModalGitCommit extends ModalBase {
562
+ type: 'git-commit'
563
+ activeField: 'title' | 'body'
564
+ contentBuffer: string
565
+ stage: 'edit' | 'generating' | 'confirm'
566
+ }
567
+
568
+ export interface ModalCreateProject extends ModalBase {
569
+ type: 'create-project'
570
+ directoryResults: DirectoryResult[]
571
+ pendingProjectPath: string | null
572
+ activeField: 'directory' | 'name'
573
+ nameBuffer: string
574
+ returnToProjectPicker: boolean
575
+ }
576
+
577
+ /**
578
+ * Creating a workspace inside the current project — the second of the three
579
+ * creation actions (project / workspace / tab). Deliberately carries no
580
+ * assistant: once the workspace exists the effect chains into the new-tab modal.
581
+ */
582
+ export interface ModalCreateWorkspace extends ModalBase {
583
+ type: 'create-workspace'
584
+ activeField: 'prompt' | 'base'
585
+ /**
586
+ * "What do you want to work on?" — the only thing the user types. It is sent
587
+ * to the assistant, and it names both the workspace and its branch.
588
+ */
589
+ prompt: string
590
+ branchError: string | null
591
+ /** Filter text typed into the "Base" picker. */
592
+ baseQuery: string
593
+ /** Resolved base ref the new workspace is forked from (a workspace's branch or a local branch). */
594
+ baseRef: string
595
+ /** Local branches available as base refs, loaded when the modal opens. */
596
+ baseBranches: string[]
597
+ }
598
+
599
+ export interface ModalSnippetEditor extends ModalBase {
600
+ type: 'snippet-editor'
601
+ activeField: 'name' | 'trigger' | 'content'
602
+ /** Persisted value of the name field when it is not the active editor. */
603
+ nameBuffer: string
604
+ /** Persisted value of the trigger field when it is not the active editor. */
605
+ triggerBuffer: string
606
+ /** Persisted value of the content field when it is not the active editor. */
607
+ contentBuffer: string
608
+ }
609
+
610
+ export interface ModalUpdateAvailable extends ModalBase {
611
+ type: 'update-available'
612
+ currentVersion: string
613
+ latestVersion: string
614
+ }
615
+
616
+ /** The status bar's usage indicator, expanded. Carries nothing: it is a readout. */
617
+ export interface ModalQuotas extends ModalBase {
618
+ type: 'quotas'
619
+ }
620
+
621
+ export interface ModalWorkspaceMove extends ModalBase {
622
+ type: 'workspace-move'
623
+ /** The workspace being moved (may differ from the active one, e.g. a tab menu). */
624
+ sourceWorkspaceId: string
625
+ deleteSource: boolean
626
+ /** Per-workspace dirty file counts, loaded async when the modal opens. */
627
+ stats: { kind: 'loading' } | { kind: 'ready'; dirtyFiles: Record<string, number> }
628
+ }
629
+
630
+ /**
631
+ * Confirmation for a recoverable move failure: the target's dirty files
632
+ * overlap the incoming changes (stash-target) or the squash conflicts
633
+ * (keep-conflicts). Both workspaces are already restored; confirming re-runs
634
+ * move-workspace with the matching flag.
635
+ */
636
+ export interface ModalWorkspaceMoveConfirm extends ModalBase {
637
+ type: 'workspace-move-confirm'
638
+ variant: 'stash-target' | 'keep-conflicts'
639
+ files: string[]
640
+ projectId: string
641
+ sourceWorkspaceId: string
642
+ targetWorkspaceId: string
643
+ deleteSource: boolean
644
+ sourceLabel: string
645
+ targetLabel: string
646
+ }
647
+
648
+ /**
649
+ * Standalone confirmation for a recoverable workspace delete failure triggered
650
+ * outside the new-tab picker (e.g. the sidebar's "Remove workspace"). Carries the
651
+ * params needed to re-run the delete with force once confirmed.
652
+ */
653
+ export interface ModalWorkspaceDeleteConfirm extends ModalBase {
654
+ type: 'workspace-delete-confirm'
655
+ projectId: string
656
+ workspaceId: string
657
+ workspaceLabel: string
658
+ reason?: string
659
+ closeTabs: boolean
660
+ /** Whether confirming force-deletes — true only after a recoverable failure. */
661
+ force: boolean
662
+ }
663
+
664
+ export type DirectoryResultType = 'git-repo' | 'workspace' | 'project'
665
+
666
+ export interface DirectoryResult {
667
+ path: string
668
+ type: DirectoryResultType
669
+ }
670
+
671
+ export type FlashJumpTargetKind = 'project' | 'workspace' | 'tab'
672
+
673
+ export interface FlashJumpTarget {
674
+ kind: FlashJumpTargetKind
675
+ /**
676
+ * 1-based index of the project in the visible project ordering — fed to
677
+ * the existing `switch-project-by-index` side effect when jumping.
678
+ */
679
+ projectIndex: number
680
+ projectId: string
681
+ /** Set for kind 'workspace' (the non-primary target) and kind 'tab' (the tab's workspace). */
682
+ workspaceId?: string
683
+ /** Set for kind 'tab'. */
684
+ tabId?: string
685
+ }
686
+
687
+ export interface FlashLabel {
688
+ /** Stable identity of the labelled row (`ws:<id>`, `wt:<id>`, `tab:<id>`). */
689
+ key: string
690
+ /** 1- or 2-char lowercase ASCII label. */
691
+ label: string
692
+ target: FlashJumpTarget
693
+ }
694
+
695
+ export interface ModalFlashJump extends ModalBase {
696
+ type: 'flash-jump'
697
+ labels: FlashLabel[]
698
+ /** Letters typed so far, narrowing the matching label set. */
699
+ buffer: string
700
+ /**
701
+ * Set by the reducer once the buffer narrows to a single match — read by
702
+ * app.tsx in a useEffect to perform the actual jump and close the modal.
703
+ */
704
+ pendingJump: FlashJumpTarget | null
705
+ }
706
+
707
+ export interface ModalPlugin extends ModalBase {
708
+ type: 'plugin-modal'
709
+ pluginId: string
710
+ /** Qualified id, `<pluginId>.<modalId>`; the key the registry is read by. */
711
+ modalId: string
712
+ /**
713
+ * Whatever the plugin passed when it opened the modal. Opaque to the
714
+ * reducer, which is the point: a plugin modal's state is the plugin's.
715
+ */
716
+ props?: unknown
717
+ }
718
+
719
+ export type ModalState =
720
+ | ModalClosed
721
+ | ModalNewTab
722
+ | ModalProjectPicker
723
+ | ModalProjectName
724
+ | ModalRenameTab
725
+ | ModalRenameWorkspace
726
+ | ModalSnippetPicker
727
+ | ModalThemePicker
728
+ | ModalHelp
729
+ | ModalSplitPicker
730
+ | ModalCreateProject
731
+ | ModalCreateWorkspace
732
+ | ModalSnippetEditor
733
+ | ModalGitCommit
734
+ | ModalUpdateAvailable
735
+ | ModalQuotas
736
+ | ModalWorkspaceMove
737
+ | ModalWorkspaceMoveConfirm
738
+ | ModalWorkspaceDeleteConfirm
739
+ | ModalFlashJump
740
+ | ModalSettingText
741
+ | ModalSettingsSearch
742
+ | ModalPlugin
743
+
744
+ export interface LayoutState {
745
+ terminalCols: number
746
+ terminalRows: number
747
+ }
748
+
749
+ export interface SnippetRecord {
750
+ id: string
751
+ name: string
752
+ content: string
753
+ trigger?: string
754
+ vars?: Record<string, SnippetVar>
755
+ }
756
+
757
+ export interface DiscoveredRepo {
758
+ /** Absolute path to the repo. */
759
+ path: string
760
+ /** Label shown in UI (relative to the project root or repo basename). */
761
+ name: string
762
+ /** True when the repo is the project's projectPath itself. */
763
+ isRoot: boolean
764
+ }
765
+
766
+ export interface MultiRepoState {
767
+ /** Discovered sub-repos, ordered so root (if any) comes first. */
768
+ repos: DiscoveredRepo[]
769
+ /** Precomputed disambiguating prefix per repo path — empty string for the root repo. */
770
+ prefixes: Record<string, string>
771
+ }
772
+
773
+ /**
774
+ * Where the cursor is in the settings screen. Only the cursor: the values being
775
+ * edited live in `src/settings/settings-store.ts`, or already have a home in
776
+ * this state (`gitPane`, `customCommands`, …). Duplicating them here would make
777
+ * two of them.
778
+ */
779
+ export interface SettingsUIState {
780
+ /**
781
+ * Where the cursor is in the screen's one list, counted across every section.
782
+ * The sections are headings in that list, not a column you move into, so this
783
+ * is the whole of the screen's state.
784
+ */
785
+ rowIndex: number
786
+ }
787
+
788
+ /**
789
+ * Where the cursor is on the stats screen. Nothing measured lives here — the
790
+ * numbers are read from disk by the pages that render them.
791
+ */
792
+ export interface StatsUIState {
793
+ pageIndex: number
794
+ /** Rows scrolled from the top of the current page. Reset when the page changes. */
795
+ scrollTop: number
796
+ }
797
+
798
+ export interface AppState {
799
+ tabs: TabSession[]
800
+ activeTabId: string | null
801
+ layoutTrees: Record<string, LayoutNode>
802
+ tabGroupMap: Record<string, string>
803
+ projects: ProjectRecord[]
804
+ currentProjectId: string | null
805
+ projectStatuses: Record<string, ProjectStatus>
806
+ projectBar: ProjectBarState
807
+ snippets: SnippetRecord[]
808
+ focusMode: FocusMode
809
+ bars: BarsState
810
+ gitPane: GitPaneState
811
+ modal: ModalState
812
+ layout: LayoutState
813
+ customCommands: Record<AssistantId, string>
814
+ gitPanel: GitPanelState
815
+ gitMode: GitModeState
816
+ autoCommit: AutoCommitState
817
+ multiRepo: MultiRepoState
818
+ settings: SettingsUIState
819
+ stats: StatsUIState
820
+ /**
821
+ * Commits each workspace's branch is ahead/behind the ref it forked from,
822
+ * keyed by workspace id. Ephemeral (polled); not persisted to the catalog.
823
+ */
824
+ workspaceDivergence: Record<string, BranchDivergence>
825
+ /**
826
+ * What each workspace's assistants are doing, keyed by workspace id. Covers
827
+ * every project the daemon knows, not just the current one — see
828
+ * `src/app-runtime/workspace-activity.ts`, which owns the aggregation.
829
+ * Ephemeral; not persisted to the catalog.
830
+ */
831
+ workspaceActivity: Record<string, WorkspaceActivity>
832
+ /**
833
+ * Last active tab a user viewed within each workspace, keyed by workspace id.
834
+ * Lets switching back to a workspace restore its last-viewed tab instead of
835
+ * snapping to the first one. Ephemeral (in-memory); not persisted to the catalog.
836
+ */
837
+ lastActiveTabByWorkspace: Record<string, string>
838
+ /** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
839
+ pendingChords: string[] | null
840
+ /**
841
+ * The plugin pane holding the keyboard, by its qualified id, or null when a
842
+ * terminal has it.
843
+ *
844
+ * Kept alongside `activeTabId` rather than replacing it: every reducer and
845
+ * every side effect in the app reads `activeTabId` as *a tab*, and widening
846
+ * it would mean auditing all of them for "what if this is not one". So the
847
+ * terminal stays named, the pane says it has the keys, and moving focus back
848
+ * to any tab clears this.
849
+ */
850
+ activePluginPaneId: string | null
851
+ /**
852
+ * The plugin view currently replacing the panes, by its qualified id
853
+ * (`<pluginId>.<viewId>`), or null. Only meaningful while `focusMode` is
854
+ * `plugin-view`; kept alongside rather than inside it so the focus union
855
+ * does not have to grow an arm per view.
856
+ */
857
+ activePluginView: string | null
858
+ /**
859
+ * One namespaced slice per plugin, keyed by plugin id. Opaque to the core
860
+ * reducer: a plugin registers a reducer for its own key and nothing else
861
+ * reads or writes it. Namespacing rather than a flat merge is what keeps a
862
+ * plugin from colliding with the app — or with another plugin — over a
863
+ * state key.
864
+ */
865
+ plugins: Record<string, unknown>
866
+ /**
867
+ * Bumped whenever a plugin registry changes — a widget registered, a view
868
+ * withdrawn, a fiber reloaded. Components that read a registry rather than
869
+ * the store subscribe to this so a hot reload repaints them; without it a
870
+ * reloaded widget would keep rendering its previous closure until something
871
+ * unrelated re-rendered the tree.
872
+ */
873
+ pluginRegistryVersion: number
874
+ }
875
+
876
+ // -- Git panel payloads --
877
+ export interface GitRefreshPayload {
878
+ branch: string | null
879
+ ahead: number
880
+ behind: number
881
+ files: GitFileEntry[]
882
+ }
883
+
884
+ /** Commits a workspace branch is ahead/behind the ref it forked from. */
885
+ export interface BranchDivergence {
886
+ ahead: number
887
+ behind: number
888
+ /** Lines changed since the fork point, working tree included. */
889
+ added?: number
890
+ removed?: number
891
+ }
892
+
893
+ // -- Auto-commit state --
894
+ export type AutoCommitSuggestion =
895
+ | { kind: 'idle' }
896
+ | {
897
+ kind: 'generating'
898
+ tabId: string
899
+ workingTreeHash: string
900
+ abortController: AbortController
901
+ startedAt: number
902
+ }
903
+ | {
904
+ kind: 'ready'
905
+ tabId: string
906
+ workingTreeHash: string
907
+ title: string
908
+ body: string
909
+ generatedAt: number
910
+ }
911
+
912
+ export interface AutoCommitState {
913
+ byProject: Record<string, AutoCommitSuggestion>
914
+ }
915
+
916
+ // ─── Modes, key results and side effects ──────────────────────────────────────
917
+
918
+ /**
919
+ * The modes aimux ships. `ModeId` is this plus the plugin namespace, so a
920
+ * `switch` over a built-in mode stays exhaustive while a plugin can still
921
+ * introduce one of its own.
922
+ */
923
+ export type BuiltinModeId =
924
+ | 'navigation'
925
+ | 'terminal-input'
926
+ | 'git-mode'
927
+ | 'modal.new-tab.command-edit'
928
+ | 'modal.new-tab.editing-command'
929
+ | 'modal.workspace-delete-confirm'
930
+ | 'modal.project-picker.filtering'
931
+ | 'modal.project-name'
932
+ | 'modal.create-project'
933
+ | 'modal.create-workspace'
934
+ | 'modal.rename-tab'
935
+ | 'modal.rename-workspace'
936
+ | 'modal.setting-text'
937
+ | 'modal.settings-search.filtering'
938
+ | 'modal.snippet-picker.filtering'
939
+ | 'modal.snippet-editor'
940
+ | 'modal.theme-picker.filtering'
941
+ | 'modal.help.filtering'
942
+ | 'modal.split-picker'
943
+ | 'modal.git-commit'
944
+ | 'modal.git-commit.confirm'
945
+ | 'modal.git-commit.generating'
946
+ | 'modal.update-available'
947
+ | 'modal.workspace-move'
948
+ | 'modal.workspace-move-confirm'
949
+ | 'modal.flash-jump'
950
+ | 'modal.quotas'
951
+ | 'settings'
952
+ | 'stats'
953
+
954
+ /**
955
+ * A mode a plugin owns. Namespaced by plugin id — `plugin.acme.thing.review` —
956
+ * so two plugins can each have a "review" mode, and so the owner of an
957
+ * unexpected mode id is readable from the id alone.
958
+ */
959
+ export type PluginModeId = `plugin.${string}`
960
+
961
+ export type ModeId = BuiltinModeId | PluginModeId
962
+
963
+ /**
964
+ * The effect half of the plugin envelope; see `PluginStateAction` for the
965
+ * action half. Same reasoning: the union stays closed so the 68-branch
966
+ * executor keeps its exhaustiveness check, and a plugin's effect is routed by
967
+ * `pluginId` to the handler that plugin registered.
968
+ */
969
+ export interface PluginSideEffect {
970
+ type: 'plugin-effect'
971
+ pluginId: string
972
+ effectId: string
973
+ payload?: unknown
974
+ }
975
+
976
+ export type SideEffect =
977
+ | { type: 'quit'; state: AppState }
978
+ | { type: 'open-new-tab' }
979
+ | { type: 'launch-selected-assistant' }
980
+ | { type: 'edit-selected-assistant' }
981
+ | { type: 'confirm-selected-project' }
982
+ | { type: 'delete-selected-project' }
983
+ | { type: 'open-rename-selected-project' }
984
+ | { type: 'create-project'; name: string; projectPath?: string }
985
+ | { type: 'create-workspace' }
986
+ | { type: 'load-create-workspace-base-branches' }
987
+ | { type: 'close-tab'; tabId: string }
988
+ | { type: 'restart-tab'; tab: TabSession }
989
+ | { type: 'paste-selected-snippet' }
990
+ | { type: 'paste-snippet-to-group' }
991
+ | { type: 'edit-selected-snippet' }
992
+ | { type: 'delete-selected-snippet' }
993
+ | { type: 'save-snippet-editor' }
994
+ | { type: 'save-custom-command' }
995
+ | { type: 'apply-theme'; action: 'open' }
996
+ | { type: 'apply-theme'; action: 'restore' }
997
+ | { type: 'apply-theme'; action: 'confirm' }
998
+ | { type: 'apply-theme'; action: 'preview'; delta: 1 | -1 }
999
+ | { type: 'rename-project'; projectId: string; name: string }
1000
+ | { type: 'rename-tab'; tabId: string; title: string }
1001
+ | {
1002
+ type: 'split-pane'
1003
+ direction: SplitDirection
1004
+ sourceTabId?: string
1005
+ }
1006
+ | { type: 'confirm-split' }
1007
+ | { type: 'scroll-git-diff'; delta: number }
1008
+ | { type: 'persist-git-diff-mode-ratio'; ratio: number }
1009
+ | { type: 'persist-git-file-list-mode'; mode: GitFileListMode }
1010
+ | { type: 'persist-git-tree-compaction'; enabled: boolean }
1011
+ | { type: 'git-stage'; path: string }
1012
+ | { type: 'git-unstage'; path: string }
1013
+ | { type: 'git-stage-all' }
1014
+ | { type: 'git-unstage-all' }
1015
+ | { type: 'git-restore'; path: string }
1016
+ | { type: 'git-rm'; path: string }
1017
+ | { type: 'git-commit'; title: string; body: string }
1018
+ | { type: 'git-commit-auto'; title: string; body: string }
1019
+ | { type: 'generate-auto-commit-now'; projectId: string }
1020
+ | { type: 'git-push' }
1021
+ | { type: 'confirm-update-selection' }
1022
+ | { type: 'switch-project-by-index'; index: number; workspaceId?: string }
1023
+ | { type: 'cycle-sidebar-item'; direction: 1 | -1 }
1024
+ | { type: 'switch-tab-by-index'; index: number }
1025
+ | { type: 'delete-project'; projectId: string }
1026
+ | {
1027
+ type: 'delete-workspace'
1028
+ projectId: string
1029
+ workspaceId: string
1030
+ // Force the git worktree removal (discards uncommitted changes in the
1031
+ // workspace). Also implies closing the workspace's tabs.
1032
+ force?: boolean
1033
+ // Close the workspace's tabs without forcing the git removal. Lets the
1034
+ // sidebar "Remove workspace" clean up tabs (avoiding orphans) while still
1035
+ // refusing to discard uncommitted work in a temp workspace.
1036
+ closeTabs?: boolean
1037
+ }
1038
+ | {
1039
+ type: 'move-workspace'
1040
+ projectId: string
1041
+ sourceWorkspaceId: string
1042
+ targetWorkspaceId: string
1043
+ deleteSource?: boolean
1044
+ // Retry flags set by the workspace-move-confirm dialog.
1045
+ stashTarget?: boolean
1046
+ keepConflicts?: boolean
1047
+ }
1048
+ | { type: 'load-workspace-move-stats' }
1049
+ | { type: 'hibernate-workspace'; workspaceId: string }
1050
+ | { type: 'toggle-transparent' }
1051
+ | { type: 'toggle-mode' }
1052
+ | { type: 'open-file-in-editor'; path: string }
1053
+ | { type: 'open-selected-snippet-source-in-editor' }
1054
+ | { type: 'run-setup' }
1055
+ | { type: 'stop-setup' }
1056
+ | { type: 'configure-setup-script'; projectId?: string }
1057
+ | { type: 'set-project-default-base-ref'; projectId: string; baseRef: string }
1058
+ | { type: 'toggle-project-collapsed'; projectId: string }
1059
+ | { type: 'ask-agent-for-setup-script' }
1060
+ | { type: 'promote-setup-tab' }
1061
+ /** Toggle a checkbox, cycle an enum, run a row's action — whatever the row is. */
1062
+ | { type: 'activate-settings-row' }
1063
+ | { type: 'adjust-settings-row'; delta: 1 | -1 }
1064
+ | { type: 'reset-settings-row' }
1065
+ | { type: 'confirm-settings-search' }
1066
+ | { type: 'commit-setting-text'; settingId: string; value: string }
1067
+ | PluginSideEffect
1068
+
1069
+ export interface KeyResult {
1070
+ actions: AppAction[]
1071
+ effects: SideEffect[]
1072
+ transition?: ModeId
1073
+ }
1074
+
1075
+ export interface ModeContext {
1076
+ readonly state: AppState
1077
+ }
1078
+
1079
+ /**
1080
+ * The subset of opentui's `KeyEvent` the keymap layer reads. Declared
1081
+ * structurally rather than as a `Pick<KeyEvent, …>` so this package keeps no
1082
+ * runtime dependency on `@opentui/core`; `src/input/modes/types.ts` asserts
1083
+ * the two agree, so a change on opentui's side fails the build here.
1084
+ */
1085
+ export interface KeyInput {
1086
+ name: string
1087
+ ctrl: boolean
1088
+ meta: boolean
1089
+ shift: boolean
1090
+ sequence: string
1091
+ }
1092
+
1093
+ // ─── AppAction union ──────────────────────────────────────────────────────────
1094
+
1095
+ export type ModalAction =
1096
+ | {
1097
+ type: 'move-modal-cursor'
1098
+ delta?: number
1099
+ to?: 'end' | 'home' | 'line-down' | 'line-up' | 'word-left' | 'word-right'
1100
+ }
1101
+ | {
1102
+ type: 'open-new-tab-modal'
1103
+ pendingWorkspace?: PendingWorkspaceLaunch
1104
+ pendingPrompt?: string
1105
+ }
1106
+ | { type: 'open-edit-custom-command'; assistantId: AssistantId }
1107
+ | { type: 'open-help-modal'; scope?: ModeId }
1108
+ | { type: 'open-split-picker'; direction: SplitDirection }
1109
+ | { type: 'open-project-picker' }
1110
+ | {
1111
+ type: 'open-project-name-modal'
1112
+ projectTargetId?: string
1113
+ initialName?: string
1114
+ returnToProjectPicker?: boolean
1115
+ }
1116
+ | { type: 'close-modal' }
1117
+ | { type: 'move-modal-selection'; delta: number }
1118
+ | { type: 'update-command-edit'; char: string }
1119
+ | { type: 'cancel-command-edit' }
1120
+ | { type: 'open-create-project-modal'; returnToProjectPicker: boolean }
1121
+ | { type: 'open-create-workspace-modal' }
1122
+ | { type: 'set-create-workspace-base-branches'; branches: string[]; defaultBranch?: string }
1123
+ | { type: 'set-create-workspace-branch-error'; message: string | null }
1124
+ | { type: 'switch-create-workspace-field' }
1125
+ | { type: 'set-directory-results'; results: DirectoryResult[] }
1126
+ | { type: 'switch-create-project-field' }
1127
+ | { type: 'select-directory' }
1128
+ | { type: 'open-rename-tab-modal' }
1129
+ | {
1130
+ type: 'open-rename-workspace-modal'
1131
+ projectId: string
1132
+ workspaceId: string
1133
+ initialName: string
1134
+ }
1135
+ | { type: 'open-snippet-picker'; returnTo?: FocusMode }
1136
+ | { type: 'open-snippet-editor'; snippetId?: string }
1137
+ | { type: 'set-help-entry-count'; count: number }
1138
+ | { type: 'set-theme-entry-count'; count: number }
1139
+ | { type: 'open-theme-picker'; returnTo?: FocusMode }
1140
+ | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
1141
+ | { type: 'open-quotas-modal' }
1142
+ | { type: 'set-modal-selection-index'; index: number }
1143
+ | { type: 'open-workspace-move-modal'; sourceWorkspaceId: string }
1144
+ | { type: 'toggle-workspace-move-delete' }
1145
+ | { type: 'set-workspace-move-stats'; dirtyFiles: Record<string, number> }
1146
+ | {
1147
+ type: 'open-workspace-move-confirm'
1148
+ variant: 'stash-target' | 'keep-conflicts'
1149
+ files: string[]
1150
+ projectId: string
1151
+ sourceWorkspaceId: string
1152
+ targetWorkspaceId: string
1153
+ deleteSource: boolean
1154
+ sourceLabel: string
1155
+ targetLabel: string
1156
+ }
1157
+ | {
1158
+ type: 'open-workspace-delete-confirm'
1159
+ projectId: string
1160
+ workspaceId: string
1161
+ workspaceLabel: string
1162
+ reason?: string
1163
+ closeTabs: boolean
1164
+ force: boolean
1165
+ }
1166
+ | { type: 'open-flash-jump-modal' }
1167
+ | { type: 'clear-flash-jump-pending' }
1168
+
1169
+ export type ProjectAction =
1170
+ | {
1171
+ type: 'load-project'
1172
+ projectId: string
1173
+ projectSnapshot?: ProjectSnapshotV1
1174
+ forceDisconnected?: boolean
1175
+ }
1176
+ | { type: 'set-projects'; projects: ProjectRecord[] }
1177
+ | { type: 'create-project-record'; project: ProjectRecord }
1178
+ | { type: 'rename-project-record'; projectId: string; name: string }
1179
+ | { type: 'delete-project-record'; projectId: string; openProjectPicker?: boolean }
1180
+ | { type: 'reorder-projects'; orderedIds: string[] }
1181
+ | { type: 'reorder-active-project'; delta: number }
1182
+ | { type: 'set-project-status'; projectId: string; status: ProjectStatus }
1183
+ /** What a workspace's assistants are doing. `done` is owned by the reducer,
1184
+ * so this carries only the two flags derived from tab statuses. */
1185
+ | { type: 'set-workspace-activity'; workspaceId: string; working: boolean; waiting: boolean }
1186
+ /** An assistant in this workspace finished a turn and nobody has looked yet. */
1187
+ | { type: 'mark-workspace-done'; workspaceId: string }
1188
+ /** The tab that rang: it finished a turn while the user was elsewhere. */
1189
+ | { type: 'mark-tab-unseen'; tabId: string }
1190
+ | {
1191
+ type: 'add-workspace-record'
1192
+ projectId: string
1193
+ workspace: WorkspaceRecord
1194
+ activate?: boolean
1195
+ }
1196
+ | { type: 'set-active-workspace'; projectId: string; workspaceId: string }
1197
+ | {
1198
+ type: 'update-workspace-record'
1199
+ projectId: string
1200
+ workspaceId: string
1201
+ patch: Partial<WorkspaceRecord>
1202
+ }
1203
+
1204
+ export type TabAction =
1205
+ | { type: 'add-tab'; tab: TabSession }
1206
+ | {
1207
+ type: 'hydrate-project'
1208
+ tabs: TabSession[]
1209
+ activeTabId: string | null
1210
+ layoutTree?: LayoutNode | null
1211
+ layoutTrees?: Record<string, LayoutNode>
1212
+ tabGroupMap?: Record<string, string>
1213
+ }
1214
+ | { type: 'close-tab'; tabId: string }
1215
+ | { type: 'close-active-tab' }
1216
+ | { type: 'set-active-tab'; tabId: string }
1217
+ | { type: 'move-active-tab'; delta: number }
1218
+ | { type: 'reorder-active-tab'; delta: number }
1219
+ | { type: 'reorder-tabs'; orderedTabIds: string[] }
1220
+ | { type: 'reset-tab-project'; tabId: string }
1221
+ | { type: 'hibernate-tab'; tabId: string }
1222
+ | {
1223
+ type: 'rename-tab'
1224
+ tabId: string
1225
+ title: string
1226
+ autoRenameStatus?: 'eligible' | 'attempted'
1227
+ }
1228
+ | {
1229
+ type: 'update-tab-metadata'
1230
+ tabId: string
1231
+ title?: string
1232
+ autoRenameStatus?: 'eligible' | 'attempted'
1233
+ /** Set to false to promote a hidden tab into the normal tab strip. */
1234
+ hidden?: boolean
1235
+ }
1236
+ | { type: 'append-tab-buffer'; tabId: string; chunk: string }
1237
+ | {
1238
+ type: 'replace-tab-viewport'
1239
+ tabId: string
1240
+ viewport: TerminalSnapshot
1241
+ terminalModes: TerminalModeState
1242
+ source?: 'resize' | 'scroll' | 'data' | 'switch'
1243
+ }
1244
+ | { type: 'set-tab-activity'; tabId: string; activity?: TabActivity }
1245
+ | { type: 'set-tab-error'; tabId: string; message: string }
1246
+
1247
+ export type LayoutAction =
1248
+ | {
1249
+ type: 'split-pane'
1250
+ direction: SplitDirection
1251
+ newTab: TabSession
1252
+ }
1253
+ | { type: 'close-pane'; tabId: string }
1254
+ | {
1255
+ type: 'focus-pane-direction'
1256
+ direction: 'left' | 'right' | 'up' | 'down'
1257
+ }
1258
+ | {
1259
+ type: 'resize-pane'
1260
+ tabId: string
1261
+ delta: number
1262
+ axis?: SplitDirection
1263
+ }
1264
+ | {
1265
+ type: 'set-split-ratio'
1266
+ tabId: string
1267
+ ratio: number
1268
+ axis?: SplitDirection
1269
+ }
1270
+ /**
1271
+ * Splits the active pane and puts a plugin's renderer beside it. Two
1272
+ * concrete arms rather than another routed `plugin-*` variant: where a pane
1273
+ * goes and when it closes is the layout's business, not the plugin's, and
1274
+ * the reducer has to reason about both.
1275
+ */
1276
+ | {
1277
+ type: 'open-plugin-pane'
1278
+ /** Qualified `<pluginId>.<paneId>`. */
1279
+ paneId: string
1280
+ direction: SplitDirection
1281
+ }
1282
+ | { type: 'close-plugin-pane'; paneId: string }
1283
+
1284
+ export type UIAction =
1285
+ | { type: 'toggle-bar'; side: BarSide }
1286
+ | { type: 'resize-bar'; side: BarSide; delta: number }
1287
+ | { type: 'set-bar-width'; side: BarSide; width: number }
1288
+ | { type: 'toggle-widget'; widgetId: string }
1289
+ | {
1290
+ type: 'add-widget'
1291
+ widgetId: string
1292
+ side: BarSide
1293
+ /** Defaults to the end of the bar. */
1294
+ index?: number
1295
+ grow?: number
1296
+ placedBy?: 'plugin'
1297
+ }
1298
+ | { type: 'remove-plugin-widget'; widgetId: string }
1299
+ | { type: 'move-widget'; widgetId: string; side: BarSide; index: number }
1300
+ | { type: 'set-bar-boundary'; side: BarSide; index: number; ratio: number }
1301
+ | { type: 'resize-widget'; widgetId: string; delta: number }
1302
+ | { type: 'set-focus-mode'; focusMode: FocusMode }
1303
+ | { type: 'set-terminal-size'; cols: number; rows: number }
1304
+ | { type: 'resize-git-diff-pane'; delta: number }
1305
+ | { type: 'set-pending-chords'; chords: string[] | null }
1306
+ | { type: 'toggle-project-bar' }
1307
+
1308
+ export type SettingsAction =
1309
+ | { type: 'enter-settings' }
1310
+ | { type: 'exit-settings' }
1311
+ | { type: 'settings-move-selection'; delta: -1 | 1 }
1312
+ | { type: 'settings-jump-section'; delta: -1 | 1 }
1313
+ | { type: 'settings-select-row'; rowIndex: number }
1314
+ | { type: 'open-settings-search' }
1315
+ | { type: 'open-setting-text-modal'; settingId: string; label: string; value: string }
1316
+
1317
+ export type StatsAction =
1318
+ | { type: 'enter-stats' }
1319
+ | { type: 'exit-stats' }
1320
+ | { type: 'stats-move-page'; delta: -1 | 1 }
1321
+ | { type: 'stats-select-page'; pageIndex: number }
1322
+ /** Rows, not pixels: the view holds a scroll offset the page applies to its box. */
1323
+ | { type: 'stats-scroll'; delta: number }
1324
+ /** The offset the scrollbox actually accepted, sent back so the state cannot run past the page. */
1325
+ | { type: 'stats-scroll-settled'; scrollTop: number }
1326
+
1327
+ export type GitPanelAction =
1328
+ | { type: 'git-refresh-success'; payload: GitRefreshPayload }
1329
+ | { type: 'git-refresh-error'; kind: GitPanelError }
1330
+ | { type: 'git-panel-reset' }
1331
+ | { type: 'set-workspace-divergence'; divergence: Record<string, BranchDivergence> }
1332
+ | { type: 'set-git-pane'; patch: Partial<GitPaneState> }
1333
+
1334
+ export type AutoCommitAction =
1335
+ | {
1336
+ type: 'auto-commit-generation-started'
1337
+ projectId: string
1338
+ tabId: string
1339
+ workingTreeHash: string
1340
+ abortController: AbortController
1341
+ startedAt: number
1342
+ }
1343
+ | {
1344
+ type: 'auto-commit-generation-ready'
1345
+ projectId: string
1346
+ workingTreeHash: string
1347
+ title: string
1348
+ body: string
1349
+ generatedAt: number
1350
+ }
1351
+ | { type: 'auto-commit-clear'; projectId: string }
1352
+
1353
+ export type GitModeAction =
1354
+ | { type: 'enter-git-mode' }
1355
+ | { type: 'exit-git-mode' }
1356
+ | { type: 'git-mode-move-selection'; delta: -1 | 1 }
1357
+ | { type: 'git-mode-move-file-selection'; delta: -1 | 1 }
1358
+ | { type: 'git-mode-select-entry-by-key'; key: string }
1359
+ | { type: 'git-mode-toggle-folder'; key: string }
1360
+ | { type: 'git-mode-toggle-selected-folder' }
1361
+ | { type: 'git-mode-collapse-selection' }
1362
+ | { type: 'git-mode-expand-selection' }
1363
+ | { type: 'git-mode-toggle-file-list-mode' }
1364
+ | { type: 'git-mode-toggle-tree-compaction' }
1365
+ | { type: 'git-mode-set-diff'; key: string; diff: DiffData; hash: string }
1366
+ | {
1367
+ type: 'git-mode-set-parsed'
1368
+ key: string
1369
+ hash: string
1370
+ file: unknown
1371
+ }
1372
+ | {
1373
+ type: 'git-mode-set-highlights'
1374
+ key: string
1375
+ hash: string
1376
+ themeId: string
1377
+ add: unknown
1378
+ del: unknown
1379
+ }
1380
+ | {
1381
+ type: 'git-mode-merge-highlights'
1382
+ key: string
1383
+ hash: string
1384
+ themeId: string
1385
+ add: { start: number; tokens: unknown }[]
1386
+ del: { start: number; tokens: unknown }[]
1387
+ }
1388
+ | { type: 'git-mode-invalidate-diffs'; paths: string[] }
1389
+ | { type: 'git-mode-set-loading'; key: string; loading: boolean }
1390
+ | { type: 'git-mode-set-pending-delete'; path: string | null }
1391
+ | { type: 'git-mode-clear-diff-cache'; path: string }
1392
+ | { type: 'git-mode-set-message'; message: string | null }
1393
+ | { type: 'snippet-picker-set-message'; message: string | null }
1394
+ | { type: 'git-mode-toggle-diff-view' }
1395
+ | { type: 'git-mode-toggle-review-base' }
1396
+ | { type: 'git-mode-shift-head-offset'; delta: number }
1397
+ | { type: 'git-mode-set-head-offset'; offset: number }
1398
+ | {
1399
+ type: 'git-mode-fold-adjust'
1400
+ key: string
1401
+ foldId: string
1402
+ side: 'top' | 'bottom'
1403
+ delta: number
1404
+ }
1405
+ | { type: 'git-mode-fold-set'; key: string; foldId: string; top: number; bottom: number }
1406
+ | { type: 'git-mode-fold-toggle-all'; key: string }
1407
+ | {
1408
+ type: 'git-mode-optimistic-move'
1409
+ path: string
1410
+ fromSection: GitFileSection
1411
+ toSection: GitFileSection | null
1412
+ }
1413
+ | { type: 'open-git-commit-modal'; projectId?: string }
1414
+ | { type: 'git-commit-enter-confirm' }
1415
+ | { type: 'git-commit-leave-confirm' }
1416
+ | { type: 'git-commit-enter-generating'; projectId: string }
1417
+ | { type: 'git-commit-leave-generating' }
1418
+ | { type: 'git-commit-use-background-suggestion'; projectId: string }
1419
+
1420
+ export type DataAction =
1421
+ | { type: 'set-snippets'; snippets: SnippetRecord[] }
1422
+ | { type: 'delete-snippet'; snippetId: string }
1423
+ | { type: 'set-custom-commands'; customCommands: Record<AssistantId, string> }
1424
+
1425
+ export type MultiRepoAction =
1426
+ | { type: 'multi-repo-set-repos'; repos: DiscoveredRepo[] }
1427
+ | { type: 'multi-repo-clear' }
1428
+
1429
+ /**
1430
+ * The one variant a plugin dispatches. `AppAction` stays a closed union — the
1431
+ * exhaustiveness check across every reducer is worth more than the ability to
1432
+ * add arms to it — so a plugin's own action travels inside this envelope and
1433
+ * is routed to that plugin's slice reducer by `pluginId`.
1434
+ */
1435
+ export type PluginStateAction =
1436
+ | { type: 'plugin-action'; pluginId: string; actionId: string; payload?: unknown }
1437
+ /** Replaces a plugin's whole slice. Used on load, and on unload to drop it. */
1438
+ | { type: 'set-plugin-slice'; pluginId: string; slice: unknown }
1439
+ /** Signals that some plugin registry changed; see `pluginRegistryVersion`. */
1440
+ | { type: 'bump-plugin-registry' }
1441
+ /** Replaces the panes with a plugin's full-screen view. */
1442
+ | { type: 'open-plugin-view'; viewId: string }
1443
+ /** Returns from a plugin view to the panes. */
1444
+ | { type: 'close-plugin-view' }
1445
+ /** Opens a plugin's modal over whatever is on screen. `close-modal` closes it. */
1446
+ | {
1447
+ type: 'open-plugin-modal'
1448
+ pluginId: string
1449
+ modalId: string
1450
+ props?: unknown
1451
+ returnTo?: FocusMode
1452
+ }
1453
+
1454
+ export type AppAction =
1455
+ | ModalAction
1456
+ | ProjectAction
1457
+ | TabAction
1458
+ | LayoutAction
1459
+ | UIAction
1460
+ | SettingsAction
1461
+ | StatsAction
1462
+ | DataAction
1463
+ | GitPanelAction
1464
+ | GitModeAction
1465
+ | AutoCommitAction
1466
+ | MultiRepoAction
1467
+ | PluginStateAction