@datename/fastcode-termux 0.0.0-stage → 0.1.6-beta

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.
Files changed (347) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -2
  3. package/bin/fastcode.mjs +59 -0
  4. package/dist/agent/approval.d.ts +50 -0
  5. package/dist/agent/approval.js +7 -0
  6. package/dist/agent/compaction.d.ts +39 -0
  7. package/dist/agent/compaction.js +227 -0
  8. package/dist/agent/context.d.ts +29 -0
  9. package/dist/agent/context.js +62 -0
  10. package/dist/agent/guards.d.ts +43 -0
  11. package/dist/agent/guards.js +269 -0
  12. package/dist/agent/ignore.d.ts +11 -0
  13. package/dist/agent/ignore.js +129 -0
  14. package/dist/agent/limits.d.ts +24 -0
  15. package/dist/agent/limits.js +71 -0
  16. package/dist/agent/process-tree.d.ts +2 -0
  17. package/dist/agent/process-tree.js +31 -0
  18. package/dist/agent/protocol-mode.d.ts +6 -0
  19. package/dist/agent/protocol-mode.js +51 -0
  20. package/dist/agent/protocol.d.ts +4 -0
  21. package/dist/agent/protocol.js +129 -0
  22. package/dist/agent/registry.d.ts +17 -0
  23. package/dist/agent/registry.js +90 -0
  24. package/dist/agent/runner.d.ts +106 -0
  25. package/dist/agent/runner.js +352 -0
  26. package/dist/agent/shell-session.d.ts +37 -0
  27. package/dist/agent/shell-session.js +400 -0
  28. package/dist/agent/stream.d.ts +29 -0
  29. package/dist/agent/stream.js +103 -0
  30. package/dist/agent/tool-schemas.d.ts +2 -0
  31. package/dist/agent/tool-schemas.js +63 -0
  32. package/dist/agent/tools/append_file.d.ts +2 -0
  33. package/dist/agent/tools/append_file.js +26 -0
  34. package/dist/agent/tools/apply_patch.d.ts +10 -0
  35. package/dist/agent/tools/apply_patch.js +52 -0
  36. package/dist/agent/tools/copy_file.d.ts +2 -0
  37. package/dist/agent/tools/copy_file.js +30 -0
  38. package/dist/agent/tools/delete_file.d.ts +2 -0
  39. package/dist/agent/tools/delete_file.js +24 -0
  40. package/dist/agent/tools/grep.d.ts +2 -0
  41. package/dist/agent/tools/grep.js +23 -0
  42. package/dist/agent/tools/index.d.ts +2 -0
  43. package/dist/agent/tools/index.js +32 -0
  44. package/dist/agent/tools/list_files.d.ts +2 -0
  45. package/dist/agent/tools/list_files.js +17 -0
  46. package/dist/agent/tools/make_dir.d.ts +2 -0
  47. package/dist/agent/tools/make_dir.js +24 -0
  48. package/dist/agent/tools/move_path.d.ts +2 -0
  49. package/dist/agent/tools/move_path.js +47 -0
  50. package/dist/agent/tools/paths.d.ts +7 -0
  51. package/dist/agent/tools/paths.js +84 -0
  52. package/dist/agent/tools/pwd.d.ts +2 -0
  53. package/dist/agent/tools/pwd.js +10 -0
  54. package/dist/agent/tools/read_file.d.ts +2 -0
  55. package/dist/agent/tools/read_file.js +21 -0
  56. package/dist/agent/tools/read_many.d.ts +2 -0
  57. package/dist/agent/tools/read_many.js +25 -0
  58. package/dist/agent/tools/readers.d.ts +6 -0
  59. package/dist/agent/tools/readers.js +216 -0
  60. package/dist/agent/tools/replace_in_file.d.ts +2 -0
  61. package/dist/agent/tools/replace_in_file.js +30 -0
  62. package/dist/agent/tools/shell.d.ts +2 -0
  63. package/dist/agent/tools/shell.js +48 -0
  64. package/dist/agent/tools/stat_path.d.ts +2 -0
  65. package/dist/agent/tools/stat_path.js +31 -0
  66. package/dist/agent/tools/types.d.ts +39 -0
  67. package/dist/agent/tools/types.js +1 -0
  68. package/dist/agent/tools/validate.d.ts +6 -0
  69. package/dist/agent/tools/validate.js +29 -0
  70. package/dist/agent/tools/write_file.d.ts +2 -0
  71. package/dist/agent/tools/write_file.js +26 -0
  72. package/dist/agent/tools.d.ts +3 -0
  73. package/dist/agent/tools.js +2 -0
  74. package/dist/app.d.ts +4 -0
  75. package/dist/app.js +708 -0
  76. package/dist/cli.d.ts +1 -0
  77. package/dist/cli.js +569 -0
  78. package/dist/commands/about.d.ts +8 -0
  79. package/dist/commands/about.js +16 -0
  80. package/dist/commands/agent.d.ts +11 -0
  81. package/dist/commands/agent.js +18 -0
  82. package/dist/commands/attach.d.ts +9 -0
  83. package/dist/commands/attach.js +15 -0
  84. package/dist/commands/bg.d.ts +7 -0
  85. package/dist/commands/bg.js +9 -0
  86. package/dist/commands/cancel.d.ts +10 -0
  87. package/dist/commands/cancel.js +10 -0
  88. package/dist/commands/clear.d.ts +11 -0
  89. package/dist/commands/clear.js +9 -0
  90. package/dist/commands/compact.d.ts +10 -0
  91. package/dist/commands/compact.js +22 -0
  92. package/dist/commands/config.d.ts +7 -0
  93. package/dist/commands/config.js +24 -0
  94. package/dist/commands/context.d.ts +7 -0
  95. package/dist/commands/context.js +24 -0
  96. package/dist/commands/cost.d.ts +9 -0
  97. package/dist/commands/cost.js +13 -0
  98. package/dist/commands/cwd.d.ts +7 -0
  99. package/dist/commands/cwd.js +7 -0
  100. package/dist/commands/edit.d.ts +10 -0
  101. package/dist/commands/edit.js +36 -0
  102. package/dist/commands/effort.d.ts +25 -0
  103. package/dist/commands/effort.js +48 -0
  104. package/dist/commands/exit.d.ts +9 -0
  105. package/dist/commands/exit.js +8 -0
  106. package/dist/commands/expand.d.ts +10 -0
  107. package/dist/commands/expand.js +8 -0
  108. package/dist/commands/help.d.ts +6 -0
  109. package/dist/commands/help.js +18 -0
  110. package/dist/commands/index.d.ts +9 -0
  111. package/dist/commands/index.js +80 -0
  112. package/dist/commands/jobs.d.ts +7 -0
  113. package/dist/commands/jobs.js +16 -0
  114. package/dist/commands/kill.d.ts +7 -0
  115. package/dist/commands/kill.js +11 -0
  116. package/dist/commands/login.d.ts +18 -0
  117. package/dist/commands/login.js +63 -0
  118. package/dist/commands/logout.d.ts +9 -0
  119. package/dist/commands/logout.js +45 -0
  120. package/dist/commands/model.d.ts +24 -0
  121. package/dist/commands/model.js +50 -0
  122. package/dist/commands/plugin.d.ts +11 -0
  123. package/dist/commands/plugin.js +74 -0
  124. package/dist/commands/provider.d.ts +24 -0
  125. package/dist/commands/provider.js +39 -0
  126. package/dist/commands/reset.d.ts +9 -0
  127. package/dist/commands/reset.js +13 -0
  128. package/dist/commands/skills.d.ts +11 -0
  129. package/dist/commands/skills.js +106 -0
  130. package/dist/commands/steer.d.ts +11 -0
  131. package/dist/commands/steer.js +13 -0
  132. package/dist/commands/system.d.ts +11 -0
  133. package/dist/commands/system.js +20 -0
  134. package/dist/commands/theme.d.ts +11 -0
  135. package/dist/commands/theme.js +22 -0
  136. package/dist/commands/tools.d.ts +7 -0
  137. package/dist/commands/tools.js +29 -0
  138. package/dist/commands/whoami.d.ts +9 -0
  139. package/dist/commands/whoami.js +52 -0
  140. package/dist/config/paths.d.ts +4 -0
  141. package/dist/config/paths.js +59 -0
  142. package/dist/config/store.d.ts +89 -0
  143. package/dist/config/store.js +260 -0
  144. package/dist/config/themes.d.ts +73 -0
  145. package/dist/config/themes.js +77 -0
  146. package/dist/config/trust.d.ts +22 -0
  147. package/dist/config/trust.js +153 -0
  148. package/dist/index.d.ts +1 -0
  149. package/dist/index.js +31 -0
  150. package/dist/lib/diagnostics.d.ts +66 -0
  151. package/dist/lib/diagnostics.js +195 -0
  152. package/dist/lib/http.d.ts +23 -0
  153. package/dist/lib/http.js +128 -0
  154. package/dist/lib/markdown.d.ts +1 -0
  155. package/dist/lib/markdown.js +32 -0
  156. package/dist/lib/model-meta.d.ts +19 -0
  157. package/dist/lib/model-meta.js +113 -0
  158. package/dist/lib/native-tools.d.ts +12 -0
  159. package/dist/lib/native-tools.js +21 -0
  160. package/dist/lib/net.d.ts +33 -0
  161. package/dist/lib/net.js +168 -0
  162. package/dist/lib/pkg.d.ts +5 -0
  163. package/dist/lib/pkg.js +27 -0
  164. package/dist/lib/platform.d.ts +7 -0
  165. package/dist/lib/platform.js +57 -0
  166. package/dist/lib/startup.d.ts +6 -0
  167. package/dist/lib/startup.js +25 -0
  168. package/dist/lib/uiMode.d.ts +2 -0
  169. package/dist/lib/uiMode.js +7 -0
  170. package/dist/patch/apply.d.ts +15 -0
  171. package/dist/patch/apply.js +172 -0
  172. package/dist/patch/parse.d.ts +29 -0
  173. package/dist/patch/parse.js +112 -0
  174. package/dist/patch/plan.d.ts +49 -0
  175. package/dist/patch/plan.js +273 -0
  176. package/dist/plugins/manager.d.ts +23 -0
  177. package/dist/plugins/manager.js +101 -0
  178. package/dist/providers/anthropic.d.ts +22 -0
  179. package/dist/providers/anthropic.js +119 -0
  180. package/dist/providers/custom.d.ts +33 -0
  181. package/dist/providers/custom.js +25 -0
  182. package/dist/providers/deepseek.d.ts +34 -0
  183. package/dist/providers/deepseek.js +16 -0
  184. package/dist/providers/google.d.ts +33 -0
  185. package/dist/providers/google.js +19 -0
  186. package/dist/providers/groq.d.ts +34 -0
  187. package/dist/providers/groq.js +21 -0
  188. package/dist/providers/index.d.ts +10 -0
  189. package/dist/providers/index.js +67 -0
  190. package/dist/providers/mistral.d.ts +34 -0
  191. package/dist/providers/mistral.js +20 -0
  192. package/dist/providers/moonshot.d.ts +34 -0
  193. package/dist/providers/moonshot.js +23 -0
  194. package/dist/providers/ollama.d.ts +33 -0
  195. package/dist/providers/ollama.js +22 -0
  196. package/dist/providers/openai.d.ts +34 -0
  197. package/dist/providers/openai.js +26 -0
  198. package/dist/providers/openaiCompatible.d.ts +26 -0
  199. package/dist/providers/openaiCompatible.js +158 -0
  200. package/dist/providers/openrouter.d.ts +34 -0
  201. package/dist/providers/openrouter.js +27 -0
  202. package/dist/providers/qwen.d.ts +34 -0
  203. package/dist/providers/qwen.js +24 -0
  204. package/dist/providers/xai.d.ts +34 -0
  205. package/dist/providers/xai.js +19 -0
  206. package/dist/providers/zhipu.d.ts +34 -0
  207. package/dist/providers/zhipu.js +24 -0
  208. package/dist/session/log.d.ts +52 -0
  209. package/dist/session/log.js +331 -0
  210. package/dist/session/projection.d.ts +43 -0
  211. package/dist/session/projection.js +257 -0
  212. package/dist/skills/manager.d.ts +18 -0
  213. package/dist/skills/manager.js +201 -0
  214. package/dist/skills/parser.d.ts +18 -0
  215. package/dist/skills/parser.js +65 -0
  216. package/dist/types.d.ts +205 -0
  217. package/dist/types.js +1 -0
  218. package/dist/ui/Banner.d.ts +5 -0
  219. package/dist/ui/Banner.js +65 -0
  220. package/dist/ui/Message.d.ts +1 -0
  221. package/dist/ui/Message.js +24 -0
  222. package/dist/ui/Picker.d.ts +8 -0
  223. package/dist/ui/Picker.js +24 -0
  224. package/dist/ui/Prompt.d.ts +9 -0
  225. package/dist/ui/Prompt.js +38 -0
  226. package/dist/ui/Spinner.d.ts +5 -0
  227. package/dist/ui/Spinner.js +39 -0
  228. package/dist/ui/StatusBar.d.ts +11 -0
  229. package/dist/ui/StatusBar.js +20 -0
  230. package/dist/ui/TrustGate.d.ts +5 -0
  231. package/dist/ui/TrustGate.js +26 -0
  232. package/package.json +96 -4
  233. package/src/agent/approval.ts +47 -0
  234. package/src/agent/compaction.ts +276 -0
  235. package/src/agent/context.ts +97 -0
  236. package/src/agent/guards.ts +270 -0
  237. package/src/agent/ignore.ts +147 -0
  238. package/src/agent/limits.ts +71 -0
  239. package/src/agent/process-tree.ts +32 -0
  240. package/src/agent/protocol-mode.ts +59 -0
  241. package/src/agent/protocol.ts +129 -0
  242. package/src/agent/registry.ts +98 -0
  243. package/src/agent/runner.ts +443 -0
  244. package/src/agent/shell-session.ts +449 -0
  245. package/src/agent/stream.ts +143 -0
  246. package/src/agent/tool-schemas.ts +67 -0
  247. package/src/agent/tools/append_file.ts +28 -0
  248. package/src/agent/tools/apply_patch.ts +54 -0
  249. package/src/agent/tools/copy_file.ts +32 -0
  250. package/src/agent/tools/delete_file.ts +26 -0
  251. package/src/agent/tools/grep.ts +25 -0
  252. package/src/agent/tools/index.ts +34 -0
  253. package/src/agent/tools/list_files.ts +19 -0
  254. package/src/agent/tools/make_dir.ts +26 -0
  255. package/src/agent/tools/move_path.ts +50 -0
  256. package/src/agent/tools/paths.ts +92 -0
  257. package/src/agent/tools/pwd.ts +12 -0
  258. package/src/agent/tools/read_file.ts +23 -0
  259. package/src/agent/tools/read_many.ts +27 -0
  260. package/src/agent/tools/readers.ts +228 -0
  261. package/src/agent/tools/replace_in_file.ts +32 -0
  262. package/src/agent/tools/shell.ts +53 -0
  263. package/src/agent/tools/stat_path.ts +33 -0
  264. package/src/agent/tools/types.ts +42 -0
  265. package/src/agent/tools/validate.ts +35 -0
  266. package/src/agent/tools/write_file.ts +28 -0
  267. package/src/agent/tools.ts +3 -0
  268. package/src/app.ts +708 -0
  269. package/src/cli.ts +335 -0
  270. package/src/commands/about.ts +16 -0
  271. package/src/commands/agent.ts +18 -0
  272. package/src/commands/attach.ts +16 -0
  273. package/src/commands/bg.ts +9 -0
  274. package/src/commands/cancel.ts +10 -0
  275. package/src/commands/clear.ts +9 -0
  276. package/src/commands/compact.ts +23 -0
  277. package/src/commands/config.ts +24 -0
  278. package/src/commands/context.ts +25 -0
  279. package/src/commands/cost.ts +13 -0
  280. package/src/commands/cwd.ts +7 -0
  281. package/src/commands/edit.ts +37 -0
  282. package/src/commands/effort.ts +49 -0
  283. package/src/commands/exit.ts +8 -0
  284. package/src/commands/expand.ts +8 -0
  285. package/src/commands/help.ts +18 -0
  286. package/src/commands/index.ts +80 -0
  287. package/src/commands/jobs.ts +17 -0
  288. package/src/commands/kill.ts +12 -0
  289. package/src/commands/login.ts +63 -0
  290. package/src/commands/logout.ts +45 -0
  291. package/src/commands/model.ts +50 -0
  292. package/src/commands/plugin.ts +74 -0
  293. package/src/commands/provider.ts +39 -0
  294. package/src/commands/reset.ts +13 -0
  295. package/src/commands/skills.ts +106 -0
  296. package/src/commands/steer.ts +13 -0
  297. package/src/commands/system.ts +20 -0
  298. package/src/commands/theme.ts +22 -0
  299. package/src/commands/tools.ts +29 -0
  300. package/src/commands/whoami.ts +52 -0
  301. package/src/config/paths.ts +59 -0
  302. package/src/config/store.ts +293 -0
  303. package/src/config/themes.ts +77 -0
  304. package/src/config/trust.ts +183 -0
  305. package/src/index.ts +31 -0
  306. package/src/lib/diagnostics.ts +195 -0
  307. package/src/lib/http.ts +132 -0
  308. package/src/lib/markdown.ts +32 -0
  309. package/src/lib/model-meta.ts +138 -0
  310. package/src/lib/native-tools.ts +32 -0
  311. package/src/lib/net.ts +204 -0
  312. package/src/lib/pkg.ts +35 -0
  313. package/src/lib/platform.ts +57 -0
  314. package/src/lib/startup.ts +25 -0
  315. package/src/lib/uiMode.ts +7 -0
  316. package/src/patch/apply.ts +194 -0
  317. package/src/patch/parse.ts +131 -0
  318. package/src/patch/plan.ts +317 -0
  319. package/src/plugins/manager.ts +101 -0
  320. package/src/providers/anthropic.ts +119 -0
  321. package/src/providers/custom.ts +25 -0
  322. package/src/providers/deepseek.ts +16 -0
  323. package/src/providers/google.ts +19 -0
  324. package/src/providers/groq.ts +21 -0
  325. package/src/providers/index.ts +68 -0
  326. package/src/providers/mistral.ts +20 -0
  327. package/src/providers/moonshot.ts +23 -0
  328. package/src/providers/ollama.ts +22 -0
  329. package/src/providers/openai.ts +26 -0
  330. package/src/providers/openaiCompatible.ts +158 -0
  331. package/src/providers/openrouter.ts +27 -0
  332. package/src/providers/qwen.ts +24 -0
  333. package/src/providers/xai.ts +19 -0
  334. package/src/providers/zhipu.ts +24 -0
  335. package/src/session/log.ts +374 -0
  336. package/src/session/projection.ts +308 -0
  337. package/src/skills/manager.ts +201 -0
  338. package/src/skills/parser.ts +65 -0
  339. package/src/types.ts +205 -0
  340. package/src/ui/Banner.ts +68 -0
  341. package/src/ui/Message.ts +24 -0
  342. package/src/ui/Picker.ts +24 -0
  343. package/src/ui/Prompt.ts +38 -0
  344. package/src/ui/Spinner.ts +39 -0
  345. package/src/ui/StatusBar.ts +20 -0
  346. package/src/ui/TrustGate.ts +26 -0
  347. package/tsconfig.json +22 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Skill manager.
3
+ *
4
+ * Skills follow Anthropic's "Agent Skills" convention: a folder containing
5
+ * a SKILL.md file plus optional resource files. FastCode supports three
6
+ * install sources:
7
+ *
8
+ * 1. skills.sh / skillshub.wtf API (default registry)
9
+ * `/skills install pdf` → resolves via the registry
10
+ * `/skills install owner/repo/skill` → fetches raw SKILL.md
11
+ * 2. Direct https URLs ending in SKILL.md.
12
+ * 3. Local paths (just symlinks into ~/.fastcode/skills/).
13
+ *
14
+ * Installed skills can be enabled per-session and are then stitched into
15
+ * the system prompt at chat time, exactly like Claude does internally.
16
+ */
17
+ import { readFileSync, writeFileSync, existsSync, readdirSync, rmSync, mkdirSync, } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { getPaths } from "../config/paths.js";
20
+ import { fetchJson } from "../lib/http.js";
21
+ import { parseSkill } from "./parser.js";
22
+ const DEFAULT_REGISTRY = "https://skillshub.wtf";
23
+ /** List skills currently installed under ~/.fastcode/skills. */
24
+ export function listInstalledSkills() {
25
+ const { skills } = getPaths();
26
+ if (!existsSync(skills))
27
+ return [];
28
+ const out = [];
29
+ for (const dir of readdirSync(skills, { withFileTypes: true })) {
30
+ if (!dir.isDirectory())
31
+ continue;
32
+ const folder = join(skills, dir.name);
33
+ const md = join(folder, "SKILL.md");
34
+ if (!existsSync(md))
35
+ continue;
36
+ const raw = readFileSync(md, "utf8");
37
+ const parsed = parseSkill(raw, dir.name);
38
+ const sourcePath = join(folder, ".fastcode-source");
39
+ const source = existsSync(sourcePath)
40
+ ? readFileSync(sourcePath, "utf8").trim()
41
+ : "local";
42
+ out.push({
43
+ name: parsed.name || dir.name,
44
+ description: parsed.description,
45
+ source,
46
+ path: folder,
47
+ body: parsed.body,
48
+ });
49
+ }
50
+ return out.sort((a, b) => a.name.localeCompare(b.name));
51
+ }
52
+ /** Look up a skill by name (matches folder name OR frontmatter name). */
53
+ export function getSkill(name) {
54
+ return listInstalledSkills().find((s) => s.name === name || basename(s.path) === name);
55
+ }
56
+ /** Install a skill. Returns the parsed manifest on success. */
57
+ export async function installSkill(spec, opts: any = {}) {
58
+ const { skills } = getPaths();
59
+ const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
60
+ let parsed;
61
+ let source;
62
+ let folderName;
63
+ if (spec.startsWith("http://") || spec.startsWith("https://")) {
64
+ const body = await fetchText(spec);
65
+ parsed = parseSkill(body, slugify(spec));
66
+ source = spec;
67
+ folderName = slugify(parsed.name || slugify(spec));
68
+ }
69
+ else if (looksLikeRepoPath(spec)) {
70
+ // owner/repo or owner/repo/skill — fetch via registry's raw endpoint.
71
+ const url = `${registry}/${spec}?format=md`;
72
+ const body = await fetchText(url);
73
+ parsed = parseSkill(body, parts(spec).pop() ?? spec);
74
+ source = url;
75
+ folderName = slugify(parsed.name || parts(spec).pop() || spec);
76
+ }
77
+ else {
78
+ // Single-token name → search the registry.
79
+ const hits = await searchRegistry(spec, registry);
80
+ const hit = hits[0];
81
+ if (!hit)
82
+ throw new Error(`No skill found for "${spec}" in ${registry}`);
83
+ const url = hit.fetchUrl ?? `${registry}/${hit.slug}?format=md`;
84
+ const body = await fetchText(url);
85
+ parsed = parseSkill(body, hit.name);
86
+ source = url;
87
+ folderName = slugify(parsed.name || hit.name);
88
+ }
89
+ const targetDir = join(skills, folderName);
90
+ if (existsSync(targetDir) && !opts.force) {
91
+ throw new Error(`Skill "${folderName}" is already installed. Use --force to overwrite.`);
92
+ }
93
+ if (existsSync(targetDir))
94
+ rmSync(targetDir, { recursive: true, force: true });
95
+ mkdirSync(targetDir, { recursive: true });
96
+ writeFileSync(join(targetDir, "SKILL.md"), reassemble(parsed));
97
+ writeFileSync(join(targetDir, ".fastcode-source"), source);
98
+ return {
99
+ name: parsed.name || folderName,
100
+ description: parsed.description,
101
+ source,
102
+ path: targetDir,
103
+ body: parsed.body,
104
+ };
105
+ }
106
+ /** Remove an installed skill by name or folder. */
107
+ export function uninstallSkill(name) {
108
+ const { skills } = getPaths();
109
+ const candidates = readdirSync(skills, { withFileTypes: true });
110
+ for (const dir of candidates) {
111
+ if (!dir.isDirectory())
112
+ continue;
113
+ if (dir.name !== name) {
114
+ // Also match by frontmatter name.
115
+ const md = join(skills, dir.name, "SKILL.md");
116
+ if (!existsSync(md))
117
+ continue;
118
+ const parsed = parseSkill(readFileSync(md, "utf8"), dir.name);
119
+ if (parsed.name !== name)
120
+ continue;
121
+ }
122
+ rmSync(join(skills, dir.name), { recursive: true, force: true });
123
+ return true;
124
+ }
125
+ return false;
126
+ }
127
+ /** Search the registry for skills matching a query. */
128
+ export async function searchRegistry(query, registry = DEFAULT_REGISTRY) {
129
+ const base = registry.replace(/\/+$/, "");
130
+ try {
131
+ const json = await fetchJson(`${base}/api/v1/skills/search?q=${encodeURIComponent(query)}`);
132
+ return json.data ?? [];
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ }
138
+ /** Compose the system-prompt addendum for a list of enabled skill names. */
139
+ export function buildSkillsPrompt(enabled) {
140
+ if (enabled.length === 0)
141
+ return "";
142
+ const installed = listInstalledSkills();
143
+ const chunks = [];
144
+ for (const name of enabled) {
145
+ const skill = installed.find((s) => s.name === name || basename(s.path) === name);
146
+ if (!skill)
147
+ continue;
148
+ chunks.push(`<skill name="${skill.name}">\n${skill.body.trim()}\n</skill>`);
149
+ }
150
+ if (chunks.length === 0)
151
+ return "";
152
+ return [
153
+ "",
154
+ "## Active skills",
155
+ "The following skills are loaded for this session. Follow their instructions when relevant:",
156
+ "",
157
+ chunks.join("\n\n"),
158
+ ].join("\n");
159
+ }
160
+ // ---------- helpers ----------
161
+ async function fetchText(url) {
162
+ const res = await fetch(url);
163
+ if (!res.ok)
164
+ throw new Error(`Fetch ${url} failed: ${res.status} ${res.statusText}`);
165
+ return await res.text();
166
+ }
167
+ function reassemble(parsed) {
168
+ const fmEntries = Object.entries(parsed.frontmatter);
169
+ if (fmEntries.length === 0 && parsed.name && parsed.description) {
170
+ return [
171
+ "---",
172
+ `name: ${parsed.name}`,
173
+ `description: ${parsed.description}`,
174
+ "---",
175
+ "",
176
+ parsed.body.trim(),
177
+ "",
178
+ ].join("\n");
179
+ }
180
+ if (fmEntries.length === 0)
181
+ return parsed.body;
182
+ const fmLines = fmEntries.map(([k, v]) => `${k}: ${v}`);
183
+ return ["---", ...fmLines, "---", "", parsed.body.trim(), ""].join("\n");
184
+ }
185
+ function slugify(s) {
186
+ return s
187
+ .toLowerCase()
188
+ .replace(/[^a-z0-9]+/g, "-")
189
+ .replace(/^-+|-+$/g, "")
190
+ .slice(0, 64) || "skill";
191
+ }
192
+ function looksLikeRepoPath(spec) {
193
+ return /^[\w-]+\/[\w.-]+(\/[\w.-]+)?$/.test(spec) && !spec.startsWith("/");
194
+ }
195
+ function parts(spec) {
196
+ return spec.split("/").filter(Boolean);
197
+ }
198
+ function basename(p) {
199
+ const i = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
200
+ return i === -1 ? p : p.slice(i + 1);
201
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * SKILL.md parser. Skills follow Anthropic's Agent Skills convention:
3
+ *
4
+ * ---
5
+ * name: my-skill
6
+ * description: ...
7
+ * ---
8
+ * <markdown body>
9
+ *
10
+ * We accept both `---` and `+++` fences and tolerate missing frontmatter
11
+ * (treating the first H1 + paragraph as a fallback name/description).
12
+ */
13
+ export function parseSkill(source, fallbackName) {
14
+ const fmMatch = source.match(/^(?:---|\+\+\+)\s*\n([\s\S]*?)\n(?:---|\+\+\+)\s*\n?/);
15
+ let frontmatter = {};
16
+ let body = source;
17
+ if (fmMatch) {
18
+ frontmatter = parseFrontmatter(fmMatch[1] ?? "");
19
+ body = source.slice(fmMatch[0].length).trimStart();
20
+ }
21
+ const name = (frontmatter["name"] ?? fallbackName).trim();
22
+ const description = (frontmatter["description"] ??
23
+ extractFirstParagraph(body) ??
24
+ "").trim();
25
+ return { name, description, body, frontmatter };
26
+ }
27
+ function parseFrontmatter(yaml) {
28
+ // Tiny YAML-ish parser: only handles `key: value` lines, which is what
29
+ // SKILL.md frontmatter uses in practice. This avoids pulling in a YAML lib.
30
+ const out = {};
31
+ for (const lineRaw of yaml.split("\n")) {
32
+ const line = lineRaw.trim();
33
+ if (!line || line.startsWith("#"))
34
+ continue;
35
+ const colon = line.indexOf(":");
36
+ if (colon === -1)
37
+ continue;
38
+ const key = line.slice(0, colon).trim();
39
+ let value = line.slice(colon + 1).trim();
40
+ if ((value.startsWith("\"") && value.endsWith("\"")) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ out[key] = value;
45
+ }
46
+ return out;
47
+ }
48
+ function extractFirstParagraph(body) {
49
+ const lines = body.split("\n");
50
+ let buf = [];
51
+ for (const line of lines) {
52
+ if (line.startsWith("#")) {
53
+ if (buf.length)
54
+ break;
55
+ continue;
56
+ }
57
+ if (!line.trim()) {
58
+ if (buf.length)
59
+ break;
60
+ continue;
61
+ }
62
+ buf.push(line.trim());
63
+ }
64
+ return buf.length ? buf.join(" ") : undefined;
65
+ }
package/src/types.ts ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Shared types used across FastCode.
3
+ *
4
+ * The provider abstraction is intentionally narrow — it only exposes
5
+ * what the chat loop needs (streaming completions, model list, auth).
6
+ * Providers can layer richer behavior on top in their own modules.
7
+ */
8
+ export type Role = "system" | "user" | "assistant" | "tool";
9
+ export type ReasoningEffort = "auto" | "low" | "medium" | "high" | "max";
10
+
11
+ export interface NativeToolCall {
12
+ id: string;
13
+ name: string;
14
+ arguments: string;
15
+ }
16
+
17
+ export interface ToolSchema {
18
+ type: "function";
19
+ function: {
20
+ name: string;
21
+ description: string;
22
+ parameters: Record<string, any>;
23
+ };
24
+ }
25
+
26
+ export interface ChatMessage {
27
+ role: Role;
28
+ content: string;
29
+ toolCalls?: NativeToolCall[];
30
+ toolCallId?: string;
31
+ }
32
+ export interface ChatRequest {
33
+ model: string;
34
+ messages: ChatMessage[];
35
+ /** Optional system prompt. Some providers fold this into messages. */
36
+ system?: string;
37
+ /** Sampling temperature. */
38
+ temperature?: number;
39
+ /** Soft cap for output tokens. */
40
+ maxOutputTokens?: number;
41
+ /**
42
+ * Requested thinking depth. Providers that support it map this to their
43
+ * native parameter (`reasoning_effort`, `thinking.budget_tokens`, …);
44
+ * providers that don't should ignore it.
45
+ */
46
+ reasoningEffort?: ReasoningEffort;
47
+ /** AbortSignal for cancellation (Esc / Ctrl+C). */
48
+ signal?: AbortSignal;
49
+ /** Stable idempotency key reused across retries of the same logical request. */
50
+ clientRequestId?: string;
51
+ /** Native function-calling schemas (auto mode only). */
52
+ tools?: ToolSchema[];
53
+ }
54
+ export interface ChatChunk {
55
+ /** Token text appended to the response. */
56
+ delta: string;
57
+ /** True on the final chunk. */
58
+ done: boolean;
59
+ /** Complete native tool calls (OpenAI-style) on the final chunk. */
60
+ toolCalls?: NativeToolCall[];
61
+ /** Provider-specific usage info on the final chunk. */
62
+ usage?: {
63
+ inputTokens?: number;
64
+ outputTokens?: number;
65
+ totalTokens?: number;
66
+ /** Approximate cost in USD if known. */
67
+ costUsd?: number;
68
+ };
69
+ }
70
+ export type ChatStream = AsyncIterable<ChatChunk>;
71
+ export interface ProviderModel {
72
+ id: string;
73
+ label?: string;
74
+ /** Optional context window in tokens. */
75
+ contextWindow?: number;
76
+ /** Pricing per 1M tokens, used for cost estimation. */
77
+ pricing?: {
78
+ inputUsdPerM?: number;
79
+ outputUsdPerM?: number;
80
+ };
81
+ }
82
+ export interface Provider {
83
+ /** Unique id, e.g. "anthropic". */
84
+ id: string;
85
+ /** Human label. */
86
+ label: string;
87
+ /** ENV var(s) that, when set, satisfy auth. */
88
+ authEnv: string[];
89
+ /** Base URL — many providers expose OpenAI-compatible endpoints. */
90
+ baseUrl: string;
91
+ /** Default model id when the user hasn't picked one. */
92
+ defaultModel: string;
93
+ /** Static or dynamic list of supported models. */
94
+ listModels(apiKey: string | undefined): Promise<ProviderModel[]>;
95
+ /** Stream a chat completion. */
96
+ stream(req: ChatRequest, apiKey: string | undefined): ChatStream;
97
+ /** Whether this provider/model honors ChatRequest.reasoningEffort. */
98
+ supportsReasoning?(model: string): boolean;
99
+ }
100
+ export interface PickerItem {
101
+ value: string;
102
+ label: string;
103
+ hint?: string;
104
+ }
105
+ /**
106
+ * An interactive list a command can hand back to the app. The app renders it
107
+ * with arrow keys, number shortcuts, and Enter (Esc cancels).
108
+ */
109
+ export interface PickerRequest {
110
+ title: string;
111
+ items: PickerItem[];
112
+ /** Currently selected value, highlighted on open. */
113
+ current?: string;
114
+ onSelect(value: string, item: PickerItem): void;
115
+ onCancel?(): void;
116
+ }
117
+ export interface SlashCommand {
118
+ name: string;
119
+ description: string;
120
+ /** Optional aliases users can also type. */
121
+ aliases?: string[];
122
+ /** Hide from /help (useful for power-user commands). */
123
+ hidden?: boolean;
124
+ /** Execute the command. Return text to render, or undefined for no output. */
125
+ run(ctx: CommandContext): Promise<CommandResult> | CommandResult;
126
+ }
127
+ export interface CommandResult {
128
+ /** Text rendered as a system message in the transcript. */
129
+ message?: string;
130
+ /** Mark this turn as silent (no echo of the slash itself). */
131
+ silent?: boolean;
132
+ /** Quit the app after this command. */
133
+ quit?: boolean;
134
+ /** Open an interactive picker after the command runs. */
135
+ picker?: PickerRequest;
136
+ }
137
+ export interface CommandContext {
138
+ args: string[];
139
+ raw: string;
140
+ state: AppStateSnapshot;
141
+ actions: AppActions;
142
+ }
143
+ export interface AppStateSnapshot {
144
+ providerId: string;
145
+ model: string;
146
+ theme: string;
147
+ agentMode: boolean;
148
+ history: ChatMessage[];
149
+ systemPrompt: string;
150
+ totalCostUsd: number;
151
+ totalInputTokens: number;
152
+ totalOutputTokens: number;
153
+ enabledSkills: string[];
154
+ enabledPlugins: string[];
155
+ skillsRegistry: string;
156
+ reasoningEffort: ReasoningEffort;
157
+ expanded: boolean;
158
+ busy: boolean;
159
+ draft: string;
160
+ }
161
+ export interface AppActions {
162
+ setProvider(id: string): void;
163
+ setModel(id: string): void;
164
+ setTheme(id: string): void;
165
+ setAgentMode(enabled: boolean): void;
166
+ setSystemPrompt(prompt: string): void;
167
+ setSkillsRegistry(url: string): void;
168
+ setReasoningEffort(effort: ReasoningEffort): void;
169
+ enableSkill(name: string): void;
170
+ disableSkill(name: string): void;
171
+ enablePlugin(name: string): void;
172
+ disablePlugin(name: string): void;
173
+ clearHistory(): void;
174
+ replaceHistory(messages: ChatMessage[]): void;
175
+ cancelRun(): void;
176
+ steer(text: string): void;
177
+ setExpanded(value: boolean): void;
178
+ setInput(text: string): void;
179
+ pushSystemMessage(text: string): void;
180
+ /** Begin the guided API-key flow (choose provider → model → key). */
181
+ startLogin(options: { provider?: string; model?: string }): void;
182
+ }
183
+ export interface SkillManifest {
184
+ name: string;
185
+ description: string;
186
+ /** Where the skill came from (e.g. "skills.sh", "anthropics/skills"). */
187
+ source: string;
188
+ /** Local on-disk path to the skill folder. */
189
+ path: string;
190
+ /** Raw SKILL.md body (the prompt content). */
191
+ body: string;
192
+ }
193
+ export interface Theme {
194
+ id: string;
195
+ label: string;
196
+ banner: [string, string, string];
197
+ user: string;
198
+ assistant: string;
199
+ system: string;
200
+ border: string;
201
+ accent: string;
202
+ muted: string;
203
+ error: string;
204
+ success: string;
205
+ }
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Animated FastCode banner shown at startup.
4
+ *
5
+ * We render the wordmark with ink-big-text + a gradient that slowly cycles
6
+ * through the theme's banner triple. The animation runs for ~1s and then
7
+ * settles on a stable gradient so we don't waste GPU cycles forever.
8
+ *
9
+ * The font is chosen to fit the terminal: `block` needs ~74 columns, so on a
10
+ * narrow phone terminal we fall back to progressively smaller fonts (see
11
+ * BANNER_FONTS). If even the smallest font is wider than the terminal, the
12
+ * banner is skipped entirely.
13
+ */
14
+ import { useEffect, useState } from "react";
15
+ import { Box, Text, useStdout } from "ink";
16
+ import BigText from "ink-big-text";
17
+ import Gradient from "ink-gradient";
18
+ const FRAMES = 16;
19
+
20
+ // (font, columns) needed to render the literal text "FastCode" with
21
+ // letterSpacing 1 and space:false. Measured with cfonts; largest first.
22
+ const BANNER_FONTS = [
23
+ { font: "block", columns: 74 },
24
+ { font: "simple", columns: 56 },
25
+ { font: "slick", columns: 49 },
26
+ { font: "shade", columns: 39 },
27
+ { font: "chrome", columns: 32 },
28
+ { font: "console", columns: 16 },
29
+ ];
30
+
31
+ /** Pick the largest font that fits `columns` (2 columns reserved for padding). */
32
+ function pickBannerFont(columns: number): string | null {
33
+ const usable = columns - 2;
34
+ for (const entry of BANNER_FONTS) {
35
+ if (entry.columns <= usable)
36
+ return entry.font;
37
+ }
38
+ return null;
39
+ }
40
+
41
+ export const Banner = ({ theme, subtitle, stable = false }) => {
42
+ const [frame, setFrame] = useState(0);
43
+ const { stdout } = useStdout();
44
+ const columns = stdout?.columns ?? process.stdout.columns ?? 80;
45
+ const font = pickBannerFont(columns);
46
+ useEffect(() => {
47
+ if (stable)
48
+ return;
49
+ if (frame >= FRAMES)
50
+ return;
51
+ const t = setTimeout(() => setFrame((f) => f + 1), 60);
52
+ return () => clearTimeout(t);
53
+ }, [frame, stable]);
54
+ if (!font)
55
+ return null;
56
+ const t = stable ? 1 : Math.min(1, frame / FRAMES);
57
+ const colors = rotate(theme.banner, t);
58
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "flex-start", marginBottom: 1, children: [_jsx(Gradient, { colors: colors, children: _jsx(BigText, { text: "FastCode", font: font, space: false }) }), _jsx(Text, { color: theme.muted, children: subtitle ?? "multi-LLM TUI · skills.sh · slash commands · /help to start" })] }));
59
+ };
60
+ /** Cycle the gradient stops by `t ∈ [0,1]` for a subtle shimmer effect. */
61
+ function rotate(stops, t) {
62
+ const [a, b, c] = stops;
63
+ if (t < 0.33)
64
+ return [a, b, c];
65
+ if (t < 0.66)
66
+ return [b, c, a];
67
+ return [c, a, b];
68
+ }
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { renderMarkdown } from "../lib/markdown.js";
4
+ const SIGIL = {
5
+ user: "▎ you",
6
+ assistant: "▍ FastCode",
7
+ system: "✦ system",
8
+ };
9
+ const markdownCache = new Map();
10
+ function cachedMarkdown(md) {
11
+ const hit = markdownCache.get(md);
12
+ if (hit !== undefined)
13
+ return hit;
14
+ const rendered = renderMarkdown(md);
15
+ if (markdownCache.size > 200)
16
+ markdownCache.clear();
17
+ markdownCache.set(md, rendered);
18
+ return rendered;
19
+ }
20
+ export const MessageBubble = ({ theme, role, content, streaming, markdown }: any) => {
21
+ const color = role === "user" ? theme.user : role === "assistant" ? theme.assistant : theme.system;
22
+ const display = markdown ? cachedMarkdown(content) : content;
23
+ return (_jsxs(Box, { flexDirection: "column", marginY: 0, children: [_jsxs(Text, { color: color, bold: true, children: [SIGIL[role], streaming ? _jsx(Text, { color: theme.muted, children: " (streaming)" }) : null] }), _jsx(Box, { marginLeft: 2, flexDirection: "column", children: _jsx(Text, { color: color, children: display || (streaming ? "…" : "") }) })] }));
24
+ };
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Presentational picker used by /model, /provider, and /effort.
4
+ *
5
+ * Keyboard handling lives in App so it can suspend the normal prompt input and
6
+ * so every picker behaves the same. This component only renders the window of
7
+ * items around the active index.
8
+ */
9
+ import { Box, Text } from "ink";
10
+ const WINDOW = 10;
11
+ export const Picker = ({ theme, title, items, index, current, hint }) => {
12
+ const max = Math.max(0, items.length - 1);
13
+ const active = Math.max(0, Math.min(index, max));
14
+ const start = Math.max(0, Math.min(active - Math.floor(WINDOW / 2), Math.max(0, items.length - WINDOW)));
15
+ const visible = items.slice(start, start + WINDOW);
16
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.accent, paddingX: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), ...visible.map((item, offset) => {
17
+ const itemIndex = start + offset;
18
+ const selected = itemIndex === active;
19
+ const marker = selected ? "\u276F" : " ";
20
+ const number = itemIndex < 9 ? String(itemIndex + 1) : " ";
21
+ const isCurrent = current !== undefined && item.value === current;
22
+ return (_jsxs(Text, { color: selected ? theme.accent : theme.muted, children: [marker, " ", number, ". ", isCurrent ? "\u25CF " : " ", item.label, item.hint ? _jsxs(Text, { color: theme.muted, children: [" ", item.hint] }) : null] }, item.value));
23
+ }), items.length > WINDOW ? (_jsx(Text, { color: theme.muted, children: ` … ${start + 1}-${start + visible.length} of ${items.length}` })) : null, _jsx(Text, { color: theme.muted, children: hint ?? " \u2191/\u2193 select \u00B7 1-9 jump \u00B7 Enter confirm \u00B7 Esc cancel" })] }));
24
+ };
@@ -0,0 +1,38 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Bottom prompt input, with a left-side caret + slash-command autocomplete.
4
+ *
5
+ * We re-use ink-text-input for low-level keyboard handling but wrap it with
6
+ * a fixed caret indicator + autocomplete preview to mirror the look of
7
+ * Claude Code's prompt.
8
+ *
9
+ * The caret used to blink on a 500 ms setInterval. That timer re-rendered the
10
+ * whole input line twice a second, which on some terminals rewrote the line
11
+ * while the text cursor was being re-positioned and made the caret occasionally
12
+ * jump one column ahead after the first keystroke. A static caret removes the
13
+ * timer-driven redraw; ink-text-input's own cursor still tracks the real caret.
14
+ */
15
+ import { Box, Text } from "ink";
16
+ import TextInput from "ink-text-input";
17
+ import { COMMANDS } from "../commands/index.js";
18
+ export const Prompt = ({ theme, value, onChange, onSubmit, disabled, placeholder, mask, }) => {
19
+ const suggestion = autocomplete(value);
20
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: disabled ? theme.muted : theme.accent, paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u276F " }), disabled ? (_jsx(Text, { color: theme.muted, children: placeholder ?? "Working… (Esc to cancel)" })) : (_jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, placeholder: placeholder ?? "Type a message or /help …", showCursor: true, mask: mask }))] }), !disabled && suggestion ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.muted, children: ["\u21AA ", suggestion] }) })) : null] }));
21
+ };
22
+ function autocomplete(value) {
23
+ if (!value.startsWith("/") || value.includes(" "))
24
+ return undefined;
25
+ const partial = value.slice(1).toLowerCase();
26
+ if (!partial) {
27
+ return COMMANDS.filter((c) => !c.hidden)
28
+ .map((c) => `/${c.name}`)
29
+ .slice(0, 6)
30
+ .join(" ");
31
+ }
32
+ const matches = COMMANDS.filter((c) => !c.hidden &&
33
+ (c.name.startsWith(partial) ||
34
+ (c.aliases ?? []).some((a) => a.startsWith(partial))));
35
+ if (matches.length === 0)
36
+ return undefined;
37
+ return matches.map((c) => `/${c.name} — ${c.description}`).slice(0, 4).join("\n ");
38
+ }
@@ -0,0 +1,39 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Custom thinking spinner.
4
+ *
5
+ * We don't use ink-spinner's built-in spinners directly because we want to
6
+ * (1) cycle through a varied set of action verbs ("Thinking…", "Pondering…")
7
+ * and (2) keep the dots animation in sync with the verb rotation.
8
+ */
9
+ import { useEffect, useState } from "react";
10
+ import { Text } from "ink";
11
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
12
+ const VERBS = [
13
+ "Thinking",
14
+ "Pondering",
15
+ "Reasoning",
16
+ "Cooking",
17
+ "Crafting",
18
+ "Brewing",
19
+ "Sketching",
20
+ "Charging",
21
+ ];
22
+ export const Spinner = ({ theme, label, stable = false }) => {
23
+ const [tick, setTick] = useState(0);
24
+ // stable UI only lowers the redraw rate; it must still animate. A frozen
25
+ // frame made long waits look like a hang.
26
+ const configured = Number(process.env["FASTCODE_SPINNER_MS"]);
27
+ const interval = Number.isFinite(configured) && configured >= 60
28
+ ? configured
29
+ : stable
30
+ ? 120
31
+ : 80;
32
+ useEffect(() => {
33
+ const id = setInterval(() => setTick((t) => t + 1), interval);
34
+ return () => clearInterval(id);
35
+ }, [interval]);
36
+ const frame = FRAMES[tick % FRAMES.length];
37
+ const verb = label ?? VERBS[Math.floor(tick / 24) % VERBS.length];
38
+ return (_jsxs(Text, { color: theme.accent, children: [frame, " ", _jsxs(Text, { color: theme.muted, children: [verb, "\u2026"] })] }));
39
+ };