opencode-snippets 2.2.4 → 3.0.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/README.md +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2292 -705
- package/dist/index.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 +38 -13
- package/dist/src/expander.js.map +1 -1
- package/dist/src/loader.js +3 -3
- package/dist/src/loader.js.map +1 -1
- package/dist/src/logger.d.ts +8 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +15 -3
- package/dist/src/logger.js.map +1 -1
- package/dist/src/pending-drafts.d.ts.map +1 -1
- package/dist/src/pending-drafts.js +5 -6
- package/dist/src/pending-drafts.js.map +1 -1
- package/dist/src/reload-signal.d.ts.map +1 -1
- package/dist/src/reload-signal.js +5 -6
- package/dist/src/reload-signal.js.map +1 -1
- package/dist/src/shell.d.ts +2 -13
- package/dist/src/shell.d.ts.map +1 -1
- package/dist/src/shell.js +5 -2
- package/dist/src/shell.js.map +1 -1
- package/dist/src/skill-loader.d.ts.map +1 -1
- package/dist/src/skill-loader.js +2 -6
- package/dist/src/skill-loader.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +1639 -0
- package/dist/tui.jsx +19 -13
- package/dist/tui.jsx.map +1 -1
- package/package.json +5 -6
- package/skill/snippets/SKILL.md +6 -25
- package/skill/snippets/references/creating-snippets.md +86 -0
package/dist/tui.js
ADDED
|
@@ -0,0 +1,1639 @@
|
|
|
1
|
+
// tui.tsx
|
|
2
|
+
import { memo as _$memo } from "@opentui/solid";
|
|
3
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
4
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
5
|
+
import { use as _$use } from "@opentui/solid";
|
|
6
|
+
import { spread as _$spread } from "@opentui/solid";
|
|
7
|
+
import { mergeProps as _$mergeProps } from "@opentui/solid";
|
|
8
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
9
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
10
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
11
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { access as access2, writeFile as writeFile4 } from "node:fs/promises";
|
|
14
|
+
import { join as join7 } from "node:path";
|
|
15
|
+
import { RGBA } from "@opentui/core";
|
|
16
|
+
import { useKeyboard } from "@opentui/solid";
|
|
17
|
+
import { createEffect, createMemo, createResource, createSignal, Index, onCleanup, onMount, Show } from "solid-js";
|
|
18
|
+
|
|
19
|
+
// src/constants.ts
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
var PATHS = {
|
|
23
|
+
CONFIG_DIR: join(homedir(), ".config", "opencode"),
|
|
24
|
+
SNIPPETS_DIR: join(homedir(), ".config", "opencode", "snippet"),
|
|
25
|
+
SNIPPETS_DIR_ALT: join(homedir(), ".config", "opencode", "snippets"),
|
|
26
|
+
CONFIG_FILE_GLOBAL: join(homedir(), ".config", "opencode", "snippet", "config.jsonc")
|
|
27
|
+
};
|
|
28
|
+
function getProjectPaths(projectDir) {
|
|
29
|
+
const snippetDir = join(projectDir, ".opencode", "snippet");
|
|
30
|
+
return {
|
|
31
|
+
SNIPPETS_DIR: snippetDir,
|
|
32
|
+
SNIPPETS_DIR_ALT: join(projectDir, ".opencode", "snippets"),
|
|
33
|
+
CONFIG_FILE: join(snippetDir, "config.jsonc")
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
var CONFIG = {
|
|
37
|
+
SNIPPET_EXTENSION: ".md"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/loader.ts
|
|
41
|
+
import { access, mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
42
|
+
import { basename, join as join3 } from "node:path";
|
|
43
|
+
|
|
44
|
+
// src/cjs-interop.ts
|
|
45
|
+
async function importCjs(pkg) {
|
|
46
|
+
let val = await import(pkg);
|
|
47
|
+
for (let i = 0;i < 4; i++) {
|
|
48
|
+
if (val == null || typeof val === "function")
|
|
49
|
+
break;
|
|
50
|
+
if (!("default" in val) || val.default === undefined)
|
|
51
|
+
break;
|
|
52
|
+
val = val.default;
|
|
53
|
+
}
|
|
54
|
+
return val;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/logger.ts
|
|
58
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
59
|
+
import { join as join2 } from "path";
|
|
60
|
+
class Logger {
|
|
61
|
+
logDir;
|
|
62
|
+
debugEnabled;
|
|
63
|
+
silent;
|
|
64
|
+
constructor(logDirOverride, debugEnabled = false, silent = false) {
|
|
65
|
+
this.logDir = logDirOverride ?? join2(PATHS.CONFIG_DIR, "logs", "snippets");
|
|
66
|
+
this.debugEnabled = debugEnabled;
|
|
67
|
+
this.silent = silent;
|
|
68
|
+
}
|
|
69
|
+
ensureLogDir() {
|
|
70
|
+
if (!existsSync(this.logDir)) {
|
|
71
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
formatData(data) {
|
|
75
|
+
if (!data)
|
|
76
|
+
return "";
|
|
77
|
+
const parts = [];
|
|
78
|
+
for (const [key, value] of Object.entries(data)) {
|
|
79
|
+
if (value === undefined || value === null)
|
|
80
|
+
continue;
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
if (value.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
parts.push(`${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`);
|
|
85
|
+
} else if (typeof value === "object") {
|
|
86
|
+
const str = JSON.stringify(value);
|
|
87
|
+
if (str.length < 50) {
|
|
88
|
+
parts.push(`${key}=${str}`);
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
parts.push(`${key}=${value}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return parts.join(" ");
|
|
95
|
+
}
|
|
96
|
+
getCallerFile() {
|
|
97
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
98
|
+
try {
|
|
99
|
+
const err = new Error;
|
|
100
|
+
Error.prepareStackTrace = (_, stack2) => stack2;
|
|
101
|
+
const stack = err.stack;
|
|
102
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
103
|
+
for (let i = 3;i < stack.length; i++) {
|
|
104
|
+
const filename = stack[i]?.getFileName();
|
|
105
|
+
if (filename && !filename.includes("logger.")) {
|
|
106
|
+
const match = filename.match(/([^/\\]+)\.[tj]s$/);
|
|
107
|
+
return match ? match[1] : "unknown";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return "unknown";
|
|
111
|
+
} catch {
|
|
112
|
+
return "unknown";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
write(level, component, message, data) {
|
|
116
|
+
if (this.silent)
|
|
117
|
+
return;
|
|
118
|
+
if (level === "DEBUG" && !this.debugEnabled)
|
|
119
|
+
return;
|
|
120
|
+
try {
|
|
121
|
+
this.ensureLogDir();
|
|
122
|
+
const timestamp = new Date().toISOString();
|
|
123
|
+
const dataStr = this.formatData(data);
|
|
124
|
+
const dailyLogDir = join2(this.logDir, "daily");
|
|
125
|
+
if (!existsSync(dailyLogDir)) {
|
|
126
|
+
mkdirSync(dailyLogDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? ` | ${dataStr}` : ""}
|
|
129
|
+
`;
|
|
130
|
+
const logFile = join2(dailyLogDir, `${new Date().toISOString().split("T")[0]}.log`);
|
|
131
|
+
writeFileSync(logFile, logLine, { flag: "a" });
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
info(message, data) {
|
|
135
|
+
const component = this.getCallerFile();
|
|
136
|
+
this.write("INFO", component, message, data);
|
|
137
|
+
}
|
|
138
|
+
debug(message, data) {
|
|
139
|
+
const component = this.getCallerFile();
|
|
140
|
+
this.write("DEBUG", component, message, data);
|
|
141
|
+
}
|
|
142
|
+
warn(message, data) {
|
|
143
|
+
const component = this.getCallerFile();
|
|
144
|
+
this.write("WARN", component, message, data);
|
|
145
|
+
}
|
|
146
|
+
error(message, data) {
|
|
147
|
+
const component = this.getCallerFile();
|
|
148
|
+
this.write("ERROR", component, message, data);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
var logger = new Logger(undefined, false, false);
|
|
152
|
+
|
|
153
|
+
// src/loader.ts
|
|
154
|
+
var matter = await importCjs("gray-matter");
|
|
155
|
+
function getGlobalSnippetDirs(globalDir) {
|
|
156
|
+
if (globalDir)
|
|
157
|
+
return [globalDir];
|
|
158
|
+
return [PATHS.SNIPPETS_DIR_ALT, PATHS.SNIPPETS_DIR];
|
|
159
|
+
}
|
|
160
|
+
function getProjectSnippetDirs(projectDir) {
|
|
161
|
+
const paths = getProjectPaths(projectDir);
|
|
162
|
+
return [paths.SNIPPETS_DIR_ALT, paths.SNIPPETS_DIR];
|
|
163
|
+
}
|
|
164
|
+
async function pathExists(path) {
|
|
165
|
+
try {
|
|
166
|
+
await access(path);
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function resolveWritableSnippetDir(projectDir) {
|
|
173
|
+
const paths = projectDir ? getProjectPaths(projectDir) : { SNIPPETS_DIR: PATHS.SNIPPETS_DIR, SNIPPETS_DIR_ALT: PATHS.SNIPPETS_DIR_ALT };
|
|
174
|
+
for (const dir of [paths.SNIPPETS_DIR, paths.SNIPPETS_DIR_ALT]) {
|
|
175
|
+
if (await pathExists(dir))
|
|
176
|
+
return dir;
|
|
177
|
+
}
|
|
178
|
+
return paths.SNIPPETS_DIR;
|
|
179
|
+
}
|
|
180
|
+
async function loadSnippets(projectDir, globalDir) {
|
|
181
|
+
const snippets = new Map;
|
|
182
|
+
for (const dir of getGlobalSnippetDirs(globalDir)) {
|
|
183
|
+
await loadFromDirectory(dir, snippets, "global");
|
|
184
|
+
}
|
|
185
|
+
if (projectDir) {
|
|
186
|
+
for (const dir of getProjectSnippetDirs(projectDir)) {
|
|
187
|
+
await loadFromDirectory(dir, snippets, "project");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return snippets;
|
|
191
|
+
}
|
|
192
|
+
async function loadFromDirectory(dir, registry, source) {
|
|
193
|
+
try {
|
|
194
|
+
const files = await readdir(dir);
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
if (!file.endsWith(CONFIG.SNIPPET_EXTENSION))
|
|
197
|
+
continue;
|
|
198
|
+
const snippet = await loadSnippetFile(dir, file, source);
|
|
199
|
+
if (snippet) {
|
|
200
|
+
registerSnippet(registry, snippet);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
logger.debug(`Loaded snippets from ${source} directory`, {
|
|
204
|
+
path: dir,
|
|
205
|
+
fileCount: files.length
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
logger.debug(`${source} snippets directory not found or unreadable`, {
|
|
209
|
+
path: dir,
|
|
210
|
+
error: error instanceof Error ? error.message : String(error)
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function loadSnippetFile(dir, filename, source) {
|
|
215
|
+
try {
|
|
216
|
+
const name = basename(filename, CONFIG.SNIPPET_EXTENSION);
|
|
217
|
+
const filePath = join3(dir, filename);
|
|
218
|
+
const fileContent = await readFile(filePath, "utf8");
|
|
219
|
+
const parsed = matter(fileContent);
|
|
220
|
+
const content = parsed.content.trim();
|
|
221
|
+
const frontmatter = parsed.data;
|
|
222
|
+
let aliases = [];
|
|
223
|
+
const aliasSource = frontmatter.aliases ?? frontmatter.alias;
|
|
224
|
+
if (aliasSource) {
|
|
225
|
+
if (Array.isArray(aliasSource)) {
|
|
226
|
+
aliases = aliasSource;
|
|
227
|
+
} else {
|
|
228
|
+
aliases = [aliasSource];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
name,
|
|
233
|
+
content,
|
|
234
|
+
aliases,
|
|
235
|
+
description: frontmatter.description,
|
|
236
|
+
filePath,
|
|
237
|
+
source
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
logger.warn("Failed to load snippet file", {
|
|
241
|
+
filename,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error)
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function registerSnippet(registry, snippet) {
|
|
248
|
+
const key = snippet.name.toLowerCase();
|
|
249
|
+
const existing = registry.get(key);
|
|
250
|
+
if (existing) {
|
|
251
|
+
for (const alias of existing.aliases) {
|
|
252
|
+
registry.delete(alias.toLowerCase());
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
registry.set(key, snippet);
|
|
256
|
+
for (const alias of snippet.aliases) {
|
|
257
|
+
registry.set(alias.toLowerCase(), snippet);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function listSnippets(registry) {
|
|
261
|
+
const seen = new Set;
|
|
262
|
+
const snippets = [];
|
|
263
|
+
for (const snippet of registry.values()) {
|
|
264
|
+
if (!seen.has(snippet.name)) {
|
|
265
|
+
seen.add(snippet.name);
|
|
266
|
+
snippets.push(snippet);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return snippets;
|
|
270
|
+
}
|
|
271
|
+
async function ensureSnippetsDir(projectDir) {
|
|
272
|
+
const dir = await resolveWritableSnippetDir(projectDir);
|
|
273
|
+
await mkdir(dir, { recursive: true });
|
|
274
|
+
return dir;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/pending-drafts.ts
|
|
278
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
279
|
+
import { join as join4 } from "node:path";
|
|
280
|
+
function statePath() {
|
|
281
|
+
return join4(PATHS.CONFIG_DIR, "state", "pending-drafts.json");
|
|
282
|
+
}
|
|
283
|
+
function scopeKey(workspaceDir) {
|
|
284
|
+
return workspaceDir || "__global__";
|
|
285
|
+
}
|
|
286
|
+
function normalizeNames(names) {
|
|
287
|
+
return [...new Set(names.map((name) => name.trim().toLowerCase()).filter(Boolean))];
|
|
288
|
+
}
|
|
289
|
+
function normalizeState(value) {
|
|
290
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
291
|
+
return {};
|
|
292
|
+
return Object.fromEntries(Object.entries(value).filter(([, names]) => Array.isArray(names)).map(([key, names]) => [
|
|
293
|
+
key,
|
|
294
|
+
normalizeNames(names.filter((name) => typeof name === "string"))
|
|
295
|
+
]).filter(([, names]) => names.length > 0));
|
|
296
|
+
}
|
|
297
|
+
async function readState() {
|
|
298
|
+
try {
|
|
299
|
+
return normalizeState(JSON.parse(await readFile2(statePath(), "utf8")));
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if (error.code === "ENOENT")
|
|
302
|
+
return {};
|
|
303
|
+
logger.warn("Failed to read pending draft state", {
|
|
304
|
+
error: error instanceof Error ? error.message : String(error),
|
|
305
|
+
path: statePath()
|
|
306
|
+
});
|
|
307
|
+
return {};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
async function writeState(state) {
|
|
311
|
+
await mkdir2(join4(PATHS.CONFIG_DIR, "state"), { recursive: true });
|
|
312
|
+
await writeFile2(statePath(), `${JSON.stringify(state, null, 2)}
|
|
313
|
+
`, "utf8");
|
|
314
|
+
}
|
|
315
|
+
async function addPendingDraft(workspaceDir, name) {
|
|
316
|
+
const key = scopeKey(workspaceDir);
|
|
317
|
+
const state = await readState();
|
|
318
|
+
const next = normalizeNames([...state[key] || [], name]);
|
|
319
|
+
if (next.length === 0)
|
|
320
|
+
return;
|
|
321
|
+
state[key] = next;
|
|
322
|
+
await writeState(state);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/reload-signal.ts
|
|
326
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
|
|
327
|
+
import { join as join5 } from "node:path";
|
|
328
|
+
function statePath2() {
|
|
329
|
+
return join5(PATHS.CONFIG_DIR, "state", "snippet-reload.json");
|
|
330
|
+
}
|
|
331
|
+
function scopeKey2(workspaceDir) {
|
|
332
|
+
return workspaceDir || "__global__";
|
|
333
|
+
}
|
|
334
|
+
function normalizeState2(value) {
|
|
335
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
336
|
+
return {};
|
|
337
|
+
return Object.fromEntries(Object.entries(value).filter(([, stamp]) => typeof stamp === "number" && Number.isFinite(stamp)).map(([key, stamp]) => [key, stamp]));
|
|
338
|
+
}
|
|
339
|
+
async function readState2() {
|
|
340
|
+
try {
|
|
341
|
+
return normalizeState2(JSON.parse(await readFile3(statePath2(), "utf8")));
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error.code === "ENOENT")
|
|
344
|
+
return {};
|
|
345
|
+
logger.warn("Failed to read snippet reload signal", {
|
|
346
|
+
error: error instanceof Error ? error.message : String(error),
|
|
347
|
+
path: statePath2()
|
|
348
|
+
});
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function writeState2(state) {
|
|
353
|
+
await mkdir3(join5(PATHS.CONFIG_DIR, "state"), { recursive: true });
|
|
354
|
+
await writeFile3(statePath2(), `${JSON.stringify(state, null, 2)}
|
|
355
|
+
`, "utf8");
|
|
356
|
+
}
|
|
357
|
+
async function markSnippetReloadRequested(workspaceDir) {
|
|
358
|
+
const key = scopeKey2(workspaceDir);
|
|
359
|
+
const state = await readState2();
|
|
360
|
+
state[key] = Date.now();
|
|
361
|
+
await writeState2(state);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/skill-loader.ts
|
|
365
|
+
import { readdir as readdir2, readFile as readFile4, stat } from "node:fs/promises";
|
|
366
|
+
import { homedir as homedir2 } from "node:os";
|
|
367
|
+
import { dirname, join as join6, resolve } from "node:path";
|
|
368
|
+
import { fileURLToPath } from "node:url";
|
|
369
|
+
var matter2 = await importCjs("gray-matter");
|
|
370
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
371
|
+
var __dirname2 = dirname(__filename2);
|
|
372
|
+
function getGlobalSkillDirs(homeDir = homedir2()) {
|
|
373
|
+
return [
|
|
374
|
+
join6(homeDir, ".config", "opencode", "skill"),
|
|
375
|
+
join6(homeDir, ".config", "opencode", "skills"),
|
|
376
|
+
join6(homeDir, ".claude", "skills"),
|
|
377
|
+
join6(homeDir, ".agents", "skills")
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
function getProjectSkillDirs(projectDir) {
|
|
381
|
+
return [
|
|
382
|
+
join6(projectDir, ".opencode", "skill"),
|
|
383
|
+
join6(projectDir, ".opencode", "skills"),
|
|
384
|
+
join6(projectDir, ".claude", "skills"),
|
|
385
|
+
join6(projectDir, ".agents", "skills")
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
function getBundledSkillDirs() {
|
|
389
|
+
return [join6(__dirname2, "..", "..", "skill")];
|
|
390
|
+
}
|
|
391
|
+
function uniqueDirs(dirs) {
|
|
392
|
+
const seen = new Set;
|
|
393
|
+
const result = [];
|
|
394
|
+
for (const dir of dirs) {
|
|
395
|
+
const key = resolve(dir);
|
|
396
|
+
if (seen.has(key))
|
|
397
|
+
continue;
|
|
398
|
+
seen.add(key);
|
|
399
|
+
result.push(dir);
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
async function exists(path) {
|
|
404
|
+
try {
|
|
405
|
+
await stat(path);
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
async function getProjectSearchRoots(projectDir) {
|
|
412
|
+
const roots = [];
|
|
413
|
+
let dir = resolve(projectDir);
|
|
414
|
+
while (true) {
|
|
415
|
+
roots.push(dir);
|
|
416
|
+
if (await exists(join6(dir, ".git"))) {
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
const parent = dirname(dir);
|
|
420
|
+
if (parent === dir) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
dir = parent;
|
|
424
|
+
}
|
|
425
|
+
return roots.reverse();
|
|
426
|
+
}
|
|
427
|
+
async function loadSkills(projectDir, options = {}) {
|
|
428
|
+
const skills = new Map;
|
|
429
|
+
const bundledDirs = options.bundledSkillDirs || getBundledSkillDirs();
|
|
430
|
+
for (const dir of uniqueDirs([...bundledDirs, ...options.opencodeSkillDirs || []])) {
|
|
431
|
+
await loadFromDirectory2(dir, skills, "global");
|
|
432
|
+
}
|
|
433
|
+
if (options.opencodeSkillDirs && options.opencodeSkillDirs.length > 0) {
|
|
434
|
+
logger.debug("Loaded OpenCode-exposed skill directories", { paths: options.opencodeSkillDirs });
|
|
435
|
+
}
|
|
436
|
+
for (const dir of getGlobalSkillDirs(options.homeDir)) {
|
|
437
|
+
await loadFromDirectory2(dir, skills, "global");
|
|
438
|
+
}
|
|
439
|
+
if (projectDir) {
|
|
440
|
+
for (const root of await getProjectSearchRoots(projectDir)) {
|
|
441
|
+
for (const dir of getProjectSkillDirs(root)) {
|
|
442
|
+
await loadFromDirectory2(dir, skills, "project");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
logger.debug("Skills loaded", { count: skills.size });
|
|
447
|
+
return skills;
|
|
448
|
+
}
|
|
449
|
+
async function loadFromDirectory2(dir, registry, source) {
|
|
450
|
+
try {
|
|
451
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
452
|
+
for (const entry of entries) {
|
|
453
|
+
if (!entry.isDirectory())
|
|
454
|
+
continue;
|
|
455
|
+
const skill = await loadSkill(dir, entry.name, source);
|
|
456
|
+
if (skill) {
|
|
457
|
+
registry.set(skill.name.toLowerCase(), skill);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
logger.debug(`Loaded skills from ${source} directory`, { path: dir });
|
|
461
|
+
} catch {
|
|
462
|
+
logger.debug(`${source} skill directory not found`, { path: dir });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async function loadSkill(baseDir, skillName, source) {
|
|
466
|
+
const filePath = join6(baseDir, skillName, "SKILL.md");
|
|
467
|
+
try {
|
|
468
|
+
const fileContent = await readFile4(filePath, "utf8");
|
|
469
|
+
const parsed = matter2(fileContent);
|
|
470
|
+
const content = parsed.content.trim();
|
|
471
|
+
const frontmatter = parsed.data;
|
|
472
|
+
const name = frontmatter.name || skillName;
|
|
473
|
+
return {
|
|
474
|
+
name,
|
|
475
|
+
content,
|
|
476
|
+
description: frontmatter.description,
|
|
477
|
+
source,
|
|
478
|
+
filePath
|
|
479
|
+
};
|
|
480
|
+
} catch (error) {
|
|
481
|
+
logger.warn("Failed to load skill", {
|
|
482
|
+
skillName,
|
|
483
|
+
error: error instanceof Error ? error.message : String(error)
|
|
484
|
+
});
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/tui-search.ts
|
|
490
|
+
function normalizeSearchText(input) {
|
|
491
|
+
return input.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
492
|
+
}
|
|
493
|
+
function isSubsequence(input, query) {
|
|
494
|
+
if (!query)
|
|
495
|
+
return true;
|
|
496
|
+
let i = 0;
|
|
497
|
+
for (const c of input) {
|
|
498
|
+
if (c !== query[i])
|
|
499
|
+
continue;
|
|
500
|
+
i += 1;
|
|
501
|
+
if (i === query.length)
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function scoreText(input, query) {
|
|
507
|
+
const raw = input.toLowerCase();
|
|
508
|
+
const needle = query.toLowerCase();
|
|
509
|
+
const compact = normalizeSearchText(input);
|
|
510
|
+
const compactNeedle = normalizeSearchText(query);
|
|
511
|
+
if (raw === needle)
|
|
512
|
+
return 0;
|
|
513
|
+
if (compactNeedle && compact === compactNeedle)
|
|
514
|
+
return 1;
|
|
515
|
+
if (raw.startsWith(needle))
|
|
516
|
+
return 2;
|
|
517
|
+
if (compactNeedle && compact.startsWith(compactNeedle))
|
|
518
|
+
return 3;
|
|
519
|
+
if (raw.includes(needle))
|
|
520
|
+
return 4;
|
|
521
|
+
if (compactNeedle && compact.includes(compactNeedle))
|
|
522
|
+
return 5;
|
|
523
|
+
if (compactNeedle && isSubsequence(compact, compactNeedle))
|
|
524
|
+
return 6;
|
|
525
|
+
return Number.POSITIVE_INFINITY;
|
|
526
|
+
}
|
|
527
|
+
function scoreSnippet(snippet, query) {
|
|
528
|
+
if (!query)
|
|
529
|
+
return 0;
|
|
530
|
+
const description = (snippet.description || "").toLowerCase();
|
|
531
|
+
const score = Math.min(scoreText(snippet.name, query), ...snippet.aliases.map((alias) => scoreText(alias, query)));
|
|
532
|
+
if (Number.isFinite(score))
|
|
533
|
+
return score;
|
|
534
|
+
const needle = query.toLowerCase();
|
|
535
|
+
if (description.startsWith(needle))
|
|
536
|
+
return 7;
|
|
537
|
+
if (description.includes(needle))
|
|
538
|
+
return 8;
|
|
539
|
+
return Number.POSITIVE_INFINITY;
|
|
540
|
+
}
|
|
541
|
+
function sourceRank(item) {
|
|
542
|
+
return item.source === "project" ? 0 : 1;
|
|
543
|
+
}
|
|
544
|
+
function skillTag(skill) {
|
|
545
|
+
return `skill(${skill.name})`;
|
|
546
|
+
}
|
|
547
|
+
function scoreSkill(skill, query) {
|
|
548
|
+
if (!query)
|
|
549
|
+
return 0;
|
|
550
|
+
const description = (skill.description || "").toLowerCase();
|
|
551
|
+
const score = Math.min(scoreText(skill.name, query), scoreText(skillTag(skill), query));
|
|
552
|
+
if (Number.isFinite(score))
|
|
553
|
+
return score;
|
|
554
|
+
const needle = query.toLowerCase();
|
|
555
|
+
if (description.startsWith(needle))
|
|
556
|
+
return 7;
|
|
557
|
+
if (description.includes(needle))
|
|
558
|
+
return 8;
|
|
559
|
+
return Number.POSITIVE_INFINITY;
|
|
560
|
+
}
|
|
561
|
+
function filterSnippets(snippets, query) {
|
|
562
|
+
return [...snippets].map((snippet) => ({
|
|
563
|
+
snippet,
|
|
564
|
+
score: scoreSnippet(snippet, query.trim())
|
|
565
|
+
})).filter((item) => Number.isFinite(item.score)).sort((a, b) => {
|
|
566
|
+
if (a.score !== b.score)
|
|
567
|
+
return a.score - b.score;
|
|
568
|
+
const sourceDiff = sourceRank(a.snippet) - sourceRank(b.snippet);
|
|
569
|
+
if (sourceDiff !== 0)
|
|
570
|
+
return sourceDiff;
|
|
571
|
+
return a.snippet.name.localeCompare(b.snippet.name);
|
|
572
|
+
}).map((item) => item.snippet);
|
|
573
|
+
}
|
|
574
|
+
function filterSkills(skills, query) {
|
|
575
|
+
return [...skills].map((skill) => ({
|
|
576
|
+
skill,
|
|
577
|
+
score: scoreSkill(skill, query.trim())
|
|
578
|
+
})).filter((item) => Number.isFinite(item.score)).sort((a, b) => {
|
|
579
|
+
if (a.score !== b.score)
|
|
580
|
+
return a.score - b.score;
|
|
581
|
+
const sourceDiff = sourceRank(a.skill) - sourceRank(b.skill);
|
|
582
|
+
if (sourceDiff !== 0)
|
|
583
|
+
return sourceDiff;
|
|
584
|
+
return a.skill.name.localeCompare(b.skill.name);
|
|
585
|
+
}).map((item) => item.skill);
|
|
586
|
+
}
|
|
587
|
+
function matchedAliases(snippet, query) {
|
|
588
|
+
const needle = query.trim();
|
|
589
|
+
if (!needle)
|
|
590
|
+
return [];
|
|
591
|
+
const exact = snippet.aliases.filter((alias) => alias === needle);
|
|
592
|
+
if (exact.length > 0)
|
|
593
|
+
return exact;
|
|
594
|
+
return snippet.aliases.filter((alias) => Number.isFinite(scoreText(alias, needle)));
|
|
595
|
+
}
|
|
596
|
+
function snippetDescription(snippet) {
|
|
597
|
+
return (snippet.description || snippet.content).replace(/\s+/g, " ").trim();
|
|
598
|
+
}
|
|
599
|
+
function highlightMatches(input, query) {
|
|
600
|
+
const needle = query.trim().toLowerCase();
|
|
601
|
+
if (!needle)
|
|
602
|
+
return [{ text: input, match: false }];
|
|
603
|
+
const haystack = input.toLowerCase();
|
|
604
|
+
const parts = [];
|
|
605
|
+
let start = 0;
|
|
606
|
+
while (start < input.length) {
|
|
607
|
+
const index = haystack.indexOf(needle, start);
|
|
608
|
+
if (index === -1) {
|
|
609
|
+
parts.push({ text: input.slice(start), match: false });
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
if (index > start) {
|
|
613
|
+
parts.push({ text: input.slice(start, index), match: false });
|
|
614
|
+
}
|
|
615
|
+
const end = index + needle.length;
|
|
616
|
+
parts.push({ text: input.slice(index, end), match: true });
|
|
617
|
+
start = end;
|
|
618
|
+
}
|
|
619
|
+
if (parts.length === 0)
|
|
620
|
+
return [{ text: input, match: false }];
|
|
621
|
+
return parts;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/tui-trigger.ts
|
|
625
|
+
var HASHTAG_TRIGGER = /(^|\s)#([^\s#]*)$/;
|
|
626
|
+
function compactTagText(input) {
|
|
627
|
+
return input.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
628
|
+
}
|
|
629
|
+
function findTrailingHashtagTrigger(input) {
|
|
630
|
+
const hit = input.match(HASHTAG_TRIGGER);
|
|
631
|
+
if (!hit)
|
|
632
|
+
return;
|
|
633
|
+
const query = hit[2] || "";
|
|
634
|
+
const token = `#${query}`;
|
|
635
|
+
return {
|
|
636
|
+
start: input.length - token.length,
|
|
637
|
+
end: input.length,
|
|
638
|
+
query,
|
|
639
|
+
token
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function replaceTrailingHashtag(input, name) {
|
|
643
|
+
const match = findTrailingHashtagTrigger(input);
|
|
644
|
+
if (!match)
|
|
645
|
+
return;
|
|
646
|
+
return `${input.slice(0, match.start)}#${name} `;
|
|
647
|
+
}
|
|
648
|
+
function insertSnippetTag(input, name) {
|
|
649
|
+
const replaced = replaceTrailingHashtag(input, name);
|
|
650
|
+
if (replaced)
|
|
651
|
+
return replaced;
|
|
652
|
+
if (!input)
|
|
653
|
+
return `#${name} `;
|
|
654
|
+
if (/\s$/.test(input))
|
|
655
|
+
return `${input}#${name} `;
|
|
656
|
+
return `${input} #${name} `;
|
|
657
|
+
}
|
|
658
|
+
function insertSkillLoad(input, name) {
|
|
659
|
+
const match = findTrailingHashtagTrigger(input);
|
|
660
|
+
const load = `#skill(${name}) `;
|
|
661
|
+
if (match) {
|
|
662
|
+
return `${input.slice(0, match.start)}${load}`;
|
|
663
|
+
}
|
|
664
|
+
if (!input)
|
|
665
|
+
return load;
|
|
666
|
+
if (/\s$/.test(input))
|
|
667
|
+
return `${input}${load}`;
|
|
668
|
+
return `${input} ${load}`;
|
|
669
|
+
}
|
|
670
|
+
function preferredSnippetTag(input, item) {
|
|
671
|
+
const query = findTrailingHashtagTrigger(input)?.query.trim();
|
|
672
|
+
if (!query)
|
|
673
|
+
return item.name;
|
|
674
|
+
const exact = item.aliases.find((alias) => alias === query);
|
|
675
|
+
if (exact)
|
|
676
|
+
return exact;
|
|
677
|
+
const compact = compactTagText(query);
|
|
678
|
+
if (!compact)
|
|
679
|
+
return item.name;
|
|
680
|
+
return item.aliases.find((alias) => compactTagText(alias) === compact) ?? item.name;
|
|
681
|
+
}
|
|
682
|
+
function insertSnippetTrigger(input) {
|
|
683
|
+
if (findTrailingHashtagTrigger(input))
|
|
684
|
+
return input;
|
|
685
|
+
if (!input)
|
|
686
|
+
return "#";
|
|
687
|
+
if (/\s$/.test(input))
|
|
688
|
+
return `${input}#`;
|
|
689
|
+
return `${input} #`;
|
|
690
|
+
}
|
|
691
|
+
function isReloadCommand(input) {
|
|
692
|
+
return input.trim() === "/snippets:reload";
|
|
693
|
+
}
|
|
694
|
+
function isDialogInputBlocked(dialogOpen, dialogHandoffUntil, now = Date.now()) {
|
|
695
|
+
return dialogOpen || dialogHandoffUntil > now;
|
|
696
|
+
}
|
|
697
|
+
function isAutocompleteNavUpKey(evt) {
|
|
698
|
+
const name = evt.name?.toLowerCase();
|
|
699
|
+
return name === "up" || name === "arrowup" || evt.raw === "\x1B[A" || evt.sequence === "\x1B[A";
|
|
700
|
+
}
|
|
701
|
+
function isAutocompleteNavDownKey(evt) {
|
|
702
|
+
const name = evt.name?.toLowerCase();
|
|
703
|
+
return name === "down" || name === "arrowdown" || evt.raw === "\x1B[B" || evt.sequence === "\x1B[B";
|
|
704
|
+
}
|
|
705
|
+
function stepSelection(current, total, delta) {
|
|
706
|
+
if (total <= 0)
|
|
707
|
+
return 0;
|
|
708
|
+
const next = current + delta;
|
|
709
|
+
if (next < 0)
|
|
710
|
+
return 0;
|
|
711
|
+
if (next >= total)
|
|
712
|
+
return total - 1;
|
|
713
|
+
return next;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// tui.tsx
|
|
717
|
+
var id = "opencode-snippets:autocomplete";
|
|
718
|
+
var PROMPT_SYNC_MS = 50;
|
|
719
|
+
var MENU_MAX_HEIGHT = 10;
|
|
720
|
+
var MOUSE_HOVER_SUPPRESS_MS = 150;
|
|
721
|
+
var HOME_PLACEHOLDERS = {
|
|
722
|
+
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
|
|
723
|
+
shell: ["ls -la", "git status", "pwd"]
|
|
724
|
+
};
|
|
725
|
+
var EMPTY_SNIPPET = `---
|
|
726
|
+
description: ""
|
|
727
|
+
---
|
|
728
|
+
|
|
729
|
+
`;
|
|
730
|
+
var INLINE_BORDER = {
|
|
731
|
+
border: ["left", "right"],
|
|
732
|
+
customBorderChars: {
|
|
733
|
+
topLeft: "",
|
|
734
|
+
bottomLeft: "",
|
|
735
|
+
vertical: "┃",
|
|
736
|
+
topRight: "",
|
|
737
|
+
bottomRight: "",
|
|
738
|
+
horizontal: " ",
|
|
739
|
+
bottomT: "",
|
|
740
|
+
topT: "",
|
|
741
|
+
cross: "",
|
|
742
|
+
leftT: "",
|
|
743
|
+
rightT: ""
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function sortSnippets(snippets) {
|
|
747
|
+
return [...snippets].sort((a, b) => {
|
|
748
|
+
if (a.source !== b.source) {
|
|
749
|
+
if (a.source === "project")
|
|
750
|
+
return -1;
|
|
751
|
+
return 1;
|
|
752
|
+
}
|
|
753
|
+
return a.name.localeCompare(b.name);
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
function sortSkills(skills) {
|
|
757
|
+
return [...skills].sort((a, b) => {
|
|
758
|
+
if (a.source !== b.source) {
|
|
759
|
+
if (a.source === "project")
|
|
760
|
+
return -1;
|
|
761
|
+
return 1;
|
|
762
|
+
}
|
|
763
|
+
return a.name.localeCompare(b.name);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
function selectedText(theme) {
|
|
767
|
+
if (theme.background.a !== 0)
|
|
768
|
+
return theme.background;
|
|
769
|
+
const {
|
|
770
|
+
r,
|
|
771
|
+
g,
|
|
772
|
+
b
|
|
773
|
+
} = theme.primary;
|
|
774
|
+
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
775
|
+
return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255);
|
|
776
|
+
}
|
|
777
|
+
function renderHighlighted(text, query, fg) {
|
|
778
|
+
return highlightMatches(text, query).map((part) => {
|
|
779
|
+
if (!part.match)
|
|
780
|
+
return part.text;
|
|
781
|
+
return (() => {
|
|
782
|
+
var _el$ = _$createElement("span");
|
|
783
|
+
_$setProp(_el$, "style", {
|
|
784
|
+
fg,
|
|
785
|
+
underline: true
|
|
786
|
+
});
|
|
787
|
+
_$insert(_el$, () => part.text);
|
|
788
|
+
return _el$;
|
|
789
|
+
})();
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
function normalizeSnippetName(input) {
|
|
793
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
794
|
+
}
|
|
795
|
+
function resolveExternalEditor() {
|
|
796
|
+
const visual = process.env.VISUAL?.trim();
|
|
797
|
+
if (visual) {
|
|
798
|
+
return {
|
|
799
|
+
command: visual,
|
|
800
|
+
env: "VISUAL"
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const editor = process.env.EDITOR?.trim();
|
|
804
|
+
if (editor) {
|
|
805
|
+
return {
|
|
806
|
+
command: editor,
|
|
807
|
+
env: "EDITOR"
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function editorBinary(editor) {
|
|
812
|
+
return editor.command.trim().split(/\s+/)[0] || "";
|
|
813
|
+
}
|
|
814
|
+
function usesTerminalUi(editor) {
|
|
815
|
+
const bin = editorBinary(editor).split(/[\\/]/).pop()?.toLowerCase();
|
|
816
|
+
if (!bin)
|
|
817
|
+
return true;
|
|
818
|
+
return !["code", "code-insiders", "cursor", "windsurf", "subl", "zed", "mate", "idea", "webstorm", "pycharm", "goland", "clion", "rubymine", "fleet", "notepad", "notepad++", "open"].includes(bin);
|
|
819
|
+
}
|
|
820
|
+
async function ensureSnippetDraft(name, projectDir) {
|
|
821
|
+
const dir = await ensureSnippetsDir(projectDir);
|
|
822
|
+
const filePath = join7(dir, `${name}${CONFIG.SNIPPET_EXTENSION}`);
|
|
823
|
+
try {
|
|
824
|
+
await access2(filePath);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
if (error.code !== "ENOENT")
|
|
827
|
+
throw error;
|
|
828
|
+
await writeFile4(filePath, EMPTY_SNIPPET, "utf8");
|
|
829
|
+
}
|
|
830
|
+
return filePath;
|
|
831
|
+
}
|
|
832
|
+
async function openExternalEditor(api, filePath, editor) {
|
|
833
|
+
if (!editor)
|
|
834
|
+
return false;
|
|
835
|
+
const interactive = usesTerminalUi(editor);
|
|
836
|
+
if (interactive) {
|
|
837
|
+
api.renderer.suspend();
|
|
838
|
+
api.renderer.currentRenderBuffer.clear();
|
|
839
|
+
}
|
|
840
|
+
try {
|
|
841
|
+
const cmd = process.platform === "win32" ? ["cmd", "/c", `${editor.command} "${filePath.replace(/"/g, "\\\"")}"`] : [...editor.command.split(" "), filePath];
|
|
842
|
+
const proc = spawn(cmd[0] ?? "", cmd.slice(1), {
|
|
843
|
+
stdio: interactive ? "inherit" : "ignore",
|
|
844
|
+
windowsHide: !interactive
|
|
845
|
+
});
|
|
846
|
+
await new Promise((resolve2, reject) => {
|
|
847
|
+
proc.once("error", reject);
|
|
848
|
+
proc.once("close", () => resolve2());
|
|
849
|
+
});
|
|
850
|
+
return true;
|
|
851
|
+
} finally {
|
|
852
|
+
if (interactive) {
|
|
853
|
+
api.renderer.currentRenderBuffer.clear();
|
|
854
|
+
api.renderer.resume();
|
|
855
|
+
}
|
|
856
|
+
api.renderer.requestRender();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function toPromptInfo(prompt, input) {
|
|
860
|
+
const current = prompt.current;
|
|
861
|
+
return {
|
|
862
|
+
input,
|
|
863
|
+
mode: current.mode,
|
|
864
|
+
parts: [...current.parts]
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
function setPromptInput(prompt, input) {
|
|
868
|
+
prompt.set(toPromptInfo(prompt, input));
|
|
869
|
+
}
|
|
870
|
+
function isSubmitKey(evt) {
|
|
871
|
+
return evt.name === "return" || evt.name === "linefeed" || evt.name === "enter" || evt.raw === "\r" || evt.raw === `
|
|
872
|
+
` || evt.sequence === "\r" || evt.sequence === `
|
|
873
|
+
` || evt.raw === "\x1BOM" || evt.sequence === "\x1BOM";
|
|
874
|
+
}
|
|
875
|
+
async function getSnippets(api) {
|
|
876
|
+
const registry = await loadSnippets(api.state.path.directory);
|
|
877
|
+
return sortSnippets(listSnippets(registry));
|
|
878
|
+
}
|
|
879
|
+
async function reloadSnippetsInTui(api) {
|
|
880
|
+
const registry = await loadSnippets(api.state.path.directory);
|
|
881
|
+
await markSnippetReloadRequested(api.state.path.directory);
|
|
882
|
+
return listSnippets(registry).length;
|
|
883
|
+
}
|
|
884
|
+
function executeReloadInPrompt(api, ref, clear, refresh) {
|
|
885
|
+
(async () => {
|
|
886
|
+
const count = await reloadSnippetsInTui(api);
|
|
887
|
+
await refresh();
|
|
888
|
+
clear();
|
|
889
|
+
ref.focus();
|
|
890
|
+
api.renderer.requestRender();
|
|
891
|
+
setTimeout(() => {
|
|
892
|
+
api.ui.toast({
|
|
893
|
+
variant: "success",
|
|
894
|
+
title: "Snippets reloaded",
|
|
895
|
+
message: `Reloaded ${count} snippet${count === 1 ? "" : "s"}.`,
|
|
896
|
+
duration: 3000
|
|
897
|
+
});
|
|
898
|
+
api.renderer.requestRender();
|
|
899
|
+
}, 0);
|
|
900
|
+
})();
|
|
901
|
+
}
|
|
902
|
+
async function getSkills(api) {
|
|
903
|
+
const native = api.client;
|
|
904
|
+
if (native.app?.skills) {
|
|
905
|
+
const response2 = await native.app.skills({
|
|
906
|
+
directory: api.state.path.directory
|
|
907
|
+
});
|
|
908
|
+
if (response2.data)
|
|
909
|
+
return sortSkills(response2.data.map(nativeSkillInfo));
|
|
910
|
+
}
|
|
911
|
+
if (native.global?.skills) {
|
|
912
|
+
const response2 = await native.global.skills({
|
|
913
|
+
directory: api.state.path.directory
|
|
914
|
+
});
|
|
915
|
+
if (response2.data)
|
|
916
|
+
return sortSkills(response2.data.map(nativeSkillInfo));
|
|
917
|
+
}
|
|
918
|
+
if (native.client?.get) {
|
|
919
|
+
const response2 = await native.client.get({
|
|
920
|
+
url: "/api/skill",
|
|
921
|
+
query: {
|
|
922
|
+
directory: api.state.path.directory
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
const skills = response2.data?.data;
|
|
926
|
+
if (skills) {
|
|
927
|
+
return sortSkills(skills.map(nativeSkillInfo));
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const response = await api.client.config.get({
|
|
931
|
+
directory: api.state.path.directory
|
|
932
|
+
});
|
|
933
|
+
const cfg = response.data || api.state.config;
|
|
934
|
+
const registry = await loadSkills(api.state.path.directory, {
|
|
935
|
+
opencodeSkillDirs: cfg.skills?.paths
|
|
936
|
+
});
|
|
937
|
+
return sortSkills([...registry.values()]);
|
|
938
|
+
}
|
|
939
|
+
function nativeSkillInfo(skill) {
|
|
940
|
+
return {
|
|
941
|
+
name: skill.name,
|
|
942
|
+
content: skill.content.trim(),
|
|
943
|
+
description: skill.description,
|
|
944
|
+
source: "global",
|
|
945
|
+
filePath: skill.location
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
function skillDescription(skill) {
|
|
949
|
+
return (skill.description || skill.content).replace(/\s+/g, " ").trim();
|
|
950
|
+
}
|
|
951
|
+
function PromptWithSnippetAutocomplete(props) {
|
|
952
|
+
const [prompt, setPrompt] = createSignal();
|
|
953
|
+
const [dismissed, setDismissed] = createSignal();
|
|
954
|
+
const [selected, setSelected] = createSignal(0);
|
|
955
|
+
const [inputMode, setInputMode] = createSignal("keyboard");
|
|
956
|
+
const [ignoreMouseUntil, setIgnoreMouseUntil] = createSignal(0);
|
|
957
|
+
const [lastMousePos, setLastMousePos] = createSignal();
|
|
958
|
+
const [input, setInput] = createSignal("");
|
|
959
|
+
const [syncingPrompt, setSyncingPrompt] = createSignal(false);
|
|
960
|
+
const [menuEpoch, setMenuEpoch] = createSignal(0);
|
|
961
|
+
const [creating, setCreating] = createSignal(false);
|
|
962
|
+
const [dialogOpen, setDialogOpen] = createSignal(false);
|
|
963
|
+
const [dialogHandoffUntil, setDialogHandoffUntil] = createSignal(0);
|
|
964
|
+
const [snippets, {
|
|
965
|
+
refetch: refetchSnippets
|
|
966
|
+
}] = createResource(() => props.api.state.path.directory, () => getSnippets(props.api), {
|
|
967
|
+
initialValue: []
|
|
968
|
+
});
|
|
969
|
+
const [skills] = createResource(() => props.api.state.path.directory, () => getSkills(props.api), {
|
|
970
|
+
initialValue: []
|
|
971
|
+
});
|
|
972
|
+
const bind = (ref) => {
|
|
973
|
+
setPrompt(ref);
|
|
974
|
+
props.bindPrompt(ref);
|
|
975
|
+
props.hostRef?.(ref);
|
|
976
|
+
};
|
|
977
|
+
const refreshSnippetOptions = async () => {
|
|
978
|
+
await refetchSnippets();
|
|
979
|
+
};
|
|
980
|
+
let pendingPromptSync;
|
|
981
|
+
let pendingPromptFocus;
|
|
982
|
+
let pendingDialogHandoff;
|
|
983
|
+
onCleanup(() => {
|
|
984
|
+
if (pendingPromptSync)
|
|
985
|
+
clearTimeout(pendingPromptSync);
|
|
986
|
+
if (pendingPromptFocus)
|
|
987
|
+
clearTimeout(pendingPromptFocus);
|
|
988
|
+
if (pendingDialogHandoff)
|
|
989
|
+
clearTimeout(pendingDialogHandoff);
|
|
990
|
+
});
|
|
991
|
+
const lockKeyboardSelection = () => {
|
|
992
|
+
setInputMode("keyboard");
|
|
993
|
+
setIgnoreMouseUntil(Date.now() + MOUSE_HOVER_SUPPRESS_MS);
|
|
994
|
+
};
|
|
995
|
+
const allowMouseHover = () => Date.now() >= ignoreMouseUntil();
|
|
996
|
+
const dialogBlockingInput = () => isDialogInputBlocked(dialogOpen(), dialogHandoffUntil());
|
|
997
|
+
const beginDialogHandoff = () => {
|
|
998
|
+
if (pendingDialogHandoff)
|
|
999
|
+
clearTimeout(pendingDialogHandoff);
|
|
1000
|
+
setDialogHandoffUntil(Date.now() + 150);
|
|
1001
|
+
pendingDialogHandoff = setTimeout(() => {
|
|
1002
|
+
pendingDialogHandoff = undefined;
|
|
1003
|
+
setDialogHandoffUntil(0);
|
|
1004
|
+
props.api.renderer.requestRender();
|
|
1005
|
+
}, 175);
|
|
1006
|
+
};
|
|
1007
|
+
const recordMouseMove = (x, y) => {
|
|
1008
|
+
const last = lastMousePos();
|
|
1009
|
+
if (last?.x === x && last.y === y) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
setLastMousePos({
|
|
1013
|
+
x,
|
|
1014
|
+
y
|
|
1015
|
+
});
|
|
1016
|
+
return true;
|
|
1017
|
+
};
|
|
1018
|
+
const restorePromptFocus = (ref) => {
|
|
1019
|
+
if (pendingPromptFocus)
|
|
1020
|
+
clearTimeout(pendingPromptFocus);
|
|
1021
|
+
pendingPromptFocus = setTimeout(() => {
|
|
1022
|
+
pendingPromptFocus = undefined;
|
|
1023
|
+
ref.focus();
|
|
1024
|
+
}, 175);
|
|
1025
|
+
};
|
|
1026
|
+
const syncPromptInput = (ref, nextInput) => {
|
|
1027
|
+
setPromptInput(ref, nextInput);
|
|
1028
|
+
setInput(nextInput);
|
|
1029
|
+
setSyncingPrompt(false);
|
|
1030
|
+
};
|
|
1031
|
+
const optionsForQuery = (value) => {
|
|
1032
|
+
const snippetOptions = filterSnippets(snippets(), value).map((snippet) => ({
|
|
1033
|
+
kind: "snippet",
|
|
1034
|
+
id: `snippet:${snippet.name}`,
|
|
1035
|
+
label: `#${snippet.name}`,
|
|
1036
|
+
description: snippetDescription(snippet),
|
|
1037
|
+
aliases: matchedAliases(snippet, value),
|
|
1038
|
+
snippet
|
|
1039
|
+
}));
|
|
1040
|
+
const skillOptions = filterSkills(skills(), value).map((skill) => ({
|
|
1041
|
+
kind: "skill",
|
|
1042
|
+
id: `skill:${skill.name}`,
|
|
1043
|
+
label: `#skill(${skill.name})`,
|
|
1044
|
+
description: skillDescription(skill),
|
|
1045
|
+
aliases: [],
|
|
1046
|
+
skill
|
|
1047
|
+
}));
|
|
1048
|
+
return [...snippetOptions, ...skillOptions];
|
|
1049
|
+
};
|
|
1050
|
+
const schedulePromptSync = () => {
|
|
1051
|
+
const ref = prompt();
|
|
1052
|
+
if (!ref)
|
|
1053
|
+
return;
|
|
1054
|
+
if (dialogBlockingInput())
|
|
1055
|
+
return;
|
|
1056
|
+
setSyncingPrompt(true);
|
|
1057
|
+
setMenuEpoch((n) => n + 1);
|
|
1058
|
+
if (pendingPromptSync)
|
|
1059
|
+
clearTimeout(pendingPromptSync);
|
|
1060
|
+
pendingPromptSync = setTimeout(() => {
|
|
1061
|
+
pendingPromptSync = undefined;
|
|
1062
|
+
const next = ref.current.input;
|
|
1063
|
+
setInput((prev) => prev === next ? prev : next);
|
|
1064
|
+
setSyncingPrompt(false);
|
|
1065
|
+
props.api.renderer.requestRender();
|
|
1066
|
+
}, 0);
|
|
1067
|
+
};
|
|
1068
|
+
createEffect(() => {
|
|
1069
|
+
const ref = prompt();
|
|
1070
|
+
if (!ref) {
|
|
1071
|
+
setInput("");
|
|
1072
|
+
setSyncingPrompt(false);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const sync = () => {
|
|
1076
|
+
const next = ref.current.input;
|
|
1077
|
+
setInput((prev) => {
|
|
1078
|
+
if (prev === next)
|
|
1079
|
+
return prev;
|
|
1080
|
+
setSyncingPrompt(false);
|
|
1081
|
+
return next;
|
|
1082
|
+
});
|
|
1083
|
+
};
|
|
1084
|
+
sync();
|
|
1085
|
+
const timer = setInterval(sync, PROMPT_SYNC_MS);
|
|
1086
|
+
onCleanup(() => clearInterval(timer));
|
|
1087
|
+
});
|
|
1088
|
+
const match = createMemo(() => {
|
|
1089
|
+
if (props.disabled || props.visible === false)
|
|
1090
|
+
return;
|
|
1091
|
+
return findTrailingHashtagTrigger(input());
|
|
1092
|
+
});
|
|
1093
|
+
const query = createMemo(() => match()?.query.trim() || "");
|
|
1094
|
+
const options = createMemo(() => {
|
|
1095
|
+
const next = match();
|
|
1096
|
+
if (!next)
|
|
1097
|
+
return [];
|
|
1098
|
+
return optionsForQuery(next.query.trim());
|
|
1099
|
+
});
|
|
1100
|
+
const draftName = createMemo(() => normalizeSnippetName(query()));
|
|
1101
|
+
const visible = createMemo(() => {
|
|
1102
|
+
const next = match();
|
|
1103
|
+
if (!next)
|
|
1104
|
+
return false;
|
|
1105
|
+
if (syncingPrompt())
|
|
1106
|
+
return false;
|
|
1107
|
+
return dismissed() !== next.token;
|
|
1108
|
+
});
|
|
1109
|
+
const canCreate = createMemo(() => {
|
|
1110
|
+
if (snippets.loading || skills.loading)
|
|
1111
|
+
return false;
|
|
1112
|
+
if (options().length > 0)
|
|
1113
|
+
return false;
|
|
1114
|
+
return !!query() && !!draftName();
|
|
1115
|
+
});
|
|
1116
|
+
const optionKey = createMemo(() => options().map((item) => item.id).join(`
|
|
1117
|
+
`));
|
|
1118
|
+
const menuHeight = createMemo(() => Math.min(MENU_MAX_HEIGHT, Math.max(1, options().length || 1)));
|
|
1119
|
+
const activeRowId = createMemo(() => {
|
|
1120
|
+
if (options().length > 0)
|
|
1121
|
+
return options()[selected()]?.id;
|
|
1122
|
+
if (canCreate())
|
|
1123
|
+
return "create-snippet";
|
|
1124
|
+
return;
|
|
1125
|
+
});
|
|
1126
|
+
let scroll;
|
|
1127
|
+
createEffect(() => {
|
|
1128
|
+
menuEpoch();
|
|
1129
|
+
if (visible()) {
|
|
1130
|
+
scroll = undefined;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
createEffect((prev) => {
|
|
1134
|
+
const next = match();
|
|
1135
|
+
if (!next) {
|
|
1136
|
+
if (dismissed())
|
|
1137
|
+
setDismissed(undefined);
|
|
1138
|
+
return "";
|
|
1139
|
+
}
|
|
1140
|
+
const key = `${next.token}
|
|
1141
|
+
${optionKey()}`;
|
|
1142
|
+
if (key !== prev) {
|
|
1143
|
+
setSelected(0);
|
|
1144
|
+
lockKeyboardSelection();
|
|
1145
|
+
setTimeout(() => {
|
|
1146
|
+
scroll?.scrollTo(0);
|
|
1147
|
+
const first = options()[0]?.id;
|
|
1148
|
+
if (first) {
|
|
1149
|
+
scroll?.scrollChildIntoView(first);
|
|
1150
|
+
}
|
|
1151
|
+
}, 0);
|
|
1152
|
+
}
|
|
1153
|
+
return key;
|
|
1154
|
+
});
|
|
1155
|
+
createEffect(() => {
|
|
1156
|
+
const row = activeRowId();
|
|
1157
|
+
if (!visible() || !row)
|
|
1158
|
+
return;
|
|
1159
|
+
setTimeout(() => {
|
|
1160
|
+
scroll?.scrollChildIntoView(row);
|
|
1161
|
+
}, 0);
|
|
1162
|
+
});
|
|
1163
|
+
const choose = (index = selected()) => {
|
|
1164
|
+
const item = options()[index];
|
|
1165
|
+
if (!item)
|
|
1166
|
+
return;
|
|
1167
|
+
chooseItem(item);
|
|
1168
|
+
};
|
|
1169
|
+
const chooseItem = (item) => {
|
|
1170
|
+
const ref = prompt();
|
|
1171
|
+
if (!ref)
|
|
1172
|
+
return;
|
|
1173
|
+
const nextInput = item.kind === "skill" ? insertSkillLoad(ref.current.input, item.skill.name) : insertSnippetTag(ref.current.input, preferredSnippetTag(ref.current.input, item.snippet));
|
|
1174
|
+
syncPromptInput(ref, nextInput);
|
|
1175
|
+
ref.focus();
|
|
1176
|
+
setDismissed(undefined);
|
|
1177
|
+
};
|
|
1178
|
+
const handleNavigationKey = (evt) => {
|
|
1179
|
+
if (dialogBlockingInput() || !visible())
|
|
1180
|
+
return false;
|
|
1181
|
+
const total = options().length;
|
|
1182
|
+
if (total <= 0)
|
|
1183
|
+
return false;
|
|
1184
|
+
if (isAutocompleteNavUpKey(evt)) {
|
|
1185
|
+
lockKeyboardSelection();
|
|
1186
|
+
setSelected(stepSelection(selected(), total, -1));
|
|
1187
|
+
evt.preventDefault();
|
|
1188
|
+
evt.stopPropagation();
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
if (isAutocompleteNavDownKey(evt)) {
|
|
1192
|
+
lockKeyboardSelection();
|
|
1193
|
+
setSelected(stepSelection(selected(), total, 1));
|
|
1194
|
+
evt.preventDefault();
|
|
1195
|
+
evt.stopPropagation();
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
return false;
|
|
1199
|
+
};
|
|
1200
|
+
createEffect(() => {
|
|
1201
|
+
const ref = prompt();
|
|
1202
|
+
if (!ref)
|
|
1203
|
+
return;
|
|
1204
|
+
let dispose;
|
|
1205
|
+
const timer = setTimeout(() => {
|
|
1206
|
+
dispose = props.api.command.register(() => [{
|
|
1207
|
+
title: "Reload snippets",
|
|
1208
|
+
value: "snippets.reload",
|
|
1209
|
+
description: "Reload snippet files from disk",
|
|
1210
|
+
category: "Prompt",
|
|
1211
|
+
slash: {
|
|
1212
|
+
name: "snippets:reload"
|
|
1213
|
+
},
|
|
1214
|
+
onSelect() {
|
|
1215
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
1216
|
+
syncPromptInput(ref, "");
|
|
1217
|
+
setDismissed(undefined);
|
|
1218
|
+
}, refreshSnippetOptions);
|
|
1219
|
+
}
|
|
1220
|
+
}]);
|
|
1221
|
+
}, 0);
|
|
1222
|
+
onCleanup(() => {
|
|
1223
|
+
clearTimeout(timer);
|
|
1224
|
+
dispose?.();
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
createEffect(() => {
|
|
1228
|
+
if (dialogBlockingInput() || !visible() || options().length === 0)
|
|
1229
|
+
return;
|
|
1230
|
+
props.api.renderer.keyInput.prependListener("keypress", handleNavigationKey);
|
|
1231
|
+
onCleanup(() => {
|
|
1232
|
+
props.api.renderer.keyInput.removeListener("keypress", handleNavigationKey);
|
|
1233
|
+
});
|
|
1234
|
+
});
|
|
1235
|
+
const createSnippetDraft = async (rawQuery) => {
|
|
1236
|
+
const ref = prompt();
|
|
1237
|
+
const name = normalizeSnippetName(rawQuery ?? query());
|
|
1238
|
+
if (!ref || !name || creating())
|
|
1239
|
+
return;
|
|
1240
|
+
const current = findTrailingHashtagTrigger(ref.current.input);
|
|
1241
|
+
const nextInput = current ? `${ref.current.input.slice(0, current.start)}#${name}` : `#${name}`;
|
|
1242
|
+
const dismissedToken = `#${name}`;
|
|
1243
|
+
const editor = resolveExternalEditor();
|
|
1244
|
+
if (!editor) {
|
|
1245
|
+
props.api.ui.toast({
|
|
1246
|
+
variant: "warning",
|
|
1247
|
+
message: "Set VISUAL or EDITOR to create snippets from the TUI."
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
props.api.ui.dialog.setSize("medium");
|
|
1252
|
+
setDialogOpen(true);
|
|
1253
|
+
props.api.ui.dialog.replace(() => _$createComponent(props.api.ui.DialogConfirm, {
|
|
1254
|
+
title: `Create snippet #${name}?`,
|
|
1255
|
+
get message() {
|
|
1256
|
+
return `This will create the snippet draft and open it in $${editor.env} (${editor.command}).`;
|
|
1257
|
+
},
|
|
1258
|
+
onCancel: () => {
|
|
1259
|
+
setDialogOpen(false);
|
|
1260
|
+
beginDialogHandoff();
|
|
1261
|
+
props.api.ui.dialog.clear();
|
|
1262
|
+
restorePromptFocus(ref);
|
|
1263
|
+
},
|
|
1264
|
+
onConfirm: () => {
|
|
1265
|
+
setDialogOpen(false);
|
|
1266
|
+
beginDialogHandoff();
|
|
1267
|
+
props.api.ui.dialog.clear();
|
|
1268
|
+
(async () => {
|
|
1269
|
+
setCreating(true);
|
|
1270
|
+
try {
|
|
1271
|
+
syncPromptInput(ref, nextInput);
|
|
1272
|
+
const filePath = await ensureSnippetDraft(name);
|
|
1273
|
+
await addPendingDraft(props.api.state.path.directory, name);
|
|
1274
|
+
setDismissed(dismissedToken);
|
|
1275
|
+
setCreating(false);
|
|
1276
|
+
const opened = await openExternalEditor(props.api, filePath, editor);
|
|
1277
|
+
if (!opened)
|
|
1278
|
+
return;
|
|
1279
|
+
} catch (error) {
|
|
1280
|
+
props.api.ui.toast({
|
|
1281
|
+
variant: "error",
|
|
1282
|
+
message: `Failed to create snippet: ${error instanceof Error ? error.message : String(error)}`
|
|
1283
|
+
});
|
|
1284
|
+
syncPromptInput(ref, nextInput);
|
|
1285
|
+
setDismissed(undefined);
|
|
1286
|
+
} finally {
|
|
1287
|
+
setCreating(false);
|
|
1288
|
+
restorePromptFocus(ref);
|
|
1289
|
+
}
|
|
1290
|
+
})();
|
|
1291
|
+
}
|
|
1292
|
+
}));
|
|
1293
|
+
};
|
|
1294
|
+
const acceptVisibleAutocomplete = () => {
|
|
1295
|
+
const total = options().length;
|
|
1296
|
+
const actionable = total > 0 || canCreate();
|
|
1297
|
+
if (!visible() || !actionable)
|
|
1298
|
+
return false;
|
|
1299
|
+
if (total > 0) {
|
|
1300
|
+
choose(selected());
|
|
1301
|
+
} else if (canCreate()) {
|
|
1302
|
+
createSnippetDraft();
|
|
1303
|
+
}
|
|
1304
|
+
return true;
|
|
1305
|
+
};
|
|
1306
|
+
onMount(() => {
|
|
1307
|
+
const submitAutocomplete = (evt) => {
|
|
1308
|
+
if (!isSubmitKey(evt))
|
|
1309
|
+
return;
|
|
1310
|
+
if (dialogBlockingInput())
|
|
1311
|
+
return;
|
|
1312
|
+
if (!acceptVisibleAutocomplete())
|
|
1313
|
+
return;
|
|
1314
|
+
evt.preventDefault();
|
|
1315
|
+
evt.stopPropagation();
|
|
1316
|
+
};
|
|
1317
|
+
props.api.renderer.keyInput.prependListener("keypress", submitAutocomplete);
|
|
1318
|
+
onCleanup(() => {
|
|
1319
|
+
props.api.renderer.keyInput.removeListener("keypress", submitAutocomplete);
|
|
1320
|
+
});
|
|
1321
|
+
});
|
|
1322
|
+
useKeyboard((evt) => {
|
|
1323
|
+
const ref = prompt();
|
|
1324
|
+
const name = evt.name?.toLowerCase();
|
|
1325
|
+
if (ref && isReloadCommand(ref.current.input) && (name === "return" || name === "enter")) {
|
|
1326
|
+
executeReloadInPrompt(props.api, ref, () => {
|
|
1327
|
+
syncPromptInput(ref, "");
|
|
1328
|
+
setDismissed(undefined);
|
|
1329
|
+
}, refreshSnippetOptions);
|
|
1330
|
+
evt.preventDefault();
|
|
1331
|
+
evt.stopPropagation();
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (dialogBlockingInput())
|
|
1335
|
+
return;
|
|
1336
|
+
if (!visible())
|
|
1337
|
+
return;
|
|
1338
|
+
if (handleNavigationKey(evt)) {
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (name === "escape") {
|
|
1342
|
+
setDismissed(match()?.token);
|
|
1343
|
+
evt.preventDefault();
|
|
1344
|
+
evt.stopPropagation();
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (name === "tab" || name === "return" || name === "linefeed" || name === "enter") {
|
|
1348
|
+
if (!acceptVisibleAutocomplete())
|
|
1349
|
+
return;
|
|
1350
|
+
evt.preventDefault();
|
|
1351
|
+
evt.stopPropagation();
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
schedulePromptSync();
|
|
1355
|
+
});
|
|
1356
|
+
const emptyLabel = createMemo(() => {
|
|
1357
|
+
if ((snippets.loading || skills.loading) && options().length === 0) {
|
|
1358
|
+
return "Loading snippets and skills...";
|
|
1359
|
+
}
|
|
1360
|
+
if (snippets().length === 0 && skills().length === 0)
|
|
1361
|
+
return "No snippets or skills found";
|
|
1362
|
+
return "No matching snippets or skills";
|
|
1363
|
+
});
|
|
1364
|
+
const addSnippetLabel = createMemo(() => {
|
|
1365
|
+
if (creating())
|
|
1366
|
+
return "Creating snippet...";
|
|
1367
|
+
return `Add new Snippet: #${draftName()}`;
|
|
1368
|
+
});
|
|
1369
|
+
const selectedFg = createMemo(() => selectedText(props.api.theme.current));
|
|
1370
|
+
return (() => {
|
|
1371
|
+
var _el$2 = _$createElement("box");
|
|
1372
|
+
_$insert(_el$2, _$createComponent(Show, {
|
|
1373
|
+
get when() {
|
|
1374
|
+
return visible();
|
|
1375
|
+
},
|
|
1376
|
+
get children() {
|
|
1377
|
+
var _el$3 = _$createElement("box"), _el$4 = _$createElement("scrollbox");
|
|
1378
|
+
_$insertNode(_el$3, _el$4);
|
|
1379
|
+
_$setProp(_el$3, "position", "absolute");
|
|
1380
|
+
_$setProp(_el$3, "left", 0);
|
|
1381
|
+
_$setProp(_el$3, "right", 0);
|
|
1382
|
+
_$setProp(_el$3, "zIndex", 100);
|
|
1383
|
+
_$spread(_el$3, _$mergeProps({
|
|
1384
|
+
get top() {
|
|
1385
|
+
return -menuHeight();
|
|
1386
|
+
},
|
|
1387
|
+
get borderColor() {
|
|
1388
|
+
return props.api.theme.current.border;
|
|
1389
|
+
}
|
|
1390
|
+
}, INLINE_BORDER), true);
|
|
1391
|
+
_$use((r) => {
|
|
1392
|
+
scroll = r;
|
|
1393
|
+
}, _el$4);
|
|
1394
|
+
_$setProp(_el$4, "scrollbarOptions", {
|
|
1395
|
+
visible: false
|
|
1396
|
+
});
|
|
1397
|
+
_$insert(_el$4, _$createComponent(Index, {
|
|
1398
|
+
get each() {
|
|
1399
|
+
return options();
|
|
1400
|
+
},
|
|
1401
|
+
get fallback() {
|
|
1402
|
+
return _$createComponent(Show, {
|
|
1403
|
+
get when() {
|
|
1404
|
+
return canCreate();
|
|
1405
|
+
},
|
|
1406
|
+
get fallback() {
|
|
1407
|
+
return (() => {
|
|
1408
|
+
var _el$7 = _$createElement("box"), _el$8 = _$createElement("text");
|
|
1409
|
+
_$insertNode(_el$7, _el$8);
|
|
1410
|
+
_$setProp(_el$7, "paddingLeft", 1);
|
|
1411
|
+
_$setProp(_el$7, "paddingRight", 1);
|
|
1412
|
+
_$insert(_el$8, emptyLabel);
|
|
1413
|
+
_$effect((_$p) => _$setProp(_el$8, "fg", props.api.theme.current.textMuted, _$p));
|
|
1414
|
+
return _el$7;
|
|
1415
|
+
})();
|
|
1416
|
+
},
|
|
1417
|
+
get children() {
|
|
1418
|
+
var _el$5 = _$createElement("box"), _el$6 = _$createElement("text");
|
|
1419
|
+
_$insertNode(_el$5, _el$6);
|
|
1420
|
+
_$setProp(_el$5, "id", "create-snippet");
|
|
1421
|
+
_$setProp(_el$5, "paddingLeft", 1);
|
|
1422
|
+
_$setProp(_el$5, "paddingRight", 1);
|
|
1423
|
+
_$setProp(_el$5, "onMouseMove", (event) => {
|
|
1424
|
+
if (!allowMouseHover())
|
|
1425
|
+
return;
|
|
1426
|
+
if (!recordMouseMove(event.x, event.y))
|
|
1427
|
+
return;
|
|
1428
|
+
setInputMode("mouse");
|
|
1429
|
+
});
|
|
1430
|
+
_$setProp(_el$5, "onMouseDown", () => {
|
|
1431
|
+
setInputMode("mouse");
|
|
1432
|
+
setLastMousePos(undefined);
|
|
1433
|
+
});
|
|
1434
|
+
_$setProp(_el$5, "onMouseUp", () => {
|
|
1435
|
+
createSnippetDraft();
|
|
1436
|
+
});
|
|
1437
|
+
_$insert(_el$6, addSnippetLabel);
|
|
1438
|
+
_$effect((_p$) => {
|
|
1439
|
+
var _v$3 = props.api.theme.current.primary, _v$4 = selectedFg();
|
|
1440
|
+
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$5, "backgroundColor", _v$3, _p$.e));
|
|
1441
|
+
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$6, "fg", _v$4, _p$.t));
|
|
1442
|
+
return _p$;
|
|
1443
|
+
}, {
|
|
1444
|
+
e: undefined,
|
|
1445
|
+
t: undefined
|
|
1446
|
+
});
|
|
1447
|
+
return _el$5;
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
},
|
|
1451
|
+
children: (option, index) => (() => {
|
|
1452
|
+
var _el$9 = _$createElement("box"), _el$0 = _$createElement("text");
|
|
1453
|
+
_$insertNode(_el$9, _el$0);
|
|
1454
|
+
_$setProp(_el$9, "paddingLeft", 1);
|
|
1455
|
+
_$setProp(_el$9, "paddingRight", 1);
|
|
1456
|
+
_$setProp(_el$9, "flexDirection", "row");
|
|
1457
|
+
_$setProp(_el$9, "onMouseMove", (event) => {
|
|
1458
|
+
if (!allowMouseHover())
|
|
1459
|
+
return;
|
|
1460
|
+
if (!recordMouseMove(event.x, event.y))
|
|
1461
|
+
return;
|
|
1462
|
+
setInputMode("mouse");
|
|
1463
|
+
});
|
|
1464
|
+
_$setProp(_el$9, "onMouseOver", () => {
|
|
1465
|
+
if (!allowMouseHover())
|
|
1466
|
+
return;
|
|
1467
|
+
if (inputMode() !== "mouse")
|
|
1468
|
+
return;
|
|
1469
|
+
setSelected(index);
|
|
1470
|
+
});
|
|
1471
|
+
_$setProp(_el$9, "onMouseDown", () => {
|
|
1472
|
+
setInputMode("mouse");
|
|
1473
|
+
setLastMousePos(undefined);
|
|
1474
|
+
setSelected(index);
|
|
1475
|
+
});
|
|
1476
|
+
_$setProp(_el$9, "onMouseUp", () => choose(index));
|
|
1477
|
+
_$setProp(_el$0, "flexShrink", 0);
|
|
1478
|
+
_$setProp(_el$0, "wrapMode", "none");
|
|
1479
|
+
_$insert(_el$0, () => renderHighlighted(option().label, query(), index === selected() ? selectedFg() : props.api.theme.current.text));
|
|
1480
|
+
_$insert(_el$9, _$createComponent(Show, {
|
|
1481
|
+
get when() {
|
|
1482
|
+
return option().aliases.length > 0;
|
|
1483
|
+
},
|
|
1484
|
+
get children() {
|
|
1485
|
+
var _el$1 = _$createElement("text");
|
|
1486
|
+
_$setProp(_el$1, "wrapMode", "none");
|
|
1487
|
+
_$setProp(_el$1, "flexShrink", 0);
|
|
1488
|
+
_$insert(_el$1, () => renderHighlighted(` ${option().aliases.length === 1 ? "alias" : "aliases"}: ${option().aliases.join(", ")}`, query(), index === selected() ? selectedFg() : props.api.theme.current.textMuted));
|
|
1489
|
+
_$effect((_$p) => _$setProp(_el$1, "fg", index === selected() ? selectedFg() : props.api.theme.current.textMuted, _$p));
|
|
1490
|
+
return _el$1;
|
|
1491
|
+
}
|
|
1492
|
+
}), null);
|
|
1493
|
+
_$insert(_el$9, _$createComponent(Show, {
|
|
1494
|
+
get when() {
|
|
1495
|
+
return option().description;
|
|
1496
|
+
},
|
|
1497
|
+
get children() {
|
|
1498
|
+
var _el$10 = _$createElement("text");
|
|
1499
|
+
_$setProp(_el$10, "wrapMode", "none");
|
|
1500
|
+
_$insert(_el$10, () => renderHighlighted(` ${option().description}`, query(), index === selected() ? selectedFg() : props.api.theme.current.textMuted));
|
|
1501
|
+
_$effect((_$p) => _$setProp(_el$10, "fg", index === selected() ? selectedFg() : props.api.theme.current.textMuted, _$p));
|
|
1502
|
+
return _el$10;
|
|
1503
|
+
}
|
|
1504
|
+
}), null);
|
|
1505
|
+
_$effect((_p$) => {
|
|
1506
|
+
var _v$5 = option().id, _v$6 = index === selected() ? props.api.theme.current.primary : undefined, _v$7 = index === selected() ? selectedFg() : props.api.theme.current.text;
|
|
1507
|
+
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$9, "id", _v$5, _p$.e));
|
|
1508
|
+
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$9, "backgroundColor", _v$6, _p$.t));
|
|
1509
|
+
_v$7 !== _p$.a && (_p$.a = _$setProp(_el$0, "fg", _v$7, _p$.a));
|
|
1510
|
+
return _p$;
|
|
1511
|
+
}, {
|
|
1512
|
+
e: undefined,
|
|
1513
|
+
t: undefined,
|
|
1514
|
+
a: undefined
|
|
1515
|
+
});
|
|
1516
|
+
return _el$9;
|
|
1517
|
+
})()
|
|
1518
|
+
}));
|
|
1519
|
+
_$effect((_p$) => {
|
|
1520
|
+
var _v$ = props.api.theme.current.backgroundMenu, _v$2 = menuHeight();
|
|
1521
|
+
_v$ !== _p$.e && (_p$.e = _$setProp(_el$4, "backgroundColor", _v$, _p$.e));
|
|
1522
|
+
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$4, "height", _v$2, _p$.t));
|
|
1523
|
+
return _p$;
|
|
1524
|
+
}, {
|
|
1525
|
+
e: undefined,
|
|
1526
|
+
t: undefined
|
|
1527
|
+
});
|
|
1528
|
+
return _el$3;
|
|
1529
|
+
}
|
|
1530
|
+
}), null);
|
|
1531
|
+
_$insert(_el$2, _$createComponent(props.api.ui.Prompt, {
|
|
1532
|
+
get sessionID() {
|
|
1533
|
+
return props.sessionID;
|
|
1534
|
+
},
|
|
1535
|
+
get workspaceID() {
|
|
1536
|
+
return props.workspaceID;
|
|
1537
|
+
},
|
|
1538
|
+
get visible() {
|
|
1539
|
+
return props.visible;
|
|
1540
|
+
},
|
|
1541
|
+
get disabled() {
|
|
1542
|
+
return props.disabled || dialogBlockingInput();
|
|
1543
|
+
},
|
|
1544
|
+
get onSubmit() {
|
|
1545
|
+
return props.onSubmit;
|
|
1546
|
+
},
|
|
1547
|
+
get placeholders() {
|
|
1548
|
+
return props.placeholders;
|
|
1549
|
+
},
|
|
1550
|
+
ref: bind,
|
|
1551
|
+
get right() {
|
|
1552
|
+
return props.right;
|
|
1553
|
+
}
|
|
1554
|
+
}), null);
|
|
1555
|
+
return _el$2;
|
|
1556
|
+
})();
|
|
1557
|
+
}
|
|
1558
|
+
var tui = async (api) => {
|
|
1559
|
+
let currentPrompt;
|
|
1560
|
+
const bindPrompt = (ref) => {
|
|
1561
|
+
currentPrompt = ref;
|
|
1562
|
+
};
|
|
1563
|
+
api.command.register(() => [{
|
|
1564
|
+
title: "Insert snippet",
|
|
1565
|
+
value: "snippets.insert",
|
|
1566
|
+
description: "Insert a # trigger into the current prompt",
|
|
1567
|
+
category: "Prompt",
|
|
1568
|
+
hidden: !currentPrompt,
|
|
1569
|
+
onSelect() {
|
|
1570
|
+
if (!currentPrompt)
|
|
1571
|
+
return;
|
|
1572
|
+
setPromptInput(currentPrompt, insertSnippetTrigger(currentPrompt.current.input));
|
|
1573
|
+
currentPrompt.focus();
|
|
1574
|
+
}
|
|
1575
|
+
}]);
|
|
1576
|
+
api.slots.register({
|
|
1577
|
+
order: 100,
|
|
1578
|
+
slots: {
|
|
1579
|
+
home_prompt(_ctx, value) {
|
|
1580
|
+
return _$createComponent(PromptWithSnippetAutocomplete, {
|
|
1581
|
+
api,
|
|
1582
|
+
bindPrompt,
|
|
1583
|
+
get hostRef() {
|
|
1584
|
+
return value.ref;
|
|
1585
|
+
},
|
|
1586
|
+
get workspaceID() {
|
|
1587
|
+
return value.workspace_id;
|
|
1588
|
+
},
|
|
1589
|
+
placeholders: HOME_PLACEHOLDERS,
|
|
1590
|
+
get right() {
|
|
1591
|
+
return _$createComponent(api.ui.Slot, {
|
|
1592
|
+
name: "home_prompt_right",
|
|
1593
|
+
get workspace_id() {
|
|
1594
|
+
return value.workspace_id;
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
},
|
|
1600
|
+
session_prompt(_ctx, value) {
|
|
1601
|
+
return _$createComponent(PromptWithSnippetAutocomplete, {
|
|
1602
|
+
api,
|
|
1603
|
+
bindPrompt,
|
|
1604
|
+
get hostRef() {
|
|
1605
|
+
return value.ref;
|
|
1606
|
+
},
|
|
1607
|
+
get sessionID() {
|
|
1608
|
+
return value.session_id;
|
|
1609
|
+
},
|
|
1610
|
+
get visible() {
|
|
1611
|
+
return value.visible;
|
|
1612
|
+
},
|
|
1613
|
+
get disabled() {
|
|
1614
|
+
return value.disabled;
|
|
1615
|
+
},
|
|
1616
|
+
get onSubmit() {
|
|
1617
|
+
return value.on_submit;
|
|
1618
|
+
},
|
|
1619
|
+
get right() {
|
|
1620
|
+
return _$createComponent(api.ui.Slot, {
|
|
1621
|
+
name: "session_prompt_right",
|
|
1622
|
+
get session_id() {
|
|
1623
|
+
return value.session_id;
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
};
|
|
1632
|
+
var plugin = {
|
|
1633
|
+
id,
|
|
1634
|
+
tui
|
|
1635
|
+
};
|
|
1636
|
+
var tui_default = plugin;
|
|
1637
|
+
export {
|
|
1638
|
+
tui_default as default
|
|
1639
|
+
};
|