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,164 @@
1
+ import { pick } from '../../core/language.ts';
2
+
3
+ const zh = {
4
+ navLabel: '扩展',
5
+ navGroup: 'Core',
6
+ introTitle: '扩展',
7
+ introDesc: '从 npm 安装第三方 World、LLM Provider 和 bot 包。安装、卸载后需重启进程才能生效。',
8
+
9
+ installedTitle: '已安装',
10
+ refresh: '↻ 刷新',
11
+ restartProcess: '重启进程',
12
+
13
+ stateLoaded: '已加载',
14
+ stateFailed: '加载失败',
15
+ statePendingRestart: '待重启',
16
+ stateRemoved: '已卸载,待重启',
17
+ stateIdle: '已装,本部署未用',
18
+
19
+ groupWorldDesc: '连接外部环境。',
20
+ groupProviderDesc: '连接模型服务。',
21
+ groupBotDesc: '提供 Persona 与 bot 装配。将包名填入 deployment.json 的 bot 字段;每个进程运行一个 bot。',
22
+ groupUnknownTitle: '未识别',
23
+ groupUnknownDesc: 'package.json 缺少有效的 cortico 声明。',
24
+
25
+ restartConfirmTitle: '重启进程?',
26
+ restartNoLoopTitle: '需要手动启动',
27
+ restartConfirmBody: '进程将重启。',
28
+ restartNoLoopBody: '进程将退出,需要手动重新启动。继续?',
29
+ finishingToast: '正在关闭…',
30
+ stepIncomplete: '未完成',
31
+ doneRestartSupervised: '已退出,等待重新启动',
32
+ doneRestart: '已退出',
33
+ resultDefault: '关闭步骤已执行。',
34
+ noReceipt: (msg: string) => `未收到关闭结果(${msg});进程可能已退出。`,
35
+
36
+ uninstallTitle: (name: string) => `卸载「${name}」?`,
37
+ uninstallBody: (name: string) => `从 extensions/ 卸载 ${name},重启进程后生效。`,
38
+ uninstalling: '正在卸载…',
39
+ uninstalled: '已卸载',
40
+ uninstallFailed: (msg: string) => `卸载失败: ${msg}`,
41
+ uninstall: '卸载',
42
+
43
+ installing: '正在安装…',
44
+ installed: '已安装',
45
+ installFailed: (msg: string) => `安装失败: ${msg}`,
46
+ installedRestartTitle: '已安装,现在重启进程加载它?',
47
+ installedRestartNote: '进程将重启。',
48
+ installedNoLoopNote: '进程将退出,需要手动重新启动。',
49
+
50
+ panelLoaded: '自定义面板已加载',
51
+ noteIdle: '当前部署未引用此扩展。',
52
+ noteConsoleMissing: '缺少浏览器构建产物。请在扩展目录构建,再重启进程。',
53
+
54
+ sumLoaded: (n: number) => `已加载 ${n}`,
55
+ sumPending: (n: number) => `待重启 ${n}`,
56
+ sumFailed: (n: number) => `加载失败 ${n}`,
57
+ noExtensions: '还没装任何扩展',
58
+ listLoadFailed: (msg: string) => `扩展清单加载失败: ${msg}`,
59
+ loading: '加载中…',
60
+
61
+ searchTitle: '从 npm 安装',
62
+ searchDesc: '扩展拥有与框架相同的文件和网络权限。',
63
+ searchPlaceholder: '关键字;留空列出全部',
64
+ search: '搜索',
65
+ searchKeywordNote: (keyword: string) => `列出 npm 上带 ${keyword} 关键字的包。`,
66
+ searching: '搜索中…',
67
+ noHits: '没有匹配的包',
68
+ hitCount: (n: number) => `${n} 个包`,
69
+ searchFailed: (msg: string) => `搜索失败: ${msg}`,
70
+ hitMeta: (version: string, downloads: number, publisher?: string) =>
71
+ `${version} · 月下载 ${downloads}${publisher ? ` · ${publisher}` : ''}`,
72
+ linkRepo: '仓库',
73
+ linkHome: '主页',
74
+ install: '安装',
75
+ alreadyInstalled: '已安装',
76
+
77
+ manualTitle: '手动安装',
78
+ manualDesc: '包名(可带 @版本)或含 package.json 的本机目录。本机目录通过链接安装。',
79
+ manualPlaceholder: '@scope/name@1.2.0 或 ../my-module',
80
+ manualEmpty: '先填包名或目录',
81
+ };
82
+
83
+ const en: typeof zh = {
84
+ navLabel: 'Extensions',
85
+ navGroup: 'Core',
86
+ introTitle: 'Extensions',
87
+ introDesc: 'Install third-party Worlds, LLM providers and bot packages from npm. '
88
+ + 'Installations and removals take effect after restarting the process.',
89
+
90
+ installedTitle: 'Installed',
91
+ refresh: '↻ Refresh',
92
+ restartProcess: 'Restart process',
93
+
94
+ stateLoaded: 'Loaded',
95
+ stateFailed: 'Load failed',
96
+ statePendingRestart: 'Restart pending',
97
+ stateRemoved: 'Uninstalled, restart pending',
98
+ stateIdle: 'Installed, unused by this deployment',
99
+
100
+ groupWorldDesc: 'Connects to an external environment.',
101
+ groupProviderDesc: 'Connects to a model service.',
102
+ groupBotDesc: 'Provides a Persona and bot assembly. Set the package name in the bot field of deployment.json; each process runs one bot.',
103
+ groupUnknownTitle: 'Unrecognised',
104
+ groupUnknownDesc: 'package.json has no valid cortico declaration.',
105
+
106
+ restartConfirmTitle: 'Restart the process?',
107
+ restartNoLoopTitle: 'Manual startup required',
108
+ restartConfirmBody: 'The process will restart.',
109
+ restartNoLoopBody: 'The process will exit and must be restarted manually. Continue?',
110
+ finishingToast: 'Shutting down…',
111
+ stepIncomplete: 'incomplete',
112
+ doneRestartSupervised: 'Exited; waiting to restart',
113
+ doneRestart: 'Exited',
114
+ resultDefault: 'Shutdown steps executed.',
115
+ noReceipt: (msg: string) => `No shutdown result received (${msg}); the process may have exited.`,
116
+
117
+ uninstallTitle: (name: string) => `Uninstall "${name}"?`,
118
+ uninstallBody: (name: string) => `Uninstall ${name} from extensions/. Takes effect after restarting the process.`,
119
+ uninstalling: 'Uninstalling…',
120
+ uninstalled: 'Uninstalled',
121
+ uninstallFailed: (msg: string) => `Uninstall failed: ${msg}`,
122
+ uninstall: 'Uninstall',
123
+
124
+ installing: 'Installing…',
125
+ installed: 'Installed',
126
+ installFailed: (msg: string) => `Install failed: ${msg}`,
127
+ installedRestartTitle: 'Installed. Restart the process now to load it?',
128
+ installedRestartNote: 'The process will restart.',
129
+ installedNoLoopNote: 'The process will exit and must be restarted manually.',
130
+
131
+ panelLoaded: 'Custom panel loaded',
132
+ noteIdle: 'This deployment does not reference the extension.',
133
+ noteConsoleMissing: 'Browser bundle missing. Build in the extension directory and restart the process.',
134
+
135
+ sumLoaded: (n: number) => `${n} loaded`,
136
+ sumPending: (n: number) => `${n} restart pending`,
137
+ sumFailed: (n: number) => `${n} failed`,
138
+ noExtensions: 'No extensions installed yet',
139
+ listLoadFailed: (msg: string) => `Extension list failed to load: ${msg}`,
140
+ loading: 'Loading…',
141
+
142
+ searchTitle: 'Install from npm',
143
+ searchDesc: 'Extensions have the same file and network access as the framework.',
144
+ searchPlaceholder: 'Keyword; empty lists everything',
145
+ search: 'Search',
146
+ searchKeywordNote: (keyword: string) => `Lists npm packages carrying the ${keyword} keyword.`,
147
+ searching: 'Searching…',
148
+ noHits: 'No matching packages',
149
+ hitCount: (n: number) => `${n} packages`,
150
+ searchFailed: (msg: string) => `Search failed: ${msg}`,
151
+ hitMeta: (version: string, downloads: number, publisher?: string) =>
152
+ `${version} · ${downloads}/month${publisher ? ` · ${publisher}` : ''}`,
153
+ linkRepo: 'Repository',
154
+ linkHome: 'Homepage',
155
+ install: 'Install',
156
+ alreadyInstalled: 'Installed',
157
+
158
+ manualTitle: 'Manual install',
159
+ manualDesc: 'A package name (optionally @version) or a local directory containing package.json. Local directories are installed as links.',
160
+ manualPlaceholder: '@scope/name@1.2.0 or ../my-module',
161
+ manualEmpty: 'Fill in a package name or directory first',
162
+ };
163
+
164
+ export const S = pick({ zh, en });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Framework Feature 是随控制台内核打包的页面,可调用框架端点。
3
+ * 控制台页扩展经 manifest 动态加载,使用仅开放本面板能力的 ConsolePanelContext。
4
+ * 两种上下文共享挂载生命周期:mount 创建 Lifecycle,离开时 abort 并 dispose。
5
+ */
6
+
7
+ import type { ConsoleUi, Disposable } from '../../shared/client-panel.ts';
8
+ import type { Lifecycle } from '../core/lifecycle.ts';
9
+ import type { Route, Router } from '../core/router.ts';
10
+ import type { ConsolePageHost } from '../console-pages/host.ts';
11
+ import type { ConsoleIconName } from '../ui/icons.ts';
12
+
13
+ /** 嵌在框架页里的控制台页宿主:manifest 与面板加载都由它管,页面只给容器与路由前缀。 */
14
+ export type EmbeddedConsolePageHost = Pick<ConsolePageHost, 'load' | 'show' | 'unmount' | 'pages' | 'find'>;
15
+
16
+ export interface FeatureContext {
17
+ /** 本页面的 DOM 根。离开时由 host 清空。 */
18
+ readonly root: HTMLElement;
19
+ /** 本次挂载的资源账本。`signal` / `interval` / `frame` / `own` 都从它拿。 */
20
+ readonly lifecycle: Lifecycle;
21
+ /** 等价于 `lifecycle.signal`,因为绝大多数用法只要这一个。 */
22
+ readonly signal: AbortSignal;
23
+ /** UI 原语,已绑本次挂载的 signal。 */
24
+ readonly ui: ConsoleUi;
25
+ /** 路由。跳转与离开拦截都经它,feature 不碰 `location.hash`。 */
26
+ readonly router: Router;
27
+ /** 进入本页时的路由(子页签靠 `segments[1]` 之类区分)。 */
28
+ readonly route: Route;
29
+ /**
30
+ * /api/capabilities 提供的框架能力挂载情况;未挂载能力不渲染。
31
+ */
32
+ readonly capabilities: Record<string, boolean>;
33
+ /** 出错上报。feature 不自己 `console.error`。 */
34
+ onError(err: unknown): void;
35
+ /**
36
+ * 在本页的某个容器里挂另一页的面板。`route` 给出面板页签该指向的路由,
37
+ * 所以页签切换留在本页内。只有内核装配的页面拿得到;缺席时页面只能给出入口链接。
38
+ */
39
+ consolePageHost?(opts: {
40
+ root: HTMLElement;
41
+ route(pageId: string, panelId: string): readonly string[];
42
+ }): EmbeddedConsolePageHost;
43
+ /**
44
+ * 重取控制台页清单并重排左栏,不动当前页。激活/停用 World 这类会改清单的
45
+ * 操作成功后调用。缺席时左栏保持旧清单。
46
+ */
47
+ refreshNav?(): Promise<void>;
48
+ }
49
+
50
+ export type FrameworkFeature = {
51
+ /**
52
+ * 路由第一段。`provider` **是保留字**(那一段归控制台页宿主),
53
+ * 其余由各 feature 认领,不得重复。
54
+ */
55
+ readonly route: string;
56
+ /** 左栏显示名。 */
57
+ readonly label: string;
58
+ /** 左栏图标。 World 那一页不走此字段。 */
59
+ readonly icon?: ConsoleIconName;
60
+ /** 状态灯表中的键;缺省时不声明状态灯。 */
61
+ readonly lampId?: string;
62
+ /** 列出的 capability 任一已挂载即显示导航项;省略或为空时始终显示。 */
63
+ readonly needsAny?: readonly string[];
64
+ /**
65
+ * 渲染。返回的 `Disposable` 在离开时被调用;用 `ctx.lifecycle` 登记过的
66
+ * 不必再返回。抛错只让这一页变成错误卡,不波及框架其余部分。
67
+ */
68
+ mount(ctx: FeatureContext): void | Disposable | Promise<void | Disposable>;
69
+ } & (
70
+ | {
71
+ /** 带标题的常规分组;省略 navMode 时同样按分组显示。 */
72
+ readonly navMode?: 'group';
73
+ /** 相同值的页面共享一个导航区。 */
74
+ readonly navGroup: string;
75
+ }
76
+ | {
77
+ /**
78
+ * primary 是独立一级入口;world-root 是 World 实例树入口;persona 排进 Persona & Memory 组,
79
+ * 在贡献的页之后;hidden 只保留路由。
80
+ */
81
+ readonly navMode: 'primary' | 'world-root' | 'persona' | 'hidden';
82
+ readonly navGroup?: string;
83
+ }
84
+ );
85
+
86
+ /** `needsAny` 里任一 capability 已挂载,或根本没有要求。 */
87
+ export function featureAvailable(
88
+ feature: FrameworkFeature,
89
+ capabilities: Record<string, boolean>,
90
+ ): boolean {
91
+ const wanted = feature.needsAny ?? [];
92
+ return wanted.length === 0 || wanted.some((k) => capabilities[k] === true);
93
+ }
@@ -0,0 +1,280 @@
1
+ /** 使用运行时 token 估算口径,将标准 Item 按显示类别汇总。 */
2
+
3
+ import { itemText, type ContextRecord } from '../../../../protocol/open-responses/context.ts';
4
+ import type { ConsoleUi } from '../../../shared/client-panel.ts';
5
+ import { estimateMessagesTokens } from '../../../../protocol/open-responses/tokens.ts';
6
+ import {
7
+ contextOf,
8
+ loopOf,
9
+ type StatusSnapshot,
10
+ type ToolSchemaDoc,
11
+ } from './protocol.ts';
12
+ import { S } from './strings.ts';
13
+
14
+ /**
15
+ * 字数 → token 估算。与服务端 `estimateTokens` 同一口径:中日韩与全角标点按 0.6,
16
+ * 其余按 0.3。**不是账单**,只求与截断判据同源。
17
+ */
18
+ export function estTok(text: unknown): number {
19
+ let cjk = 0;
20
+ let other = 0;
21
+ for (const ch of String(text ?? '')) {
22
+ const c = ch.codePointAt(0) ?? 0;
23
+ if ((c >= 0x4e00 && c <= 0x9fff) || (c >= 0x3000 && c <= 0x30ff) || (c >= 0xff00 && c <= 0xffef)) {
24
+ cjk++;
25
+ } else {
26
+ other++;
27
+ }
28
+ }
29
+ return Math.ceil(cjk * 0.6 + other * 0.3);
30
+ }
31
+
32
+ /** 一个 JSON 值折算成 token(工具参数、工具表 schema 都这么算)。 */
33
+ export function jsonTok(v: unknown): number {
34
+ return v ? estTok(JSON.stringify(v)) : 0;
35
+ }
36
+
37
+ /** 分类词表。`color` 直接取主题里的图表色变量,随主题走。 */
38
+ export interface ContextCategoryDef {
39
+ key: string;
40
+ group: string;
41
+ label: string;
42
+ color: string;
43
+ }
44
+
45
+ const CTX_CATS: readonly ContextCategoryDef[] = [
46
+ { key: 'orient', group: S.groupPrefix, label: S.catOrient, color: 'var(--chart-1)' },
47
+ { key: 'constitution', group: S.groupPrefix, label: S.catConstitution, color: 'var(--chart-5)' },
48
+ { key: 'env', group: S.groupPrefix, label: S.catEnv, color: 'var(--chart-miss)' },
49
+ { key: 'toolsUsage', group: S.groupPrefix, label: S.catToolsUsage, color: 'var(--chart-3)' },
50
+ { key: 'memory', group: S.groupPrefix, label: S.catMemory, color: 'var(--chart-6)' },
51
+ { key: 'prefixMisc', group: S.groupPrefix, label: S.catPrefixMisc, color: 'var(--chart-7)' },
52
+ { key: 'toolsSchema', group: S.groupTools, label: S.catToolsSchema, color: 'var(--chart-hit)' },
53
+ { key: 'head', group: S.groupDialogue, label: S.catHead, color: 'var(--chart-8)' },
54
+ { key: 'reasoning', group: S.groupDialogue, label: S.catReasoning, color: 'var(--chart-3)' },
55
+ { key: 'dialogue', group: S.groupDialogue, label: S.catDialogue, color: 'var(--chart-2)' },
56
+ { key: 'toolIO', group: S.groupDialogue, label: S.catToolIO, color: 'var(--chart-4)' },
57
+ ];
58
+
59
+ const CTX_GROUPS: readonly string[] = [S.groupPrefix, S.groupTools, S.groupDialogue];
60
+
61
+ /** 按 ━━━ 段名 ━━━ 拆分前缀;未匹配的文本归入前言。上下文与提示词页共用。 */
62
+ export function parsePrefixSegments(systemText: string): Record<string, string> {
63
+ const map: Record<string, string> = {};
64
+ if (!systemText) return map;
65
+ for (const part of systemText.split(/(?=━━━ .+? ━━━)/)) {
66
+ const m = part.match(/━━━ (.+?) ━━━/);
67
+ const title = m ? m[1].trim() : '(前言)';
68
+ map[title] = part.replace(/━━━ .+? ━━━/, '').trim();
69
+ }
70
+ return map;
71
+ }
72
+
73
+ /** 消息里第一条 system 的正文(没有就是空串)。 */
74
+ function systemTextOf(messages: readonly ContextRecord[]): string {
75
+ const first = messages[0];
76
+ return first?.item.type === 'message' && first.item.role === 'system' ? itemText(first.item) : '';
77
+ }
78
+
79
+ export interface ContextCategory extends ContextCategoryDef {
80
+ tok: number;
81
+ }
82
+
83
+ export interface ContextBreakdown {
84
+ /** 分类占用,按 core 报的总数等比缩放(上游只给总数,分类只能本地估) */
85
+ cats: ContextCategory[];
86
+ /** core 报的下一次请求输入数;没报时本地估算之和 */
87
+ total: number;
88
+ /** total 里上游数过的部分;0 = 整份估算 */
89
+ countedTokens: number;
90
+ /** 圈的分母:Persona的阶段预算,没报时模型物理上限;两者都没有则 null */
91
+ maxTokens: number | null;
92
+ /** 软预警线比例;Persona没报则 null(不画黄线) */
93
+ softRatio: number | null;
94
+ keepOn: boolean;
95
+ /** 摘除历史思维链省掉的量(只用于脚注) */
96
+ strippedThinking: number;
97
+ /** 工具表里有几个工具(脚注里印) */
98
+ toolCount: number;
99
+ }
100
+
101
+ export interface ContextInput {
102
+ messages: readonly ContextRecord[];
103
+ /** messages 之外的合成开头;估算包含其 reasoning,不应用历史思维链摘除规则。 */
104
+ head?: readonly ContextRecord[];
105
+ toolSchemas: readonly ToolSchemaDoc[];
106
+ status: StatusSnapshot | null;
107
+ }
108
+
109
+ /** 每条消息的结构开销,与服务端 `estimateMessagesTokens` 一致。 */
110
+ const PER_MESSAGE_OVERHEAD = 8;
111
+
112
+ /** 分类占用。没有消息时返回 null(调试通道还没连上,不是"占用为 0")。 */
113
+ export function computeCtx(input: ContextInput): ContextBreakdown | null {
114
+ const msgs = input.messages;
115
+ if (!msgs.length) return null;
116
+ const cx = contextOf(input.status);
117
+ const maxTokens = cx.maxTokens ?? cx.hardTokens ?? null;
118
+ const softRatio = cx.softRatio ?? null;
119
+ const keepOn = cx.keepPastThinking !== false;
120
+ const tok: Record<string, number> = {};
121
+ for (const c of CTX_CATS) tok[c.key] = 0;
122
+
123
+ // 1) 系统前缀按段拆;prefixMisc 兜住分隔线、前言与结构开销。
124
+ const sys = systemTextOf(msgs);
125
+ if (msgs[0].item.type === 'message' && msgs[0].item.role === 'system') {
126
+ const seg = parsePrefixSegments(sys);
127
+ tok.orient = estTok(seg['ORIENTATION'] || '');
128
+ tok.constitution = estTok(seg['宪法'] || '');
129
+ tok.toolsUsage = estTok(seg['Using your tools'] || '');
130
+ tok.memory = estTok(seg['记忆'] || '');
131
+ for (const k of Object.keys(seg)) if (/^环境/.test(k)) tok.env += estTok(seg[k]);
132
+ const sysTotal = PER_MESSAGE_OVERHEAD + estTok(sys);
133
+ tok.prefixMisc = Math.max(
134
+ 0,
135
+ sysTotal - tok.orient - tok.constitution - tok.env - tok.toolsUsage - tok.memory,
136
+ );
137
+ }
138
+
139
+ // 合成开头单独计入估算,包含其中的 reasoning。
140
+ for (const m of input.head ?? []) {
141
+ tok.head += estimateMessagesTokens([m]);
142
+ }
143
+
144
+ // 2) 工具表 schema:作为 tools 参数随每次调用发送,与消息分开计。
145
+ tok.toolsSchema = input.toolSchemas.reduce(
146
+ (s, t) => s + estTok(t.name) + estTok(t.description) + jsonTok(t.parameters),
147
+ 0,
148
+ );
149
+
150
+ const first = msgs[0].item;
151
+ const rest = first.type === 'message' && first.role === 'system' ? msgs.slice(1) : msgs;
152
+ let strippedThinking = 0;
153
+ for (const entry of rest) {
154
+ const tokens = estimateMessagesTokens([entry]);
155
+ if (entry.context.head) tok.head += tokens;
156
+ else if (entry.item.type === 'reasoning') {
157
+ if (keepOn) tok.reasoning += tokens;
158
+ else strippedThinking += tokens;
159
+ } else if (entry.item.type === 'function_call' || entry.item.type === 'function_call_output') {
160
+ tok.toolIO += tokens;
161
+ } else tok.dialogue += tokens;
162
+ }
163
+
164
+ const local = CTX_CATS.map((c) => ({ ...c, tok: tok[c.key] }));
165
+ const localTotal = local.reduce((s, c) => s + c.tok, 0);
166
+ const reported = loopOf(input.status).estTokens;
167
+ const total = typeof reported === 'number' ? reported : localTotal;
168
+ const scale = localTotal > 0 ? total / localTotal : 0;
169
+ const cats: ContextCategory[] = local.map((c) => ({ ...c, tok: Math.round(c.tok * scale) }));
170
+ return {
171
+ cats,
172
+ total,
173
+ countedTokens: cx.countedTokens ?? 0,
174
+ maxTokens,
175
+ softRatio,
176
+ keepOn,
177
+ strippedThinking,
178
+ toolCount: input.toolSchemas.length,
179
+ };
180
+ }
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // 画面
184
+ // ---------------------------------------------------------------------------
185
+
186
+ /** 一行脚注:纯文本片段与要加粗的片段交替。 */
187
+ function footLine(ui: ConsoleUi, parts: readonly (string | { b: string })[]): HTMLDivElement {
188
+ const line = ui.h('div');
189
+ line.appendChild(ui.h('span', null, '· '));
190
+ for (const p of parts) {
191
+ if (typeof p === 'string') line.appendChild(ui.h('span', null, p));
192
+ else line.appendChild(ui.h('b', null, p.b));
193
+ }
194
+ return line;
195
+ }
196
+
197
+ /**
198
+ * 分类分布面板。定位由终端输入器里的 `.ctxanchor` 提供。
199
+ */
200
+ export function buildCtxPanel(ui: ConsoleUi, d: ContextBreakdown | null): HTMLElement {
201
+ const box = ui.h('div', 'ctxpanel');
202
+ if (!d || !d.total) {
203
+ box.appendChild(ui.h('div', 'cx-sub', S.ctxNone));
204
+ return box;
205
+ }
206
+ const head = ui.h('div', 'cx-head');
207
+ head.appendChild(ui.h('div', 'cx-title', S.ctxUsage));
208
+ const total = ui.h('div', 'cx-total');
209
+ total.appendChild(ui.h('b', null, `${d.countedTokens > 0 ? '' : '~'}${ui.fmt.count(d.total)}`));
210
+ total.appendChild(ui.h('span', null, d.maxTokens === null ? ' tok' : ` / ${ui.fmt.count(d.maxTokens)} tok`));
211
+ head.appendChild(total);
212
+ box.appendChild(head);
213
+ box.appendChild(
214
+ ui.h(
215
+ 'div',
216
+ 'cx-sub',
217
+ S.ctxSub(d.maxTokens === null ? null : ui.fmt.percent(d.total / d.maxTokens), d.keepOn),
218
+ ),
219
+ );
220
+
221
+ const bar = ui.h('div', 'cx-bar');
222
+ for (const c of d.cats) {
223
+ if (c.tok <= 0) continue;
224
+ const sp = ui.h('span');
225
+ sp.setAttribute('style', `width:${(c.tok / d.total) * 100}%;background:${c.color}`);
226
+ sp.title = `${c.label} · ${ui.fmt.count(c.tok)}`;
227
+ bar.appendChild(sp);
228
+ }
229
+ box.appendChild(bar);
230
+
231
+ for (const g of CTX_GROUPS) {
232
+ const inG = d.cats.filter((c) => c.group === g && c.tok > 0);
233
+ if (!inG.length) continue;
234
+ box.appendChild(ui.h('div', 'cx-grp', g));
235
+ for (const c of inG) {
236
+ const row = ui.h('div', 'cx-row');
237
+ const sw = ui.h('span', 'cx-sw');
238
+ sw.setAttribute('style', `background:${c.color}`);
239
+ row.appendChild(sw);
240
+ const lbl = ui.h('span', 'cx-lbl', c.label);
241
+ if (c.key === 'toolsSchema') lbl.appendChild(ui.h('span', 'cx-note', S.toolCount(d.toolCount)));
242
+ row.appendChild(lbl);
243
+ row.appendChild(ui.h('span', 'cx-pctcol', ui.fmt.percent(c.tok / d.total)));
244
+ row.appendChild(ui.h('span', 'cx-val', ui.fmt.count(c.tok)));
245
+ box.appendChild(row);
246
+ }
247
+ }
248
+
249
+ const foot = ui.h('div', 'cx-foot');
250
+ foot.appendChild(
251
+ footLine(
252
+ ui,
253
+ d.countedTokens > 0
254
+ ? [S.footCountedPre, { b: ui.fmt.count(d.countedTokens) }, S.footCountedPost]
255
+ : [S.footEstimated],
256
+ ),
257
+ );
258
+ foot.appendChild(footLine(ui, [{ b: S.footSchemaB }, S.footSchemaPost]));
259
+ if (!d.keepOn && d.strippedThinking > 0) {
260
+ foot.appendChild(
261
+ footLine(ui, [
262
+ S.footStrippedPre,
263
+ { b: ui.fmt.count(d.strippedThinking) },
264
+ S.footStrippedPost,
265
+ ]),
266
+ );
267
+ }
268
+ if (d.maxTokens !== null) {
269
+ foot.appendChild(
270
+ footLine(
271
+ ui,
272
+ d.softRatio === null
273
+ ? [S.footHardPre, { b: ui.fmt.count(d.maxTokens) }, S.footHardPost]
274
+ : [S.footSoftPre, { b: ui.fmt.count(Math.round(d.maxTokens * d.softRatio)) }, S.footSoftPost(ui.fmt.count(d.maxTokens))],
275
+ ),
276
+ );
277
+ }
278
+ box.appendChild(foot);
279
+ return box;
280
+ }
@@ -0,0 +1,156 @@
1
+ import type { ContextRecord } from '../../../../protocol/open-responses/context.ts';
2
+ /**
3
+ * fork 视图选择当前显示的 session。主 session 使用调试推送;其他 session 通过 GET /api/sessions/messages?id= 轮询。
4
+ * 轮询受 lifecycle 管理,session 结束或离开视图时停止;内容变化才重画,并保留用户滚动位置。
5
+ */
6
+
7
+ import type { ConsoleUi, Disposable } from '../../../shared/client-panel.ts';
8
+ import { get } from '../../core/api.ts';
9
+ import type { Lifecycle } from '../../core/lifecycle.ts';
10
+ import type { SessionStat } from './protocol.ts';
11
+ import { S } from './strings.ts';
12
+ import type { TimelineView } from './timeline.ts';
13
+
14
+ /** 主 session 的固定 id 与显示名(服务端 `/ws/debug` 推的就是这一份)。 */
15
+ export const MAIN_ID = 'main';
16
+ export const MAIN_LABEL = S.mainLabel;
17
+
18
+ const FORK_POLL_MS = 3000;
19
+
20
+ /** 同一 Response 的多个 Item 可以交错增长。 */
21
+ function messageRevision(messages: readonly ContextRecord[]): string {
22
+ return JSON.stringify(messages);
23
+ }
24
+
25
+ export interface ForkDeps {
26
+ ui: ConsoleUi;
27
+ lifecycle: Lifecycle;
28
+ signal: AbortSignal;
29
+ timeline: TimelineView;
30
+ /** 主 session 的消息(切回主视图时重画用) */
31
+ mainMessages(): readonly ContextRecord[];
32
+ /** 主 session 的合成开头(切回主视图时把标注块画回来) */
33
+ mainHead?(): readonly ContextRecord[];
34
+ /** 当前 session 列表(判断 fork 是否已结束) */
35
+ sessions(): readonly SessionStat[];
36
+ /** 当前观察目标变了(卡条要重画选中态) */
37
+ onChange(): void;
38
+ onError(err: unknown): void;
39
+ }
40
+
41
+ export interface ForkView {
42
+ readonly id: string;
43
+ readonly label: string;
44
+ /** 切去看某个 session。`MAIN_ID` 回主视图。 */
45
+ switchTo(id: string, label: string): void;
46
+ /** 主 session 的消息变了(实时帧):在主视图时才需要动。 */
47
+ isMain(): boolean;
48
+ /** session 列表更新后调一次:目标已结束就停轮询。 */
49
+ noteSessions(): void;
50
+ }
51
+
52
+ export function createForkView(deps: ForkDeps): ForkView {
53
+ const { ui, timeline } = deps;
54
+ let id = MAIN_ID;
55
+ let label = MAIN_LABEL;
56
+ let lastRevision = '';
57
+ let poll: Disposable | null = null;
58
+
59
+ const stopPolling = (): void => {
60
+ poll?.dispose();
61
+ poll = null;
62
+ };
63
+
64
+ /** 目标 session 已结束就不必再轮。 */
65
+ const maybeStop = (): void => {
66
+ const st = deps.sessions().find((x) => x.id === id);
67
+ if (st && st.endedAt != null) stopPolling();
68
+ };
69
+
70
+ const banner = (count: number, estTokens: number): HTMLElement => {
71
+ const bar = ui.h('div', 'forkbanner');
72
+ const st = deps.sessions().find((x) => x.id === id);
73
+ const ended = !!st && st.endedAt != null;
74
+ const info = ui.h('span');
75
+ info.appendChild(ui.h('span', null, S.forkViewing));
76
+ info.appendChild(ui.h('b', null, label));
77
+ info.appendChild(ui.h('span', 'meta2', ` (${id})`));
78
+ info.appendChild(
79
+ ui.h(
80
+ 'span',
81
+ null,
82
+ S.forkInfo(count, ui.fmt.count(estTokens || 0), ended, Math.round(FORK_POLL_MS / 1000)),
83
+ ),
84
+ );
85
+ const back = ui.button(S.forkBack, {
86
+ size: 'sm',
87
+ onClick: () => view.switchTo(MAIN_ID, MAIN_LABEL),
88
+ });
89
+ bar.append(info, back);
90
+ return bar;
91
+ };
92
+
93
+ const refresh = async (force: boolean): Promise<void> => {
94
+ const asked = id;
95
+ if (asked === MAIN_ID) return;
96
+ try {
97
+ const data = await get<{ messages: ContextRecord[]; estTokens?: number }>(
98
+ `/api/sessions/messages?id=${encodeURIComponent(asked)}`,
99
+ { signal: deps.signal },
100
+ );
101
+ if (id !== asked) return; // 期间用户又切了,这份回应已经过期
102
+ const messages = Array.isArray(data?.messages) ? data.messages : [];
103
+ const revision = messageRevision(messages);
104
+ if (!force && revision === lastRevision) {
105
+ maybeStop();
106
+ return;
107
+ }
108
+ lastRevision = revision;
109
+ timeline.rebuild(messages, {
110
+ banner: banner(messages.length, data?.estTokens ?? 0),
111
+ empty: S.forkEmpty,
112
+ keepScroll: !force,
113
+ });
114
+ maybeStop();
115
+ } catch (err) {
116
+ if ((err as { name?: string } | null)?.name === 'AbortError') return; // 页面走了,不是失败
117
+ if (id !== asked) return;
118
+ deps.onError(err);
119
+ timeline.rebuild([], {
120
+ banner: banner(0, 0),
121
+ empty: S.forkFetchFailed(String((err as Error)?.message ?? err)),
122
+ });
123
+ stopPolling();
124
+ }
125
+ };
126
+
127
+ const view: ForkView = {
128
+ get id(): string {
129
+ return id;
130
+ },
131
+ get label(): string {
132
+ return label;
133
+ },
134
+ switchTo(next, nextLabel) {
135
+ if (id === next) return;
136
+ stopPolling();
137
+ id = next;
138
+ label = nextLabel;
139
+ lastRevision = '';
140
+ deps.onChange();
141
+ if (next === MAIN_ID) {
142
+ timeline.rebuild(deps.mainMessages(), { head: deps.mainHead?.() ?? null });
143
+ return;
144
+ }
145
+ void refresh(true);
146
+ poll = deps.lifecycle.interval(() => void refresh(false), FORK_POLL_MS);
147
+ },
148
+ isMain() {
149
+ return id === MAIN_ID;
150
+ },
151
+ noteSessions() {
152
+ maybeStop();
153
+ },
154
+ };
155
+ return view;
156
+ }