cortico 0.1.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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/package.json +43 -0
  4. package/src/boot.ts +46 -0
  5. package/src/bot.ts +1342 -0
  6. package/src/config-file.ts +41 -0
  7. package/src/core/README.md +92 -0
  8. package/src/core/billing.ts +26 -0
  9. package/src/core/blobs.ts +106 -0
  10. package/src/core/bus.ts +311 -0
  11. package/src/core/config-schema.ts +201 -0
  12. package/src/core/config.ts +262 -0
  13. package/src/core/core.ts +699 -0
  14. package/src/core/cost.ts +244 -0
  15. package/src/core/event-store.ts +304 -0
  16. package/src/core/fork.ts +113 -0
  17. package/src/core/generation.ts +118 -0
  18. package/src/core/instance-lock.ts +215 -0
  19. package/src/core/ipc-logger.ts +105 -0
  20. package/src/core/language.ts +62 -0
  21. package/src/core/log-context.ts +38 -0
  22. package/src/core/loop.ts +1858 -0
  23. package/src/core/markers.ts +48 -0
  24. package/src/core/prefix.ts +121 -0
  25. package/src/core/run.ts +95 -0
  26. package/src/core/secrets.ts +18 -0
  27. package/src/core/session.ts +56 -0
  28. package/src/core/sessions.ts +261 -0
  29. package/src/core/state.ts +104 -0
  30. package/src/core/template.ts +72 -0
  31. package/src/core/timers.ts +129 -0
  32. package/src/core/tool-log.ts +135 -0
  33. package/src/core/transcript.ts +92 -0
  34. package/src/core/truncate.ts +121 -0
  35. package/src/core/types.ts +1140 -0
  36. package/src/core/usage-log.ts +79 -0
  37. package/src/core/util.ts +386 -0
  38. package/src/deploy-listing.ts +160 -0
  39. package/src/deploy.ts +162 -0
  40. package/src/extensions/README.md +72 -0
  41. package/src/extensions/dry-mount.ts +390 -0
  42. package/src/extensions/manifest.ts +153 -0
  43. package/src/extensions/runtime.ts +42 -0
  44. package/src/extensions.ts +519 -0
  45. package/src/launcher.ts +285 -0
  46. package/src/paths.ts +155 -0
  47. package/src/protocol/open-responses/LICENSE +201 -0
  48. package/src/protocol/open-responses/README.md +14 -0
  49. package/src/protocol/open-responses/context-helpers.ts +27 -0
  50. package/src/protocol/open-responses/context-log.ts +59 -0
  51. package/src/protocol/open-responses/context.ts +89 -0
  52. package/src/protocol/open-responses/generated.ts +115 -0
  53. package/src/protocol/open-responses/index.ts +33 -0
  54. package/src/protocol/open-responses/openapi.json +4230 -0
  55. package/src/protocol/open-responses/stream.ts +146 -0
  56. package/src/protocol/open-responses/tokens.ts +31 -0
  57. package/src/providers/README.md +86 -0
  58. package/src/providers/base.ts +111 -0
  59. package/src/providers/configuration.ts +97 -0
  60. package/src/providers/console/config.ts +21 -0
  61. package/src/providers/console/settings.ts +443 -0
  62. package/src/providers/console/strings.ts +74 -0
  63. package/src/providers/console/types.ts +11 -0
  64. package/src/providers/llamacpp/archive.ts +148 -0
  65. package/src/providers/llamacpp/catalog.ts +247 -0
  66. package/src/providers/llamacpp/config.ts +36 -0
  67. package/src/providers/llamacpp/console/client.ts +6 -0
  68. package/src/providers/llamacpp/console/models-panel.ts +120 -0
  69. package/src/providers/llamacpp/console/runtime-panel.ts +173 -0
  70. package/src/providers/llamacpp/console/server.ts +153 -0
  71. package/src/providers/llamacpp/index.ts +95 -0
  72. package/src/providers/llamacpp/native.ts +51 -0
  73. package/src/providers/llamacpp/options.ts +117 -0
  74. package/src/providers/llamacpp/runtime-store.ts +139 -0
  75. package/src/providers/llamacpp/runtime.ts +214 -0
  76. package/src/providers/llamacpp/server.ts +286 -0
  77. package/src/providers/llamacpp/strings.ts +230 -0
  78. package/src/providers/openai-responses-compat/config.ts +20 -0
  79. package/src/providers/openai-responses-compat/index.ts +89 -0
  80. package/src/providers/openai-responses-compat/native.ts +127 -0
  81. package/src/providers/openai-responses-compat/strings.ts +19 -0
  82. package/src/providers/pricebook.ts +75 -0
  83. package/src/providers/registry.ts +137 -0
  84. package/src/providers/strings.ts +71 -0
  85. package/src/providers/transport/chat.ts +156 -0
  86. package/src/providers/transport/errors.ts +48 -0
  87. package/src/providers/transport/history.ts +68 -0
  88. package/src/providers/transport/native-input.ts +83 -0
  89. package/src/providers/transport/native-types.ts +13 -0
  90. package/src/providers/transport/response-assembly.ts +191 -0
  91. package/src/providers/transport/response-http.ts +196 -0
  92. package/src/providers/transport/response-meters.ts +27 -0
  93. package/src/providers/transport/responses-input.ts +56 -0
  94. package/src/web/README.md +84 -0
  95. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +625 -0
  96. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +168 -0
  97. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +213 -0
  98. package/src/web/client/console-pages/builtins.ts +10 -0
  99. package/src/web/client/console-pages/context.ts +135 -0
  100. package/src/web/client/console-pages/host.ts +633 -0
  101. package/src/web/client/console-pages/loader.ts +132 -0
  102. package/src/web/client/console-pages/strings.ts +90 -0
  103. package/src/web/client/console-pages/tools/strings.ts +49 -0
  104. package/src/web/client/console-pages/tools/view.ts +215 -0
  105. package/src/web/client/core/api.ts +200 -0
  106. package/src/web/client/core/language.ts +56 -0
  107. package/src/web/client/core/lifecycle.ts +132 -0
  108. package/src/web/client/core/router.ts +199 -0
  109. package/src/web/client/core/stream.ts +157 -0
  110. package/src/web/client/core/websocket.ts +82 -0
  111. package/src/web/client/features/appearance/index.ts +333 -0
  112. package/src/web/client/features/appearance/strings.ts +87 -0
  113. package/src/web/client/features/config/index.ts +11 -0
  114. package/src/web/client/features/config/strings.ts +41 -0
  115. package/src/web/client/features/config/view.ts +428 -0
  116. package/src/web/client/features/core/events.ts +197 -0
  117. package/src/web/client/features/core/index.ts +254 -0
  118. package/src/web/client/features/core/run.ts +56 -0
  119. package/src/web/client/features/core/runlog.ts +214 -0
  120. package/src/web/client/features/core/sessions.ts +53 -0
  121. package/src/web/client/features/core/strings.ts +157 -0
  122. package/src/web/client/features/extensions/index.ts +422 -0
  123. package/src/web/client/features/extensions/strings.ts +164 -0
  124. package/src/web/client/features/feature.ts +93 -0
  125. package/src/web/client/features/live/context.ts +280 -0
  126. package/src/web/client/features/live/fork.ts +156 -0
  127. package/src/web/client/features/live/index.ts +392 -0
  128. package/src/web/client/features/live/onboarding.ts +113 -0
  129. package/src/web/client/features/live/protocol.ts +122 -0
  130. package/src/web/client/features/live/sessions.ts +71 -0
  131. package/src/web/client/features/live/status.ts +68 -0
  132. package/src/web/client/features/live/strings.ts +252 -0
  133. package/src/web/client/features/live/timeline.ts +590 -0
  134. package/src/web/client/features/prompts/editor.ts +292 -0
  135. package/src/web/client/features/prompts/index.ts +346 -0
  136. package/src/web/client/features/prompts/strings.ts +136 -0
  137. package/src/web/client/features/prompts/view.ts +256 -0
  138. package/src/web/client/features/providers/index.ts +100 -0
  139. package/src/web/client/features/providers/strings.ts +23 -0
  140. package/src/web/client/features/settings/general.ts +28 -0
  141. package/src/web/client/features/settings/index.ts +82 -0
  142. package/src/web/client/features/settings/strings.ts +33 -0
  143. package/src/web/client/features/storage/index.ts +1 -0
  144. package/src/web/client/features/storage/strings.ts +45 -0
  145. package/src/web/client/features/storage/view.ts +154 -0
  146. package/src/web/client/features/usage/chart.ts +486 -0
  147. package/src/web/client/features/usage/index.ts +407 -0
  148. package/src/web/client/features/usage/labels.ts +46 -0
  149. package/src/web/client/features/usage/range.ts +36 -0
  150. package/src/web/client/features/usage/state.ts +37 -0
  151. package/src/web/client/features/usage/strings.ts +191 -0
  152. package/src/web/client/features/usage/tooltip.ts +86 -0
  153. package/src/web/client/features/usage/types.ts +96 -0
  154. package/src/web/client/features/worlds/index.ts +424 -0
  155. package/src/web/client/features/worlds/strings.ts +121 -0
  156. package/src/web/client/main.ts +273 -0
  157. package/src/web/client/shell/avatar.ts +233 -0
  158. package/src/web/client/shell/index.ts +549 -0
  159. package/src/web/client/shell/strings.ts +89 -0
  160. package/src/web/client/theme/handoff.ts +43 -0
  161. package/src/web/client/theme/palette.ts +109 -0
  162. package/src/web/client/theme/registry.ts +130 -0
  163. package/src/web/client/theme/storage.ts +72 -0
  164. package/src/web/client/theme/strings.ts +123 -0
  165. package/src/web/client/theme/studio.ts +366 -0
  166. package/src/web/client/ui/actions.ts +109 -0
  167. package/src/web/client/ui/data.ts +185 -0
  168. package/src/web/client/ui/dom.ts +23 -0
  169. package/src/web/client/ui/fields.ts +191 -0
  170. package/src/web/client/ui/format.ts +75 -0
  171. package/src/web/client/ui/icons.ts +183 -0
  172. package/src/web/client/ui/images.ts +91 -0
  173. package/src/web/client/ui/index.ts +89 -0
  174. package/src/web/client/ui/lamp.ts +125 -0
  175. package/src/web/client/ui/log.ts +98 -0
  176. package/src/web/client/ui/overlay.ts +184 -0
  177. package/src/web/client/ui/page.ts +8 -0
  178. package/src/web/client/ui/prompt-input.tsx +311 -0
  179. package/src/web/client/ui/sheet.ts +131 -0
  180. package/src/web/client/ui/strings.ts +71 -0
  181. package/src/web/console-pages.ts +367 -0
  182. package/src/web/files.ts +107 -0
  183. package/src/web/path-picker.ts +296 -0
  184. package/src/web/public/index.html +17 -0
  185. package/src/web/public/styles.css +1715 -0
  186. package/src/web/server.ts +1883 -0
  187. package/src/web/shared/client-panel.ts +603 -0
  188. package/src/web/shared/console-protocol.ts +560 -0
  189. package/src/web/shared/css.d.ts +1 -0
  190. package/src/web/shared/path-picker.ts +23 -0
  191. package/src/web/shared/theme.ts +256 -0
  192. package/src/web/theme-store.ts +37 -0
  193. package/src/world.ts +449 -0
  194. package/src/worlds/bilibili/ENV_PROMPT.md +5 -0
  195. package/src/worlds/bilibili/README.md +176 -0
  196. package/src/worlds/bilibili/audience-admission.ts +995 -0
  197. package/src/worlds/bilibili/client.ts +528 -0
  198. package/src/worlds/bilibili/coalescing-buffer.ts +125 -0
  199. package/src/worlds/bilibili/config.ts +185 -0
  200. package/src/worlds/bilibili/console/client.ts +202 -0
  201. package/src/worlds/bilibili/definition.ts +35 -0
  202. package/src/worlds/bilibili/gift-frame.ts +131 -0
  203. package/src/worlds/bilibili/normalize.ts +452 -0
  204. package/src/worlds/bilibili/overlay/announcement.ts +76 -0
  205. package/src/worlds/bilibili/overlay/assets.ts +82 -0
  206. package/src/worlds/bilibili/overlay/model.ts +494 -0
  207. package/src/worlds/bilibili/overlay/project.ts +202 -0
  208. package/src/worlds/bilibili/overlay/server.ts +286 -0
  209. package/src/worlds/bilibili/overlay/types.ts +217 -0
  210. package/src/worlds/bilibili/overlay/web/app.js +690 -0
  211. package/src/worlds/bilibili/overlay/web/editor.css +297 -0
  212. package/src/worlds/bilibili/overlay/web/editor.html +102 -0
  213. package/src/worlds/bilibili/overlay/web/editor.js +2116 -0
  214. package/src/worlds/bilibili/overlay/web/overlay.html +13 -0
  215. package/src/worlds/bilibili/overlay/web/styles.css +258 -0
  216. package/src/worlds/bilibili/protobuf.ts +123 -0
  217. package/src/worlds/bilibili/wire.ts +94 -0
  218. package/src/worlds/bilibili/world.ts +1389 -0
  219. package/src/worlds/console-fixture/ENV_PROMPT.md +1 -0
  220. package/src/worlds/console-fixture/console/client.ts +72 -0
  221. package/src/worlds/console-fixture/world.ts +73 -0
  222. package/src/worlds/index.ts +15 -0
  223. package/src/worlds/minecraft/ENV_PROMPT.md +32 -0
  224. package/src/worlds/minecraft/ENV_PROMPT_CAMERA.md +2 -0
  225. package/src/worlds/minecraft/LICENSE-mineflayer-pathfinder.txt +21 -0
  226. package/src/worlds/minecraft/README.md +917 -0
  227. package/src/worlds/minecraft/blueprint-plan.ts +915 -0
  228. package/src/worlds/minecraft/blueprint-registry.ts +566 -0
  229. package/src/worlds/minecraft/blueprint-repair.ts +236 -0
  230. package/src/worlds/minecraft/blueprint-resource.ts +272 -0
  231. package/src/worlds/minecraft/blueprint.ts +492 -0
  232. package/src/worlds/minecraft/body-lease.ts +340 -0
  233. package/src/worlds/minecraft/bridge.ts +749 -0
  234. package/src/worlds/minecraft/check.ts +651 -0
  235. package/src/worlds/minecraft/chests.ts +292 -0
  236. package/src/worlds/minecraft/client-launch.ts +272 -0
  237. package/src/worlds/minecraft/client-options.ts +85 -0
  238. package/src/worlds/minecraft/client-skins.ts +164 -0
  239. package/src/worlds/minecraft/client-window.ps1 +91 -0
  240. package/src/worlds/minecraft/client.ts +423 -0
  241. package/src/worlds/minecraft/combat-context.ts +109 -0
  242. package/src/worlds/minecraft/combat.ts +1552 -0
  243. package/src/worlds/minecraft/config.ts +499 -0
  244. package/src/worlds/minecraft/console/access.ts +218 -0
  245. package/src/worlds/minecraft/console/client.ts +244 -0
  246. package/src/worlds/minecraft/console/log.ts +110 -0
  247. package/src/worlds/minecraft/console/mount.ts +420 -0
  248. package/src/worlds/minecraft/console/skin.ts +257 -0
  249. package/src/worlds/minecraft/console/style.css +154 -0
  250. package/src/worlds/minecraft/console/world.ts +449 -0
  251. package/src/worlds/minecraft/deaths.ts +135 -0
  252. package/src/worlds/minecraft/definition.ts +18 -0
  253. package/src/worlds/minecraft/engine-child.ts +274 -0
  254. package/src/worlds/minecraft/engine-ipc.ts +98 -0
  255. package/src/worlds/minecraft/entity-facts.ts +132 -0
  256. package/src/worlds/minecraft/escape.ts +407 -0
  257. package/src/worlds/minecraft/executor.ts +14494 -0
  258. package/src/worlds/minecraft/explored.ts +156 -0
  259. package/src/worlds/minecraft/geometry.ts +205 -0
  260. package/src/worlds/minecraft/goal-plan.ts +647 -0
  261. package/src/worlds/minecraft/item-break.ts +126 -0
  262. package/src/worlds/minecraft/item-facts.ts +146 -0
  263. package/src/worlds/minecraft/item-pick.ts +67 -0
  264. package/src/worlds/minecraft/level-dat.ts +176 -0
  265. package/src/worlds/minecraft/log.ts +132 -0
  266. package/src/worlds/minecraft/mineflayer-fixes.ts +1180 -0
  267. package/src/worlds/minecraft/names.ts +377 -0
  268. package/src/worlds/minecraft/pathfinder-lib.d.ts +13 -0
  269. package/src/worlds/minecraft/pathfinder-perf.ts +796 -0
  270. package/src/worlds/minecraft/piglin.ts +49 -0
  271. package/src/worlds/minecraft/policy.ts +547 -0
  272. package/src/worlds/minecraft/precheck.ts +655 -0
  273. package/src/worlds/minecraft/proxy.ts +445 -0
  274. package/src/worlds/minecraft/ranged.ts +635 -0
  275. package/src/worlds/minecraft/readouts.ts +102 -0
  276. package/src/worlds/minecraft/round.ts +26 -0
  277. package/src/worlds/minecraft/search-observation.ts +92 -0
  278. package/src/worlds/minecraft/server-config.ts +467 -0
  279. package/src/worlds/minecraft/server.ts +658 -0
  280. package/src/worlds/minecraft/show.ts +84 -0
  281. package/src/worlds/minecraft/skills.ts +1763 -0
  282. package/src/worlds/minecraft/terrain.ts +1735 -0
  283. package/src/worlds/minecraft/window.ts +96 -0
  284. package/src/worlds/minecraft/works.ts +179 -0
  285. package/src/worlds/minecraft/world.ts +6438 -0
  286. package/src/worlds/qq/ENV_PROMPT.md +5 -0
  287. package/src/worlds/qq/config.ts +108 -0
  288. package/src/worlds/qq/console/client.ts +126 -0
  289. package/src/worlds/qq/console/events.ts +155 -0
  290. package/src/worlds/qq/console/gate.ts +232 -0
  291. package/src/worlds/qq/console/roster.ts +203 -0
  292. package/src/worlds/qq/console/style.css +65 -0
  293. package/src/worlds/qq/conversation.ts +28 -0
  294. package/src/worlds/qq/definition.ts +62 -0
  295. package/src/worlds/qq/driver.ts +375 -0
  296. package/src/worlds/qq/history-tools.ts +209 -0
  297. package/src/worlds/qq/normalize.ts +323 -0
  298. package/src/worlds/qq/qface-map.ts +282 -0
  299. package/src/worlds/qq/vision-prompt.ts +27 -0
  300. package/src/worlds/qq/vision.ts +642 -0
  301. package/src/worlds/qq/vlm.ts +142 -0
  302. package/src/worlds/qq/world.ts +1503 -0
  303. package/src/worlds/terminal/ENV_PROMPT.md +11 -0
  304. package/src/worlds/terminal/config.ts +9 -0
  305. package/src/worlds/terminal/definition.ts +14 -0
  306. package/src/worlds/terminal/world.ts +700 -0
  307. package/src/worlds/websearch/ENV_PROMPT.md +1 -0
  308. package/src/worlds/websearch/brave-client.ts +261 -0
  309. package/src/worlds/websearch/config.ts +75 -0
  310. package/src/worlds/websearch/definition.ts +20 -0
  311. package/src/worlds/websearch/world.ts +171 -0
@@ -0,0 +1,292 @@
1
+ /** 前缀以单文档编辑;模板对应可跟随编辑变化的区间,只读区间由 changeFilter 拦截。 */
2
+
3
+ import { EditorState, StateEffect, StateField, type Extension, type Range } from '@codemirror/state';
4
+ import {
5
+ Decoration, EditorView, keymap, lineNumbers, type DecorationSet,
6
+ } from '@codemirror/view';
7
+ import { S } from './strings.ts';
8
+
9
+ /** 一块 = 一份可编辑模板(或一段只读文本)在文档里占的那一截。 */
10
+ export interface PrefixBlock {
11
+ title: string;
12
+ /** 可编辑源的 key;无 = 这一块来自代码,只读 */
13
+ sourceKey?: string;
14
+ /** 按来源分档,决定底色 */
15
+ tone: 'persona' | 'world' | 'derived';
16
+ /** 这一块此刻的文本 */
17
+ text: string;
18
+ }
19
+
20
+ /** 块在文档里的位置。`from`/`to` 随编辑实时更新。 */
21
+ interface BlockRange {
22
+ block: PrefixBlock;
23
+ from: number;
24
+ to: number;
25
+ }
26
+
27
+ /** 整份重排(重新载入时用)。 */
28
+ const setBlocks = StateEffect.define<BlockRange[]>();
29
+ /** 鼠标此刻停在第几块上(-1 = 不在任何块上)。纯装饰用,不影响文档。 */
30
+ const setHover = StateEffect.define<number>();
31
+
32
+ const hoverField = StateField.define<number>({
33
+ create: () => -1,
34
+ update(value, tr) {
35
+ for (const e of tr.effects) if (e.is(setHover)) return e.value;
36
+ return value;
37
+ },
38
+ });
39
+
40
+ /** 保存状态。`idle` = 这次打开还没动过,什么也不提示。 */
41
+ export type SaveState = 'idle' | 'dirty' | 'saving' | 'saved';
42
+ const setSaveState = StateEffect.define<SaveState>();
43
+
44
+ const SAVE_LABEL: Record<SaveState, string> = {
45
+ idle: '',
46
+ dirty: S.saveHintDirty,
47
+ saving: S.saving,
48
+ saved: S.saved,
49
+ };
50
+
51
+ /**
52
+ * 保存状态,外加它落在哪一行(`at` = 换状态那一刻的光标位置)。
53
+ *
54
+ * **文档一变就是 `dirty`**,不等页面来通知——提示的时效性不该押在上层的回调链上;
55
+ * 页面只在保存前后覆盖成 `saving` / `saved`。
56
+ */
57
+ const saveField = StateField.define<{ state: SaveState; at: number }>({
58
+ create: () => ({ state: 'idle', at: 0 }),
59
+ update(value, tr) {
60
+ for (const e of tr.effects) {
61
+ if (e.is(setSaveState)) return { state: e.value, at: tr.state.selection.main.head };
62
+ }
63
+ if (tr.docChanged) return { state: 'dirty', at: tr.state.selection.main.head };
64
+ return value;
65
+ },
66
+ });
67
+
68
+ /** 编辑时映射块边界:to 的 assoc=1、from 的 assoc=-1,使边界插入内容归入该块。 */
69
+ const blockField = StateField.define<BlockRange[]>({
70
+ create: () => [],
71
+ update(value, tr) {
72
+ for (const e of tr.effects) if (e.is(setBlocks)) return e.value;
73
+ if (!tr.docChanged) return value;
74
+ return value.map((r) => ({
75
+ block: r.block,
76
+ from: tr.changes.mapPos(r.from, -1),
77
+ to: tr.changes.mapPos(r.to, 1),
78
+ }));
79
+ },
80
+ });
81
+
82
+ /** 悬停与光标高亮覆盖所属块的全部行。 */
83
+ const blockDecorations = EditorView.decorations.compute(
84
+ [blockField, hoverField, saveField, 'doc', 'selection'],
85
+ (state) => {
86
+ const ranges: Range<Decoration>[] = [];
87
+ const blocks = state.field(blockField);
88
+ const hovered = state.field(hoverField);
89
+ const caret = state.selection.main.head;
90
+ for (let i = 0; i < blocks.length; i++) {
91
+ const r = blocks[i];
92
+ const first = state.doc.lineAt(Math.min(r.from, state.doc.length)).number;
93
+ const last = state.doc.lineAt(Math.max(r.from, Math.min(r.to, state.doc.length))).number;
94
+ const active = r.block.sourceKey && caret >= r.from && caret <= r.to;
95
+ for (let n = first; n <= last; n++) {
96
+ const line = state.doc.line(n);
97
+ const cls = `cm-block cm-block-${r.block.tone}`
98
+ + (n === first ? ' cm-block-first' : '')
99
+ + (r.block.sourceKey ? '' : ' cm-block-readonly')
100
+ + (i === hovered ? ' cm-block-hover' : '')
101
+ + (active ? ' cm-block-active' : '');
102
+ // 块首行挂上序号:左侧标签与右栏靠它读**真实**位置(见 blockGeometry)
103
+ ranges.push(Decoration.line(
104
+ n === first ? { class: cls, attributes: { 'data-block': String(i) } } : { class: cls },
105
+ ).range(line.from));
106
+ }
107
+ }
108
+ /* 保存提示使用属性与 CSS 伪元素,不插入 contentEditable 节点,以保留空行退格行为。已保存提示仅留在保存时的光标行。 */
109
+ const save = state.field(saveField);
110
+ const line = state.doc.lineAt(Math.min(caret, state.doc.length));
111
+ const pinned = save.state === 'saved'
112
+ && state.doc.lineAt(Math.min(save.at, state.doc.length)).number !== line.number;
113
+ const label = pinned ? '' : SAVE_LABEL[save.state];
114
+ if (label) {
115
+ ranges.push(Decoration.line({
116
+ class: `cm-save-${save.state}`,
117
+ attributes: { 'data-save-hint': label },
118
+ }).range(line.from));
119
+ }
120
+ return Decoration.set(ranges, true) as DecorationSet;
121
+ },
122
+ );
123
+
124
+ /** 鼠标移动时算出停在第几块上。只在结果变了时 dispatch,免得每帧一次事务。 */
125
+ const hoverTracker = EditorView.domEventHandlers({
126
+ mousemove(event, view) {
127
+ const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
128
+ const blocks = view.state.field(blockField);
129
+ let idx = -1;
130
+ if (pos != null) idx = blocks.findIndex((r) => pos >= r.from && pos <= r.to);
131
+ if (idx !== view.state.field(hoverField)) view.dispatch({ effects: setHover.of(idx) });
132
+ return false;
133
+ },
134
+ mouseleave(_event, view) {
135
+ if (view.state.field(hoverField) !== -1) view.dispatch({ effects: setHover.of(-1) });
136
+ return false;
137
+ },
138
+ });
139
+
140
+ /* 块标签由页面按 blockGeometry 绝对定位。 */
141
+
142
+ /** 落在只读块里的改动一律拦掉。 */
143
+ const readonlyGuard = EditorState.changeFilter.of((tr) => {
144
+ const blocks = tr.startState.field(blockField, false);
145
+ if (!blocks) return true;
146
+ let ok = true;
147
+ tr.changes.iterChangedRanges((fromA, toA) => {
148
+ for (const r of blocks) {
149
+ if (r.block.sourceKey) continue;
150
+ // 只读块内部(不含两端边界)被碰到就否决整笔改动
151
+ if (fromA < r.to && toA > r.from) ok = false;
152
+ }
153
+ });
154
+ return ok;
155
+ });
156
+
157
+ export interface PrefixEditorOptions {
158
+ parent: HTMLElement;
159
+ /** 文档变化时报出每块的当前文本(已按区间切好)。只报可编辑块。 */
160
+ onChange(changed: Array<{ sourceKey: string; text: string }>): void;
161
+ /** Ctrl/Cmd+S。**不自动保存**,写盘只从这里发起。 */
162
+ onSave?(): void;
163
+ /** 几何变化(行数、滚动、尺寸)时通知,用来重排右栏。 */
164
+ onGeometry?(): void;
165
+ }
166
+
167
+ export interface PrefixEditor {
168
+ view: EditorView;
169
+ /** 整份重载:重新拼文档、重设区间。 */
170
+ setBlocks(blocks: PrefixBlock[]): void;
171
+ /** 覆盖行尾提示的状态(保存前后各一次)。打字会自动回到 `dirty`。 */
172
+ setSaveState(state: SaveState): void;
173
+ /** 内容区顶边的视口坐标。左右两层据此换算到与正文同一条基线上。 */
174
+ contentTop(): number;
175
+ /** 某一块此刻在视口里的垂直位置(相对编辑器内容顶部),用来对齐左侧标签与右栏。 */
176
+ blockGeometry(): Array<{
177
+ sourceKey?: string;
178
+ title: string;
179
+ tone: PrefixBlock['tone'];
180
+ top: number;
181
+ height: number;
182
+ }>;
183
+ dispose(): void;
184
+ }
185
+
186
+ /** 块文本拼成一份文档:块之间**恰好一个换行**,那一格属于分隔本身。 */
187
+ function joinBlocks(blocks: PrefixBlock[]): { doc: string; ranges: BlockRange[] } {
188
+ const ranges: BlockRange[] = [];
189
+ let doc = '';
190
+ for (const block of blocks) {
191
+ const from = doc.length;
192
+ doc += block.text;
193
+ ranges.push({ block, from, to: doc.length });
194
+ doc += '\n';
195
+ }
196
+ return { doc: doc.replace(/\n$/, ''), ranges };
197
+ }
198
+
199
+ export function createPrefixEditor(opts: PrefixEditorOptions): PrefixEditor {
200
+ let last = new Map<string, string>();
201
+
202
+ const view = new EditorView({
203
+ parent: opts.parent,
204
+ state: EditorState.create({
205
+ doc: '',
206
+ extensions: [
207
+ blockField,
208
+ hoverField,
209
+ saveField,
210
+ blockDecorations,
211
+ hoverTracker,
212
+ lineNumbers(),
213
+ readonlyGuard,
214
+ keymap.of([{
215
+ key: 'Mod-s',
216
+ preventDefault: true,
217
+ run: () => { opts.onSave?.(); return true; },
218
+ }]),
219
+ EditorView.lineWrapping,
220
+ EditorView.updateListener.of((u) => {
221
+ if (u.docChanged) {
222
+ const changed: Array<{ sourceKey: string; text: string }> = [];
223
+ for (const r of u.state.field(blockField)) {
224
+ const key = r.block.sourceKey;
225
+ if (!key) continue;
226
+ const text = u.state.doc.sliceString(
227
+ Math.min(r.from, u.state.doc.length),
228
+ Math.min(r.to, u.state.doc.length),
229
+ );
230
+ if (last.get(key) !== text) {
231
+ last.set(key, text);
232
+ changed.push({ sourceKey: key, text });
233
+ }
234
+ }
235
+ if (changed.length) opts.onChange(changed);
236
+ }
237
+ if (u.docChanged || u.geometryChanged || u.viewportChanged) opts.onGeometry?.();
238
+ }),
239
+ ] as Extension[],
240
+ }),
241
+ });
242
+
243
+ return {
244
+ view,
245
+ setBlocks(blocks) {
246
+ const { doc, ranges } = joinBlocks(blocks);
247
+ last = new Map(blocks.filter((b) => b.sourceKey).map((b) => [b.sourceKey!, b.text]));
248
+ view.dispatch({
249
+ // 载入不是"改动":`setSaveState` 与 `setBlocks` 同批发,把 docChanged
250
+ // 顺带置上的 dirty 压回去,否则一进页面就挂着"按 Ctrl+S 保存"
251
+ changes: { from: 0, to: view.state.doc.length, insert: doc },
252
+ effects: [setBlocks.of(ranges), setSaveState.of('idle')],
253
+ });
254
+ opts.onGeometry?.();
255
+ },
256
+ setSaveState(state) {
257
+ view.dispatch({ effects: setSaveState.of(state) });
258
+ },
259
+ contentTop: () => view.contentDOM.getBoundingClientRect().top,
260
+ /** 优先测量已渲染块首行的 DOM 位置;不可见行回退到 lineBlockAt 估算值。 */
261
+ blockGeometry() {
262
+ const out: Array<{
263
+ sourceKey?: string; title: string; tone: PrefixBlock['tone']; top: number; height: number;
264
+ }> = [];
265
+ const len = view.state.doc.length;
266
+ const blocks = view.state.field(blockField);
267
+ const base = view.contentDOM.getBoundingClientRect().top;
268
+ const topOf = (i: number, pos: number): number => {
269
+ const el = view.contentDOM.querySelector(`[data-block="${i}"]`);
270
+ if (el) return el.getBoundingClientRect().top - base;
271
+ return view.lineBlockAt(Math.min(pos, len)).top;
272
+ };
273
+ for (let i = 0; i < blocks.length; i++) {
274
+ const r = blocks[i];
275
+ const top = topOf(i, r.from);
276
+ // 高度取到下一块的起点;最后一块吃到内容底部
277
+ const next = i + 1 < blocks.length
278
+ ? topOf(i + 1, blocks[i + 1].from)
279
+ : view.contentDOM.getBoundingClientRect().height;
280
+ out.push({
281
+ sourceKey: r.block.sourceKey,
282
+ title: r.block.title,
283
+ tone: r.block.tone,
284
+ top,
285
+ height: Math.max(next - top, 0),
286
+ });
287
+ }
288
+ return out;
289
+ },
290
+ dispose: () => view.destroy(),
291
+ };
292
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * 系统提示词以单个编辑器中的一份文档呈现,分段表示为行区间。段与模板源的映射来自后端 PrefixSegment.sourceKey。
3
+ * 编辑不自动保存,用户通过 Ctrl+S 写回;未保存状态在界面显示。
4
+ */
5
+
6
+ import { get, post } from '../../core/api.ts';
7
+ import { pageIntro } from '../../ui/page.ts';
8
+ import type { FeatureContext, FrameworkFeature } from '../feature.ts';
9
+ import { estTok } from '../live/context.ts';
10
+ import { createPrefixEditor, type PrefixBlock } from './editor.ts';
11
+ import { S } from './strings.ts';
12
+ import { varWarnings, type PromptDoc, type PromptVar } from './view.ts';
13
+
14
+ const PROMPTS_ROUTE = 'prompts';
15
+
16
+ /**
17
+ * 两侧栏宽度保存在模块作用域,切换控制台页面时保留,浏览器刷新后恢复默认值。
18
+ */
19
+ const colWidth = { flags: 158, rail: 190 };
20
+ const COL_MIN = 96;
21
+ const COL_MAX = 460;
22
+
23
+ interface PrefixSegment {
24
+ title: string;
25
+ text: string;
26
+ sourceKey?: string;
27
+ }
28
+
29
+ /** 段按来源分三档上色:人格自有 / World 环境 / 运行时现拼。 */
30
+ function segmentTone(sourceKey: string | undefined): PrefixBlock['tone'] {
31
+ if (!sourceKey) return 'derived';
32
+ return sourceKey.startsWith('worlds.') ? 'world' : 'persona';
33
+ }
34
+
35
+ export async function mountPrompts(ctx: FeatureContext, opts: { embedded?: boolean } = {}): Promise<void> {
36
+ const { ui, root } = ctx;
37
+ const intro = pageIntro(ui, S.title, S.introDesc);
38
+ const status = ui.msgline('');
39
+ const toolbar = ui.h('div', 'prompt-toolbar');
40
+ toolbar.append(status, ui.h('span', 'grow'));
41
+
42
+ // 标签、分割线与右栏均按 blockGeometry 定位。
43
+ const stage = ui.h('div', 'prefix-stage');
44
+ const flags = ui.h('div', 'prefix-flags');
45
+ const host = ui.h('div', 'prefix-host');
46
+ const rail = ui.h('div', 'prefix-rail');
47
+ const gripL = ui.h('div', 'prefix-grip');
48
+ const gripR = ui.h('div', 'prefix-grip');
49
+ // 分割线单独一层:它要从旗尖一路延伸到右栏末端,横跨三列,谁的内部都装不下
50
+ const dividers = ui.h('div', 'prefix-dividers');
51
+ stage.append(flags, gripL, host, gripR, rail, dividers);
52
+ if (ctx.capabilities.sessionControl === true) {
53
+ const reload = ui.button(S.reloadSession, {
54
+ size: 'sm',
55
+ onClick: () => {
56
+ const lock = ui.disable(reload);
57
+ status.textContent = S.reloading;
58
+ void post<{ result?: string }>('/api/session/reload-prefix', undefined, { signal: ctx.signal })
59
+ .then(async (out) => {
60
+ status.textContent = out?.result || S.prefixReloaded;
61
+ await load();
62
+ })
63
+ .catch((err: unknown) => {
64
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
65
+ ctx.onError(err);
66
+ status.className = 'msgline bad';
67
+ status.textContent = err instanceof Error ? err.message : String(err);
68
+ })
69
+ .finally(() => lock.dispose());
70
+ },
71
+ });
72
+ toolbar.appendChild(reload);
73
+ }
74
+ if (!opts.embedded) root.appendChild(intro);
75
+ root.append(toolbar, stage);
76
+
77
+ /** key → 那份模板的声明与 revision(保存要带 baseRevision 做乐观锁)。 */
78
+ const docs = new Map<string, PromptDoc>();
79
+ /** 改过但还没写盘的块文本(按 key 覆盖)。Ctrl+S 之前它只躺在这儿。 */
80
+ const dirty = new Map<string, string>();
81
+
82
+ function setStatus(text: string, bad?: boolean): void {
83
+ status.className = bad ? 'msgline bad' : 'msgline';
84
+ status.textContent = text;
85
+ }
86
+
87
+ /** 未存的改动别让它随手翻页丢掉——这一页没有自动保存兜底。 */
88
+ ctx.lifecycle.own(ctx.router.addLeaveGuard(
89
+ () => (dirty.size ? S.leaveUnsaved : null),
90
+ ));
91
+
92
+ async function flushSaves(): Promise<void> {
93
+ const batch = [...dirty.entries()];
94
+ dirty.clear();
95
+ if (!batch.length) return;
96
+ setStatus(S.saving);
97
+ editor.setSaveState('saving');
98
+ const done: string[] = [];
99
+ for (const [key, text] of batch) {
100
+ const doc = docs.get(key);
101
+ if (!doc) continue;
102
+ try {
103
+ const out = await post<{ error?: string; result?: string; revision?: string }>(
104
+ '/api/prompts',
105
+ { key, content: text, baseRevision: doc.revision },
106
+ { signal: ctx.signal },
107
+ );
108
+ if (out?.error) throw new Error(out.error);
109
+ doc.content = text;
110
+ doc.revision = out?.revision;
111
+ done.push(doc.title);
112
+ } catch (err) {
113
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
114
+ ctx.onError(err);
115
+ // 保存失败的改动留在待保存队列。
116
+ dirty.set(key, text);
117
+ editor.setSaveState('dirty');
118
+ setStatus(S.saveFailedFor(doc.title, String((err as Error)?.message ?? err)), true);
119
+ return;
120
+ }
121
+ }
122
+ editor.setSaveState('saved');
123
+ setStatus(S.savedList(done.join(S.listSep)));
124
+ refreshRail();
125
+ }
126
+
127
+ const editor = createPrefixEditor({
128
+ parent: host,
129
+ onChange: (changed) => {
130
+ for (const c of changed) dirty.set(c.sourceKey, c.text);
131
+ setStatus(S.dirtyCount(dirty.size));
132
+ refreshRail();
133
+ },
134
+ onSave: () => { void flushSaves(); },
135
+ onGeometry: () => positionSides(),
136
+ });
137
+ ctx.lifecycle.own({ dispose: () => editor.dispose() });
138
+
139
+ // ── 右栏:每块一格,列它那份模板的占位符;点开是浮层 ─────────────────
140
+ let openPop: HTMLElement | null = null;
141
+ /** 捕获阶段关闭的块键,用于将同键点击处理为关闭。 */
142
+ let justClosed: string | null = null;
143
+ function closePop(): void { openPop?.remove(); openPop = null; }
144
+ ctx.lifecycle.own({ dispose: closePop });
145
+
146
+ // 点浮层以外的任何地方都立刻收起来。**用捕获阶段**:chip 自己的 onClick 在冒泡阶段
147
+ // 才跑,所以这里先收、它再决定要不要开,于是"再点一次同一个"照样是收起。
148
+ const onDocClick = (ev: Event): void => {
149
+ if (!openPop) return;
150
+ const t = ev.target as Node | null;
151
+ if (t && openPop.contains(t)) return;
152
+ // 只有点的**正是打开它的那颗 chip** 才算"收起";点别处纯粹是关掉,
153
+ // 不能留下标记——否则下次点那颗 chip 会被当成收起而打不开。
154
+ const chip = t instanceof Element ? t.closest('.varchip') : null;
155
+ const key = openPop.getAttribute('data-var');
156
+ justClosed = chip?.getAttribute('data-chip') === key ? key : null;
157
+ closePop();
158
+ };
159
+ document.addEventListener('click', onDocClick, true);
160
+ ctx.lifecycle.own({ dispose: () => document.removeEventListener('click', onDocClick, true) });
161
+ const onEsc = (ev: KeyboardEvent): void => { if (ev.key === 'Escape') closePop(); };
162
+ document.addEventListener('keydown', onEsc);
163
+ ctx.lifecycle.own({ dispose: () => document.removeEventListener('keydown', onEsc) });
164
+
165
+ function varPopup(v: PromptVar): HTMLElement {
166
+ const pop = ui.h('div', 'varpop');
167
+ pop.appendChild(ui.h('div', 'varpop-name mono', `{{${v.name}}}`));
168
+ pop.appendChild(ui.h('div', 'varpop-desc', v.description));
169
+ pop.appendChild(ui.h('div', 'varpop-label', v.multiline ? S.fillNowMultiline : S.fillNow));
170
+ const val = ui.h('pre', 'mono varpop-value');
171
+ val.textContent = v.value === undefined
172
+ ? S.varUnreported
173
+ : v.value === ''
174
+ ? S.varEmpty
175
+ : v.value;
176
+ pop.appendChild(val);
177
+ return pop;
178
+ }
179
+
180
+ /** 按块几何重排;测量编辑器内容与 stage 的坐标差以换算位置。 */
181
+ function positionSides(): void {
182
+ const geo = editor.blockGeometry();
183
+ const off = editor.contentTop() - stage.getBoundingClientRect().top;
184
+ for (let i = 0; i < rail.children.length && i < geo.length; i++) {
185
+ const el = rail.children[i] as HTMLElement;
186
+ el.style.top = `${geo[i].top + off}px`;
187
+ el.style.minHeight = `${geo[i].height}px`;
188
+ }
189
+ for (let i = 0; i < flags.children.length && i < geo.length; i++) {
190
+ (flags.children[i] as HTMLElement).style.top = `${geo[i].top + off}px`;
191
+ }
192
+ for (let i = 0; i < dividers.children.length && i < geo.length; i++) {
193
+ const el = dividers.children[i] as HTMLElement;
194
+ el.style.top = `${geo[i].top + off}px`;
195
+ el.style.left = `${flags.offsetWidth}px`;
196
+ }
197
+ const h = `${host.scrollHeight}px`;
198
+ rail.style.height = h;
199
+ flags.style.height = h;
200
+ }
201
+
202
+ /** 两侧栏宽度落到 DOM 上。 */
203
+ function applyWidths(): void {
204
+ flags.style.flexBasis = `${colWidth.flags}px`;
205
+ rail.style.flexBasis = `${colWidth.rail}px`;
206
+ positionSides();
207
+ }
208
+
209
+ /** document 接收拖动与松手事件,监听随生命周期释放。 */
210
+ function bindGrip(grip: HTMLElement, side: 'flags' | 'rail'): void {
211
+ grip.addEventListener('pointerdown', (ev: Event) => {
212
+ const startX = (ev as MouseEvent).clientX;
213
+ const start = colWidth[side];
214
+ ev.preventDefault();
215
+ grip.className = 'prefix-grip grip-dragging';
216
+ const move = (e: Event): void => {
217
+ const d = (e as MouseEvent).clientX - startX;
218
+ // 右栏在右边:往左拖是变宽,所以它的符号是反的
219
+ const next = start + (side === 'flags' ? d : -d);
220
+ colWidth[side] = Math.max(COL_MIN, Math.min(COL_MAX, next));
221
+ applyWidths();
222
+ };
223
+ const up = (): void => {
224
+ document.removeEventListener('pointermove', move);
225
+ document.removeEventListener('pointerup', up);
226
+ grip.className = 'prefix-grip';
227
+ };
228
+ document.addEventListener('pointermove', move);
229
+ document.addEventListener('pointerup', up);
230
+ }, { signal: ctx.signal });
231
+ }
232
+ bindGrip(gripL, 'flags');
233
+ bindGrip(gripR, 'rail');
234
+ applyWidths();
235
+ window.addEventListener('resize', positionSides, { signal: ctx.signal });
236
+
237
+ /** 重建左侧标签与右栏(块集合或警告变了时)。 */
238
+ function refreshRail(): void {
239
+ closePop();
240
+ rail.replaceChildren();
241
+ flags.replaceChildren();
242
+ dividers.replaceChildren();
243
+ for (const g of editor.blockGeometry()) {
244
+ const flag = ui.h('div', `prefix-flag tone-${g.tone}${g.sourceKey ? '' : ' flag-readonly'}`);
245
+ flag.appendChild(ui.h('span', 'prefix-flag-text', g.title));
246
+ const origin = g.sourceKey ? docs.get(g.sourceKey)?.origin : undefined;
247
+ flag.title = g.sourceKey
248
+ ? S.flagEditable(g.sourceKey, origin)
249
+ : S.flagReadonly;
250
+ flags.appendChild(flag);
251
+ dividers.appendChild(ui.h('div', `prefix-divider tone-${g.tone}`));
252
+
253
+ const cell = ui.h('div', 'prefix-railcell');
254
+ const doc = g.sourceKey ? docs.get(g.sourceKey) : undefined;
255
+ if (!doc) {
256
+ cell.appendChild(ui.h('span', 'prefix-railnote', g.sourceKey ? S.sourceMissing(g.sourceKey) : S.fromCode));
257
+ rail.appendChild(cell);
258
+ continue;
259
+ }
260
+ const text = dirty.get(doc.key) ?? doc.content;
261
+ if (doc.origin) {
262
+ cell.appendChild(ui.h('span', 'prefix-railnote',
263
+ doc.origin === 'deployment' ? S.originDeployment
264
+ : doc.origin === 'package' ? S.originPackage
265
+ : S.originWorld));
266
+ }
267
+ for (const v of doc.vars ?? []) {
268
+ const chip = ui.button(v.name, {
269
+ size: 'sm',
270
+ onClick: () => {
271
+ // 捕获阶段的 onDocClick 已经把浮层收了;这里靠 justClosed 认出"再点同一个"
272
+ if (justClosed === `${doc.key}:${v.name}`) { justClosed = null; return; }
273
+ const pop = varPopup(v);
274
+ pop.setAttribute('data-var', `${doc.key}:${v.name}`);
275
+ cell.appendChild(pop);
276
+ openPop = pop;
277
+ },
278
+ });
279
+ chip.className += ' varchip mono';
280
+ chip.setAttribute('data-chip', `${doc.key}:${v.name}`);
281
+ // 模板里没用到的占位符标灰:它声明了,但这一块此刻不引用它
282
+ if (!text.includes(`{{${v.name}`)) chip.className += ' varchip-unused';
283
+ cell.appendChild(chip);
284
+ }
285
+ const warns = varWarnings(text, doc.vars ?? []);
286
+ if (warns.length) {
287
+ const w = ui.h('div', 'prefix-railwarn');
288
+ w.textContent = warns.join(S.warnSep);
289
+ cell.appendChild(w);
290
+ }
291
+ rail.appendChild(cell);
292
+ }
293
+ positionSides();
294
+ }
295
+
296
+ async function load(): Promise<void> {
297
+ try {
298
+ const [docsRes, prefixRes] = await Promise.all([
299
+ get<{ prompts?: PromptDoc[] }>('/api/prompts', { signal: ctx.signal }),
300
+ get<{ segments?: PrefixSegment[] }>('/api/prompts/prefix', { signal: ctx.signal })
301
+ .catch(() => null),
302
+ ]);
303
+ docs.clear();
304
+ for (const d of docsRes?.prompts ?? []) docs.set(d.key, d);
305
+ const segments = prefixRes?.segments ?? [];
306
+
307
+ if (!segments.length) {
308
+ setStatus(prefixRes ? S.prefixEmpty : S.prefixUnavailable, true);
309
+ editor.setBlocks([]);
310
+ refreshRail();
311
+ return;
312
+ }
313
+
314
+ // 可编辑块装**模板原文**(带 {{}}),只读块装渲染后的文本——后者没有源可给。
315
+ editor.setBlocks(segments.map((seg): PrefixBlock => {
316
+ const doc = seg.sourceKey ? docs.get(seg.sourceKey) : undefined;
317
+ return {
318
+ title: seg.title,
319
+ tone: segmentTone(seg.sourceKey),
320
+ ...(doc ? { sourceKey: doc.key } : {}),
321
+ text: (doc ? doc.content : seg.text).replace(/\s+$/, ''),
322
+ };
323
+ }));
324
+ // 字数不是这条前缀真正的成本,token 才是。口径与截断判据同源(`estTok`),
325
+ // 所以这里报的数和「上下文」页、和真去截断时算的是同一套。
326
+ const tok = estTok(segments.map((s) => s.text).join(''));
327
+ setStatus(S.prefixStats(segments.length, tok.toLocaleString()));
328
+ refreshRail();
329
+ } catch (err) {
330
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
331
+ ctx.onError(err);
332
+ setStatus(S.loadFailed(String((err as Error)?.message ?? err)), true);
333
+ }
334
+ }
335
+
336
+ await load();
337
+ }
338
+
339
+ export const promptsFeature: FrameworkFeature = {
340
+ route: PROMPTS_ROUTE,
341
+ label: S.title,
342
+ icon: 'text',
343
+ navMode: 'persona',
344
+ needsAny: ['prompts'],
345
+ mount: mountPrompts,
346
+ };