@brimveyn/aimux-config 0.5.13 → 0.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux-config",
3
- "version": "0.5.13",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript configuration API for aimux — keymaps, themes, and backends with a fluent builder.",
5
5
  "keywords": [
6
6
  "aimux",
package/src/actions.ts CHANGED
@@ -80,7 +80,7 @@ export const splitHorizontal: KeyResult = r(
80
80
  )
81
81
 
82
82
  export const enterInsert: ActionFn = (ctx: ModeContext) => {
83
- if (!ctx.state.activeTabId) return null
83
+ if (!(ctx.state.activeTabId != null && ctx.state.activeTabId !== '')) return null
84
84
  return r([{ focusMode: 'terminal-input', type: 'set-focus-mode' }], [], 'terminal-input')
85
85
  }
86
86
 
@@ -88,13 +88,15 @@ export const closeModal: KeyResult = r([{ type: 'close-modal' }], [], 'navigatio
88
88
 
89
89
  export const closeOverlayModal: KeyResult = r([{ type: 'close-modal' }])
90
90
 
91
+ export const cancelNewTabModal: KeyResult = r([{ type: 'cancel-command-edit' }])
92
+
91
93
  // ---------------------------------------------------------------------------
92
94
  // Dynamic actions (need ctx at runtime — ActionFn)
93
95
  // ---------------------------------------------------------------------------
94
96
 
95
97
  export const closeTab: ActionFn = (ctx: ModeContext) => {
96
98
  const tabId = ctx.state.activeTabId
97
- if (!tabId) return null
99
+ if (!(tabId != null && tabId !== '')) return null
98
100
  return r([{ type: 'close-active-tab' }], [{ tabId, type: 'close-tab' }])
99
101
  }
100
102
 
@@ -153,7 +155,7 @@ export function focusPane(direction: 'left' | 'right' | 'up' | 'down'): KeyResul
153
155
  export function resizePane(delta: number, axis: 'horizontal' | 'vertical'): ActionFn {
154
156
  return (ctx: ModeContext) => {
155
157
  const tabId = ctx.state.activeTabId
156
- if (!tabId) return null
158
+ if (!(tabId != null && tabId !== '')) return null
157
159
  return r([{ axis, delta, tabId, type: 'resize-pane' }])
158
160
  }
159
161
  }
@@ -173,7 +175,45 @@ export function moveModalSelectionWithPreview(
173
175
  // Modal-specific actions
174
176
  // ---------------------------------------------------------------------------
175
177
 
176
- export const launchSelectedAssistant: KeyResult = r([], [{ type: 'launch-selected-assistant' }])
178
+ export const launchSelectedAssistant: ActionFn = (ctx: ModeContext) => {
179
+ if (ctx.state.modal.type === 'new-tab' && ctx.state.modal.step === 'assistant') {
180
+ return r([{ type: 'select-new-tab-assistant' }])
181
+ }
182
+ if (
183
+ ctx.state.modal.type === 'new-tab' &&
184
+ ctx.state.modal.step === 'worktree' &&
185
+ ctx.state.modal.createWorktree
186
+ ) {
187
+ return r([{ type: 'enter-new-tab-worktree-create' }])
188
+ }
189
+ return r([], [{ type: 'launch-selected-assistant' }])
190
+ }
191
+
192
+ export const toggleNewTabWorktree: KeyResult = r([{ type: 'toggle-new-tab-worktree' }])
193
+
194
+ export const deleteSelectedWorktree: ActionFn = (ctx: ModeContext) => {
195
+ const modal = ctx.state.modal
196
+ const sessionId = ctx.state.currentSessionId
197
+ if (modal.type !== 'new-tab' || modal.step !== 'worktree' || modal.createWorktree) return null
198
+ if (!(sessionId != null && sessionId !== '')) return null
199
+ const session = ctx.state.sessions.find((entry) => entry.id === sessionId)
200
+ const worktree = session?.worktrees?.[modal.selectedIndex]
201
+ if (!worktree) return null
202
+ return r(
203
+ [
204
+ { index: modal.selectedIndex, type: 'set-modal-selection-index' },
205
+ { message: null, type: 'set-new-tab-worktree-delete-state' },
206
+ ],
207
+ [
208
+ {
209
+ force: modal.worktreeDeleteConfirmId === worktree.id,
210
+ sessionId,
211
+ type: 'delete-worktree',
212
+ worktreeId: worktree.id,
213
+ },
214
+ ]
215
+ )
216
+ }
177
217
 
178
218
  export const cancelCommandEdit = (returnTo: KeyResult['transition']): KeyResult =>
179
219
  r([{ type: 'cancel-command-edit' }], [], returnTo)
@@ -201,6 +241,10 @@ export const openSnippetEditor: KeyResult = r(
201
241
 
202
242
  export const editSelectedSnippet: KeyResult = r([], [{ type: 'edit-selected-snippet' }])
203
243
  export const deleteSelectedSnippet: KeyResult = r([], [{ type: 'delete-selected-snippet' }])
244
+ export const openSelectedSnippetSourceInEditor: KeyResult = r(
245
+ [],
246
+ [{ type: 'open-selected-snippet-source-in-editor' }]
247
+ )
204
248
 
205
249
  export const pasteSelectedSnippet: KeyResult = r(
206
250
  [{ type: 'close-modal' }],
@@ -278,6 +322,12 @@ export const cancelEditCustomCommand: KeyResult = r(
278
322
  'modal.new-tab.command-edit'
279
323
  )
280
324
 
325
+ export const editSelectedAssistant: KeyResult = r(
326
+ [],
327
+ [{ type: 'edit-selected-assistant' }],
328
+ 'modal.new-tab.editing-command'
329
+ )
330
+
281
331
  // ---------------------------------------------------------------------------
282
332
  // Update-available modal
283
333
  // ---------------------------------------------------------------------------
@@ -288,9 +338,66 @@ export const confirmUpdateSelection: KeyResult = r(
288
338
  'navigation'
289
339
  )
290
340
 
341
+ // ---------------------------------------------------------------------------
342
+ // Worktree-move modal
343
+ // ---------------------------------------------------------------------------
344
+
345
+ export const openWorktreeMove: ActionFn = (ctx: ModeContext) => {
346
+ if (ctx.state.gitMode.headOffset > 0) {
347
+ return r([
348
+ {
349
+ message: 'disabled while viewing HEAD~N (press 0 or [ to return)',
350
+ type: 'git-mode-set-message',
351
+ },
352
+ ])
353
+ }
354
+ const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId)
355
+ const worktrees = session?.worktrees ?? []
356
+ // From git mode, the source is the active worktree (the one being reviewed).
357
+ const source = worktrees.find((w) => w.id === session?.activeWorktreeId) ?? worktrees[0]
358
+ const hasBranch = source != null && source.branch != null && source.branch !== ''
359
+ const others = worktrees.filter((w) => w.id !== source?.id)
360
+ if (!hasBranch || source == null || others.length < 1) {
361
+ return r([{ message: 'no other worktree to move into', type: 'git-mode-set-message' }])
362
+ }
363
+ // Overlay: no mode transition — deriveModeId routes input to the picker and
364
+ // focusMode stays 'git' so the git view remains mounted underneath.
365
+ return r([{ sourceWorktreeId: source.id, type: 'open-worktree-move-modal' }])
366
+ }
367
+
368
+ export const toggleWorktreeMoveDelete: KeyResult = r([{ type: 'toggle-worktree-move-delete' }])
369
+
370
+ export const confirmWorktreeMove: ActionFn = (ctx: ModeContext) => {
371
+ const modal = ctx.state.modal
372
+ const session = ctx.state.sessions.find((entry) => entry.id === ctx.state.currentSessionId)
373
+ const worktrees = session?.worktrees ?? []
374
+ const sourceId = modal.type === 'worktree-move' ? modal.sourceWorktreeId : undefined
375
+ const source = worktrees.find((w) => w.id === sourceId)
376
+ const targets = worktrees.filter((w) => w.id !== sourceId)
377
+ const selectedIndex = modal.type === 'worktree-move' ? modal.selectedIndex : 0
378
+ const target = targets[selectedIndex]
379
+ if (!target || !session || !source || modal.type !== 'worktree-move') {
380
+ return r([{ type: 'close-modal' }])
381
+ }
382
+ // Overlay close keeps focusMode 'git' (see close-modal reducer), so the move
383
+ // result lands back in the git view that was underneath the picker.
384
+ return r(
385
+ [{ type: 'close-modal' }],
386
+ [
387
+ {
388
+ deleteSource: modal.deleteSource,
389
+ sessionId: session.id,
390
+ sourceWorktreeId: source.id,
391
+ targetWorktreeId: target.id,
392
+ type: 'move-worktree',
393
+ },
394
+ ]
395
+ )
396
+ }
397
+
291
398
  export const closePane: ActionFn = (ctx: ModeContext) => {
292
399
  const tabId = ctx.state.activeTabId
293
- if (!tabId) return null
400
+ if (!(tabId != null && tabId !== '')) return null
294
401
  return r(
295
402
  [
296
403
  { tabId, type: 'close-pane' },
@@ -335,7 +442,9 @@ export const confirmSessionRename: ActionFn = (ctx: ModeContext) => {
335
442
  ? 'modal.session-picker.filtering'
336
443
  : 'navigation'
337
444
  const effects: KeyResult['effects'] =
338
- trimmed && sessionId ? [{ name: trimmed, sessionId, type: 'rename-session' }] : []
445
+ trimmed && sessionId != null && sessionId !== ''
446
+ ? [{ name: trimmed, sessionId, type: 'rename-session' }]
447
+ : []
339
448
  return r([closeAction], effects, transition)
340
449
  }
341
450
 
@@ -344,7 +453,7 @@ export const confirmRenameTab: ActionFn = (ctx: ModeContext) => {
344
453
  const trimmed = (ctx.state.modal.editBuffer ?? '').trim()
345
454
  const tabId = ctx.state.modal.sessionTargetId
346
455
  const actions: KeyResult['actions'] = []
347
- if (trimmed && tabId) {
456
+ if (trimmed && tabId != null && tabId !== '') {
348
457
  actions.push({ tabId, title: trimmed, type: 'rename-tab' })
349
458
  }
350
459
  actions.push({ type: 'close-modal' })
@@ -377,14 +486,14 @@ export const confirmCreateSession: ActionFn = (ctx: ModeContext) => {
377
486
  }
378
487
 
379
488
  function getDefaultSessionName(projectPath?: string): string {
380
- if (!projectPath) return ''
489
+ if (!(projectPath != null && projectPath !== '')) return ''
381
490
  const segments = projectPath.split('/').filter(Boolean)
382
491
  return segments.at(-1) ?? ''
383
492
  }
384
493
 
385
494
  // Session picker escape (conditional)
386
495
  export const sessionPickerEscape: ActionFn = (ctx: ModeContext) => {
387
- if (!ctx.state.currentSessionId) return null
496
+ if (!(ctx.state.currentSessionId != null && ctx.state.currentSessionId !== '')) return null
388
497
  return r([{ type: 'close-modal' }], [], 'navigation')
389
498
  }
390
499
 
@@ -410,15 +519,19 @@ function clearPendingDelete(ctx: ModeContext): AppAction[] {
410
519
  return [{ path: null, type: 'git-mode-set-pending-delete' }]
411
520
  }
412
521
 
413
- function gitFileKey(section: string, path: string): string {
414
- return `${section}:${path}`
522
+ function gitFileKey(section: string, path: string, repoPath?: string): string {
523
+ return repoPath != null && repoPath !== ''
524
+ ? `${section}:${repoPath}:${path}`
525
+ : `${section}:${path}`
415
526
  }
416
527
 
417
528
  function selectedGitFile(ctx: ModeContext) {
418
529
  const key = ctx.state.gitMode.selectedEntryKey
419
- if (!key) return null
530
+ if (!(key != null && key !== '')) return null
420
531
  return (
421
- ctx.state.gitPanel.files.find((file) => gitFileKey(file.section, file.path) === key) ?? null
532
+ ctx.state.gitPanel.files.find(
533
+ (file) => gitFileKey(file.section, file.path, file.repoPath) === key
534
+ ) ?? null
422
535
  )
423
536
  }
424
537
 
@@ -439,17 +552,20 @@ export function selectGitFileOnly(delta: -1 | 1): ActionFn {
439
552
  }
440
553
 
441
554
  export const toggleSelectedGitFolder: ActionFn = (ctx: ModeContext) => {
442
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
555
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
556
+ return r([])
443
557
  return r([{ type: 'git-mode-toggle-selected-folder' }])
444
558
  }
445
559
 
446
560
  export const collapseGitSelection: ActionFn = (ctx: ModeContext) => {
447
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
561
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
562
+ return r([])
448
563
  return r([{ type: 'git-mode-collapse-selection' }])
449
564
  }
450
565
 
451
566
  export const expandGitSelection: ActionFn = (ctx: ModeContext) => {
452
- if (!ctx.state.gitMode.selectedEntryKey) return r([])
567
+ if (!(ctx.state.gitMode.selectedEntryKey != null && ctx.state.gitMode.selectedEntryKey !== ''))
568
+ return r([])
453
569
  return r([{ type: 'git-mode-expand-selection' }])
454
570
  }
455
571
 
@@ -475,6 +591,8 @@ export function scrollGitDiff(delta: number): KeyResult {
475
591
 
476
592
  export const toggleGitDiffView: KeyResult = r([{ type: 'git-mode-toggle-diff-view' }])
477
593
 
594
+ export const toggleGitReviewBase: KeyResult = r([{ type: 'git-mode-toggle-review-base' }])
595
+
478
596
  export function shiftGitHeadOffset(delta: number): KeyResult {
479
597
  return r([
480
598
  { delta, type: 'git-mode-shift-head-offset' },
@@ -551,7 +669,7 @@ export const gitDestructiveSelected: ActionFn = (ctx: ModeContext) => {
551
669
  export const gitToggleFoldAll: ActionFn = (ctx: ModeContext) => {
552
670
  const file = selectedGitFile(ctx)
553
671
  if (!file) return r([])
554
- const key = gitFileKey(file.section, file.path)
672
+ const key = gitFileKey(file.section, file.path, file.repoPath)
555
673
  return r([{ key, type: 'git-mode-fold-toggle-all' }])
556
674
  }
557
675
 
@@ -677,7 +795,7 @@ export const gitCommitEnterConfirm: ActionFn = (ctx: ModeContext) => {
677
795
  return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm')
678
796
  }
679
797
  const sessionId = ctx.state.currentSessionId
680
- if (!sessionId) {
798
+ if (!(sessionId != null && sessionId !== '')) {
681
799
  return r([{ type: 'git-commit-enter-confirm' }], [], 'modal.git-commit.confirm')
682
800
  }
683
801
  return r(
@@ -696,7 +814,7 @@ export const gitCommitLeaveConfirm: KeyResult = r(
696
814
  export const gitCommitLeaveGenerating: ActionFn = (ctx: ModeContext) => {
697
815
  const sessionId = ctx.state.currentSessionId
698
816
  const actionsList: AppAction[] = [{ type: 'git-commit-leave-generating' }]
699
- if (sessionId) {
817
+ if (sessionId != null && sessionId !== '') {
700
818
  actionsList.push({ sessionId, type: 'auto-commit-clear' })
701
819
  }
702
820
  return r(actionsList, [], 'modal.git-commit')
package/src/backends.ts CHANGED
@@ -6,7 +6,7 @@ import type { BackendConfig } from './types'
6
6
  */
7
7
  export function claudeBackend(opts?: { model?: string }): BackendConfig {
8
8
  return {
9
- args: opts?.model ? ['--model', opts.model] : [],
9
+ args: opts?.model != null && opts.model !== '' ? ['--model', opts.model] : [],
10
10
  command: 'claude',
11
11
  }
12
12
  }
package/src/defaults.ts CHANGED
@@ -104,8 +104,10 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
104
104
  .map('e', actions.gitToggleFoldAll, 'Expand/collapse all folds')
105
105
  .map('o', actions.openSelectedGitFileInEditor, 'Open in editor')
106
106
  .map('c', actions.gitCommitOpen, 'Commit')
107
+ .map('m', actions.openWorktreeMove, 'Move worktree')
107
108
  .map('p', actions.gitPush, 'Push')
108
109
  .map('v', actions.toggleGitDiffView, 'Toggle split/stacked')
110
+ .map('b', actions.toggleGitReviewBase, 'Review vs base')
109
111
  .map('t', actions.toggleGitFileListMode, 'Toggle flat/tree')
110
112
  .map('T', actions.toggleTreeCompaction, 'Toggle tree compaction')
111
113
  .map(']', actions.shiftGitHeadOffset(-1), 'Newer commit')
@@ -182,6 +184,22 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
182
184
  .map('<CR>', actions.confirmUpdateSelection, 'Confirm')
183
185
  )
184
186
 
187
+ // -----------------------------------------------------------------------
188
+ // Modal: worktree-move
189
+ // -----------------------------------------------------------------------
190
+ .mode('modal.worktree-move', (m) =>
191
+ m
192
+ .map('<Esc>', actions.closeModal, 'Cancel')
193
+ .map('j', actions.moveModalSelection(1), 'Next')
194
+ .map('k', actions.moveModalSelection(-1), 'Prev')
195
+ .map('<Down>', actions.moveModalSelection(1))
196
+ .map('<Up>', actions.moveModalSelection(-1))
197
+ .map('<C-n>', actions.moveModalSelection(1))
198
+ .map('<C-p>', actions.moveModalSelection(-1))
199
+ .map('d', actions.toggleWorktreeMoveDelete, 'Toggle delete source')
200
+ .map('<CR>', actions.confirmWorktreeMove, 'Move')
201
+ )
202
+
185
203
  // -----------------------------------------------------------------------
186
204
  // Modal: rename-tab
187
205
  // -----------------------------------------------------------------------
@@ -197,12 +215,16 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
197
215
  // -----------------------------------------------------------------------
198
216
  .mode('modal.new-tab.command-edit', (m) =>
199
217
  m
200
- .map('<Esc>', actions.closeModal, 'Cancel')
218
+ .map('<Esc>', actions.cancelNewTabModal, 'Cancel')
201
219
  .map('<CR>', actions.launchSelectedAssistant, 'Launch')
220
+ .map('<C-w>', actions.toggleNewTabWorktree, 'WT')
221
+ .map('<C-d>', actions.deleteSelectedWorktree, 'Delete WT')
222
+ .map('<Tab>', actions.switchField, 'Next field')
202
223
  .map('<C-n>', actions.moveModalSelection(1), 'Next')
203
224
  .map('<C-p>', actions.moveModalSelection(-1), 'Prev')
204
225
  .map('<Down>', actions.moveModalSelection(1))
205
226
  .map('<Up>', actions.moveModalSelection(-1))
227
+ .map('<C-e>', actions.editSelectedAssistant, 'Edit')
206
228
  .passthrough()
207
229
  )
208
230
 
@@ -262,6 +284,7 @@ export function getDefaultKeymapConfig(): ResolvedKeymapConfig {
262
284
  .map('<C-a>', actions.snippetFilterPasteToGroup, 'Send to group')
263
285
  .map('<C-n>', actions.moveModalSelection(1), 'Next')
264
286
  .map('<C-p>', actions.moveModalSelection(-1), 'Prev')
287
+ .map('<C-o>', actions.openSelectedSnippetSourceInEditor, 'Open source file')
265
288
  .map('<Down>', actions.moveModalSelection(1))
266
289
  .map('<Up>', actions.moveModalSelection(-1))
267
290
  .passthrough()
package/src/index.ts CHANGED
@@ -80,6 +80,8 @@ export type {
80
80
  SideEffect,
81
81
  SnippetDef,
82
82
  SnippetRecord,
83
+ SnippetShellVar,
84
+ SnippetVar,
83
85
  SplitDirection,
84
86
  StatusBarConfig,
85
87
  TabSession,
package/src/resolver.ts CHANGED
@@ -44,11 +44,16 @@ export function resolveConfig(userConfig: AimuxUserConfig): ResolvedConfig {
44
44
  sessionBar: resolveSessionBar(userConfig.sessionBar),
45
45
  sidebar: userConfig.sidebar ?? {},
46
46
  snippets: userConfig.snippets ?? [],
47
+ snippetTriggerChar: resolveSnippetTriggerChar(userConfig.snippetTriggerChar),
47
48
  statusBar: userConfig.statusBar ?? {},
48
49
  theme: resolveTheme(userConfig.theme),
49
50
  }
50
51
  }
51
52
 
53
+ function resolveSnippetTriggerChar(value: string | undefined): string {
54
+ return typeof value === 'string' && value.length === 1 ? value : ':'
55
+ }
56
+
52
57
  function resolveTheme(userConfig: AimuxUserConfig['theme']): ResolvedConfig['theme'] {
53
58
  if (!userConfig) return undefined
54
59
  return {
@@ -76,7 +76,7 @@ export const TUI_THEMES: Record<string, TuiThemeJson> = {
76
76
  'zenburn': zenburn as TuiThemeJson,
77
77
  }
78
78
 
79
- export type ThemeId = keyof typeof TUI_THEMES & string
79
+ export type ThemeId = keyof typeof TUI_THEMES
80
80
 
81
81
  export const THEME_IDS: ThemeId[] = Object.keys(TUI_THEMES).sort() as ThemeId[]
82
82
 
@@ -12,13 +12,7 @@ const TRANSPARENT: RGBA = { a: 0, b: 0, g: 0, r: 0 }
12
12
  function rgbaFromHex(hex: string): RGBA {
13
13
  const h = hex.replace('#', '')
14
14
  // expand short forms (#rgb, #rgba)
15
- const expanded =
16
- h.length === 3 || h.length === 4
17
- ? h
18
- .split('')
19
- .map((c) => c + c)
20
- .join('')
21
- : h
15
+ const expanded = h.length === 3 || h.length === 4 ? h.replaceAll(/(.)/g, '$1$1') : h
22
16
  if (expanded.length === 6) {
23
17
  const num = parseInt(expanded, 16)
24
18
  return {
package/src/types.ts CHANGED
@@ -27,11 +27,12 @@ export type ModeId =
27
27
  | 'modal.git-commit.confirm'
28
28
  | 'modal.git-commit.generating'
29
29
  | 'modal.update-available'
30
+ | 'modal.worktree-move'
30
31
  | 'modal.ai-usage'
31
32
 
32
33
  // ─── Primitive app types ──────────────────────────────────────────────────────
33
34
 
34
- export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
35
+ export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
35
36
  export type AssistantId = BuiltinAssistantId | (string & {})
36
37
  export type TabStatus = 'starting' | 'running' | 'disconnected' | 'error'
37
38
 
@@ -82,8 +83,11 @@ export interface TerminalModeState {
82
83
 
83
84
  // ─── Layout ───────────────────────────────────────────────────────────────────
84
85
 
85
- export type LayoutLeaf = { type: 'leaf'; tabId: string }
86
- export type LayoutSplit = {
86
+ export interface LayoutLeaf {
87
+ type: 'leaf'
88
+ tabId: string
89
+ }
90
+ export interface LayoutSplit {
87
91
  type: 'split'
88
92
  direction: SplitDirection
89
93
  ratio: number
@@ -105,6 +109,7 @@ export interface PersistedTabSnapshot {
105
109
  terminalModes: TerminalModeState
106
110
  errorMessage?: string
107
111
  exitCode?: number
112
+ worktreeId?: string
108
113
  }
109
114
 
110
115
  export interface WorkspaceSnapshotV1 {
@@ -121,6 +126,23 @@ export interface WorkspaceSnapshotV1 {
121
126
  tabGroupMap?: Record<string, string>
122
127
  }
123
128
 
129
+ export type WorktreeSource = 'primary' | 'aimux-temp' | 'external'
130
+
131
+ export interface WorktreeRecord {
132
+ id: string
133
+ name: string
134
+ path: string
135
+ repoRoot: string
136
+ branch?: string
137
+ baseRef?: string
138
+ commitSha?: string
139
+ source: WorktreeSource
140
+ createdByAimux: boolean
141
+ color?: string
142
+ createdAt: string
143
+ updatedAt: string
144
+ }
145
+
124
146
  export interface SessionRecord {
125
147
  id: string
126
148
  name: string
@@ -130,6 +152,8 @@ export interface SessionRecord {
130
152
  lastOpenedAt: string
131
153
  order?: number
132
154
  workspaceSnapshot?: WorkspaceSnapshotV1
155
+ worktrees?: WorktreeRecord[]
156
+ activeWorktreeId?: string
133
157
  }
134
158
 
135
159
  export interface TabSession {
@@ -144,12 +168,15 @@ export interface TabSession {
144
168
  command: string
145
169
  errorMessage?: string
146
170
  exitCode?: number
171
+ worktreeId?: string
147
172
  }
148
173
 
149
174
  export interface SnippetRecord {
150
175
  id: string
151
176
  name: string
152
177
  content: string
178
+ trigger?: string
179
+ vars?: Record<string, SnippetVar>
153
180
  }
154
181
 
155
182
  export type DirectoryResultType = 'git-repo' | 'worktree' | 'workspace'
@@ -192,6 +219,7 @@ export interface GitFileEntry {
192
219
  status: GitFileStatus
193
220
  added: number | null
194
221
  removed: number | null
222
+ repoPath?: string
195
223
  }
196
224
 
197
225
  export type GitPanelError = 'not-a-repo' | 'unknown'
@@ -242,6 +270,7 @@ export interface GitModeState {
242
270
  diffView: GitDiffView
243
271
  folds: Record<string, Record<string, FoldState>>
244
272
  headOffset: number
273
+ reviewBase: boolean
245
274
  }
246
275
 
247
276
  interface ModalBase {
@@ -260,6 +289,16 @@ export interface ModalClosed extends ModalBase {
260
289
  export interface ModalNewTab extends ModalBase {
261
290
  type: 'new-tab'
262
291
  editingCommand: AssistantId | null
292
+ activeField: 'assistant' | 'branch-name' | 'target-worktree' | 'worktree-name'
293
+ branchError: string | null
294
+ branchName: string
295
+ createWorktree: boolean
296
+ selectedAssistantId: AssistantId | null
297
+ step: 'assistant' | 'worktree' | 'worktree-create'
298
+ targetWorktreeIndex: number
299
+ worktreeDeleteConfirmId: string | null
300
+ worktreeDeleteMessage: string | null
301
+ worktreeName: string
263
302
  }
264
303
  export interface ModalSessionPicker extends ModalBase {
265
304
  type: 'session-picker'
@@ -273,6 +312,7 @@ export interface ModalRenameTab extends ModalBase {
273
312
  }
274
313
  export interface ModalSnippetPicker extends ModalBase {
275
314
  type: 'snippet-picker'
315
+ actionMessage?: string | null
276
316
  }
277
317
  export interface ModalThemePicker extends ModalBase {
278
318
  type: 'theme-picker'
@@ -303,7 +343,9 @@ export interface ModalGitCommit extends ModalBase {
303
343
  }
304
344
  export interface ModalSnippetEditor extends ModalBase {
305
345
  type: 'snippet-editor'
306
- activeField: 'name' | 'content'
346
+ activeField: 'name' | 'trigger' | 'content'
347
+ nameBuffer: string
348
+ triggerBuffer: string
307
349
  contentBuffer: string
308
350
  }
309
351
 
@@ -317,6 +359,13 @@ export interface ModalAIUsage extends ModalBase {
317
359
  type: 'ai-usage'
318
360
  }
319
361
 
362
+ export interface ModalWorktreeMove extends ModalBase {
363
+ type: 'worktree-move'
364
+ /** The worktree being moved (may differ from the active one, e.g. a tab menu). */
365
+ sourceWorktreeId: string
366
+ deleteSource: boolean
367
+ }
368
+
320
369
  export type ModalState =
321
370
  | ModalClosed
322
371
  | ModalNewTab
@@ -332,6 +381,7 @@ export type ModalState =
332
381
  | ModalGitCommit
333
382
  | ModalUpdateAvailable
334
383
  | ModalAIUsage
384
+ | ModalWorktreeMove
335
385
 
336
386
  export interface LayoutState {
337
387
  terminalCols: number
@@ -398,6 +448,7 @@ export interface AppState {
398
448
  gitMode: GitModeState
399
449
  autoCommit: AutoCommitState
400
450
  multiRepo: MultiRepoState
451
+ worktreeDivergence: Record<string, BranchDivergence>
401
452
  pendingChords: string[] | null
402
453
  }
403
454
 
@@ -405,6 +456,15 @@ export interface AppState {
405
456
 
406
457
  export type ModalAction =
407
458
  | { type: 'open-new-tab-modal' }
459
+ | { type: 'set-new-tab-branch-error'; message: string | null }
460
+ | {
461
+ type: 'set-new-tab-worktree-delete-state'
462
+ confirmWorktreeId?: string | null
463
+ message: string | null
464
+ }
465
+ | { type: 'enter-new-tab-worktree-create' }
466
+ | { type: 'select-new-tab-assistant'; assistantId?: AssistantId }
467
+ | { type: 'toggle-new-tab-worktree'; assistantId?: AssistantId }
408
468
  | { type: 'open-help-modal'; scope?: ModeId }
409
469
  | { type: 'open-split-picker'; direction: SplitDirection }
410
470
  | { type: 'open-session-picker' }
@@ -429,7 +489,11 @@ export type ModalAction =
429
489
  | { type: 'set-theme-entry-count'; count: number }
430
490
  | { type: 'open-theme-picker' }
431
491
  | { type: 'open-update-available-modal'; currentVersion: string; latestVersion: string }
492
+ | { type: 'set-modal-selection-index'; index: number }
432
493
  | { type: 'open-ai-usage-modal' }
494
+ | { type: 'open-edit-custom-command'; assistantId: AssistantId }
495
+ | { type: 'open-worktree-move-modal'; sourceWorktreeId: string }
496
+ | { type: 'toggle-worktree-move-delete' }
433
497
 
434
498
  export type SessionAction =
435
499
  | { type: 'load-session'; sessionId: string; workspaceSnapshot?: WorkspaceSnapshotV1 }
@@ -439,6 +503,15 @@ export type SessionAction =
439
503
  | { type: 'delete-session-record'; sessionId: string; openSessionPicker?: boolean }
440
504
  | { type: 'reorder-sessions'; orderedIds: string[] }
441
505
  | { type: 'set-session-status'; sessionId: string; status: SessionStatus }
506
+ | { type: 'add-worktree-record'; sessionId: string; worktree: WorktreeRecord; activate?: boolean }
507
+ | { type: 'remove-worktree-record'; sessionId: string; worktreeId: string }
508
+ | { type: 'set-active-worktree'; sessionId: string; worktreeId: string }
509
+ | {
510
+ type: 'update-worktree-record'
511
+ sessionId: string
512
+ worktreeId: string
513
+ patch: Partial<WorktreeRecord>
514
+ }
442
515
 
443
516
  export type TabAction =
444
517
  | { type: 'add-tab'; tab: TabSession }
@@ -514,10 +587,17 @@ export interface GitRefreshPayload {
514
587
  files: GitFileEntry[]
515
588
  }
516
589
 
590
+ /** Commits a worktree branch is ahead/behind the ref it forked from. */
591
+ export interface BranchDivergence {
592
+ ahead: number
593
+ behind: number
594
+ }
595
+
517
596
  export type GitPanelAction =
518
597
  | { type: 'git-refresh-success'; payload: GitRefreshPayload }
519
598
  | { type: 'git-refresh-error'; kind: GitPanelError }
520
599
  | { type: 'git-panel-reset' }
600
+ | { type: 'set-worktree-divergence'; divergence: Record<string, BranchDivergence> }
521
601
 
522
602
  export type GitModeAction =
523
603
  | { type: 'enter-git-mode' }
@@ -536,7 +616,9 @@ export type GitModeAction =
536
616
  | { type: 'git-mode-set-pending-delete'; path: string | null }
537
617
  | { type: 'git-mode-clear-diff-cache'; path: string }
538
618
  | { type: 'git-mode-set-message'; message: string | null }
619
+ | { type: 'snippet-picker-set-message'; message: string | null }
539
620
  | { type: 'git-mode-toggle-diff-view' }
621
+ | { type: 'git-mode-toggle-review-base' }
540
622
  | { type: 'git-mode-shift-head-offset'; delta: number }
541
623
  | { type: 'git-mode-set-head-offset'; offset: number }
542
624
  | {
@@ -599,6 +681,7 @@ export type AppAction =
599
681
  export type SideEffect =
600
682
  | { type: 'quit'; state: AppState }
601
683
  | { type: 'launch-selected-assistant' }
684
+ | { type: 'edit-selected-assistant' }
602
685
  | { type: 'confirm-selected-session' }
603
686
  | { type: 'delete-selected-session' }
604
687
  | { type: 'open-rename-selected-session' }
@@ -635,9 +718,18 @@ export type SideEffect =
635
718
  | { type: 'confirm-update-selection' }
636
719
  | { type: 'switch-session-by-index'; index: number }
637
720
  | { type: 'delete-session'; sessionId: string }
721
+ | { type: 'delete-worktree'; sessionId: string; worktreeId: string; force?: boolean }
722
+ | {
723
+ type: 'move-worktree'
724
+ sessionId: string
725
+ sourceWorktreeId: string
726
+ targetWorktreeId: string
727
+ deleteSource?: boolean
728
+ }
638
729
  | { type: 'toggle-transparent' }
639
730
  | { type: 'toggle-mode' }
640
731
  | { type: 'open-file-in-editor'; path: string }
732
+ | { type: 'open-selected-snippet-source-in-editor' }
641
733
 
642
734
  // ─── Key input / KeyResult / ModeContext ──────────────────────────────────────
643
735
 
@@ -689,10 +781,30 @@ export interface HooksConfig {
689
781
 
690
782
  // ─── Snippet config (stub) ────────────────────────────────────────────────────
691
783
 
784
+ /**
785
+ * A snippet variable resolved at expansion time. The shape is a tagged union
786
+ * discriminated by which key is present (`sh` for now; future: `env`, `date`, …).
787
+ */
788
+ export interface SnippetShellVar {
789
+ /** Shell command run via `sh -c`. The trimmed stdout is interpolated. */
790
+ sh: string
791
+ /** Kill the process after this many ms. Default 5000. */
792
+ timeout?: number
793
+ /** Trim trailing whitespace from stdout. Default true. */
794
+ trim?: boolean
795
+ }
796
+
797
+ export type SnippetVar = SnippetShellVar
798
+
692
799
  export interface SnippetDef {
693
800
  name: string
694
801
  trigger?: string
695
802
  text: string
803
+ /**
804
+ * Optional named variables. Reference them in `text` as `{{name}}`.
805
+ * The key is the variable name; the value declares how to resolve it.
806
+ */
807
+ vars?: Record<string, SnippetVar>
696
808
  }
697
809
 
698
810
  // ─── Action value types ───────────────────────────────────────────────────────
@@ -759,7 +871,9 @@ export type GitPanePathConfig =
759
871
  | { enabled: false }
760
872
  | { enabled: true; pathFn?: (path: string) => string }
761
873
 
762
- export type GitPaneDiffCountConfig = { enabled: boolean }
874
+ export interface GitPaneDiffCountConfig {
875
+ enabled: boolean
876
+ }
763
877
 
764
878
  interface GitPaneBaseConfig {
765
879
  /** Startup override for git pane visibility. Reapplied on each launch. */
@@ -891,6 +1005,12 @@ export interface AimuxUserConfig {
891
1005
  gitPane?: GitPaneConfig
892
1006
  hooks?: HooksConfig
893
1007
  snippets?: SnippetDef[]
1008
+ /**
1009
+ * Single-character prefix that opens an inline snippet trigger.
1010
+ * Defaults to `:` (Espanso-style). Typing `<char><trigger><separator>` in
1011
+ * any non-alternate-screen terminal expands the matching snippet.
1012
+ */
1013
+ snippetTriggerChar?: string
894
1014
  autoCommit?: Partial<AutoCommitConfig>
895
1015
  multiRepo?: Partial<MultiRepoConfig>
896
1016
  statusBar?: StatusBarConfig
@@ -964,6 +1084,7 @@ export interface ResolvedConfig {
964
1084
  }
965
1085
  hooks: HooksConfig
966
1086
  snippets: SnippetDef[]
1087
+ snippetTriggerChar: string
967
1088
  autoCommit: AutoCommitConfig
968
1089
  multiRepo: MultiRepoConfig
969
1090
  statusBar: StatusBarConfig