@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.
- package/package.json +1 -1
- package/src/actions.ts +1 -1
- package/src/app-types.ts +1467 -0
- package/src/index.ts +24 -13
- package/src/keymap-builder.ts +6 -0
- package/src/plugin-actions-runtime.ts +50 -0
- package/src/resolver.ts +22 -0
- package/src/settings-types.ts +160 -0
- package/src/tui/index.ts +11 -1
- package/src/tui/registry.ts +46 -3
- package/src/types.ts +95 -994
package/src/app-types.ts
ADDED
|
@@ -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
|