opencode-snippets 2.1.1 → 2.1.3
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/README.md +9 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -4
- package/dist/index.js.map +1 -1
- package/dist/src/commands.d.ts +5 -1
- package/dist/src/commands.d.ts.map +1 -1
- package/dist/src/commands.js +44 -21
- package/dist/src/commands.js.map +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/expander.d.ts.map +1 -1
- package/dist/src/expander.js +6 -0
- package/dist/src/expander.js.map +1 -1
- package/dist/src/pending-drafts.d.ts +6 -0
- package/dist/src/pending-drafts.d.ts.map +1 -0
- package/dist/src/pending-drafts.js +106 -0
- package/dist/src/pending-drafts.js.map +1 -0
- package/dist/src/reload-signal.d.ts +3 -0
- package/dist/src/reload-signal.d.ts.map +1 -0
- package/dist/src/reload-signal.js +52 -0
- package/dist/src/reload-signal.js.map +1 -0
- package/dist/src/tui-trigger.d.ts +1 -0
- package/dist/src/tui-trigger.d.ts.map +1 -1
- package/dist/src/tui-trigger.js +3 -0
- package/dist/src/tui-trigger.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.jsx +310 -72
- package/dist/tui.jsx.map +1 -1
- package/package.json +1 -1
- package/skill/snippets/SKILL.md +6 -5
package/dist/tui.jsx
CHANGED
|
@@ -5,9 +5,11 @@ import { useKeyboard } from "@opentui/solid";
|
|
|
5
5
|
import { createEffect, createMemo, createResource, createSignal, Index, onCleanup, Show, } from "solid-js";
|
|
6
6
|
import { CONFIG } from "./src/constants.js";
|
|
7
7
|
import { ensureSnippetsDir, listSnippets, loadSnippets } from "./src/loader.js";
|
|
8
|
+
import { addPendingDraft } from "./src/pending-drafts.js";
|
|
9
|
+
import { markSnippetReloadRequested } from "./src/reload-signal.js";
|
|
8
10
|
import { loadSkills } from "./src/skill-loader.js";
|
|
9
11
|
import { filterSkills, filterSnippets, highlightMatches, matchedAliases, snippetDescription, } from "./src/tui-search.js";
|
|
10
|
-
import { findTrailingHashtagTrigger, insertSkillLoad, insertSnippetTag, insertSnippetTrigger, preferredSnippetTag, stepSelection, } from "./src/tui-trigger.js";
|
|
12
|
+
import { findTrailingHashtagTrigger, insertSkillLoad, insertSnippetTag, insertSnippetTrigger, isReloadCommand, preferredSnippetTag, stepSelection, } from "./src/tui-trigger.js";
|
|
11
13
|
const id = "opencode-snippets:autocomplete";
|
|
12
14
|
const PROMPT_SYNC_MS = 50;
|
|
13
15
|
const MENU_MAX_HEIGHT = 10;
|
|
@@ -24,7 +26,6 @@ const EMPTY_SNIPPET = `---
|
|
|
24
26
|
description: ""
|
|
25
27
|
---
|
|
26
28
|
|
|
27
|
-
|
|
28
29
|
`;
|
|
29
30
|
const INLINE_BORDER = {
|
|
30
31
|
border: ["left", "right"],
|
|
@@ -89,6 +90,49 @@ function normalizeSnippetName(input) {
|
|
|
89
90
|
.replace(/-{2,}/g, "-")
|
|
90
91
|
.replace(/^-+|-+$/g, "");
|
|
91
92
|
}
|
|
93
|
+
function resolveExternalEditor() {
|
|
94
|
+
const visual = Bun.env.VISUAL?.trim();
|
|
95
|
+
if (visual) {
|
|
96
|
+
return {
|
|
97
|
+
command: visual,
|
|
98
|
+
env: "VISUAL",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const editor = Bun.env.EDITOR?.trim();
|
|
102
|
+
if (editor) {
|
|
103
|
+
return {
|
|
104
|
+
command: editor,
|
|
105
|
+
env: "EDITOR",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function editorBinary(editor) {
|
|
110
|
+
return editor.command.trim().split(/\s+/)[0] || "";
|
|
111
|
+
}
|
|
112
|
+
function usesTerminalUi(editor) {
|
|
113
|
+
const bin = editorBinary(editor).split(/[\\/]/).pop()?.toLowerCase();
|
|
114
|
+
if (!bin)
|
|
115
|
+
return true;
|
|
116
|
+
return ![
|
|
117
|
+
"code",
|
|
118
|
+
"code-insiders",
|
|
119
|
+
"cursor",
|
|
120
|
+
"windsurf",
|
|
121
|
+
"subl",
|
|
122
|
+
"zed",
|
|
123
|
+
"mate",
|
|
124
|
+
"idea",
|
|
125
|
+
"webstorm",
|
|
126
|
+
"pycharm",
|
|
127
|
+
"goland",
|
|
128
|
+
"clion",
|
|
129
|
+
"rubymine",
|
|
130
|
+
"fleet",
|
|
131
|
+
"notepad",
|
|
132
|
+
"notepad++",
|
|
133
|
+
"open",
|
|
134
|
+
].includes(bin);
|
|
135
|
+
}
|
|
92
136
|
async function ensureSnippetDraft(name, projectDir) {
|
|
93
137
|
const dir = await ensureSnippetsDir(projectDir);
|
|
94
138
|
const filePath = join(dir, `${name}${CONFIG.SNIPPET_EXTENSION}`);
|
|
@@ -97,27 +141,31 @@ async function ensureSnippetDraft(name, projectDir) {
|
|
|
97
141
|
}
|
|
98
142
|
return filePath;
|
|
99
143
|
}
|
|
100
|
-
async function openExternalEditor(api, filePath) {
|
|
101
|
-
const editor = Bun.env.VISUAL || Bun.env.EDITOR;
|
|
144
|
+
async function openExternalEditor(api, filePath, editor) {
|
|
102
145
|
if (!editor)
|
|
103
146
|
return false;
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
const interactive = usesTerminalUi(editor);
|
|
148
|
+
if (interactive) {
|
|
149
|
+
api.renderer.suspend();
|
|
150
|
+
api.renderer.currentRenderBuffer.clear();
|
|
151
|
+
}
|
|
106
152
|
try {
|
|
107
153
|
const cmd = process.platform === "win32"
|
|
108
|
-
? ["cmd", "/c", `${editor} "${filePath.replace(/"/g, '\\"')}"`]
|
|
109
|
-
: [...editor.split(" "), filePath];
|
|
154
|
+
? ["cmd", "/c", `${editor.command} "${filePath.replace(/"/g, '\\"')}"`]
|
|
155
|
+
: [...editor.command.split(" "), filePath];
|
|
110
156
|
const proc = Bun.spawn(cmd, {
|
|
111
|
-
stdin: "inherit",
|
|
112
|
-
stdout: "inherit",
|
|
113
|
-
stderr: "inherit",
|
|
157
|
+
stdin: interactive ? "inherit" : "ignore",
|
|
158
|
+
stdout: interactive ? "inherit" : "ignore",
|
|
159
|
+
stderr: interactive ? "inherit" : "ignore",
|
|
114
160
|
});
|
|
115
161
|
await proc.exited;
|
|
116
162
|
return true;
|
|
117
163
|
}
|
|
118
164
|
finally {
|
|
119
|
-
|
|
120
|
-
|
|
165
|
+
if (interactive) {
|
|
166
|
+
api.renderer.currentRenderBuffer.clear();
|
|
167
|
+
api.renderer.resume();
|
|
168
|
+
}
|
|
121
169
|
api.renderer.requestRender();
|
|
122
170
|
}
|
|
123
171
|
}
|
|
@@ -136,6 +184,29 @@ async function getSnippets(api) {
|
|
|
136
184
|
const registry = await loadSnippets(api.state.path.directory);
|
|
137
185
|
return sortSnippets(listSnippets(registry));
|
|
138
186
|
}
|
|
187
|
+
async function reloadSnippetsInTui(api) {
|
|
188
|
+
const registry = await loadSnippets(api.state.path.directory);
|
|
189
|
+
await markSnippetReloadRequested(api.state.path.directory);
|
|
190
|
+
return listSnippets(registry).length;
|
|
191
|
+
}
|
|
192
|
+
function executeReloadInPrompt(api, ref, clear, refresh) {
|
|
193
|
+
void (async () => {
|
|
194
|
+
const count = await reloadSnippetsInTui(api);
|
|
195
|
+
await refresh();
|
|
196
|
+
clear();
|
|
197
|
+
ref.focus();
|
|
198
|
+
api.renderer.requestRender();
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
api.ui.toast({
|
|
201
|
+
variant: "success",
|
|
202
|
+
title: "Snippets reloaded",
|
|
203
|
+
message: `Reloaded ${count} snippet${count === 1 ? "" : "s"}.`,
|
|
204
|
+
duration: 3000,
|
|
205
|
+
});
|
|
206
|
+
api.renderer.requestRender();
|
|
207
|
+
}, 0);
|
|
208
|
+
})();
|
|
209
|
+
}
|
|
139
210
|
async function getSkills(api) {
|
|
140
211
|
const registry = await loadSkills(api.state.path.directory);
|
|
141
212
|
return sortSkills([...registry.values()]);
|
|
@@ -147,11 +218,15 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
147
218
|
const [prompt, setPrompt] = createSignal();
|
|
148
219
|
const [dismissed, setDismissed] = createSignal();
|
|
149
220
|
const [selected, setSelected] = createSignal(0);
|
|
150
|
-
const [, setInputMode] = createSignal("keyboard");
|
|
221
|
+
const [inputMode, setInputMode] = createSignal("keyboard");
|
|
151
222
|
const [ignoreMouseUntil, setIgnoreMouseUntil] = createSignal(0);
|
|
152
223
|
const [lastMousePos, setLastMousePos] = createSignal();
|
|
153
224
|
const [input, setInput] = createSignal("");
|
|
225
|
+
const [syncingPrompt, setSyncingPrompt] = createSignal(false);
|
|
226
|
+
const [menuEpoch, setMenuEpoch] = createSignal(0);
|
|
154
227
|
const [creating, setCreating] = createSignal(false);
|
|
228
|
+
const [dialogOpen, setDialogOpen] = createSignal(false);
|
|
229
|
+
const [dialogHandoffUntil, setDialogHandoffUntil] = createSignal(0);
|
|
155
230
|
const [snippets, { refetch: refetchSnippets }] = createResource(() => props.api.state.path.directory, () => getSnippets(props.api), {
|
|
156
231
|
initialValue: [],
|
|
157
232
|
});
|
|
@@ -163,11 +238,50 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
163
238
|
props.bindPrompt(ref);
|
|
164
239
|
props.hostRef?.(ref);
|
|
165
240
|
};
|
|
241
|
+
const refreshSnippetOptions = async () => {
|
|
242
|
+
await refetchSnippets();
|
|
243
|
+
};
|
|
244
|
+
let pendingPromptSync;
|
|
245
|
+
let pendingPromptFocus;
|
|
246
|
+
let pendingDialogHandoff;
|
|
247
|
+
onCleanup(() => {
|
|
248
|
+
if (pendingPromptSync)
|
|
249
|
+
clearTimeout(pendingPromptSync);
|
|
250
|
+
if (pendingPromptFocus)
|
|
251
|
+
clearTimeout(pendingPromptFocus);
|
|
252
|
+
if (pendingDialogHandoff)
|
|
253
|
+
clearTimeout(pendingDialogHandoff);
|
|
254
|
+
});
|
|
166
255
|
const lockKeyboardSelection = () => {
|
|
167
256
|
setInputMode("keyboard");
|
|
168
257
|
setIgnoreMouseUntil(Date.now() + MOUSE_HOVER_SUPPRESS_MS);
|
|
169
258
|
};
|
|
170
259
|
const allowMouseHover = () => Date.now() >= ignoreMouseUntil();
|
|
260
|
+
const dialogBlockingInput = () => dialogOpen() || dialogHandoffUntil() > 0;
|
|
261
|
+
const beginDialogHandoff = () => {
|
|
262
|
+
if (pendingDialogHandoff)
|
|
263
|
+
clearTimeout(pendingDialogHandoff);
|
|
264
|
+
setDialogHandoffUntil(Date.now() + 150);
|
|
265
|
+
pendingDialogHandoff = setTimeout(() => {
|
|
266
|
+
pendingDialogHandoff = undefined;
|
|
267
|
+
setDialogHandoffUntil(0);
|
|
268
|
+
props.api.renderer.requestRender();
|
|
269
|
+
}, 175);
|
|
270
|
+
};
|
|
271
|
+
const handlePromptSubmit = () => {
|
|
272
|
+
if (dialogBlockingInput()) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const ref = prompt();
|
|
276
|
+
if (ref && isReloadCommand(ref.current.input)) {
|
|
277
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
278
|
+
syncPromptInput(ref, "");
|
|
279
|
+
setDismissed(undefined);
|
|
280
|
+
}, refreshSnippetOptions);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
props.onSubmit?.();
|
|
284
|
+
};
|
|
171
285
|
const recordMouseMove = (x, y) => {
|
|
172
286
|
const last = lastMousePos();
|
|
173
287
|
if (last?.x === x && last.y === y) {
|
|
@@ -176,20 +290,85 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
176
290
|
setLastMousePos({ x, y });
|
|
177
291
|
return true;
|
|
178
292
|
};
|
|
293
|
+
const restorePromptFocus = (ref) => {
|
|
294
|
+
if (pendingPromptFocus)
|
|
295
|
+
clearTimeout(pendingPromptFocus);
|
|
296
|
+
pendingPromptFocus = setTimeout(() => {
|
|
297
|
+
pendingPromptFocus = undefined;
|
|
298
|
+
ref.focus();
|
|
299
|
+
}, 175);
|
|
300
|
+
};
|
|
179
301
|
const syncPromptInput = (ref, nextInput) => {
|
|
180
302
|
setPromptInput(ref, nextInput);
|
|
181
303
|
setInput(nextInput);
|
|
304
|
+
setSyncingPrompt(false);
|
|
305
|
+
};
|
|
306
|
+
const optionsForQuery = (value) => {
|
|
307
|
+
const snippetOptions = filterSnippets(snippets(), value).map((snippet) => ({
|
|
308
|
+
kind: "snippet",
|
|
309
|
+
id: `snippet:${snippet.name}`,
|
|
310
|
+
label: `#${snippet.name}`,
|
|
311
|
+
description: snippetDescription(snippet),
|
|
312
|
+
aliases: matchedAliases(snippet, value),
|
|
313
|
+
snippet,
|
|
314
|
+
}));
|
|
315
|
+
const skillOptions = filterSkills(skills(), value).map((skill) => ({
|
|
316
|
+
kind: "skill",
|
|
317
|
+
id: `skill:${skill.name}`,
|
|
318
|
+
label: `#skill(${skill.name})`,
|
|
319
|
+
description: skillDescription(skill),
|
|
320
|
+
aliases: [],
|
|
321
|
+
skill,
|
|
322
|
+
}));
|
|
323
|
+
return [...snippetOptions, ...skillOptions];
|
|
324
|
+
};
|
|
325
|
+
const canCreateForQuery = (value) => {
|
|
326
|
+
const q = value.trim();
|
|
327
|
+
if (snippets.loading || skills.loading)
|
|
328
|
+
return false;
|
|
329
|
+
if (!q)
|
|
330
|
+
return false;
|
|
331
|
+
if (!normalizeSnippetName(q))
|
|
332
|
+
return false;
|
|
333
|
+
return optionsForQuery(q).length === 0;
|
|
334
|
+
};
|
|
335
|
+
const schedulePromptSync = () => {
|
|
336
|
+
const ref = prompt();
|
|
337
|
+
if (!ref)
|
|
338
|
+
return;
|
|
339
|
+
if (dialogBlockingInput())
|
|
340
|
+
return;
|
|
341
|
+
const prev = input();
|
|
342
|
+
setSyncingPrompt(true);
|
|
343
|
+
setMenuEpoch((n) => n + 1);
|
|
344
|
+
if (pendingPromptSync)
|
|
345
|
+
clearTimeout(pendingPromptSync);
|
|
346
|
+
pendingPromptSync = setTimeout(() => {
|
|
347
|
+
pendingPromptSync = undefined;
|
|
348
|
+
const next = ref.current.input;
|
|
349
|
+
setInput((prev) => (prev === next ? prev : next));
|
|
350
|
+
if (next !== prev) {
|
|
351
|
+
setSyncingPrompt(false);
|
|
352
|
+
}
|
|
353
|
+
props.api.renderer.requestRender();
|
|
354
|
+
}, 0);
|
|
182
355
|
};
|
|
183
356
|
createEffect(() => {
|
|
184
357
|
const ref = prompt();
|
|
185
358
|
if (!ref) {
|
|
186
359
|
setInput("");
|
|
360
|
+
setSyncingPrompt(false);
|
|
187
361
|
return;
|
|
188
362
|
}
|
|
189
363
|
// The prompt ref exposes current state but not an onInput hook, so mirror it.
|
|
190
364
|
const sync = () => {
|
|
191
365
|
const next = ref.current.input;
|
|
192
|
-
setInput((prev) =>
|
|
366
|
+
setInput((prev) => {
|
|
367
|
+
if (prev === next)
|
|
368
|
+
return prev;
|
|
369
|
+
setSyncingPrompt(false);
|
|
370
|
+
return next;
|
|
371
|
+
});
|
|
193
372
|
};
|
|
194
373
|
sync();
|
|
195
374
|
const timer = setInterval(sync, PROMPT_SYNC_MS);
|
|
@@ -205,29 +384,15 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
205
384
|
const next = match();
|
|
206
385
|
if (!next)
|
|
207
386
|
return [];
|
|
208
|
-
|
|
209
|
-
kind: "snippet",
|
|
210
|
-
id: `snippet:${snippet.name}`,
|
|
211
|
-
label: `#${snippet.name}`,
|
|
212
|
-
description: snippetDescription(snippet),
|
|
213
|
-
aliases: matchedAliases(snippet, next.query.trim()),
|
|
214
|
-
snippet,
|
|
215
|
-
}));
|
|
216
|
-
const skillOptions = filterSkills(skills(), next.query.trim()).map((skill) => ({
|
|
217
|
-
kind: "skill",
|
|
218
|
-
id: `skill:${skill.name}`,
|
|
219
|
-
label: `#skill(${skill.name})`,
|
|
220
|
-
description: skillDescription(skill),
|
|
221
|
-
aliases: [],
|
|
222
|
-
skill,
|
|
223
|
-
}));
|
|
224
|
-
return [...snippetOptions, ...skillOptions];
|
|
387
|
+
return optionsForQuery(next.query.trim());
|
|
225
388
|
});
|
|
226
389
|
const draftName = createMemo(() => normalizeSnippetName(query()));
|
|
227
390
|
const visible = createMemo(() => {
|
|
228
391
|
const next = match();
|
|
229
392
|
if (!next)
|
|
230
393
|
return false;
|
|
394
|
+
if (syncingPrompt())
|
|
395
|
+
return false;
|
|
231
396
|
return dismissed() !== next.token;
|
|
232
397
|
});
|
|
233
398
|
const canCreate = createMemo(() => {
|
|
@@ -249,6 +414,12 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
249
414
|
return undefined;
|
|
250
415
|
});
|
|
251
416
|
let scroll;
|
|
417
|
+
createEffect(() => {
|
|
418
|
+
menuEpoch();
|
|
419
|
+
if (visible()) {
|
|
420
|
+
scroll = undefined;
|
|
421
|
+
}
|
|
422
|
+
});
|
|
252
423
|
createEffect((prev) => {
|
|
253
424
|
const next = match();
|
|
254
425
|
if (!next) {
|
|
@@ -304,6 +475,19 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
304
475
|
let dispose;
|
|
305
476
|
const timer = setTimeout(() => {
|
|
306
477
|
dispose = props.api.command.register(() => [
|
|
478
|
+
{
|
|
479
|
+
title: "Reload snippets",
|
|
480
|
+
value: "snippets.reload",
|
|
481
|
+
description: "Reload snippet files from disk",
|
|
482
|
+
category: "Prompt",
|
|
483
|
+
slash: { name: "snippets:reload" },
|
|
484
|
+
onSelect() {
|
|
485
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
486
|
+
syncPromptInput(ref, "");
|
|
487
|
+
setDismissed(undefined);
|
|
488
|
+
}, refreshSnippetOptions);
|
|
489
|
+
},
|
|
490
|
+
},
|
|
307
491
|
{
|
|
308
492
|
title: "Accept snippet autocomplete",
|
|
309
493
|
value: "snippets.accept",
|
|
@@ -312,11 +496,35 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
312
496
|
hidden: true,
|
|
313
497
|
enabled: ref.focused,
|
|
314
498
|
onSelect() {
|
|
499
|
+
if (isReloadCommand(ref.current.input)) {
|
|
500
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
501
|
+
syncPromptInput(ref, "");
|
|
502
|
+
setDismissed(undefined);
|
|
503
|
+
}, refreshSnippetOptions);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (dialogBlockingInput()) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
315
509
|
const current = findTrailingHashtagTrigger(ref.current.input);
|
|
316
510
|
if (!current || dismissed() === current.token) {
|
|
317
511
|
ref.submit();
|
|
318
512
|
return;
|
|
319
513
|
}
|
|
514
|
+
const live = optionsForQuery(current.query.trim());
|
|
515
|
+
const index = Math.min(selected(), Math.max(live.length - 1, 0));
|
|
516
|
+
if (syncingPrompt()) {
|
|
517
|
+
if (live.length > 0) {
|
|
518
|
+
chooseItem(live[index] ?? live[0]);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (canCreateForQuery(current.query)) {
|
|
522
|
+
void createSnippetDraft(current.query);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
ref.submit();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
320
528
|
// Prefer the rendered dropdown state so Enter follows what the user can see.
|
|
321
529
|
if (visible()) {
|
|
322
530
|
const rendered = options();
|
|
@@ -325,23 +533,16 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
325
533
|
chooseItem(rendered[index] ?? rendered[0]);
|
|
326
534
|
return;
|
|
327
535
|
}
|
|
328
|
-
if (canCreate()) {
|
|
329
|
-
void createSnippetDraft();
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
536
|
}
|
|
333
537
|
if (snippets.loading || skills.loading) {
|
|
334
538
|
return;
|
|
335
539
|
}
|
|
336
|
-
const live = options();
|
|
337
540
|
if (live.length > 0) {
|
|
338
|
-
const index = Math.min(selected(), live.length - 1);
|
|
339
541
|
chooseItem(live[index] ?? live[0]);
|
|
340
542
|
return;
|
|
341
543
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
void createSnippetDraft();
|
|
544
|
+
if (canCreateForQuery(current.query)) {
|
|
545
|
+
void createSnippetDraft(current.query);
|
|
345
546
|
return;
|
|
346
547
|
}
|
|
347
548
|
ref.submit();
|
|
@@ -354,14 +555,15 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
354
555
|
dispose?.();
|
|
355
556
|
});
|
|
356
557
|
});
|
|
357
|
-
const createSnippetDraft = async () => {
|
|
558
|
+
const createSnippetDraft = async (rawQuery) => {
|
|
358
559
|
const ref = prompt();
|
|
359
|
-
const name =
|
|
560
|
+
const name = normalizeSnippetName(rawQuery ?? query());
|
|
360
561
|
if (!ref || !name || creating())
|
|
361
562
|
return;
|
|
362
563
|
const current = findTrailingHashtagTrigger(ref.current.input);
|
|
363
564
|
const nextInput = current ? `${ref.current.input.slice(0, current.start)}#${name}` : `#${name}`;
|
|
364
|
-
const
|
|
565
|
+
const dismissedToken = `#${name}`;
|
|
566
|
+
const editor = resolveExternalEditor();
|
|
365
567
|
if (!editor) {
|
|
366
568
|
props.api.ui.toast({
|
|
367
569
|
variant: "warning",
|
|
@@ -369,35 +571,60 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
369
571
|
});
|
|
370
572
|
return;
|
|
371
573
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
574
|
+
props.api.ui.dialog.setSize("medium");
|
|
575
|
+
setDialogOpen(true);
|
|
576
|
+
props.api.ui.dialog.replace(() => (<props.api.ui.DialogConfirm title={`Create snippet #${name}?`} message={`This will create the snippet draft and open it in $${editor.env} (${editor.command}).`} onCancel={() => {
|
|
577
|
+
setDialogOpen(false);
|
|
578
|
+
beginDialogHandoff();
|
|
579
|
+
props.api.ui.dialog.clear();
|
|
580
|
+
restorePromptFocus(ref);
|
|
581
|
+
}} onConfirm={() => {
|
|
582
|
+
setDialogOpen(false);
|
|
583
|
+
beginDialogHandoff();
|
|
584
|
+
props.api.ui.dialog.clear();
|
|
585
|
+
void (async () => {
|
|
586
|
+
setCreating(true);
|
|
587
|
+
try {
|
|
588
|
+
syncPromptInput(ref, nextInput);
|
|
589
|
+
const filePath = await ensureSnippetDraft(name);
|
|
590
|
+
await addPendingDraft(props.api.state.path.directory, name);
|
|
591
|
+
setDismissed(dismissedToken);
|
|
592
|
+
setCreating(false);
|
|
593
|
+
const opened = await openExternalEditor(props.api, filePath, editor);
|
|
594
|
+
if (!opened)
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
props.api.ui.toast({
|
|
599
|
+
variant: "error",
|
|
600
|
+
message: `Failed to create snippet: ${error instanceof Error ? error.message : String(error)}`,
|
|
601
|
+
});
|
|
602
|
+
syncPromptInput(ref, nextInput);
|
|
603
|
+
setDismissed(undefined);
|
|
604
|
+
}
|
|
605
|
+
finally {
|
|
606
|
+
setCreating(false);
|
|
607
|
+
restorePromptFocus(ref);
|
|
608
|
+
}
|
|
609
|
+
})();
|
|
610
|
+
}}/>));
|
|
396
611
|
};
|
|
397
612
|
useKeyboard((evt) => {
|
|
613
|
+
const ref = prompt();
|
|
614
|
+
const name = evt.name?.toLowerCase();
|
|
615
|
+
if (ref && isReloadCommand(ref.current.input) && (name === "return" || name === "enter")) {
|
|
616
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
617
|
+
syncPromptInput(ref, "");
|
|
618
|
+
setDismissed(undefined);
|
|
619
|
+
}, refreshSnippetOptions);
|
|
620
|
+
evt.preventDefault();
|
|
621
|
+
evt.stopPropagation();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (dialogBlockingInput())
|
|
625
|
+
return;
|
|
398
626
|
if (!visible())
|
|
399
627
|
return;
|
|
400
|
-
const name = evt.name?.toLowerCase();
|
|
401
628
|
const total = options().length;
|
|
402
629
|
const actionable = total > 0 || canCreate();
|
|
403
630
|
const isNavUp = name === "up";
|
|
@@ -441,7 +668,10 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
441
668
|
}
|
|
442
669
|
evt.preventDefault();
|
|
443
670
|
evt.stopPropagation();
|
|
671
|
+
return;
|
|
444
672
|
}
|
|
673
|
+
// Mirror the host prompt state right after normal typing so stale matches disappear.
|
|
674
|
+
schedulePromptSync();
|
|
445
675
|
});
|
|
446
676
|
const emptyLabel = createMemo(() => {
|
|
447
677
|
if ((snippets.loading || skills.loading) && options().length === 0) {
|
|
@@ -454,7 +684,7 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
454
684
|
const addSnippetLabel = createMemo(() => {
|
|
455
685
|
if (creating())
|
|
456
686
|
return "Creating snippet...";
|
|
457
|
-
return `Add new Snippet
|
|
687
|
+
return `Add new Snippet: #${draftName()}`;
|
|
458
688
|
});
|
|
459
689
|
const selectedFg = createMemo(() => selectedText(props.api.theme.current));
|
|
460
690
|
return (<box>
|
|
@@ -467,9 +697,11 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
467
697
|
<text fg={props.api.theme.current.textMuted}>{emptyLabel()}</text>
|
|
468
698
|
</box>}>
|
|
469
699
|
{/* biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI rows intentionally handle mouse selection. */}
|
|
470
|
-
<box id="create-snippet" paddingLeft={1} paddingRight={1} backgroundColor={props.api.theme.current.primary} onMouseMove={() => {
|
|
700
|
+
<box id="create-snippet" paddingLeft={1} paddingRight={1} backgroundColor={props.api.theme.current.primary} onMouseMove={(event) => {
|
|
471
701
|
if (!allowMouseHover())
|
|
472
702
|
return;
|
|
703
|
+
if (!recordMouseMove(event.x, event.y))
|
|
704
|
+
return;
|
|
473
705
|
setInputMode("mouse");
|
|
474
706
|
}} onMouseDown={() => {
|
|
475
707
|
setInputMode("mouse");
|
|
@@ -482,6 +714,7 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
482
714
|
</Show>}>
|
|
483
715
|
{(option, index) => (
|
|
484
716
|
// biome-ignore lint/a11y/noStaticElementInteractions: OpenTUI rows intentionally handle mouse selection.
|
|
717
|
+
// biome-ignore lint/a11y/useKeyWithMouseEvents: OpenTUI boxes do not expose DOM-style focus events.
|
|
485
718
|
<box id={option().id} paddingLeft={1} paddingRight={1} backgroundColor={index === selected() ? props.api.theme.current.primary : undefined} flexDirection="row" onMouseMove={(event) => {
|
|
486
719
|
if (!allowMouseHover())
|
|
487
720
|
return;
|
|
@@ -489,6 +722,11 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
489
722
|
if (!recordMouseMove(event.x, event.y))
|
|
490
723
|
return;
|
|
491
724
|
setInputMode("mouse");
|
|
725
|
+
}} onMouseOver={() => {
|
|
726
|
+
if (!allowMouseHover())
|
|
727
|
+
return;
|
|
728
|
+
if (inputMode() !== "mouse")
|
|
729
|
+
return;
|
|
492
730
|
setSelected(index);
|
|
493
731
|
}} onMouseDown={() => {
|
|
494
732
|
setInputMode("mouse");
|
|
@@ -513,7 +751,7 @@ function PromptWithSnippetAutocomplete(props) {
|
|
|
513
751
|
</scrollbox>
|
|
514
752
|
</box>
|
|
515
753
|
</Show>
|
|
516
|
-
<props.api.ui.Prompt sessionID={props.sessionID} workspaceID={props.workspaceID} visible={props.visible} disabled={props.disabled} onSubmit={
|
|
754
|
+
<props.api.ui.Prompt sessionID={props.sessionID} workspaceID={props.workspaceID} visible={props.visible} disabled={props.disabled || dialogBlockingInput()} onSubmit={handlePromptSubmit} placeholders={props.placeholders} ref={bind} right={props.right}/>
|
|
517
755
|
</box>);
|
|
518
756
|
}
|
|
519
757
|
const tui = async (api) => {
|