@brimveyn/aimux 1.12.1 → 1.12.6
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/pty-write.ts +52 -0
- package/src/app-runtime/side-effects.ts +62 -21
- package/src/app-runtime/snippet-actions.ts +23 -9
- package/src/app-runtime/use-pane-size-report.ts +95 -0
- package/src/app-runtime/use-renderer-bindings.ts +137 -3
- package/src/app-runtime/use-terminal-resize.ts +45 -2
- package/src/app.tsx +14 -2
- package/src/input/modes/types.ts +2 -0
- package/src/input/raw-input-handler.ts +33 -0
- package/src/platform/clipboard.ts +14 -0
- package/src/pty/command-registry.ts +6 -0
- package/src/pty/pty-manager.ts +51 -21
- package/src/pty/terminal-snapshot.ts +8 -1
- package/src/snippets/expand-variables.ts +112 -0
- package/src/snippets/run-shell-var.ts +93 -0
- package/src/snippets/trigger-detector.ts +105 -0
- package/src/state/reducers/modal-state.ts +30 -3
- package/src/state/snippet-catalog.ts +65 -4
- package/src/state/types.ts +16 -3
- package/src/state/validation.ts +22 -1
- package/src/ui/components/layout/split-layout.tsx +6 -0
- package/src/ui/components/layout/status-bar.tsx +3 -1
- package/src/ui/components/layout/terminal-pane.tsx +17 -3
- package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +10 -6
- package/src/ui/components/modals/snippets/snippet-picker-modal.tsx +16 -5
- package/src/ui/root.tsx +13 -11
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SnippetDef, SnippetVar } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
1
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
4
|
import { join } from 'node:path'
|
|
3
5
|
|
|
@@ -9,9 +11,15 @@ export interface SnippetRecord {
|
|
|
9
11
|
id: string
|
|
10
12
|
name: string
|
|
11
13
|
content: string
|
|
14
|
+
trigger?: string
|
|
15
|
+
vars?: Record<string, SnippetVar>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getSnippetsCatalogPath(): string {
|
|
19
|
+
return join(getProfileConfigDir(), 'aimux-snippets.json')
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
const SNIPPETS_PATH =
|
|
22
|
+
const SNIPPETS_PATH = getSnippetsCatalogPath()
|
|
15
23
|
|
|
16
24
|
const DEFAULT_SNIPPETS: SnippetRecord[] = [
|
|
17
25
|
{
|
|
@@ -52,7 +60,7 @@ export function loadSnippetCatalog(): SnippetRecord[] {
|
|
|
52
60
|
version?: unknown
|
|
53
61
|
snippets?: unknown
|
|
54
62
|
}
|
|
55
|
-
if (parsed.version !== 1 || !Array.isArray(parsed.snippets)) {
|
|
63
|
+
if ((parsed.version !== 1 && parsed.version !== 2) || !Array.isArray(parsed.snippets)) {
|
|
56
64
|
logDebug('snippets.catalog.loadIssue', {
|
|
57
65
|
issue: 'invalid snippet catalog header',
|
|
58
66
|
path: SNIPPETS_PATH,
|
|
@@ -66,7 +74,7 @@ export function loadSnippetCatalog(): SnippetRecord[] {
|
|
|
66
74
|
})
|
|
67
75
|
return []
|
|
68
76
|
}
|
|
69
|
-
return parsed.snippets
|
|
77
|
+
return parsed.snippets.map(stripUserVars)
|
|
70
78
|
} catch (error) {
|
|
71
79
|
logDebug('snippets.catalog.loadIssue', {
|
|
72
80
|
issue: error instanceof Error ? error.message : String(error),
|
|
@@ -79,7 +87,16 @@ export function loadSnippetCatalog(): SnippetRecord[] {
|
|
|
79
87
|
export function saveSnippetCatalog(snippets: SnippetRecord[]): void {
|
|
80
88
|
try {
|
|
81
89
|
mkdirSync(getProfileConfigDir(), { recursive: true })
|
|
82
|
-
|
|
90
|
+
// Persist only user-owned snippets. Config-pinned entries are reapplied
|
|
91
|
+
// at boot from `aimux.config.ts`.
|
|
92
|
+
const userSnippets = snippets.filter((s) => !isConfigSnippetId(s.id)).map(stripUserVars)
|
|
93
|
+
// Schema v2 adds the optional `trigger` and `vars` fields. v1 files are
|
|
94
|
+
// still accepted on read (they validate as v2 — both fields are optional)
|
|
95
|
+
// and get rewritten as v2 on the next save.
|
|
96
|
+
writeFileSync(
|
|
97
|
+
SNIPPETS_PATH,
|
|
98
|
+
`${JSON.stringify({ snippets: userSnippets, version: 2 }, null, 2)}\n`
|
|
99
|
+
)
|
|
83
100
|
} catch (error) {
|
|
84
101
|
logDebug('snippets.catalog.saveError', {
|
|
85
102
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -88,3 +105,47 @@ export function saveSnippetCatalog(snippets: SnippetRecord[]): void {
|
|
|
88
105
|
})
|
|
89
106
|
}
|
|
90
107
|
}
|
|
108
|
+
|
|
109
|
+
export const CONFIG_SNIPPET_ID_PREFIX = 'config:'
|
|
110
|
+
|
|
111
|
+
export function isConfigSnippetId(id: string): boolean {
|
|
112
|
+
return id.startsWith(CONFIG_SNIPPET_ID_PREFIX)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Shell `vars` are only authorized on config-pinned snippets (those defined in
|
|
117
|
+
* `aimux.config.ts`). If they ever appear on a user-edited JSON snippet — by
|
|
118
|
+
* hand edit, restore, or import — strip them. This keeps shell execution
|
|
119
|
+
* gated by the user's TypeScript config file.
|
|
120
|
+
*
|
|
121
|
+
* Exported for testing; called at both load and save time.
|
|
122
|
+
*/
|
|
123
|
+
export function stripUserVars(snippet: SnippetRecord): SnippetRecord {
|
|
124
|
+
if (snippet.vars === undefined) return snippet
|
|
125
|
+
if (isConfigSnippetId(snippet.id)) return snippet
|
|
126
|
+
logDebug('snippets.catalog.strippedVars', { id: snippet.id, name: snippet.name })
|
|
127
|
+
const { vars, ...clean } = snippet
|
|
128
|
+
void vars
|
|
129
|
+
return clean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge config-defined snippets with user-edited snippets.
|
|
134
|
+
* Config-pinned snippets ("sticky") get a stable id `config:${name}` and win
|
|
135
|
+
* over a user-edited snippet with the same id (they're read-only in the UI).
|
|
136
|
+
*/
|
|
137
|
+
export function mergeConfigSnippets(
|
|
138
|
+
userSnippets: readonly SnippetRecord[],
|
|
139
|
+
configSnippets: readonly SnippetDef[]
|
|
140
|
+
): SnippetRecord[] {
|
|
141
|
+
const fromConfig: SnippetRecord[] = configSnippets.map((s) => ({
|
|
142
|
+
content: s.text,
|
|
143
|
+
id: `${CONFIG_SNIPPET_ID_PREFIX}${s.name}`,
|
|
144
|
+
name: s.name,
|
|
145
|
+
trigger: s.trigger,
|
|
146
|
+
vars: s.vars,
|
|
147
|
+
}))
|
|
148
|
+
const configIds = new Set(fromConfig.map((s) => s.id))
|
|
149
|
+
const userKept = userSnippets.filter((s) => !configIds.has(s.id))
|
|
150
|
+
return [...fromConfig, ...userKept]
|
|
151
|
+
}
|
package/src/state/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ModeId } from '@brimveyn/aimux-config'
|
|
1
|
+
import type { ModeId, SnippetVar } from '@brimveyn/aimux-config'
|
|
2
2
|
import type { ThemedToken } from 'shiki'
|
|
3
3
|
|
|
4
|
-
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal'
|
|
4
|
+
export type BuiltinAssistantId = 'claude' | 'codex' | 'opencode' | 'terminal' | 'antigravity'
|
|
5
5
|
|
|
6
6
|
export type AssistantId = BuiltinAssistantId | (string & {})
|
|
7
7
|
|
|
@@ -306,6 +306,11 @@ export interface ModalRenameTab extends ModalBase {
|
|
|
306
306
|
|
|
307
307
|
export interface ModalSnippetPicker extends ModalBase {
|
|
308
308
|
type: 'snippet-picker'
|
|
309
|
+
/**
|
|
310
|
+
* Transient status line shown at the bottom of the picker (e.g. error from
|
|
311
|
+
* an open-in-editor attempt). Cleared automatically when the modal closes.
|
|
312
|
+
*/
|
|
313
|
+
actionMessage?: string | null
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
export interface ModalThemePicker extends ModalBase {
|
|
@@ -342,7 +347,12 @@ export interface ModalCreateSession extends ModalBase {
|
|
|
342
347
|
|
|
343
348
|
export interface ModalSnippetEditor extends ModalBase {
|
|
344
349
|
type: 'snippet-editor'
|
|
345
|
-
activeField: 'name' | 'content'
|
|
350
|
+
activeField: 'name' | 'trigger' | 'content'
|
|
351
|
+
/** Persisted value of the name field when it is not the active editor. */
|
|
352
|
+
nameBuffer: string
|
|
353
|
+
/** Persisted value of the trigger field when it is not the active editor. */
|
|
354
|
+
triggerBuffer: string
|
|
355
|
+
/** Persisted value of the content field when it is not the active editor. */
|
|
346
356
|
contentBuffer: string
|
|
347
357
|
}
|
|
348
358
|
|
|
@@ -388,6 +398,8 @@ export interface SnippetRecord {
|
|
|
388
398
|
id: string
|
|
389
399
|
name: string
|
|
390
400
|
content: string
|
|
401
|
+
trigger?: string
|
|
402
|
+
vars?: Record<string, SnippetVar>
|
|
391
403
|
}
|
|
392
404
|
|
|
393
405
|
export interface DiscoveredRepo {
|
|
@@ -643,6 +655,7 @@ export type GitModeAction =
|
|
|
643
655
|
| { type: 'git-mode-set-pending-delete'; path: string | null }
|
|
644
656
|
| { type: 'git-mode-clear-diff-cache'; path: string }
|
|
645
657
|
| { type: 'git-mode-set-message'; message: string | null }
|
|
658
|
+
| { type: 'snippet-picker-set-message'; message: string | null }
|
|
646
659
|
| { type: 'git-mode-toggle-diff-view' }
|
|
647
660
|
| { type: 'git-mode-shift-head-offset'; delta: number }
|
|
648
661
|
| { type: 'git-mode-set-head-offset'; offset: number }
|
package/src/state/validation.ts
CHANGED
|
@@ -155,8 +155,29 @@ export function isSessionRecord(value: unknown): value is SessionRecord {
|
|
|
155
155
|
)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function isSnippetVar(value: unknown): boolean {
|
|
159
|
+
if (!isObjectRecord(value)) return false
|
|
160
|
+
if (!isString(value.sh)) return false
|
|
161
|
+
if (value.timeout !== undefined && !isFiniteNumber(value.timeout)) return false
|
|
162
|
+
if (value.trim !== undefined && !isBoolean(value.trim)) return false
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isSnippetVarRecord(value: unknown): boolean {
|
|
167
|
+
if (!isObjectRecord(value)) return false
|
|
168
|
+
for (const entry of Object.values(value)) {
|
|
169
|
+
if (!isSnippetVar(entry)) return false
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
158
174
|
export function isSnippetRecord(value: unknown): value is SnippetRecord {
|
|
159
175
|
return (
|
|
160
|
-
isObjectRecord(value) &&
|
|
176
|
+
isObjectRecord(value) &&
|
|
177
|
+
isString(value.id) &&
|
|
178
|
+
isString(value.name) &&
|
|
179
|
+
isString(value.content) &&
|
|
180
|
+
(value.trigger === undefined || isString(value.trigger)) &&
|
|
181
|
+
(value.vars === undefined || isSnippetVarRecord(value.vars))
|
|
161
182
|
)
|
|
162
183
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
+
import type { MeasuredPaneRect } from '../../../app-runtime/use-pane-size-report'
|
|
3
4
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
4
5
|
import type { FocusMode, TabSession } from '../../../state/types'
|
|
5
6
|
|
|
@@ -39,6 +40,7 @@ interface SplitLayoutProps {
|
|
|
39
40
|
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
40
41
|
onSeparatorDragEnd?: () => void
|
|
41
42
|
onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
|
|
43
|
+
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
42
44
|
contentOrigin: TerminalContentOrigin
|
|
43
45
|
bounds: PaneRect
|
|
44
46
|
}
|
|
@@ -52,6 +54,7 @@ export function SplitLayout({
|
|
|
52
54
|
mouseForwardingEnabled,
|
|
53
55
|
node,
|
|
54
56
|
onLeftEdgeMouseDown,
|
|
57
|
+
onMeasure,
|
|
55
58
|
onPaneActivate,
|
|
56
59
|
onSeparatorDrag,
|
|
57
60
|
onSeparatorDragEnd,
|
|
@@ -104,6 +107,7 @@ export function SplitLayout({
|
|
|
104
107
|
onSeparatorDrag={onSeparatorDrag}
|
|
105
108
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
106
109
|
onLeftEdgeMouseDown={onLeftEdgeMouseDown}
|
|
110
|
+
onMeasure={onMeasure}
|
|
107
111
|
/>
|
|
108
112
|
)
|
|
109
113
|
}
|
|
@@ -144,6 +148,7 @@ export function SplitLayout({
|
|
|
144
148
|
onSeparatorDrag={onSeparatorDrag}
|
|
145
149
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
146
150
|
onLeftEdgeMouseDown={onLeftEdgeMouseDown}
|
|
151
|
+
onMeasure={onMeasure}
|
|
147
152
|
contentOrigin={contentOrigin}
|
|
148
153
|
bounds={firstBounds}
|
|
149
154
|
/>
|
|
@@ -187,6 +192,7 @@ export function SplitLayout({
|
|
|
187
192
|
onSeparatorDrag={onSeparatorDrag}
|
|
188
193
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
189
194
|
onLeftEdgeMouseDown={secondLeftEdgeMouseDown}
|
|
195
|
+
onMeasure={onMeasure}
|
|
190
196
|
contentOrigin={contentOrigin}
|
|
191
197
|
bounds={secondBounds}
|
|
192
198
|
/>
|
|
@@ -5,6 +5,7 @@ import { memo, type ReactNode } from 'react'
|
|
|
5
5
|
import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
|
|
6
6
|
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
|
|
7
7
|
|
|
8
|
+
import { type MeasuredPaneRect, usePaneSizeReport } from '../../../app-runtime/use-pane-size-report'
|
|
8
9
|
import { logInputDebug } from '../../../debug/input-log'
|
|
9
10
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
|
|
10
11
|
import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
|
|
@@ -28,6 +29,7 @@ interface TerminalPaneProps {
|
|
|
28
29
|
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
29
30
|
onSeparatorDragEnd?: () => void
|
|
30
31
|
onLeftEdgeMouseDown?: (event: OtuiMouseEvent) => boolean
|
|
32
|
+
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
function getTitle(
|
|
@@ -112,6 +114,7 @@ export function TerminalPane({
|
|
|
112
114
|
localScrollbackEnabled,
|
|
113
115
|
mouseForwardingEnabled,
|
|
114
116
|
onLeftEdgeMouseDown,
|
|
117
|
+
onMeasure,
|
|
115
118
|
onPaneActivate,
|
|
116
119
|
onSeparatorDrag,
|
|
117
120
|
onSeparatorDragEnd,
|
|
@@ -124,6 +127,7 @@ export function TerminalPane({
|
|
|
124
127
|
tabId,
|
|
125
128
|
}: TerminalPaneProps) {
|
|
126
129
|
const t = useTheme()
|
|
130
|
+
const setContentBox = usePaneSizeReport(tabId, !!tab, onMeasure)
|
|
127
131
|
const editorBg = t.background
|
|
128
132
|
const paneIsActive = isActive ?? true
|
|
129
133
|
const canForwardMouse = focusMode === 'terminal-input' && !!tab && mouseForwardingEnabled
|
|
@@ -287,9 +291,11 @@ export function TerminalPane({
|
|
|
287
291
|
</box>
|
|
288
292
|
) : (
|
|
289
293
|
<box
|
|
294
|
+
ref={setContentBox}
|
|
290
295
|
flexDirection="column"
|
|
291
296
|
flexGrow={1}
|
|
292
297
|
width="100%"
|
|
298
|
+
overflow="hidden"
|
|
293
299
|
onMouseDown={(e) => {
|
|
294
300
|
e.stopPropagation()
|
|
295
301
|
forwardMouseEvent(e)
|
|
@@ -302,10 +308,18 @@ export function TerminalPane({
|
|
|
302
308
|
</box>
|
|
303
309
|
)}
|
|
304
310
|
</ContextMenuBox>
|
|
305
|
-
{tab?.status === 'disconnected' ? (
|
|
306
|
-
|
|
311
|
+
{tab?.status === 'disconnected' || tab?.errorMessage ? (
|
|
312
|
+
// Absolutely positioned so it overlays the bordered box instead of
|
|
313
|
+
// consuming a flex row. If it took a row, the rendered terminal area
|
|
314
|
+
// would be one line shorter than the size sent to the PTY/xterm,
|
|
315
|
+
// re-introducing the shifted-content / dead-row bug.
|
|
316
|
+
<box position="absolute" bottom={0} left={0} backgroundColor={editorBg}>
|
|
317
|
+
{tab?.status === 'disconnected' ? (
|
|
318
|
+
<text fg={t.warning}>Restored snapshot. Press Ctrl+r to restart this workspace.</text>
|
|
319
|
+
) : null}
|
|
320
|
+
{tab?.errorMessage ? <text fg={t.error}>{tab.errorMessage}</text> : null}
|
|
321
|
+
</box>
|
|
307
322
|
) : null}
|
|
308
|
-
{tab?.errorMessage ? <text fg={t.error}>{tab.errorMessage}</text> : null}
|
|
309
323
|
</box>
|
|
310
324
|
)
|
|
311
325
|
}
|
|
@@ -2,8 +2,9 @@ import { uiTokens } from '../../../ui-tokens'
|
|
|
2
2
|
import { Form, TextField } from '../shared/form'
|
|
3
3
|
|
|
4
4
|
interface SnippetEditorModalProps {
|
|
5
|
-
activeField: 'name' | 'content'
|
|
5
|
+
activeField: 'name' | 'trigger' | 'content'
|
|
6
6
|
snippetName: string
|
|
7
|
+
snippetTrigger: string
|
|
7
8
|
snippetContent: string
|
|
8
9
|
isEditing: boolean
|
|
9
10
|
}
|
|
@@ -13,18 +14,21 @@ export function SnippetEditorModal({
|
|
|
13
14
|
isEditing,
|
|
14
15
|
snippetContent,
|
|
15
16
|
snippetName,
|
|
17
|
+
snippetTrigger,
|
|
16
18
|
}: SnippetEditorModalProps) {
|
|
17
|
-
const nameActive = activeField === 'name'
|
|
18
|
-
const contentActive = activeField === 'content'
|
|
19
|
-
|
|
20
19
|
return (
|
|
21
20
|
<Form
|
|
22
21
|
title={isEditing ? 'Edit snippet' : 'Create snippet'}
|
|
23
22
|
keybindsModeId="modal.snippet-editor"
|
|
24
23
|
width={uiTokens.modalWidth.xl}
|
|
25
24
|
>
|
|
26
|
-
<TextField active={
|
|
27
|
-
<TextField
|
|
25
|
+
<TextField active={activeField === 'name'} label="Name" value={snippetName} />
|
|
26
|
+
<TextField
|
|
27
|
+
active={activeField === 'trigger'}
|
|
28
|
+
label="Trigger (optional)"
|
|
29
|
+
value={snippetTrigger}
|
|
30
|
+
/>
|
|
31
|
+
<TextField active={activeField === 'content'} label="Content" value={snippetContent} />
|
|
28
32
|
</Form>
|
|
29
33
|
)
|
|
30
34
|
}
|
|
@@ -2,6 +2,7 @@ import type { SnippetRecord } from '../../../../state/types'
|
|
|
2
2
|
|
|
3
3
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
4
4
|
import { filterSnippets } from '../../../../state/selectors'
|
|
5
|
+
import { isConfigSnippetId } from '../../../../state/snippet-catalog'
|
|
5
6
|
import { useTheme } from '../../../theme'
|
|
6
7
|
import { uiTokens } from '../../../ui-tokens'
|
|
7
8
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
@@ -11,6 +12,7 @@ interface SnippetPickerModalProps {
|
|
|
11
12
|
selectedIndex: number
|
|
12
13
|
filter: string | null
|
|
13
14
|
cursorPos?: number
|
|
15
|
+
actionMessage?: string | null
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const MAX_PREVIEW_LENGTH = 60
|
|
@@ -22,6 +24,7 @@ function truncateContent(content: string): string {
|
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function SnippetPickerModal({
|
|
27
|
+
actionMessage,
|
|
25
28
|
cursorPos,
|
|
26
29
|
filter,
|
|
27
30
|
selectedIndex,
|
|
@@ -32,19 +35,26 @@ export function SnippetPickerModal({
|
|
|
32
35
|
|
|
33
36
|
const items: PickerItem[] = filtered.map((snippet, index) => {
|
|
34
37
|
const active = index === selectedIndex
|
|
38
|
+
const fromConfig = isConfigSnippetId(snippet.id)
|
|
35
39
|
return {
|
|
36
40
|
key: snippet.id,
|
|
37
41
|
onClick: () => {
|
|
38
42
|
dispatchGlobal({ type: 'close-modal' })
|
|
39
43
|
runSideEffectGlobal({ type: 'paste-selected-snippet' })
|
|
40
44
|
},
|
|
41
|
-
onDelete:
|
|
42
|
-
|
|
45
|
+
onDelete: fromConfig
|
|
46
|
+
? undefined
|
|
47
|
+
: () => runSideEffectGlobal({ type: 'delete-selected-snippet' }),
|
|
48
|
+
onEdit: fromConfig ? undefined : () => runSideEffectGlobal({ type: 'edit-selected-snippet' }),
|
|
43
49
|
subtitle: <text fg={t.textMuted}>{truncateContent(snippet.content)}</text>,
|
|
44
50
|
title: (
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
51
|
+
<box flexDirection="row">
|
|
52
|
+
<text fg={active ? t.text : t.textMuted}>
|
|
53
|
+
<strong>{snippet.name}</strong>
|
|
54
|
+
</text>
|
|
55
|
+
{snippet.trigger ? <text fg={t.textMuted}>{` :${snippet.trigger}`}</text> : null}
|
|
56
|
+
{fromConfig ? <text fg={t.textMuted}>{' [config]'}</text> : null}
|
|
57
|
+
</box>
|
|
48
58
|
),
|
|
49
59
|
}
|
|
50
60
|
})
|
|
@@ -64,6 +74,7 @@ export function SnippetPickerModal({
|
|
|
64
74
|
{filter ? 'No matching snippets.' : 'No snippets yet. Press n to create one.'}
|
|
65
75
|
</text>
|
|
66
76
|
}
|
|
77
|
+
footer={actionMessage ? <text fg={t.error}>{actionMessage}</text> : undefined}
|
|
67
78
|
onHover={(index) => dispatchGlobal({ index, type: 'set-modal-selection-index' })}
|
|
68
79
|
/>
|
|
69
80
|
)
|
package/src/ui/root.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MouseEvent } from '@opentui/core'
|
|
2
2
|
|
|
3
|
+
import type { MeasuredPaneRect } from '../app-runtime/use-pane-size-report'
|
|
3
4
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
4
5
|
import type { FocusMode, ModalState, SessionRecord, SnippetRecord } from '../state/types'
|
|
5
6
|
import type { ThemeId } from './themes'
|
|
@@ -52,19 +53,14 @@ function getCreateSessionFields(modal: ModalState) {
|
|
|
52
53
|
|
|
53
54
|
function getSnippetEditorFields(modal: ModalState) {
|
|
54
55
|
if (modal.type !== 'snippet-editor') {
|
|
55
|
-
return { snippetContent: '', snippetName: '' }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (modal.activeField === 'name') {
|
|
59
|
-
return {
|
|
60
|
-
snippetContent: modal.contentBuffer,
|
|
61
|
-
snippetName: modal.editBuffer ?? '',
|
|
62
|
-
}
|
|
56
|
+
return { snippetContent: '', snippetName: '', snippetTrigger: '' }
|
|
63
57
|
}
|
|
64
58
|
|
|
59
|
+
const editValue = modal.editBuffer ?? ''
|
|
65
60
|
return {
|
|
66
|
-
snippetContent: modal.
|
|
67
|
-
snippetName: modal.
|
|
61
|
+
snippetContent: modal.activeField === 'content' ? editValue : modal.contentBuffer,
|
|
62
|
+
snippetName: modal.activeField === 'name' ? editValue : modal.nameBuffer,
|
|
63
|
+
snippetTrigger: modal.activeField === 'trigger' ? editValue : modal.triggerBuffer,
|
|
68
64
|
}
|
|
69
65
|
}
|
|
70
66
|
|
|
@@ -78,7 +74,7 @@ function renderModal(
|
|
|
78
74
|
snippets: SnippetRecord[]
|
|
79
75
|
themeId: ThemeId
|
|
80
76
|
createSessionFields: { directoryQuery: string; sessionName: string }
|
|
81
|
-
snippetEditorFields: { snippetName: string; snippetContent: string }
|
|
77
|
+
snippetEditorFields: { snippetName: string; snippetTrigger: string; snippetContent: string }
|
|
82
78
|
focusMode: FocusMode
|
|
83
79
|
activeAssistant?: string
|
|
84
80
|
autoCommitModel?: string
|
|
@@ -135,6 +131,7 @@ function renderModal(
|
|
|
135
131
|
selectedIndex={modal.selectedIndex}
|
|
136
132
|
filter={modal.editBuffer}
|
|
137
133
|
cursorPos={modal.cursorPos}
|
|
134
|
+
actionMessage={modal.actionMessage}
|
|
138
135
|
/>
|
|
139
136
|
)
|
|
140
137
|
case 'snippet-editor':
|
|
@@ -142,6 +139,7 @@ function renderModal(
|
|
|
142
139
|
<SnippetEditorModal
|
|
143
140
|
activeField={modal.activeField}
|
|
144
141
|
snippetName={options.snippetEditorFields.snippetName}
|
|
142
|
+
snippetTrigger={options.snippetEditorFields.snippetTrigger}
|
|
145
143
|
snippetContent={options.snippetEditorFields.snippetContent}
|
|
146
144
|
isEditing={modal.sessionTargetId !== null}
|
|
147
145
|
/>
|
|
@@ -229,6 +227,7 @@ interface RootViewProps {
|
|
|
229
227
|
}) => void
|
|
230
228
|
onSeparatorDrag?: (event: MouseEvent) => boolean
|
|
231
229
|
onSeparatorDragEnd?: () => void
|
|
230
|
+
onMeasure?: (tabId: string, rect: MeasuredPaneRect) => void
|
|
232
231
|
terminalCols: number
|
|
233
232
|
terminalRows: number
|
|
234
233
|
}
|
|
@@ -239,6 +238,7 @@ export function RootView({
|
|
|
239
238
|
mouseForwardingEnabled,
|
|
240
239
|
onEmbeddedGitResizeStart,
|
|
241
240
|
onGitPaneResizeStart,
|
|
241
|
+
onMeasure,
|
|
242
242
|
onPaneActivate,
|
|
243
243
|
onSeparatorDrag,
|
|
244
244
|
onSeparatorDragEnd,
|
|
@@ -375,6 +375,7 @@ export function RootView({
|
|
|
375
375
|
onSeparatorDrag={onSeparatorDrag}
|
|
376
376
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
377
377
|
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
378
|
+
onMeasure={onMeasure}
|
|
378
379
|
bounds={{
|
|
379
380
|
cols: terminalCols + splitChrome,
|
|
380
381
|
rows: terminalRows + splitChrome,
|
|
@@ -398,6 +399,7 @@ export function RootView({
|
|
|
398
399
|
onTerminalMouseUp={onTerminalMouseUp}
|
|
399
400
|
onPaneActivate={onPaneActivate}
|
|
400
401
|
onLeftEdgeMouseDown={handleTerminalLeftEdgeMouseDown}
|
|
402
|
+
onMeasure={onMeasure}
|
|
401
403
|
/>
|
|
402
404
|
)}
|
|
403
405
|
{gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (
|