@brimveyn/aimux 1.14.14 → 1.14.15
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 +2 -2
- package/src/app-runtime/side-effects.ts +171 -5
- package/src/app.tsx +4 -0
- package/src/config.ts +97 -0
- package/src/state/reducers/modal-state.ts +94 -14
- package/src/state/selectors.ts +11 -1
- package/src/state/store.ts +4 -0
- package/src/state/types.ts +6 -1
- package/src/ui/components/modals/tabs/new-tab-modal.tsx +113 -23
- package/src/ui/root.tsx +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.15",
|
|
4
4
|
"description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"bump": "bun run scripts/bump.ts"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@brimveyn/aimux-config": "0.6.
|
|
63
|
+
"@brimveyn/aimux-config": "0.6.9",
|
|
64
64
|
"@opentui/core": "^0.1.90",
|
|
65
65
|
"@opentui/react": "^0.1.90",
|
|
66
66
|
"@resvg/resvg-wasm": "^2.6.2",
|
|
@@ -16,7 +16,7 @@ import type { SessionBackend } from '../session-backend/types'
|
|
|
16
16
|
import type { AppAction, AppState, AssistantId, TabSession, WorktreeRecord } from '../state/types'
|
|
17
17
|
import type { ThemeId } from '../ui/themes'
|
|
18
18
|
|
|
19
|
-
import { loadConfig, saveConfig } from '../config'
|
|
19
|
+
import { loadConfig, saveConfig, type WorktreeTemplate, type WorktreeTemplatePane } from '../config'
|
|
20
20
|
import { logInputDebug } from '../debug/input-log'
|
|
21
21
|
import { enqueueGitOp } from '../git/command-queue'
|
|
22
22
|
import { moveWorktree } from '../git/move-worktree'
|
|
@@ -54,7 +54,12 @@ import {
|
|
|
54
54
|
type SplitDirection,
|
|
55
55
|
splitNode,
|
|
56
56
|
} from '../state/layout-tree'
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
filterAssistants,
|
|
59
|
+
filterSessions,
|
|
60
|
+
filterSnippets,
|
|
61
|
+
getTemplateNoneOffset,
|
|
62
|
+
} from '../state/selectors'
|
|
58
63
|
import { saveSessionCatalog } from '../state/session-catalog'
|
|
59
64
|
import { pruneSnapshotOfWorktree } from '../state/session-persistence'
|
|
60
65
|
import {
|
|
@@ -73,6 +78,7 @@ import { filterThemeIds } from '../ui/filter-themes'
|
|
|
73
78
|
import { scrollGitDiff } from '../ui/git-view-controls'
|
|
74
79
|
import { applyTheme, getCurrentMode, getTransparent, setMode, setTransparent } from '../ui/theme'
|
|
75
80
|
import { triggerAutoCommitNow } from './auto-commit-ref'
|
|
81
|
+
import { writeToTab } from './pty-write'
|
|
76
82
|
import {
|
|
77
83
|
handleCreateSessionEffect,
|
|
78
84
|
handleDeleteSessionEffect,
|
|
@@ -88,6 +94,15 @@ import {
|
|
|
88
94
|
} from './snippet-actions'
|
|
89
95
|
|
|
90
96
|
const STARTUP_GRACE_MS = 5_000
|
|
97
|
+
/**
|
|
98
|
+
* Delay before injecting a template pane's `send` payload into its PTY.
|
|
99
|
+
* Short enough that shells (which print a prompt within ~100 ms) receive the
|
|
100
|
+
* command after their prompt is drawn; long enough to clear most PTY init
|
|
101
|
+
* races. Not tied to STARTUP_GRACE_MS because that is the assistant timeout,
|
|
102
|
+
* not a readiness signal. See review note: a `tab.status === 'running'`
|
|
103
|
+
* subscription would be more robust and is the planned follow-up.
|
|
104
|
+
*/
|
|
105
|
+
const TEMPLATE_SEND_DELAY_MS = 600
|
|
91
106
|
|
|
92
107
|
export interface SideEffectContext {
|
|
93
108
|
state: AppState
|
|
@@ -403,7 +418,8 @@ async function launchAssistantInNewWorktree(
|
|
|
403
418
|
assistant: AssistantId,
|
|
404
419
|
worktreeName: string,
|
|
405
420
|
branchName?: string,
|
|
406
|
-
sourceWorktreeId?: string
|
|
421
|
+
sourceWorktreeId?: string,
|
|
422
|
+
templateId?: string
|
|
407
423
|
): Promise<void> {
|
|
408
424
|
const sessionId = ctx.state.currentSessionId
|
|
409
425
|
if (!(sessionId != null && sessionId !== '')) return
|
|
@@ -416,10 +432,23 @@ async function launchAssistantInNewWorktree(
|
|
|
416
432
|
sourceWorktreeId
|
|
417
433
|
)
|
|
418
434
|
if (!worktree) return
|
|
435
|
+
|
|
436
|
+
const template =
|
|
437
|
+
templateId != null && templateId !== ''
|
|
438
|
+
? ctx.state.worktreeTemplates.find((entry) => entry.id === templateId)
|
|
439
|
+
: undefined
|
|
440
|
+
|
|
441
|
+
ctx.dispatch({ type: 'close-modal' })
|
|
442
|
+
|
|
443
|
+
if (template) {
|
|
444
|
+
applyWorktreeTemplate(ctx, template, worktree.id, worktree.path)
|
|
445
|
+
ctx.dispatch({ focusMode: 'terminal-input', type: 'set-focus-mode' })
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
419
449
|
const customCommand = ctx.state.customCommands[assistant]
|
|
420
450
|
const tab = createTabSession(assistant, customCommand, ctx.state.customCommands, worktree.id)
|
|
421
451
|
ctx.dispatch({ tab, type: 'add-tab' })
|
|
422
|
-
ctx.dispatch({ type: 'close-modal' })
|
|
423
452
|
ctx.dispatch({ focusMode: 'terminal-input', type: 'set-focus-mode' })
|
|
424
453
|
startTabSession(
|
|
425
454
|
ctx.backend,
|
|
@@ -433,6 +462,126 @@ async function launchAssistantInNewWorktree(
|
|
|
433
462
|
)
|
|
434
463
|
}
|
|
435
464
|
|
|
465
|
+
function applyWorktreeTemplate(
|
|
466
|
+
ctx: SideEffectContext,
|
|
467
|
+
template: WorktreeTemplate,
|
|
468
|
+
worktreeId: string,
|
|
469
|
+
worktreePath: string
|
|
470
|
+
): void {
|
|
471
|
+
let firstTabId: string | null = null
|
|
472
|
+
|
|
473
|
+
for (const templateTab of template.tabs) {
|
|
474
|
+
const localToTabId = new Map<string, string>()
|
|
475
|
+
|
|
476
|
+
for (let i = 0; i < templateTab.panes.length; i++) {
|
|
477
|
+
const pane = templateTab.panes[i]
|
|
478
|
+
if (!pane) continue
|
|
479
|
+
const tab = createPaneTab(ctx, pane, worktreeId)
|
|
480
|
+
localToTabId.set(pane.id, tab.id)
|
|
481
|
+
|
|
482
|
+
if (i === 0) {
|
|
483
|
+
if (firstTabId == null) firstTabId = tab.id
|
|
484
|
+
ctx.dispatch({ tab, type: 'add-tab' })
|
|
485
|
+
startTabSession(
|
|
486
|
+
ctx.backend,
|
|
487
|
+
ctx.dispatch,
|
|
488
|
+
ctx.clearStartupGrace,
|
|
489
|
+
(tabId) => ctx.startStartupGrace(tabId, STARTUP_GRACE_MS),
|
|
490
|
+
tab,
|
|
491
|
+
ctx.state.layout.terminalCols,
|
|
492
|
+
ctx.state.layout.terminalRows,
|
|
493
|
+
worktreePath
|
|
494
|
+
)
|
|
495
|
+
} else {
|
|
496
|
+
const splitFromId =
|
|
497
|
+
pane.splitFrom != null && pane.splitFrom !== ''
|
|
498
|
+
? localToTabId.get(pane.splitFrom)
|
|
499
|
+
: undefined
|
|
500
|
+
const direction: SplitDirection = pane.direction ?? 'vertical'
|
|
501
|
+
if (splitFromId == null || splitFromId === '') {
|
|
502
|
+
logInputDebug('template.splitFrom.unresolved', {
|
|
503
|
+
paneId: pane.id,
|
|
504
|
+
splitFrom: pane.splitFrom ?? null,
|
|
505
|
+
templateId: template.id,
|
|
506
|
+
})
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
splitFromTab(ctx, splitFromId, direction, tab, worktreePath)
|
|
510
|
+
if (pane.ratio != null) {
|
|
511
|
+
const sourceRatio = clampSplitRatio(1 - pane.ratio)
|
|
512
|
+
ctx.dispatch({
|
|
513
|
+
axis: direction,
|
|
514
|
+
ratio: sourceRatio,
|
|
515
|
+
tabId: tab.id,
|
|
516
|
+
type: 'set-split-ratio',
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (pane.send != null && pane.send !== '') {
|
|
522
|
+
const payload = `${pane.send}\n`
|
|
523
|
+
const targetTabId = tab.id
|
|
524
|
+
setTimeout(() => {
|
|
525
|
+
const latest = ctx.getState()
|
|
526
|
+
const latestTab = latest.tabs.find((entry) => entry.id === targetTabId)
|
|
527
|
+
writeToTab(ctx.backend, targetTabId, latestTab, payload)
|
|
528
|
+
}, TEMPLATE_SEND_DELAY_MS)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (firstTabId != null) {
|
|
534
|
+
ctx.dispatch({ tabId: firstTabId, type: 'set-active-tab' })
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function createPaneTab(
|
|
539
|
+
ctx: SideEffectContext,
|
|
540
|
+
pane: WorktreeTemplatePane,
|
|
541
|
+
worktreeId: string
|
|
542
|
+
): TabSession {
|
|
543
|
+
// Accept `'shell'` as an alias for the registered `'terminal'` assistant so
|
|
544
|
+
// template examples using the more intuitive name don't silently fall back
|
|
545
|
+
// to Claude (createTabSession's unknown-id fallback resolves to index 0).
|
|
546
|
+
const assistantId = (pane.assistant === 'shell' ? 'terminal' : pane.assistant) as AssistantId
|
|
547
|
+
const customCommand = ctx.state.customCommands[assistantId]
|
|
548
|
+
return createTabSession(assistantId, customCommand, ctx.state.customCommands, worktreeId)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function splitFromTab(
|
|
552
|
+
ctx: SideEffectContext,
|
|
553
|
+
baseTabId: string,
|
|
554
|
+
direction: SplitDirection,
|
|
555
|
+
newTab: TabSession,
|
|
556
|
+
cwd?: string
|
|
557
|
+
): void {
|
|
558
|
+
ctx.dispatch({ tabId: baseTabId, type: 'set-active-tab' })
|
|
559
|
+
|
|
560
|
+
const latest = ctx.getState()
|
|
561
|
+
const existingTree = getTreeForTab(latest.layoutTrees, latest.tabGroupMap, baseTabId)
|
|
562
|
+
const baseTree = existingTree ?? createLeaf(baseTabId)
|
|
563
|
+
const newTree = splitNode(baseTree, baseTabId, direction, newTab.id)
|
|
564
|
+
const bounds = createTerminalBounds(latest.layout.terminalCols, latest.layout.terminalRows)
|
|
565
|
+
const paneRect = computePaneRects(newTree, bounds).get(newTab.id)
|
|
566
|
+
|
|
567
|
+
ctx.dispatch({ direction, newTab, type: 'split-pane' })
|
|
568
|
+
startTabSession(
|
|
569
|
+
ctx.backend,
|
|
570
|
+
ctx.dispatch,
|
|
571
|
+
ctx.clearStartupGrace,
|
|
572
|
+
(tabId) => ctx.startStartupGrace(tabId, STARTUP_GRACE_MS),
|
|
573
|
+
newTab,
|
|
574
|
+
Math.max(1, (paneRect?.cols ?? latest.layout.terminalCols) - PANE_BORDER * 2),
|
|
575
|
+
Math.max(1, (paneRect?.rows ?? latest.layout.terminalRows) - PANE_BORDER * 2),
|
|
576
|
+
cwd
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function clampSplitRatio(value: number): number {
|
|
581
|
+
if (!Number.isFinite(value)) return 0.5
|
|
582
|
+
return Math.min(0.85, Math.max(0.15, value))
|
|
583
|
+
}
|
|
584
|
+
|
|
436
585
|
function getTabProjectPath(
|
|
437
586
|
ctx: SideEffectContext,
|
|
438
587
|
tab: Pick<TabSession, 'worktreeId'>
|
|
@@ -504,11 +653,27 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
504
653
|
return
|
|
505
654
|
}
|
|
506
655
|
case 'launch-selected-assistant': {
|
|
656
|
+
if (
|
|
657
|
+
state.modal.type === 'new-tab' &&
|
|
658
|
+
state.modal.step === 'worktree-create' &&
|
|
659
|
+
state.worktreeTemplates.length > 0
|
|
660
|
+
) {
|
|
661
|
+
dispatch({ type: 'enter-new-tab-template-pick' })
|
|
662
|
+
return
|
|
663
|
+
}
|
|
507
664
|
const option = getSelectedAssistantOption(state)
|
|
508
665
|
if (state.modal.type === 'new-tab' && state.modal.createWorktree) {
|
|
509
666
|
const worktreeName = state.modal.worktreeName
|
|
510
667
|
const branchName = state.modal.branchName
|
|
511
668
|
const sourceWorktreeId = getNewTabTargetWorktreeId(state)
|
|
669
|
+
let templateId: string | undefined
|
|
670
|
+
if (state.modal.step === 'template') {
|
|
671
|
+
const templateIndex =
|
|
672
|
+
state.modal.selectedIndex - getTemplateNoneOffset(state.modal.selectedAssistantId)
|
|
673
|
+
if (templateIndex >= 0) {
|
|
674
|
+
templateId = state.worktreeTemplates[templateIndex]?.id
|
|
675
|
+
}
|
|
676
|
+
}
|
|
512
677
|
void (async () => {
|
|
513
678
|
try {
|
|
514
679
|
await enqueueGitOp(async () =>
|
|
@@ -517,7 +682,8 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
517
682
|
option.id,
|
|
518
683
|
worktreeName,
|
|
519
684
|
branchName,
|
|
520
|
-
sourceWorktreeId
|
|
685
|
+
sourceWorktreeId,
|
|
686
|
+
templateId
|
|
521
687
|
)
|
|
522
688
|
)
|
|
523
689
|
} catch (error) {
|
package/src/app.tsx
CHANGED
|
@@ -152,6 +152,10 @@ export function App({
|
|
|
152
152
|
gitPane: gitPaneOverrides,
|
|
153
153
|
sessionBarVisible,
|
|
154
154
|
sidebar: sidebarOverrides,
|
|
155
|
+
worktreeTemplates:
|
|
156
|
+
resolvedConfig.worktreeTemplates.length > 0
|
|
157
|
+
? resolvedConfig.worktreeTemplates
|
|
158
|
+
: json.worktreeTemplates,
|
|
155
159
|
}
|
|
156
160
|
)
|
|
157
161
|
// Replace the module-level default with the fully-resolved initial state.
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
+
import type { SplitDirection } from './state/layout-tree'
|
|
3
4
|
import type { GitFileListMode, WorkspaceSnapshotV1 } from './state/types'
|
|
4
5
|
|
|
5
6
|
import { logDebug } from './debug/input-log'
|
|
@@ -32,6 +33,26 @@ export interface PersistedSidebar {
|
|
|
32
33
|
width: number
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export interface WorktreeTemplatePane {
|
|
37
|
+
id: string
|
|
38
|
+
assistant: string
|
|
39
|
+
splitFrom?: string
|
|
40
|
+
direction?: SplitDirection
|
|
41
|
+
ratio?: number
|
|
42
|
+
send?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WorktreeTemplateTab {
|
|
46
|
+
panes: WorktreeTemplatePane[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorktreeTemplate {
|
|
50
|
+
id: string
|
|
51
|
+
name: string
|
|
52
|
+
description?: string
|
|
53
|
+
tabs: WorktreeTemplateTab[]
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
export interface AimuxConfig {
|
|
36
57
|
version: 2
|
|
37
58
|
customCommands: Record<string, string>
|
|
@@ -43,6 +64,7 @@ export interface AimuxConfig {
|
|
|
43
64
|
sessionBarVisible?: boolean
|
|
44
65
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
45
66
|
skippedUpdateVersion?: string
|
|
67
|
+
worktreeTemplates?: WorktreeTemplate[]
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
@@ -105,6 +127,77 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
|
|
|
105
127
|
return true
|
|
106
128
|
}
|
|
107
129
|
|
|
130
|
+
function isRatioValid(value: unknown): boolean {
|
|
131
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0.15 && value < 0.85
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isWorktreeTemplateTab(value: unknown): value is WorktreeTemplateTab {
|
|
135
|
+
if (typeof value !== 'object' || value === null) return false
|
|
136
|
+
const v = value as Record<string, unknown>
|
|
137
|
+
if (!Array.isArray(v.panes) || v.panes.length === 0) return false
|
|
138
|
+
const seenIds = new Set<string>()
|
|
139
|
+
for (let i = 0; i < v.panes.length; i++) {
|
|
140
|
+
const rawPane = v.panes[i]
|
|
141
|
+
if (typeof rawPane !== 'object' || rawPane === null) return false
|
|
142
|
+
const pane = rawPane as Record<string, unknown>
|
|
143
|
+
if (typeof pane.id !== 'string' || pane.id.length === 0) return false
|
|
144
|
+
if (seenIds.has(pane.id)) return false
|
|
145
|
+
seenIds.add(pane.id)
|
|
146
|
+
if (typeof pane.assistant !== 'string' || pane.assistant.length === 0) return false
|
|
147
|
+
if (i === 0) {
|
|
148
|
+
if (pane.splitFrom !== undefined) return false
|
|
149
|
+
if (pane.direction !== undefined) return false
|
|
150
|
+
if (pane.ratio !== undefined) return false
|
|
151
|
+
} else {
|
|
152
|
+
if (typeof pane.splitFrom !== 'string' || !seenIds.has(pane.splitFrom)) return false
|
|
153
|
+
if (pane.splitFrom === pane.id) return false
|
|
154
|
+
if (pane.direction !== 'horizontal' && pane.direction !== 'vertical') return false
|
|
155
|
+
if (pane.ratio !== undefined && !isRatioValid(pane.ratio)) return false
|
|
156
|
+
}
|
|
157
|
+
if (pane.send !== undefined && typeof pane.send !== 'string') return false
|
|
158
|
+
}
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function isWorktreeTemplate(value: unknown): value is WorktreeTemplate {
|
|
163
|
+
if (typeof value !== 'object' || value === null) return false
|
|
164
|
+
const v = value as Record<string, unknown>
|
|
165
|
+
if (typeof v.id !== 'string' || v.id.length === 0) return false
|
|
166
|
+
if (typeof v.name !== 'string' || v.name.length === 0) return false
|
|
167
|
+
if (v.description !== undefined && typeof v.description !== 'string') return false
|
|
168
|
+
if (!Array.isArray(v.tabs) || v.tabs.length === 0) return false
|
|
169
|
+
for (const tab of v.tabs) {
|
|
170
|
+
if (!isWorktreeTemplateTab(tab)) return false
|
|
171
|
+
}
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseWorktreeTemplates(
|
|
176
|
+
value: unknown,
|
|
177
|
+
issues: string[]
|
|
178
|
+
): WorktreeTemplate[] | undefined {
|
|
179
|
+
if (value === undefined) return undefined
|
|
180
|
+
if (!Array.isArray(value)) {
|
|
181
|
+
issues.push('ignored invalid worktreeTemplates (not an array)')
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
const seen = new Set<string>()
|
|
185
|
+
const valid: WorktreeTemplate[] = []
|
|
186
|
+
for (const entry of value) {
|
|
187
|
+
if (!isWorktreeTemplate(entry)) {
|
|
188
|
+
issues.push('ignored invalid worktreeTemplate entry')
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
if (seen.has(entry.id)) {
|
|
192
|
+
issues.push(`ignored duplicate worktreeTemplate id "${entry.id}"`)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
seen.add(entry.id)
|
|
196
|
+
valid.push(entry)
|
|
197
|
+
}
|
|
198
|
+
return valid.length > 0 ? valid : undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
108
201
|
function isPersistedSidebar(value: unknown): value is PersistedSidebar {
|
|
109
202
|
if (typeof value !== 'object' || value === null) return false
|
|
110
203
|
const v = value as Record<string, unknown>
|
|
@@ -162,6 +255,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
162
255
|
sessionBarVisible?: unknown
|
|
163
256
|
workspaceSnapshot?: unknown
|
|
164
257
|
skippedUpdateVersion?: unknown
|
|
258
|
+
worktreeTemplates?: unknown
|
|
165
259
|
}
|
|
166
260
|
|
|
167
261
|
const issues: string[] = []
|
|
@@ -246,6 +340,8 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
246
340
|
issues.push('ignored invalid skippedUpdateVersion')
|
|
247
341
|
}
|
|
248
342
|
|
|
343
|
+
const validWorktreeTemplates = parseWorktreeTemplates(parsed.worktreeTemplates, issues)
|
|
344
|
+
|
|
249
345
|
if (issues.length > 0) {
|
|
250
346
|
logDebug('config.load.validationIssue', { issues, path: CONFIG_PATH })
|
|
251
347
|
}
|
|
@@ -264,6 +360,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
264
360
|
workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
|
|
265
361
|
? parsed.workspaceSnapshot
|
|
266
362
|
: undefined,
|
|
363
|
+
worktreeTemplates: validWorktreeTemplates,
|
|
267
364
|
},
|
|
268
365
|
issues,
|
|
269
366
|
source: 'file',
|
|
@@ -6,7 +6,12 @@ import { collectHelpEntries } from '../../input/keymap/help-entries'
|
|
|
6
6
|
import { getActiveKeymap } from '../../input/keymap/keymap-ref'
|
|
7
7
|
import { getAllAssistantOptions } from '../../pty/command-registry'
|
|
8
8
|
import { filterThemeIds } from '../../ui/filter-themes'
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
filterAssistants,
|
|
11
|
+
filterSessions,
|
|
12
|
+
filterSnippets,
|
|
13
|
+
getTemplateNoneOffset,
|
|
14
|
+
} from '../selectors'
|
|
10
15
|
import { reduceAutoCommitState } from './auto-commit-state'
|
|
11
16
|
|
|
12
17
|
function emptyModal() {
|
|
@@ -90,6 +95,36 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
90
95
|
},
|
|
91
96
|
}
|
|
92
97
|
}
|
|
98
|
+
case 'enter-new-tab-template-pick': {
|
|
99
|
+
if (state.modal.type !== 'new-tab') return state
|
|
100
|
+
return {
|
|
101
|
+
...state,
|
|
102
|
+
modal: {
|
|
103
|
+
...state.modal,
|
|
104
|
+
activeField: 'target-worktree',
|
|
105
|
+
cursorPos: 0,
|
|
106
|
+
selectedIndex: 0,
|
|
107
|
+
step: 'template',
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
case 'enter-new-tab-template-shortcut': {
|
|
112
|
+
if (state.modal.type !== 'new-tab') return state
|
|
113
|
+
const defaultName = state.modal.worktreeName || 'wt-template'
|
|
114
|
+
return {
|
|
115
|
+
...state,
|
|
116
|
+
modal: {
|
|
117
|
+
...state.modal,
|
|
118
|
+
activeField: 'worktree-name',
|
|
119
|
+
createWorktree: true,
|
|
120
|
+
cursorPos: defaultName.length,
|
|
121
|
+
selectedAssistantId: null,
|
|
122
|
+
selectedIndex: 0,
|
|
123
|
+
step: 'worktree-create',
|
|
124
|
+
worktreeName: defaultName,
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}
|
|
93
128
|
case 'set-new-tab-worktree-delete-state': {
|
|
94
129
|
if (state.modal.type !== 'new-tab') return state
|
|
95
130
|
return {
|
|
@@ -115,6 +150,32 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
115
150
|
}
|
|
116
151
|
case 'select-new-tab-assistant': {
|
|
117
152
|
if (state.modal.type !== 'new-tab' || state.modal.editingCommand !== null) return state
|
|
153
|
+
if (
|
|
154
|
+
action.assistantId === undefined &&
|
|
155
|
+
state.modal.step === 'assistant' &&
|
|
156
|
+
state.worktreeTemplates.length > 0
|
|
157
|
+
) {
|
|
158
|
+
const filtered = filterAssistants(
|
|
159
|
+
getAllAssistantOptions(state.customCommands),
|
|
160
|
+
state.modal.editBuffer
|
|
161
|
+
)
|
|
162
|
+
if (state.modal.selectedIndex >= filtered.length) {
|
|
163
|
+
const defaultName = state.modal.worktreeName || 'wt-template'
|
|
164
|
+
return {
|
|
165
|
+
...state,
|
|
166
|
+
modal: {
|
|
167
|
+
...state.modal,
|
|
168
|
+
activeField: 'worktree-name',
|
|
169
|
+
createWorktree: true,
|
|
170
|
+
cursorPos: defaultName.length,
|
|
171
|
+
selectedAssistantId: null,
|
|
172
|
+
selectedIndex: 0,
|
|
173
|
+
step: 'worktree-create',
|
|
174
|
+
worktreeName: defaultName,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
118
179
|
const option = getSelectedNewTabAssistant(state, action.assistantId)
|
|
119
180
|
if (!option) return state
|
|
120
181
|
const targetWorktreeIndex = getCurrentWorktreeIndex(state)
|
|
@@ -562,11 +623,13 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
562
623
|
if (state.modal.step === 'worktree') {
|
|
563
624
|
if (state.modal.activeField === 'worktree-name') return state
|
|
564
625
|
optionCount = getCurrentWorktreeCount(state) + 1
|
|
626
|
+
} else if (state.modal.step === 'template') {
|
|
627
|
+
optionCount =
|
|
628
|
+
state.worktreeTemplates.length + getTemplateNoneOffset(state.modal.selectedAssistantId)
|
|
565
629
|
} else {
|
|
566
|
-
optionCount =
|
|
567
|
-
getAllAssistantOptions(state.customCommands),
|
|
568
|
-
|
|
569
|
-
).length
|
|
630
|
+
optionCount =
|
|
631
|
+
filterAssistants(getAllAssistantOptions(state.customCommands), state.modal.editBuffer)
|
|
632
|
+
.length + (state.worktreeTemplates.length > 0 ? 1 : 0)
|
|
570
633
|
}
|
|
571
634
|
} else if (state.modal.type === 'split-picker') {
|
|
572
635
|
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
@@ -626,11 +689,16 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
626
689
|
if (state.modal.type === 'help') {
|
|
627
690
|
optionCount = state.modal.entryCount
|
|
628
691
|
} else if (state.modal.type === 'new-tab') {
|
|
629
|
-
|
|
630
|
-
state
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
692
|
+
if (state.modal.step === 'worktree') {
|
|
693
|
+
optionCount = getCurrentWorktreeCount(state) + 1
|
|
694
|
+
} else if (state.modal.step === 'template') {
|
|
695
|
+
optionCount =
|
|
696
|
+
state.worktreeTemplates.length + getTemplateNoneOffset(state.modal.selectedAssistantId)
|
|
697
|
+
} else {
|
|
698
|
+
optionCount =
|
|
699
|
+
filterAssistants(getAllAssistantOptions(state.customCommands), state.modal.editBuffer)
|
|
700
|
+
.length + (state.worktreeTemplates.length > 0 ? 1 : 0)
|
|
701
|
+
}
|
|
634
702
|
} else if (state.modal.type === 'split-picker') {
|
|
635
703
|
optionCount = getAllAssistantOptions(state.customCommands).length
|
|
636
704
|
} else if (state.modal.type === 'create-session') {
|
|
@@ -677,8 +745,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
677
745
|
if (
|
|
678
746
|
state.modal.type === 'new-tab' &&
|
|
679
747
|
state.modal.editingCommand === null &&
|
|
680
|
-
state.modal.step === '
|
|
681
|
-
|
|
748
|
+
(state.modal.step === 'template' ||
|
|
749
|
+
(state.modal.step === 'worktree' && state.modal.activeField === 'target-worktree'))
|
|
682
750
|
) {
|
|
683
751
|
return state
|
|
684
752
|
}
|
|
@@ -707,8 +775,8 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
707
775
|
if (
|
|
708
776
|
state.modal.type === 'new-tab' &&
|
|
709
777
|
state.modal.editingCommand === null &&
|
|
710
|
-
state.modal.step === '
|
|
711
|
-
|
|
778
|
+
(state.modal.step === 'template' ||
|
|
779
|
+
(state.modal.step === 'worktree' && state.modal.activeField === 'target-worktree'))
|
|
712
780
|
) {
|
|
713
781
|
return state
|
|
714
782
|
}
|
|
@@ -785,6 +853,18 @@ export function reduceModalState(state: AppState, action: AppAction): AppState |
|
|
|
785
853
|
}
|
|
786
854
|
}
|
|
787
855
|
if (state.modal.type === 'new-tab') {
|
|
856
|
+
if (state.modal.step === 'template') {
|
|
857
|
+
return {
|
|
858
|
+
...state,
|
|
859
|
+
modal: {
|
|
860
|
+
...state.modal,
|
|
861
|
+
activeField: 'worktree-name',
|
|
862
|
+
cursorPos: state.modal.worktreeName.length,
|
|
863
|
+
selectedIndex: 0,
|
|
864
|
+
step: 'worktree-create',
|
|
865
|
+
},
|
|
866
|
+
}
|
|
867
|
+
}
|
|
788
868
|
if (state.modal.step === 'worktree-create') {
|
|
789
869
|
const optionCount = getCurrentWorktreeCount(state) + 1
|
|
790
870
|
return {
|
package/src/state/selectors.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { AssistantOption } from '../pty/command-registry'
|
|
2
|
-
import type { SessionRecord, SnippetRecord } from './types'
|
|
2
|
+
import type { AssistantId, SessionRecord, SnippetRecord } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 0 when the template picker should NOT show the "None" fallback (no assistant
|
|
6
|
+
* was picked — typical for the template shortcut path), 1 otherwise. Shared
|
|
7
|
+
* by the modal reducer, the side-effect that resolves templateId, and the
|
|
8
|
+
* picker UI so the three stay in sync.
|
|
9
|
+
*/
|
|
10
|
+
export function getTemplateNoneOffset(selectedAssistantId: AssistantId | null): 0 | 1 {
|
|
11
|
+
return selectedAssistantId == null ? 0 : 1
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export function filterAssistants(
|
|
5
15
|
options: AssistantOption[],
|
package/src/state/store.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WorktreeTemplate } from '../config'
|
|
2
|
+
|
|
1
3
|
import { reduceAutoCommit } from './reducers/auto-commit-state'
|
|
2
4
|
import { emptyGitMode, reduceGitModeState } from './reducers/git-mode-state'
|
|
3
5
|
import { emptyGitPanel, reduceGitPanelState } from './reducers/git-panel-state'
|
|
@@ -31,6 +33,7 @@ export interface InitialStateOverrides {
|
|
|
31
33
|
gitPane?: Partial<GitPaneState>
|
|
32
34
|
sidebar?: Pick<AppState['sidebar'], 'visible' | 'width'>
|
|
33
35
|
sessionBarVisible?: boolean
|
|
36
|
+
worktreeTemplates?: WorktreeTemplate[]
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
const DEFAULT_GIT_PANE: GitPaneState = {
|
|
@@ -116,6 +119,7 @@ export function createInitialState(
|
|
|
116
119
|
tabGroupMap: {},
|
|
117
120
|
tabs: [],
|
|
118
121
|
worktreeDivergence: {},
|
|
122
|
+
worktreeTemplates: overrides.worktreeTemplates ?? [],
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
|
package/src/state/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ModeId, SnippetVar } from '@brimveyn/aimux-config'
|
|
2
2
|
import type { ThemedToken } from 'shiki'
|
|
3
3
|
|
|
4
|
+
import type { WorktreeTemplate } from '../config'
|
|
4
5
|
import type { LayoutNode, SplitDirection } from './layout-tree'
|
|
5
6
|
|
|
6
7
|
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
|
|
@@ -327,7 +328,7 @@ export interface ModalNewTab extends ModalBase {
|
|
|
327
328
|
branchName: string
|
|
328
329
|
createWorktree: boolean
|
|
329
330
|
selectedAssistantId: AssistantId | null
|
|
330
|
-
step: 'assistant' | 'worktree' | 'worktree-create'
|
|
331
|
+
step: 'assistant' | 'worktree' | 'worktree-create' | 'template'
|
|
331
332
|
targetWorktreeIndex: number
|
|
332
333
|
worktreeDeleteConfirmId: string | null
|
|
333
334
|
worktreeDeleteMessage: string | null
|
|
@@ -504,6 +505,8 @@ export interface AppState {
|
|
|
504
505
|
lastActiveTabByWorktree: Record<string, string>
|
|
505
506
|
/** Chord prefix the sequence resolver is currently waiting on, or null when idle. */
|
|
506
507
|
pendingChords: string[] | null
|
|
508
|
+
/** User-defined templates applied at worktree creation. Loaded from aimux.json. */
|
|
509
|
+
worktreeTemplates: WorktreeTemplate[]
|
|
507
510
|
}
|
|
508
511
|
|
|
509
512
|
// -- Modal actions --
|
|
@@ -517,6 +520,8 @@ export type ModalAction =
|
|
|
517
520
|
message: string | null
|
|
518
521
|
}
|
|
519
522
|
| { type: 'enter-new-tab-worktree-create' }
|
|
523
|
+
| { type: 'enter-new-tab-template-pick' }
|
|
524
|
+
| { type: 'enter-new-tab-template-shortcut' }
|
|
520
525
|
| { type: 'select-new-tab-assistant'; assistantId?: AssistantId }
|
|
521
526
|
| { type: 'toggle-new-tab-worktree'; assistantId?: AssistantId }
|
|
522
527
|
| { type: 'open-edit-custom-command'; assistantId: AssistantId }
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react'
|
|
2
2
|
|
|
3
|
+
import type { WorktreeTemplate } from '../../../../config'
|
|
3
4
|
import type { AssistantId, WorktreeRecord } from '../../../../state/types'
|
|
4
5
|
|
|
5
6
|
import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
|
|
6
7
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
7
|
-
import { filterAssistants } from '../../../../state/selectors'
|
|
8
|
+
import { filterAssistants, getTemplateNoneOffset } from '../../../../state/selectors'
|
|
8
9
|
import { useTheme } from '../../../theme'
|
|
9
10
|
import { uiTokens } from '../../../ui-tokens'
|
|
10
11
|
import { Form, TextField } from '../shared/form'
|
|
@@ -23,13 +24,22 @@ interface NewTabModalProps {
|
|
|
23
24
|
branchName: string
|
|
24
25
|
createWorktree: boolean
|
|
25
26
|
selectedAssistantId: AssistantId | null
|
|
26
|
-
step: 'assistant' | 'worktree' | 'worktree-create'
|
|
27
|
+
step: 'assistant' | 'worktree' | 'worktree-create' | 'template'
|
|
27
28
|
worktreeDeleteConfirmId: string | null
|
|
28
29
|
worktreeDeleteMessage: string | null
|
|
29
30
|
worktrees: WorktreeRecord[]
|
|
30
31
|
worktreeName: string
|
|
32
|
+
worktreeTemplates: WorktreeTemplate[]
|
|
33
|
+
/**
|
|
34
|
+
* True when this modal is the worktree-aware new-tab flow. False when the
|
|
35
|
+
* component is reused for `split-picker`, which can only choose an
|
|
36
|
+
* assistant — the template shortcut entry must be hidden there.
|
|
37
|
+
*/
|
|
38
|
+
allowTemplateShortcut: boolean
|
|
31
39
|
}
|
|
32
40
|
|
|
41
|
+
const TEMPLATE_SHORTCUT_KEY = '__template-shortcut__'
|
|
42
|
+
|
|
33
43
|
function getDeleteBlockedReason({
|
|
34
44
|
currentSessionId,
|
|
35
45
|
worktree,
|
|
@@ -52,6 +62,7 @@ function getDeleteBlockedReason({
|
|
|
52
62
|
|
|
53
63
|
export function NewTabModal({
|
|
54
64
|
activeField,
|
|
65
|
+
allowTemplateShortcut,
|
|
55
66
|
branchError,
|
|
56
67
|
branchName,
|
|
57
68
|
createWorktree,
|
|
@@ -68,6 +79,7 @@ export function NewTabModal({
|
|
|
68
79
|
worktreeDeleteMessage,
|
|
69
80
|
worktreeName,
|
|
70
81
|
worktrees,
|
|
82
|
+
worktreeTemplates,
|
|
71
83
|
}: NewTabModalProps) {
|
|
72
84
|
const t = useTheme()
|
|
73
85
|
const options = useMemo(() => getAllAssistantOptions(customCommands), [customCommands])
|
|
@@ -123,30 +135,86 @@ export function NewTabModal({
|
|
|
123
135
|
[createWorktree, currentSessionId, selectedIndex, t, worktreeDeleteConfirmId, worktrees]
|
|
124
136
|
)
|
|
125
137
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
const noneOffset = getTemplateNoneOffset(selectedAssistantId)
|
|
139
|
+
const showNoneOption = noneOffset === 1
|
|
140
|
+
const templateItems = useMemo<PickerItem[]>(() => {
|
|
141
|
+
const noneItem: PickerItem[] = showNoneOption
|
|
142
|
+
? [
|
|
143
|
+
{
|
|
144
|
+
key: '__none__',
|
|
145
|
+
onClick: () => {
|
|
146
|
+
dispatchGlobal({ index: 0, type: 'set-modal-selection-index' })
|
|
147
|
+
runSideEffectGlobal({ type: 'launch-selected-assistant' })
|
|
148
|
+
},
|
|
149
|
+
subtitle: <text fg={t.textMuted}>Single pane — current assistant only</text>,
|
|
150
|
+
title: <text fg={selectedIndex === 0 ? t.text : t.textMuted}>None</text>,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
: []
|
|
154
|
+
return [
|
|
155
|
+
...noneItem,
|
|
156
|
+
...worktreeTemplates.map((template, idx) => {
|
|
157
|
+
const itemIndex = idx + noneOffset
|
|
158
|
+
const active = itemIndex === selectedIndex
|
|
159
|
+
const tabCount = template.tabs.length
|
|
160
|
+
const paneCount = template.tabs.reduce((sum, tab) => sum + tab.panes.length, 0)
|
|
131
161
|
return {
|
|
132
|
-
key:
|
|
133
|
-
onClick: () =>
|
|
134
|
-
dispatchGlobal({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
subtitle:
|
|
138
|
-
|
|
139
|
-
<text fg={t.textMuted}>{
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
key: template.id,
|
|
163
|
+
onClick: () => {
|
|
164
|
+
dispatchGlobal({ index: itemIndex, type: 'set-modal-selection-index' })
|
|
165
|
+
runSideEffectGlobal({ type: 'launch-selected-assistant' })
|
|
166
|
+
},
|
|
167
|
+
subtitle:
|
|
168
|
+
template.description != null && template.description !== '' ? (
|
|
169
|
+
<text fg={t.textMuted}>{template.description}</text>
|
|
170
|
+
) : (
|
|
171
|
+
<text fg={t.textMuted}>
|
|
172
|
+
{tabCount} tab{tabCount === 1 ? '' : 's'}, {paneCount} pane
|
|
173
|
+
{paneCount === 1 ? '' : 's'}
|
|
174
|
+
</text>
|
|
175
|
+
),
|
|
176
|
+
title: <text fg={active ? t.text : t.textMuted}>{template.name}</text>,
|
|
146
177
|
}
|
|
147
178
|
}),
|
|
148
|
-
|
|
149
|
-
)
|
|
179
|
+
]
|
|
180
|
+
}, [noneOffset, selectedIndex, showNoneOption, t, worktreeTemplates])
|
|
181
|
+
|
|
182
|
+
const showShortcutEntry = allowTemplateShortcut && worktreeTemplates.length > 0
|
|
183
|
+
const items = useMemo<PickerItem[]>(() => {
|
|
184
|
+
const assistantItems: PickerItem[] = filtered.map((option, index) => {
|
|
185
|
+
const active = index === selectedIndex
|
|
186
|
+
const customCmd = customCommands[option.id]
|
|
187
|
+
return {
|
|
188
|
+
key: option.id,
|
|
189
|
+
onClick: () => dispatchGlobal({ assistantId: option.id, type: 'select-new-tab-assistant' }),
|
|
190
|
+
onEdit: () => dispatchGlobal({ assistantId: option.id, type: 'open-edit-custom-command' }),
|
|
191
|
+
subtitle: (
|
|
192
|
+
<box flexDirection="column">
|
|
193
|
+
<text fg={t.textMuted}>{option.description}</text>
|
|
194
|
+
{customCmd != null && customCmd !== '' ? <text fg={t.primary}>{customCmd}</text> : null}
|
|
195
|
+
</box>
|
|
196
|
+
),
|
|
197
|
+
title: <text fg={active ? t.text : t.textMuted}>{option.label}</text>,
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
if (!showShortcutEntry) return assistantItems
|
|
201
|
+
const shortcutIndex = filtered.length
|
|
202
|
+
const shortcutActive = shortcutIndex === selectedIndex
|
|
203
|
+
return [
|
|
204
|
+
...assistantItems,
|
|
205
|
+
{
|
|
206
|
+
key: TEMPLATE_SHORTCUT_KEY,
|
|
207
|
+
onClick: () => {
|
|
208
|
+
dispatchGlobal({ index: shortcutIndex, type: 'set-modal-selection-index' })
|
|
209
|
+
dispatchGlobal({ type: 'enter-new-tab-template-shortcut' })
|
|
210
|
+
},
|
|
211
|
+
subtitle: (
|
|
212
|
+
<text fg={t.textMuted}>Create a worktree and pick a template — no assistant step</text>
|
|
213
|
+
),
|
|
214
|
+
title: <text fg={shortcutActive ? t.text : t.textMuted}>Worktree from template…</text>,
|
|
215
|
+
},
|
|
216
|
+
]
|
|
217
|
+
}, [customCommands, filtered, showShortcutEntry, selectedIndex, t])
|
|
150
218
|
|
|
151
219
|
if (editingCommand !== null) {
|
|
152
220
|
const option = options.find((o) => o.id === editingCommand) ?? getAssistantOption(0)
|
|
@@ -167,6 +235,28 @@ export function NewTabModal({
|
|
|
167
235
|
)
|
|
168
236
|
}
|
|
169
237
|
|
|
238
|
+
if (step === 'template') {
|
|
239
|
+
return (
|
|
240
|
+
<Picker
|
|
241
|
+
title="Pick worktree template"
|
|
242
|
+
keybindsModeId="modal.new-tab.command-edit"
|
|
243
|
+
width={uiTokens.modalWidth.md}
|
|
244
|
+
gap={1}
|
|
245
|
+
filter={null}
|
|
246
|
+
items={templateItems}
|
|
247
|
+
selectedIndex={selectedIndex}
|
|
248
|
+
emptyState={<text fg={t.textMuted}>No templates configured.</text>}
|
|
249
|
+
onHover={handleHover}
|
|
250
|
+
footer={
|
|
251
|
+
<box flexDirection="column">
|
|
252
|
+
<text fg={t.textMuted}>Step 4/4: choose template</text>
|
|
253
|
+
<text fg={t.textMuted}>Enter launches, Esc returns to worktree settings</text>
|
|
254
|
+
</box>
|
|
255
|
+
}
|
|
256
|
+
/>
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
170
260
|
if (step === 'worktree-create') {
|
|
171
261
|
const selectedAssistant =
|
|
172
262
|
options.find((option) => option.id === selectedAssistantId) ?? options[0]
|
package/src/ui/root.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import type { MouseEvent } from '@opentui/core'
|
|
|
3
3
|
import { useCallback, useMemo } from 'react'
|
|
4
4
|
|
|
5
5
|
import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
|
|
6
|
+
import type { WorktreeTemplate } from '../config'
|
|
6
7
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
7
8
|
import type {
|
|
8
9
|
FocusMode,
|
|
@@ -92,6 +93,7 @@ function renderModal(
|
|
|
92
93
|
activeAssistant?: string
|
|
93
94
|
autoCommitModel?: string
|
|
94
95
|
worktreeDivergence: Record<string, { ahead: number; behind: number }>
|
|
96
|
+
worktreeTemplates: WorktreeTemplate[]
|
|
95
97
|
}
|
|
96
98
|
) {
|
|
97
99
|
switch (modal.type) {
|
|
@@ -121,6 +123,8 @@ function renderModal(
|
|
|
121
123
|
: EMPTY_WORKTREES
|
|
122
124
|
}
|
|
123
125
|
worktreeName={modal.type === 'new-tab' ? modal.worktreeName : ''}
|
|
126
|
+
worktreeTemplates={options.worktreeTemplates}
|
|
127
|
+
allowTemplateShortcut={modal.type === 'new-tab'}
|
|
124
128
|
/>
|
|
125
129
|
)
|
|
126
130
|
case 'session-picker':
|
|
@@ -316,6 +320,7 @@ export function RootView({
|
|
|
316
320
|
const sessions = useAppStore((s) => s.sessions)
|
|
317
321
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
318
322
|
const worktreeDivergence = useAppStore((s) => s.worktreeDivergence)
|
|
323
|
+
const worktreeTemplates = useAppStore((s) => s.worktreeTemplates)
|
|
319
324
|
const gitPaneMode = useAppStore((s) => s.gitPane.mode)
|
|
320
325
|
const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
|
|
321
326
|
const gitPanePosition = useAppStore((s) => s.gitPane.position)
|
|
@@ -410,6 +415,7 @@ export function RootView({
|
|
|
410
415
|
snippets,
|
|
411
416
|
themeId,
|
|
412
417
|
worktreeDivergence,
|
|
418
|
+
worktreeTemplates,
|
|
413
419
|
})}
|
|
414
420
|
<ToastViewport />
|
|
415
421
|
</box>
|
|
@@ -507,6 +513,7 @@ export function RootView({
|
|
|
507
513
|
snippets,
|
|
508
514
|
themeId,
|
|
509
515
|
worktreeDivergence,
|
|
516
|
+
worktreeTemplates,
|
|
510
517
|
})}
|
|
511
518
|
<ToastViewport />
|
|
512
519
|
</box>
|