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,549 @@
1
+ /**
2
+ * 控制台外壳管理左栏导航和品牌区域;host 与 feature 管理右侧内容。
3
+ * 框架页来自 FrameworkFeature[],贡献方的页来自 manifest;只展示可用 feature 与 availability 为 active 的页,不内置具体 World 或 bot 名称。
4
+ * 外壳不探测整站可达性,单个接口失败不代表整站状态。DOM 复用 ConsoleUi 与现存样式,窄屏折叠交给 CSS。
5
+ */
6
+
7
+ import { get, post, setConfig } from '../core/api.ts';
8
+ import { buildHash, type Route, type Router } from '../core/router.ts';
9
+ import { featureAvailable, type FrameworkFeature } from '../features/feature.ts';
10
+ import { PROVIDER_ROUTE } from '../console-pages/host.ts';
11
+ import { icon, wordmark, type ConsoleIconName } from '../ui/icons.ts';
12
+ import { lampRow, paintLamps } from '../ui/lamp.ts';
13
+ import type { ConsoleUi } from '../../shared/client-panel.ts';
14
+ import type { ConsoleLamp, ConsolePageManifest } from '../../shared/console-protocol.ts';
15
+ import { createAvatarControl } from './avatar.ts';
16
+ import { S } from './strings.ts';
17
+
18
+ const DEFAULT_BRAND = 'bot';
19
+ const FRAMEWORK_NAME = 'Cortico';
20
+
21
+ const GROUP_PERSONAS = S.groupPersonas;
22
+ const GROUP_WORLDS = S.groupWorlds;
23
+
24
+
25
+ export interface ShellDeps {
26
+ doc: Document;
27
+ /** UI 原语。DOM 只经它造,外壳不手搓 class。 */
28
+ ui: ConsoleUi;
29
+ /** 跳转经它——`location.hash` 只有 router 能写。 */
30
+ router: Router;
31
+ /** 控制台自己的页面。顺序即左栏顺序。 */
32
+ features?: readonly FrameworkFeature[];
33
+ /** 框架级表面的挂载情况。晚到就先传空,之后 `setCapabilities` 补。 */
34
+ capabilities?: Record<string, boolean>;
35
+ /** 控制台页清单(`/api/console/manifest`)。晚到就之后 `setPages` 补。 */
36
+ pages?: readonly ConsolePageManifest[];
37
+ /** 外壳的生命周期。abort 等同于 `dispose()`。 */
38
+ signal?: AbortSignal;
39
+ onError?(err: unknown): void;
40
+ }
41
+
42
+ export interface ConsoleShell {
43
+ /** 整个左栏(`#rail`)。调用方把它插进 `body` 的最前面。 */
44
+ readonly el: HTMLElement;
45
+ /** 当前路由变了。驱动高亮,不做别的。 */
46
+ setRoute(route: Route): void;
47
+ /** 展示名。空值退回中性缺省。 */
48
+ setBrand(name: string | null | undefined): void;
49
+ /** 能力清单到齐/变了,重排框架页那一段。 */
50
+ setCapabilities(capabilities: Record<string, boolean>): void;
51
+ /** manifest 到齐/变了,重排贡献方那一段。 */
52
+ setPages(pages: readonly ConsolePageManifest[]): void;
53
+ /**
54
+ * 灯的新读数(`/api/console/lamps` 的一拍)。只改灯,不碰导航结构——
55
+ * 这是每秒两次的调用,重排一次 DOM 就是每秒两次重排。
56
+ */
57
+ setLamps(lamps: Record<string, ConsoleLamp[]>): void;
58
+ dispose(): void;
59
+ }
60
+
61
+ /**
62
+ * 一条导航项。`segments` 既用来跳转,也用来判高亮;`lamp` 是那颗灯的节点
63
+ * (灯变了只改它的 class 与 title,不重建导航——那是每秒两次的操作)。
64
+ */
65
+ interface NavEntry {
66
+ el: HTMLElement;
67
+ segments: readonly string[];
68
+ /** 贡献方那一行才有:灯按这个 id 对号入座。 */
69
+ pageId?: string;
70
+ /** 那一排灯的容器(灯变了只改它,不重建导航)。 */
71
+ lamps?: HTMLSpanElement;
72
+ }
73
+
74
+ /**
75
+ * 这一项是不是当前所在处。判据是**前缀匹配**:框架页只有一段(`['usage']`),
76
+ * 贡献方的页有两段(`['provider','world:sample']`),同一条规则两边都成立,
77
+ * 而且页内子页签(第三段)变了不会让高亮掉下来。
78
+ */
79
+ function matches(entry: NavEntry, route: Route | null): boolean {
80
+ if (!route) return false;
81
+ return entry.segments.every((s, i) => route.segments[i] === s);
82
+ }
83
+
84
+ export function createShell(deps: ShellDeps): ConsoleShell {
85
+ const { doc, ui, router } = deps;
86
+
87
+ /** 外壳自己的账本。`dispose()` 与传进来的 signal 都收敛到这里。 */
88
+ const life = new AbortController();
89
+ const signal = life.signal;
90
+ if (deps.signal) {
91
+ if (deps.signal.aborted) life.abort();
92
+ else deps.signal.addEventListener('abort', () => life.abort(), { once: true, signal });
93
+ }
94
+
95
+ const onError = deps.onError ?? ((): void => {});
96
+
97
+ const features: readonly FrameworkFeature[] = deps.features ?? [];
98
+ let capabilities: Record<string, boolean> = deps.capabilities ?? {};
99
+ let consolePages: readonly ConsolePageManifest[] = deps.pages ?? [];
100
+ let route: Route | null = null;
101
+ let entries: NavEntry[] = [];
102
+ /**
103
+ * 灯的当茬读数。`setLamps` 写它,导航重排时再照它把灯补回去——否则一次
104
+ * `setPages` 会让所有灯退回 manifest 那一帧的旧值。
105
+ */
106
+ let lamps: Record<string, ConsoleLamp[]> = {};
107
+ /**
108
+ * 每次重排换一茬监听。挂在外壳总 signal 上的话,反复 `setPages` 会在
109
+ * 已经摘掉的节点上攒下一堆永不触发、也永不回收的监听。
110
+ */
111
+ let navLife: AbortController | null = null;
112
+ // 外壳一停,当茬导航的监听跟着停(`dispose()` 之外,传进来的 signal 也走这条路)。
113
+ signal.addEventListener('abort', () => navLife?.abort(), { once: true });
114
+
115
+ // ---- 骨架 -------------------------------------------------------------
116
+
117
+ const el = ui.h('div');
118
+ el.id = 'rail';
119
+
120
+ const brand = ui.h('div', 'brand');
121
+ brand.setAttribute('role', 'img');
122
+ brand.setAttribute('aria-label', FRAMEWORK_NAME);
123
+ brand.appendChild(wordmark(doc));
124
+
125
+ const nav = ui.h('nav', 'stack');
126
+ nav.setAttribute('aria-label', S.navAria);
127
+
128
+ const foot = ui.h('div', 'railfoot');
129
+ const avatar = createAvatarControl({ doc, ui, signal, onError });
130
+ const botName = ui.h('button', 'rail-name', DEFAULT_BRAND);
131
+ botName.type = 'button';
132
+ const who = ui.h('div', 'rail-who');
133
+ who.append(avatar.el, botName);
134
+ const footActions = ui.h('div', 'rail-actions');
135
+ const runButton = ui.h('button', 'rail-action rail-run');
136
+ runButton.type = 'button';
137
+ const shutdownButton = ui.h('button', 'rail-action rail-shutdown');
138
+ shutdownButton.type = 'button';
139
+ shutdownButton.appendChild(icon(doc, 'power'));
140
+ // 重启 = 同一套收尾 + 退出前落重启标志,启动器循环把进程拉起来。
141
+ const restartButton = ui.h('button', 'rail-action rail-restart');
142
+ restartButton.type = 'button';
143
+ restartButton.appendChild(icon(doc, 'refresh'));
144
+ const settingsButton = ui.h('button', 'rail-action');
145
+ settingsButton.type = 'button';
146
+ settingsButton.setAttribute('aria-label', S.settingsAria);
147
+ settingsButton.title = S.settingsTitle;
148
+ settingsButton.appendChild(icon(doc, 'settings'));
149
+ footActions.append(runButton, settingsButton, restartButton, shutdownButton);
150
+ foot.append(who, footActions);
151
+
152
+ let paused = false;
153
+ let runPending = false;
154
+ const renderRun = (): void => {
155
+ const available = capabilities.run === true;
156
+ runButton.disabled = !available || runPending;
157
+ runButton.replaceChildren(icon(doc, paused ? 'play' : 'pause'));
158
+ runButton.setAttribute('aria-label', paused ? S.runResume : S.runPause);
159
+ runButton.title = available ? (paused ? S.runResume : S.runPause) : S.runUnavailable;
160
+ runButton.classList.toggle('paused', paused);
161
+ };
162
+ renderRun();
163
+
164
+ /** 关机和重启共用请求锁,等待请求完成后释放。 */
165
+ let shuttingDown = false;
166
+ const renderPower = (): void => {
167
+ const canShutdown = capabilities.shutdown === true;
168
+ shutdownButton.hidden = !canShutdown;
169
+ shutdownButton.disabled = !canShutdown || shuttingDown;
170
+ shutdownButton.setAttribute('aria-label', S.shutdownAria);
171
+ shutdownButton.title = shuttingDown ? S.finishing : S.shutdownTitle;
172
+ const canRestart = capabilities.restart === true;
173
+ restartButton.hidden = !canRestart;
174
+ restartButton.disabled = !canRestart || shuttingDown;
175
+ restartButton.setAttribute('aria-label', S.restartAria);
176
+ restartButton.title = shuttingDown
177
+ ? S.finishing
178
+ : capabilities.supervised === true
179
+ ? S.restartTitleSupervised
180
+ : S.restartTitleUnsupervised;
181
+ };
182
+ renderPower();
183
+
184
+ const powerAction = async (kind: 'shutdown' | 'restart'): Promise<void> => {
185
+ if (shuttingDown) return;
186
+ if (kind === 'shutdown' ? capabilities.shutdown !== true : capabilities.restart !== true) return;
187
+ const supervised = capabilities.supervised === true;
188
+ const first = await ui.confirm(kind === 'shutdown'
189
+ ? {
190
+ title: S.confirmShutdownTitle,
191
+ body: S.shutdownBody,
192
+ danger: true,
193
+ }
194
+ : {
195
+ title: S.confirmRestartTitle,
196
+ body: supervised ? S.restartSupervisedNote : S.restartUnsupervisedNote,
197
+ danger: true,
198
+ });
199
+ if (!first || signal.aborted) return;
200
+ const second = await ui.confirm(kind === 'shutdown'
201
+ ? {
202
+ title: S.confirmShutdownAgainTitle,
203
+ body: S.confirmShutdownAgainBody,
204
+ danger: true,
205
+ }
206
+ : {
207
+ title: S.confirmRestartAgainTitle,
208
+ body: supervised ? S.confirmRestartAgainSupervised : S.confirmRestartAgainUnsupervised,
209
+ danger: true,
210
+ });
211
+ if (!second || signal.aborted) return;
212
+ shuttingDown = true;
213
+ renderPower();
214
+ const hold = ui.toast(S.finishingToast);
215
+ try {
216
+ const out = await post<{
217
+ ok?: boolean; localComplete?: boolean; result?: string; error?: string;
218
+ steps?: Array<{ label: string; ok: boolean; ms: number; detail?: string }>;
219
+ externalChecks?: Array<{ status: 'verified-ended' | 'still-live' | 'unknown' }>;
220
+ }>(kind === 'shutdown' ? '/api/run/shutdown' : '/api/run/restart', undefined, { signal });
221
+ if (out?.error) throw new Error(out.error);
222
+ const steps = out?.steps ?? [];
223
+ const localComplete = out?.localComplete ?? steps.every((step) => step.ok);
224
+ const externalUnverified = (out?.externalChecks ?? [])
225
+ .some((check) => check.status !== 'verified-ended');
226
+ const lines = steps.map((s) =>
227
+ `${s.ok ? '✓' : '✗'} ${s.label} · ${(s.ms / 1000).toFixed(1)}s${s.ok ? '' : ` — ${s.detail ?? S.stepIncomplete}`}`);
228
+ const done = kind === 'shutdown' ? S.doneShutdown : supervised ? S.doneRestartSupervised : S.doneRestart;
229
+ void ui.confirm({
230
+ title: !localComplete
231
+ ? S.resultLocalIncomplete
232
+ : externalUnverified
233
+ ? S.resultExternalUnverified
234
+ : out?.ok === false ? S.resultUnverified : done,
235
+ body: [out?.result ?? S.resultDefault, '', ...lines].join('\n'),
236
+ });
237
+ } catch (err) {
238
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
239
+ // 连接在收尾途中断了是**预期之一**:进程退出得比回执快时就会这样。
240
+ // 所以不说"失败",只说不知道 —— 逐步明细在 runlog 的 shutdown 条目里。
241
+ onError(err);
242
+ ui.toast(S.noReceipt(String((err as Error)?.message ?? err)), 'bad');
243
+ } finally {
244
+ hold.dispose();
245
+ shuttingDown = false;
246
+ renderPower();
247
+ }
248
+ };
249
+ shutdownButton.addEventListener('click', () => { void powerAction('shutdown'); }, { signal });
250
+ restartButton.addEventListener('click', () => { void powerAction('restart'); }, { signal });
251
+
252
+ runButton.addEventListener('click', () => {
253
+ if (runPending || capabilities.run !== true) return;
254
+ runPending = true;
255
+ renderRun();
256
+ void post<{ paused?: boolean; result?: string }>(
257
+ paused ? '/api/run/resume' : '/api/run/pause',
258
+ undefined,
259
+ { signal },
260
+ ).then((out) => {
261
+ paused = out.paused === true;
262
+ if (out.result) ui.toast(out.result);
263
+ }).catch((err: unknown) => {
264
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
265
+ onError(err);
266
+ ui.toast(err instanceof Error ? err.message : String(err), 'bad');
267
+ }).finally(() => {
268
+ runPending = false;
269
+ renderRun();
270
+ });
271
+ }, { signal });
272
+ settingsButton.addEventListener('click', () => {
273
+ try { router.navigate(['settings']); } catch (err) { onError(err); }
274
+ }, { signal });
275
+
276
+ el.append(brand, nav, foot);
277
+
278
+ // ---- 导航 -------------------------------------------------------------
279
+
280
+ /** 某一页此刻该点哪盏灯。轮询的读数优先于 manifest 那一帧。 */
281
+ const lampsFor = (pageId: string): readonly ConsoleLamp[] =>
282
+ lamps[pageId] ?? consolePages.find((p) => p.id === pageId)?.lamps ?? [];
283
+
284
+ const addItem = (
285
+ parent: HTMLElement,
286
+ opts: {
287
+ label: string;
288
+ icon?: ConsoleIconName;
289
+ /** 带灯的行(贡献方的页)。给了就画灯,`null` 是"还没报"的空位。 */
290
+ pageId?: string;
291
+ segments: readonly string[];
292
+ },
293
+ itemSignal: AbortSignal,
294
+ ): NavEntry => {
295
+ const href = buildHash(opts.segments);
296
+ const item = ui.h('a', 'navitem');
297
+ item.href = href;
298
+ if (opts.icon) item.appendChild(icon(doc, opts.icon, 'navicon'));
299
+ const label = ui.h('span', 'lbl', opts.label);
300
+ // 名字给灯让位后可能被截断,悬停仍读得到全名(灯自己的说明挂在各自那颗上)。
301
+ if (opts.pageId) label.title = opts.label;
302
+ item.appendChild(label);
303
+ const lampHost = opts.pageId ? lampRow(doc, lampsFor(opts.pageId)) : undefined;
304
+ if (lampHost) item.appendChild(lampHost);
305
+ // 修饰键与中键保留浏览器默认行为;普通左键经 router 导航。
306
+ item.addEventListener('click', (ev) => {
307
+ if (ev.button !== 0) return;
308
+ if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
309
+ ev.preventDefault();
310
+ try {
311
+ router.navigate(opts.segments);
312
+ } catch (err) {
313
+ onError(err);
314
+ }
315
+ }, { signal: itemSignal });
316
+ parent.appendChild(item);
317
+ const entry: NavEntry = {
318
+ el: item,
319
+ segments: opts.segments,
320
+ ...(opts.pageId && lampHost ? { pageId: opts.pageId, lamps: lampHost } : {}),
321
+ };
322
+ entries.push(entry);
323
+ return entry;
324
+ };
325
+
326
+ const addGroup = (label: string, kind: 'framework' | 'persona'): HTMLDivElement => {
327
+ // CSS class 仍写 `navgroup-provider`:样式表与构建产物同步,不随类型改名。
328
+ const pageClass = kind === 'framework' ? '' : ' navgroup-provider';
329
+ const group = ui.h('div', `navgroup navgroup-${kind}${pageClass}`);
330
+ group.setAttribute('role', 'group');
331
+ group.setAttribute('aria-label', label);
332
+ group.appendChild(ui.h('div', 'stacklabel', label));
333
+ nav.appendChild(group);
334
+ return group;
335
+ };
336
+
337
+ const addPrimary = (label: string): HTMLDivElement => {
338
+ const group = ui.h('div', 'navgroup navgroup-framework navgroup-primary');
339
+ group.setAttribute('role', 'group');
340
+ group.setAttribute('aria-label', label);
341
+ nav.appendChild(group);
342
+ return group;
343
+ };
344
+
345
+ const renderNav = (): void => {
346
+ navLife?.abort();
347
+ if (signal.aborted) return;
348
+ navLife = new AbortController();
349
+ const itemSignal = navLife.signal;
350
+
351
+ entries = [];
352
+ nav.replaceChildren();
353
+
354
+ const pages = features.filter((f) => featureAvailable(f, capabilities));
355
+ const worldRoots = pages.filter((f) => f.navMode === 'world-root');
356
+ const personaTail = pages.filter((f) => f.navMode === 'persona');
357
+ const groups = new Map<string, HTMLElement>();
358
+ for (const f of pages) {
359
+ if (f.navMode === undefined || f.navMode === 'group') {
360
+ const label = f.navGroup;
361
+ let group = groups.get(label);
362
+ if (!group) {
363
+ group = addGroup(label, 'framework');
364
+ groups.set(label, group);
365
+ }
366
+ addItem(group, {
367
+ label: f.label,
368
+ icon: f.icon,
369
+ ...(f.lampId ? { pageId: f.lampId } : {}),
370
+ segments: [f.route],
371
+ }, itemSignal);
372
+ } else if (f.navMode === 'primary') {
373
+ addItem(addPrimary(f.label), {
374
+ label: f.label,
375
+ icon: f.icon,
376
+ ...(f.lampId ? { pageId: f.lampId } : {}),
377
+ segments: [f.route],
378
+ }, itemSignal);
379
+ }
380
+ }
381
+
382
+ // 只列已装配的:左栏是"能去的地方",不是全量清单。未装配的仍由各自的
383
+ // 总览页负责露面(那里才有"为什么没装上"的位置)。供应模块(kind `llm`)不在
384
+ // 这里逐个列出:它们的入口是框架的「语言模型」页,模块清单是那一页里的次级菜单。
385
+ const listed = consolePages.filter((p) => p.availability === 'active');
386
+ // Persona 页在前,它的 Memory 页跟在后面,再是归这一组的框架页(系统提示词),同一组。
387
+ const personas = [
388
+ ...listed.filter((page) => page.kind === 'persona'),
389
+ ...listed.filter((page) => page.kind === 'memory'),
390
+ ];
391
+ if (personas.length || personaTail.length) {
392
+ const group = addGroup(GROUP_PERSONAS, 'persona');
393
+ for (const p of personas) {
394
+ addItem(group, {
395
+ label: p.label || p.id,
396
+ icon: p.kind === 'memory' ? 'folder-open' : 'bot',
397
+ pageId: p.id,
398
+ segments: [PROVIDER_ROUTE, p.id],
399
+ }, itemSignal);
400
+ }
401
+ for (const f of personaTail) {
402
+ addItem(group, { label: f.label, icon: f.icon, segments: [f.route] }, itemSignal);
403
+ }
404
+ }
405
+
406
+ const worlds = listed.filter((page) => page.kind === 'world');
407
+ if (worldRoots.length || worlds.length) {
408
+ const group = ui.h('div', 'navgroup navgroup-world-tree');
409
+ group.setAttribute('role', 'group');
410
+ group.setAttribute('aria-label', GROUP_WORLDS);
411
+ group.appendChild(ui.h('div', 'stacklabel', GROUP_WORLDS));
412
+ nav.appendChild(group);
413
+ for (const f of worldRoots) {
414
+ addItem(group, {
415
+ label: f.label,
416
+ icon: f.icon,
417
+ segments: [f.route],
418
+ }, itemSignal);
419
+ }
420
+ if (worlds.length) {
421
+ const list = ui.h('div', 'navmodule-list');
422
+ list.setAttribute('role', 'group');
423
+ list.setAttribute('aria-label', S.moduleInstancesAria);
424
+ group.appendChild(list);
425
+ for (const p of worlds) {
426
+ addItem(list, {
427
+ label: p.label || p.id,
428
+ pageId: p.id,
429
+ segments: [PROVIDER_ROUTE, p.id],
430
+ }, itemSignal);
431
+ }
432
+ }
433
+ }
434
+
435
+ applyRoute();
436
+ };
437
+
438
+ const applyRoute = (): void => {
439
+ for (const entry of entries) {
440
+ const on = matches(entry, route);
441
+ entry.el.classList.toggle('active', on);
442
+ if (on) entry.el.setAttribute('aria-current', 'page');
443
+ else entry.el.removeAttribute('aria-current');
444
+ }
445
+ const settingsOn = route?.segments[0] === 'settings';
446
+ settingsButton.classList.toggle('active', settingsOn);
447
+ if (settingsOn) settingsButton.setAttribute('aria-current', 'page');
448
+ else settingsButton.removeAttribute('aria-current');
449
+ };
450
+
451
+ // ---- bot 实例名 -------------------------------------------------------
452
+
453
+ /** 当前展示名;改名时拿它作输入框的初值。 */
454
+ let shownName = DEFAULT_BRAND;
455
+
456
+ const setBrand = (name: string | null | undefined): void => {
457
+ const shown = typeof name === 'string' && name.trim() !== '' ? name.trim() : DEFAULT_BRAND;
458
+ shownName = shown;
459
+ botName.textContent = shown;
460
+ botName.title = S.renameTitle;
461
+ avatar.setLabel(shown);
462
+ doc.title = S.docTitle(shown);
463
+ };
464
+
465
+ /**
466
+ * 就地改名。写的是 core 配置里的 `displayName`,与配置页上那一项同一个值。
467
+ * Esc 放弃,回车或失焦提交;名字没变或清空就当放弃。
468
+ */
469
+ const startRename = (): void => {
470
+ const field = ui.input({ value: shownName });
471
+ field.className = 'field rail-rename';
472
+ botName.replaceWith(field);
473
+ field.focus();
474
+ field.select();
475
+ let done = false;
476
+ const finish = (save: boolean): void => {
477
+ if (done) return;
478
+ done = true;
479
+ const next = field.value.trim();
480
+ field.replaceWith(botName);
481
+ if (!save || !next || next === shownName) return;
482
+ setBrand(next);
483
+ void setConfig('core', { displayName: next }).then(
484
+ () => ui.toast(S.renameSaved),
485
+ (err) => {
486
+ ui.toast(S.renameFailed(String((err as Error)?.message ?? err)), 'bad');
487
+ onError(err);
488
+ },
489
+ );
490
+ };
491
+ field.addEventListener('keydown', (event) => {
492
+ if (event.key === 'Enter') finish(true);
493
+ else if (event.key === 'Escape') finish(false);
494
+ }, { signal });
495
+ field.addEventListener('blur', () => finish(true), { signal });
496
+ };
497
+ botName.addEventListener('click', startRename, { signal });
498
+ botName.title = S.renameTitle;
499
+
500
+ /**
501
+ * 展示名来自部署配置。**取不到只影响这一个字**:不改状态灯、不报错卡——
502
+ * 外壳没有"整站是否可达"的判据,一个接口的失败也不该被当成那个判据。
503
+ */
504
+ void (async () => {
505
+ try {
506
+ const st = await get<{
507
+ displayName?: unknown;
508
+ loop?: { paused?: unknown };
509
+ } | null>('/api/status', { signal });
510
+ if (signal.aborted) return;
511
+ const name = st?.displayName;
512
+ if (typeof name === 'string' && name.trim() !== '') setBrand(name);
513
+ paused = st?.loop?.paused === true;
514
+ renderRun();
515
+ } catch { /* 留中性缺省 */ }
516
+ })();
517
+
518
+ renderNav();
519
+
520
+ return {
521
+ el,
522
+ setRoute(next: Route): void {
523
+ route = next;
524
+ applyRoute();
525
+ },
526
+ setBrand,
527
+ setCapabilities(next: Record<string, boolean>): void {
528
+ capabilities = next;
529
+ renderRun();
530
+ renderPower();
531
+ renderNav();
532
+ },
533
+ setPages(next: readonly ConsolePageManifest[]): void {
534
+ consolePages = next;
535
+ renderNav();
536
+ },
537
+ setLamps(next: Record<string, ConsoleLamp[]>): void {
538
+ lamps = next;
539
+ for (const entry of entries) {
540
+ if (entry.lamps && entry.pageId) paintLamps(entry.lamps, lampsFor(entry.pageId));
541
+ }
542
+ },
543
+ dispose(): void {
544
+ navLife?.abort();
545
+ life.abort();
546
+ el.remove();
547
+ },
548
+ };
549
+ }
@@ -0,0 +1,89 @@
1
+ import { pick } from '../core/language.ts';
2
+
3
+ const zh = {
4
+ groupPersonas: 'Persona & Memory',
5
+ groupWorlds: 'World',
6
+ moduleInstancesAria: 'World 实例',
7
+ navAria: '控制台导航',
8
+ runResume: '继续运行',
9
+ runPause: '暂停运行',
10
+ runUnavailable: '运行控制不可用',
11
+ shutdownAria: '关机',
12
+ settingsAria: '设置',
13
+ settingsTitle: '设置',
14
+ finishing: '正在关闭…',
15
+ shutdownTitle: '关机',
16
+ restartAria: '重启',
17
+ restartTitleSupervised: '重启',
18
+ restartTitleUnsupervised: '退出后需手动启动',
19
+ shutdownBody: '进程将退出,控制台连接将断开。',
20
+ confirmShutdownTitle: '⚠ 关机',
21
+ confirmRestartTitle: '⚠ 重启',
22
+ restartSupervisedNote: '进程将重启。',
23
+ restartUnsupervisedNote: '进程将退出,需要手动重新启动。',
24
+ confirmShutdownAgainTitle: '⚠⚠ 确认关机?',
25
+ confirmShutdownAgainBody: '停止运行并退出进程?',
26
+ confirmRestartAgainTitle: '⚠⚠ 确认重启?',
27
+ confirmRestartAgainSupervised: '退出并重新启动进程?',
28
+ confirmRestartAgainUnsupervised: '退出进程?退出后需手动启动。',
29
+ finishingToast: '正在关闭…',
30
+ stepIncomplete: '未完成',
31
+ doneShutdown: '已关机',
32
+ doneRestartSupervised: '已退出,等待重新启动',
33
+ doneRestart: '已退出',
34
+ resultLocalIncomplete: '本地关闭步骤未全部完成',
35
+ resultExternalUnverified: '本地已关机,外部状态未确认',
36
+ resultUnverified: '关闭步骤已执行,状态未确认',
37
+ resultDefault: '关闭步骤已执行。',
38
+ noReceipt: (msg: string) =>
39
+ `未收到关闭结果(${msg});进程可能已退出。详情见当前运行的 data/runs/<run>/log.jsonl 中的 shutdown 记录。`,
40
+ docTitle: (name: string) => `控制台 · ${name}`,
41
+ renameTitle: '点击改名',
42
+ renameSaved: '展示名已保存',
43
+ renameFailed: (msg: string) => `展示名没保存成:${msg}`,
44
+ };
45
+
46
+ const en: typeof zh = {
47
+ groupPersonas: 'Persona & Memory',
48
+ groupWorlds: 'World',
49
+ moduleInstancesAria: 'World instances',
50
+ navAria: 'Console navigation',
51
+ runResume: 'Resume',
52
+ runPause: 'Pause',
53
+ runUnavailable: 'Run control unavailable',
54
+ shutdownAria: 'Shut down',
55
+ settingsAria: 'Settings',
56
+ settingsTitle: 'Settings',
57
+ finishing: 'Shutting down…',
58
+ shutdownTitle: 'Shut down',
59
+ restartAria: 'Restart',
60
+ restartTitleSupervised: 'Restart',
61
+ restartTitleUnsupervised: 'Manual startup required after exit',
62
+ shutdownBody: 'The process will exit and disconnect the console.',
63
+ confirmShutdownTitle: '⚠ Shut down',
64
+ confirmRestartTitle: '⚠ Restart',
65
+ restartSupervisedNote: 'The process will restart.',
66
+ restartUnsupervisedNote: 'The process will exit and must be restarted manually.',
67
+ confirmShutdownAgainTitle: '⚠⚠ Confirm shutdown?',
68
+ confirmShutdownAgainBody: 'Stop running and exit the process?',
69
+ confirmRestartAgainTitle: '⚠⚠ Confirm restart?',
70
+ confirmRestartAgainSupervised: 'Exit and restart the process?',
71
+ confirmRestartAgainUnsupervised: 'Exit the process? You must start it manually afterwards.',
72
+ finishingToast: 'Shutting down…',
73
+ stepIncomplete: 'incomplete',
74
+ doneShutdown: 'Shut down',
75
+ doneRestartSupervised: 'Exited; waiting to restart',
76
+ doneRestart: 'Exited',
77
+ resultLocalIncomplete: 'Local shutdown steps incomplete',
78
+ resultExternalUnverified: 'Shut down locally; external state unverified',
79
+ resultUnverified: 'Shutdown steps executed; state unverified',
80
+ resultDefault: 'Shutdown steps executed.',
81
+ noReceipt: (msg: string) =>
82
+ `No shutdown result received (${msg}); the process may have exited. See the shutdown records in data/runs/<run>/log.jsonl for the current run.`,
83
+ docTitle: (name: string) => `Console · ${name}`,
84
+ renameTitle: 'Click to rename',
85
+ renameSaved: 'Display name saved',
86
+ renameFailed: (msg: string) => `Display name not saved: ${msg}`,
87
+ };
88
+
89
+ export const S = pick({ zh, en });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 独立页面主题交接。链接只携带点击瞬间的已解析颜色快照,接收方无需依赖控制台。
3
+ */
4
+
5
+ import type { ConsoleLink } from '../../shared/console-protocol.ts';
6
+ import {
7
+ isHexColor,
8
+ THEME_TOKENS,
9
+ type ThemeAppearance,
10
+ } from './registry.ts';
11
+
12
+ export const THEME_HANDOFF_FRAGMENT_KEY = 'cortico-theme';
13
+
14
+ export interface ThemeHandoffSnapshot {
15
+ v: 1;
16
+ appearance: ThemeAppearance;
17
+ palette: Record<string, string>;
18
+ }
19
+
20
+ /** 无既有 fragment 的继承链接附加主题快照;其余链接逐字保持原值。 */
21
+ export function resolveConsoleLinkHref(doc: Document, link: ConsoleLink): string {
22
+ if (link.inheritTheme !== true) return link.href;
23
+ if (link.href.includes('#')) return link.href;
24
+
25
+ const appearance = doc.documentElement.getAttribute('data-color-mode');
26
+ if (appearance !== 'light' && appearance !== 'dark') return link.href;
27
+ const view = doc.defaultView;
28
+ if (!view) return link.href;
29
+
30
+ const computed = view.getComputedStyle(doc.documentElement);
31
+ const palette: Record<string, string> = {};
32
+ for (const token of THEME_TOKENS) {
33
+ const value = computed.getPropertyValue(`--${token.key}`).trim();
34
+ if (isHexColor(value)) palette[token.key] = value.toLowerCase();
35
+ }
36
+
37
+ const snapshot: ThemeHandoffSnapshot = { v: 1, appearance, palette };
38
+ const payload = btoa(JSON.stringify(snapshot))
39
+ .replaceAll('+', '-')
40
+ .replaceAll('/', '_')
41
+ .replace(/=+$/u, '');
42
+ return `${link.href}#${THEME_HANDOFF_FRAGMENT_KEY}=${payload}`;
43
+ }