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,392 @@
1
+ import type { ContextRecord } from '../../../../protocol/open-responses/context.ts';
2
+ /**
3
+ * 页面状态由 /ws/debug 推送,保存在挂载闭包中,随 Lifecycle 释放。
4
+ * 调试通道不可用时改用 /ws/sessions,只接收 session 列表。
5
+ */
6
+
7
+ import type {
8
+ ConsoleStreamHandle,
9
+ Disposable,
10
+ } from '../../../shared/client-panel.ts';
11
+ import { PROVIDERS_LAMP_ID, panelStreamRoute } from '../../../shared/console-protocol.ts';
12
+ import type { FeatureContext, FrameworkFeature } from '../feature.ts';
13
+ import { get, post } from '../../core/api.ts';
14
+ import { openStream } from '../../core/stream.ts';
15
+ import { browserSocketEnv, openFrameworkSocket, type SocketEnv } from '../../core/websocket.ts';
16
+ import { buildCtxPanel, computeCtx, type ContextBreakdown } from './context.ts';
17
+ import { createForkView, MAIN_ID, MAIN_LABEL } from './fork.ts';
18
+ import {
19
+ arr,
20
+ str,
21
+ type SessionStat,
22
+ type StatusSnapshot,
23
+ type ToolSchemaDoc,
24
+ } from './protocol.ts';
25
+ import { subscribeLamps } from '../../ui/lamp.ts';
26
+ import { createOnboarding, type OnboardingView } from './onboarding.ts';
27
+ import { createSessionBand } from './sessions.ts';
28
+ import { applyDisplayName, createStatusBand } from './status.ts';
29
+ import { S } from './strings.ts';
30
+ import { createTimeline } from './timeline.ts';
31
+
32
+ const DEBUG_WS_PATH = '/ws/debug';
33
+ const SESSIONS_WS_PATH = '/ws/sessions';
34
+
35
+ /** 测试注入点:不传就是真浏览器那一套。 */
36
+ export interface LiveFeatureOptions {
37
+ env?: SocketEnv;
38
+ }
39
+
40
+ export function createLiveFeature(opts: LiveFeatureOptions = {}): FrameworkFeature {
41
+ return {
42
+ route: 'live',
43
+ label: S.navLabel,
44
+ icon: 'terminal',
45
+ navMode: 'primary',
46
+ // 两条通道任一挂着这一页就有意义;都没有的话导航里根本不出现它。
47
+ needsAny: ['debug', 'sessions'],
48
+ mount(ctx) {
49
+ return mountLive(ctx, opts.env ?? browserSocketEnv(window));
50
+ },
51
+ };
52
+ }
53
+
54
+ /** 缺省实例。host 直接用这个。 */
55
+ export const liveFeature: FrameworkFeature = createLiveFeature();
56
+
57
+ function ctxRingStyle(d: ContextBreakdown | null): string {
58
+ if (!d || !d.total || d.maxTokens === null) return '--ctx-ring:conic-gradient(var(--border) 0 100%)';
59
+ const stops: string[] = [];
60
+ let cursor = 0;
61
+ for (const cat of d.cats) {
62
+ if (cat.tok <= 0) continue;
63
+ const next = Math.min(100, cursor + cat.tok / d.maxTokens * 100);
64
+ if (next > cursor) stops.push(`${cat.color} ${cursor.toFixed(2)}% ${next.toFixed(2)}%`);
65
+ cursor = next;
66
+ }
67
+ stops.push(`var(--border) ${cursor.toFixed(2)}% 100%`);
68
+ return `--ctx-ring:conic-gradient(${stops.join(',')})`;
69
+ }
70
+
71
+ function mountLive(ctx: FeatureContext, env: SocketEnv): Disposable | void {
72
+ const { ui } = ctx;
73
+ const doc = ctx.root.ownerDocument;
74
+
75
+ // ── 这一页的全部活数据 ────────────────────────────────────────────
76
+ const state = {
77
+ messages: [] as ContextRecord[],
78
+ /** 合成开头,不写入 session 记录。 */
79
+ head: [] as ContextRecord[],
80
+ toolSchemas: [] as ToolSchemaDoc[],
81
+ status: null as StatusSnapshot | null,
82
+ sessions: [] as SessionStat[],
83
+ displayName: '',
84
+ };
85
+
86
+ const view = ui.h('div', 'liveview');
87
+ const band = ui.h('div', 'liveband');
88
+ view.appendChild(band);
89
+ ctx.root.appendChild(view);
90
+
91
+ // ── 顶栏 ──────────────────────────────────────────────────────────
92
+ const status = createStatusBand(ui);
93
+ status.render(null);
94
+
95
+ const timeline = createTimeline({ ui, lifecycle: ctx.lifecycle, signal: ctx.signal });
96
+
97
+ const sessionBand = createSessionBand({
98
+ ui,
99
+ signal: ctx.signal,
100
+ onPick: (id, label) => fork.switchTo(id, label),
101
+ });
102
+
103
+ const ctxAnchor = ui.h('span', 'ctxanchor');
104
+ const ctxOpen = ui.h('button', 'ctxdonut');
105
+ ctxOpen.type = 'button';
106
+ ctxOpen.setAttribute('aria-haspopup', 'dialog');
107
+ ctxOpen.setAttribute('aria-expanded', 'false');
108
+ ctxOpen.setAttribute('aria-label', S.ctxOpenAria);
109
+ const ctxPct = ui.h('span', 'ctxpct', '—');
110
+ ctxOpen.appendChild(ctxPct);
111
+ ctxAnchor.appendChild(ctxOpen);
112
+
113
+ let ctxPanel: HTMLElement | null = null;
114
+ const closeCtx = (): void => {
115
+ ctxPanel?.remove();
116
+ ctxPanel = null;
117
+ ctxOpen.setAttribute('aria-expanded', 'false');
118
+ };
119
+ ctxOpen.addEventListener('click', () => {
120
+ if (ctxPanel) {
121
+ closeCtx();
122
+ return;
123
+ }
124
+ ctxPanel = buildCtxPanel(
125
+ ui,
126
+ computeCtx({
127
+ messages: state.messages,
128
+ head: state.head,
129
+ toolSchemas: state.toolSchemas,
130
+ status: state.status,
131
+ }),
132
+ );
133
+ ctxPanel.setAttribute('role', 'dialog');
134
+ ctxPanel.setAttribute('aria-label', S.ctxPanelAria);
135
+ ctxAnchor.appendChild(ctxPanel);
136
+ ctxOpen.setAttribute('aria-expanded', 'true');
137
+ }, { signal: ctx.signal });
138
+ doc.addEventListener('pointerdown', (event) => {
139
+ if (ctxPanel && !ctxAnchor.contains(event.target as Node)) closeCtx();
140
+ }, { signal: ctx.signal });
141
+ doc.addEventListener('keydown', (event) => {
142
+ if (event.key === 'Escape') closeCtx();
143
+ }, { signal: ctx.signal });
144
+
145
+ let netEl: HTMLElement = ui.pill(S.netConnecting, 'plain');
146
+ const setNet = (online: boolean): void => {
147
+ const next = ui.pill(online ? S.netOnline : S.netOffline, online ? 'on' : 'off');
148
+ netEl.replaceWith(next);
149
+ netEl = next;
150
+ };
151
+
152
+ const summary = ui.h('div', 'live-summary');
153
+ summary.append(status.el, ui.h('span', 'grow'), netEl);
154
+ band.append(summary, sessionBand.el);
155
+
156
+ const composer = ui.promptInput({
157
+ label: S.composerLabel,
158
+ placeholder: S.composerPlaceholder,
159
+ hint: S.composerHint,
160
+ tools: ctxAnchor,
161
+ images: { max: 8 },
162
+ onSubmit: (text, images) => {
163
+ if (!chatStream?.open) ui.toast(S.composerQueued);
164
+ // 首启时运行是停着的(见 launcher);发话与按下那颗按钮同样是让它开始跑。
165
+ if (onboarding) {
166
+ void resumeRun();
167
+ dismissOnboarding();
168
+ }
169
+ const attached = images.map((i) => ({ mime: i.mime, base64: i.base64, name: i.name }));
170
+ chatStream?.send(JSON.stringify({ type: 'msg', text, ...(attached.length ? { images: attached } : {}) }));
171
+ return true;
172
+ },
173
+ });
174
+ view.append(timeline.el, composer.el);
175
+ timeline.rebuild([], { empty: S.emptyConnecting });
176
+
177
+ let providerReady = false;
178
+ ctx.lifecycle.own(subscribeLamps(ctx.root.ownerDocument, (lamps) => {
179
+ const lamp = lamps[PROVIDERS_LAMP_ID]?.[0];
180
+ composer.setPlaceholder(lamp && lamp.state !== 'online' ? S.composerNoProvider : null);
181
+ providerReady = lamp?.state === 'online';
182
+ onboarding?.setProvider(providerReady);
183
+ }));
184
+
185
+ const resumeRun = (): Promise<unknown> => post('/api/run/resume', {}, { signal: ctx.signal });
186
+
187
+ let onboarding: OnboardingView | null = null;
188
+ /** 本次挂载里已经收过一次;销标记的请求在路上时状态帧还会报 pending。 */
189
+ let dismissed = false;
190
+ const dismissOnboarding = (): void => {
191
+ dismissed = true;
192
+ void post('/api/onboarding/dismiss', {}, { signal: ctx.signal }).catch(() => { /* 删除失败时标记仍在,下次挂载仍显示引导。 */ });
193
+ syncOnboarding();
194
+ };
195
+ const syncOnboarding = (): void => {
196
+ const fresh = !dismissed && state.status?.onboardingPending === true;
197
+ if (fresh && !onboarding) {
198
+ onboarding = createOnboarding({
199
+ ui,
200
+ doc,
201
+ signal: ctx.signal,
202
+ go: (segments) => ctx.router.navigate(segments),
203
+ start: (label) => {
204
+ void resumeRun().then(
205
+ () => {
206
+ chatStream?.send(JSON.stringify({ type: 'greet', label }));
207
+ dismissOnboarding();
208
+ },
209
+ (err) => ctx.onError(err),
210
+ );
211
+ },
212
+ });
213
+ onboarding.setProvider(providerReady);
214
+ timeline.setHeader(onboarding.el);
215
+ timeline.setHideSystem(true);
216
+ timeline.rebuild(state.messages, { head: state.head });
217
+ return;
218
+ }
219
+ if (!fresh && onboarding) {
220
+ timeline.setHeader(null);
221
+ onboarding = null;
222
+ timeline.setHideSystem(false);
223
+ timeline.rebuild(state.messages, { head: state.head });
224
+ }
225
+ };
226
+
227
+ const hello = JSON.stringify({ type: 'hello', name: '控制台' });
228
+ let chatStream: ConsoleStreamHandle | null = null;
229
+
230
+ // ── fork 视图 ─────────────────────────────────────────────────────
231
+ const fork = createForkView({
232
+ ui,
233
+ lifecycle: ctx.lifecycle,
234
+ signal: ctx.signal,
235
+ timeline,
236
+ mainMessages: () => state.messages,
237
+ mainHead: () => state.head,
238
+ sessions: () => state.sessions,
239
+ onChange: () => sessionBand.render(state.sessions, fork.id),
240
+ onError: (err) => ctx.onError(err),
241
+ });
242
+
243
+ const updateCtx = (): void => {
244
+ const d = computeCtx({
245
+ messages: state.messages,
246
+ head: state.head,
247
+ toolSchemas: state.toolSchemas,
248
+ status: state.status,
249
+ });
250
+ ctxOpen.setAttribute('style', ctxRingStyle(d));
251
+ const known = d !== null && d.maxTokens !== null && d.maxTokens > 0;
252
+ const ratio = known ? d.total / d.maxTokens! : 0;
253
+ // 分母未知(Persona没报预算、Provider 也没报窗口)时圈里只写计数,不编百分比
254
+ ctxPct.textContent = d ? (known ? ui.fmt.percent(ratio) : ui.fmt.count(d.total)) : '—';
255
+ ctxOpen.className = `ctxdonut${ratio >= 1 ? ' danger' : d && d.softRatio !== null && ratio >= d.softRatio ? ' warn' : ''}`;
256
+ ctxOpen.title = d
257
+ ? S.ctxTitle(ui.fmt.count(d.total), known ? ui.fmt.count(d.maxTokens!) : null)
258
+ : S.ctxWaiting;
259
+ };
260
+
261
+ const setStatus = (st: StatusSnapshot | null): void => {
262
+ state.status = st;
263
+ status.render(st);
264
+ if (st && applyDisplayName(doc, st.displayName, state.displayName)) {
265
+ state.displayName = str(st.displayName);
266
+ }
267
+ if (st) timeline.setSpeaker(str(st.displayName));
268
+ updateCtx();
269
+ };
270
+
271
+ const setSessions = (list: SessionStat[]): void => {
272
+ state.sessions = list;
273
+ sessionBand.render(list, fork.id);
274
+ fork.noteSessions();
275
+ };
276
+
277
+ // ── 调试帧 ────────────────────────────────────────────────────────
278
+ const onFrame = (f: Readonly<Record<string, unknown>>): void => {
279
+ switch (f.t) {
280
+ case 'hello': {
281
+ state.toolSchemas = arr<ToolSchemaDoc>(f.toolSchemas);
282
+ state.messages = arr<ContextRecord>(f.session);
283
+ state.head = arr<ContextRecord>(f.head);
284
+ // 重连即回到主视图:那个 fork 的轮询若还开着,这里连它一起收。
285
+ fork.switchTo(MAIN_ID, MAIN_LABEL);
286
+ // 先记说话人再铺时间线:头像占位圆的首字在画气泡时就定了。
287
+ setStatus((f.status as StatusSnapshot | null) ?? null);
288
+ timeline.rebuild(state.messages, { head: state.head });
289
+ setSessions(arr<SessionStat>(f.sessions));
290
+ break;
291
+ }
292
+ case 'session.append': {
293
+ const index = f.index;
294
+ if (typeof index === 'number' && Number.isInteger(index) && index >= 0) {
295
+ if (index < state.messages.length) break;
296
+ if (index === state.messages.length) {
297
+ const message = f.message as ContextRecord;
298
+ state.messages.push(message);
299
+ if (fork.isMain()) timeline.append(message, index, true);
300
+ break;
301
+ }
302
+ }
303
+ // 缺失、非法或跳跃的索引需要重新获取完整会话。
304
+ socket?.dispose();
305
+ reopenDebug();
306
+ break;
307
+ }
308
+ case 'session.reset':
309
+ state.messages = arr<ContextRecord>(f.messages);
310
+ if (f.head !== undefined) state.head = arr<ContextRecord>(f.head);
311
+ if (fork.isMain()) {
312
+ timeline.rebuild(state.messages, { note: S.sessionResetNote, head: state.head });
313
+ }
314
+ break;
315
+ case 'status':
316
+ setStatus((f.status as StatusSnapshot | null) ?? null);
317
+ break;
318
+ case 'sessions':
319
+ setSessions(arr<SessionStat>(f.sessions));
320
+ break;
321
+ case 'sys':
322
+ // 服务端明说通道不可用:别再画"连接中",也别让 fork 轮询留着。
323
+ fork.switchTo(MAIN_ID, MAIN_LABEL);
324
+ state.messages = [];
325
+ timeline.rebuild([], { empty: str(f.text) || S.debugUnavailable });
326
+ break;
327
+ default:
328
+ break; // 认不出的帧忽略:服务端以后加帧型不该让这一页炸
329
+ }
330
+ updateCtx();
331
+ syncOnboarding();
332
+ };
333
+
334
+ let socket: Disposable | null = null;
335
+ const reopenDebug = (): void => {
336
+ socket = openFrameworkSocket({
337
+ path: DEBUG_WS_PATH,
338
+ lifecycle: ctx.lifecycle,
339
+ env,
340
+ onFrame,
341
+ onNet: setNet,
342
+ onError: (err) => ctx.onError(err),
343
+ });
344
+ };
345
+
346
+ if (ctx.capabilities.debug) {
347
+ reopenDebug();
348
+ } else {
349
+ timeline.rebuild([], { empty: S.debugNotMounted });
350
+ // 状态读数仍然有:`/api/status` 是框架端点,不随调试通道走。
351
+ void get<StatusSnapshot>('/api/status', { signal: ctx.signal }).then(
352
+ (st) => {
353
+ setStatus(st);
354
+ setNet(true);
355
+ syncOnboarding();
356
+ },
357
+ (err) => {
358
+ if ((err as { name?: string } | null)?.name === 'AbortError') return;
359
+ setNet(false);
360
+ ctx.onError(err);
361
+ },
362
+ );
363
+ if (ctx.capabilities.sessions) {
364
+ openFrameworkSocket({
365
+ path: SESSIONS_WS_PATH,
366
+ lifecycle: ctx.lifecycle,
367
+ env,
368
+ onFrame: (f) => {
369
+ if (f.t === 'sessions') setSessions(arr<SessionStat>(f.sessions));
370
+ },
371
+ onNet: setNet,
372
+ onError: (err) => ctx.onError(err),
373
+ });
374
+ }
375
+ }
376
+
377
+ chatStream = ctx.lifecycle.own(openStream({
378
+ url: env.wsUrl(panelStreamRoute('world:terminal', 'chat')),
379
+ signal: ctx.signal,
380
+ createSocket: env.createSocket,
381
+ setTimer: env.setTimer,
382
+ clearTimer: env.clearTimer,
383
+ onError: (err) => ctx.onError(err),
384
+ handlers: {
385
+ message: () => {},
386
+ close: (willRetry) => {
387
+ if (willRetry) chatStream?.send(hello);
388
+ },
389
+ },
390
+ }));
391
+ chatStream.send(hello);
392
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * 开场引导的配置状态与操作入口。
3
+ * 端点状态由终端页从 providers 灯推进来,已启用的 World 在挂载时读一次接口。
4
+ * 是否显示由终端页判定,这里只画。
5
+ */
6
+
7
+ import type { ConsoleUi } from '../../../shared/client-panel.ts';
8
+ import { get } from '../../core/api.ts';
9
+ import { brandMark, icon, type ConsoleIconName } from '../../ui/icons.ts';
10
+ import { S } from './strings.ts';
11
+
12
+ /** 这一页用得到的 `/api/worlds` 字段。 */
13
+ interface WorldRow {
14
+ label?: unknown;
15
+ status?: unknown;
16
+ }
17
+
18
+ export interface OnboardingDeps {
19
+ ui: ConsoleUi;
20
+ doc: Document;
21
+ signal: AbortSignal;
22
+ /** 跳到控制台的另一页。 */
23
+ go(segments: readonly string[]): void;
24
+ /** 继续运行并请对方开口;`label` 是这颗按钮上的字,逐字进事件正文。 */
25
+ start(label: string): void;
26
+ }
27
+
28
+ export interface OnboardingView {
29
+ el: HTMLElement;
30
+ /** 端点可用性来自 providers 灯。 */
31
+ setProvider(ready: boolean): void;
32
+ }
33
+
34
+ /** 状态行的语气:成不成事各有各的颜色,不报读数时收起这一行。 */
35
+ type Tone = 'ok' | 'bad';
36
+
37
+ interface Bubble {
38
+ setState(text: string | null, tone?: Tone): void;
39
+ button: HTMLButtonElement;
40
+ }
41
+
42
+ export function createOnboarding(deps: OnboardingDeps): OnboardingView {
43
+ const { ui, doc, signal } = deps;
44
+
45
+ const el = ui.h('div', 'onboarding');
46
+ const grid = ui.h('div', 'ob-grid');
47
+ const gutter = ui.h('div', 'gutter ob-mark');
48
+ gutter.appendChild(brandMark(doc));
49
+ const col = ui.h('div', 'ob-col');
50
+ col.appendChild(ui.h('div', 'ob-who', S.obWho));
51
+ const body = ui.h('div', 'turnbody');
52
+ col.appendChild(body);
53
+ grid.append(gutter, col);
54
+ el.appendChild(grid);
55
+
56
+ const bubble = (
57
+ line: string,
58
+ action?: { label: string; icon?: ConsoleIconName; accent?: boolean; onClick(): void },
59
+ ): Bubble => {
60
+ const box = ui.h('div', 'monolog');
61
+ box.appendChild(ui.h('div', 'monolog-body', line));
62
+ const state = ui.h('div', 'ob-state hidden');
63
+ const button = ui.h('button', action?.accent ? 'seg active ob-btn ob-go' : 'seg active ob-btn');
64
+ button.type = 'button';
65
+ if (action?.icon) button.appendChild(icon(doc, action.icon));
66
+ button.appendChild(ui.h('span', null, action?.label ?? ''));
67
+ if (action) button.addEventListener('click', () => action.onClick(), { signal });
68
+ box.appendChild(state);
69
+ if (action) {
70
+ const acts = ui.h('div', 'ob-acts');
71
+ acts.appendChild(button);
72
+ box.appendChild(acts);
73
+ }
74
+ body.appendChild(box);
75
+ return {
76
+ button,
77
+ setState(text, tone) {
78
+ state.className = text === null ? 'ob-state hidden' : `ob-state ${tone ?? ''}`.trim();
79
+ state.textContent = text ?? '';
80
+ },
81
+ };
82
+ };
83
+
84
+ bubble(S.obWelcome);
85
+ const provider = bubble(S.obProvider, { label: S.obGoConfigure, onClick: () => deps.go(['providers']) });
86
+ const worlds = bubble(S.obWorlds, { label: S.obGoConfigure, onClick: () => deps.go(['world']) });
87
+ bubble(S.obPrompts, { label: S.obGoEdit, onClick: () => deps.go(['prompts']) });
88
+
89
+ const startLabel = S.obStart;
90
+ const ready = bubble(S.obReady, {
91
+ label: startLabel,
92
+ icon: 'play',
93
+ accent: true,
94
+ onClick: () => deps.start(startLabel),
95
+ });
96
+
97
+ // 读不到就让这一行空着:引导区少一行读数,不该变成错误卡。
98
+ void get<{ worlds?: WorldRow[] }>('/api/worlds', { signal }).then((data) => {
99
+ const active = (data?.worlds ?? [])
100
+ .filter((w) => w.status === 'active' && typeof w.label === 'string')
101
+ .map((w) => w.label as string);
102
+ if (active.length) worlds.setState(S.obWorldsState(active));
103
+ }, () => {});
104
+
105
+ return {
106
+ el,
107
+ setProvider(available) {
108
+ provider.setState(available ? S.obProviderReady : S.obProviderNone, available ? 'ok' : 'bad');
109
+ ready.setState(available ? null : S.obProviderNone, 'bad');
110
+ ready.button.disabled = !available;
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * 调试帧与 /api/status 的前端读取形状。服务端 contribution 可提供可选字段,视图按字段是否存在决定展示;保留服务端键名,集中读取当前状态字段。
3
+ */
4
+
5
+ export interface ToolOwner {
6
+ kind?: string;
7
+ id?: string;
8
+ label?: string;
9
+ }
10
+
11
+ /** `/api/tool-schemas` 与 debug hello 里的工具表条目。 */
12
+ export interface ToolSchemaDoc {
13
+ name: string;
14
+ description?: string;
15
+ parameters?: unknown;
16
+ owner?: ToolOwner | null;
17
+ tags?: readonly string[];
18
+ }
19
+
20
+ /** `/api/sessions` 的一行(主意识与各 fork)。 */
21
+ export interface SessionStat {
22
+ id: string;
23
+ label: string;
24
+ role: string;
25
+ calls: number;
26
+ promptTokens: number;
27
+ completionTokens: number;
28
+ cacheHitTokens: number;
29
+ cacheHitRate: number | null;
30
+ messageCount: number;
31
+ /** null = 还在跑 */
32
+ endedAt?: string | null;
33
+ }
34
+
35
+ export interface UsageSnapshot {
36
+ promptTokens?: number;
37
+ cacheHitTokens?: number;
38
+ }
39
+
40
+ /**
41
+ * 上下文预算。两个来源合成一份:core 在 `loop.context` 报物理上限、上游数过的
42
+ * 部分与摘除思维链开关;Persona在顶层 `context` 报自己的阶段预算与软预警线。
43
+ */
44
+ export interface ContextBudget {
45
+ /** Persona的阶段预算(圈的分母);没报时分母退到 hardTokens */
46
+ maxTokens?: number;
47
+ softRatio?: number;
48
+ keepPastThinking?: boolean;
49
+ /** 模型一次请求能收的输入上限;窗口未知时 null */
50
+ hardTokens?: number | null;
51
+ /** estTokens 里上游数过的部分;整份估算时 0 */
52
+ countedTokens?: number;
53
+ }
54
+
55
+ export interface LoopStatus {
56
+ estTokens?: number | null;
57
+ messageCount?: number | null;
58
+ lastUsage?: UsageSnapshot | null;
59
+ batchesHandled?: number | null;
60
+ roundsLastBatch?: number | null;
61
+ paused?: boolean;
62
+ scheduleBlocked?: boolean;
63
+ truncating?: boolean;
64
+ context?: ContextBudget | null;
65
+ }
66
+
67
+ /** Persona自报的一枚状态筹码。框架照文本渲染,不解释里面说的是什么。 */
68
+ export interface StatusChip {
69
+ label: string;
70
+ tone?: 'plain' | 'warn' | 'accent';
71
+ }
72
+
73
+ export interface StatusSnapshot {
74
+ displayName?: string;
75
+ loop?: LoopStatus | null;
76
+ /**
77
+ * Persona自报的状态筹码(「梦中」这类)。框架不认识任何一个人格概念,只把
78
+ * 文本按 tone 画出来——这是人格概念上状态条的**唯一**出口。
79
+ */
80
+ chips?: StatusChip[] | null;
81
+ eventCount?: number | null;
82
+ /** 部署目录里还挂着开场引导的标记。 */
83
+ onboardingPending?: boolean;
84
+ terminalOnline?: number | null;
85
+ context?: ContextBudget | null;
86
+ [key: string]: unknown;
87
+ }
88
+
89
+ export function loopOf(st: StatusSnapshot | null): LoopStatus {
90
+ return st?.loop ?? {};
91
+ }
92
+
93
+ /** 上下文预算:`status.loop.context`(core)与 `status.context`(Persona)合成,后者的键覆盖前者。 */
94
+ export function contextOf(st: StatusSnapshot | null): ContextBudget {
95
+ if (!st) return {};
96
+ const inLoop = loopOf(st).context;
97
+ const own = st.context;
98
+ return {
99
+ ...(inLoop && typeof inLoop === 'object' ? inLoop : {}),
100
+ ...(own && typeof own === 'object' ? own : {}),
101
+ };
102
+ }
103
+
104
+ /** 一个字符串字段,不是字符串就当没有。 */
105
+ export function str(v: unknown): string {
106
+ return typeof v === 'string' ? v : '';
107
+ }
108
+
109
+ /** 一个数组字段,不是数组就当空。 */
110
+ export function arr<T>(v: unknown): T[] {
111
+ return Array.isArray(v) ? (v as T[]) : [];
112
+ }
113
+
114
+ /** Persona自报的状态筹码;形状不对的条目丢掉,坏数据不该让状态条整条消失。 */
115
+ export function chipsOf(st: StatusSnapshot | null): StatusChip[] {
116
+ return arr<unknown>(st?.chips).flatMap((c) => {
117
+ if (!c || typeof c !== 'object') return [];
118
+ const { label, tone } = c as { label?: unknown; tone?: unknown };
119
+ if (typeof label !== 'string' || !label) return [];
120
+ return [{ label, ...(tone === 'warn' || tone === 'accent' ? { tone } : {}) }];
121
+ });
122
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * session 卡条 —— 终端页顶上那一排"主意识 + 各 fork"。
3
+ *
4
+ * 一张卡就是一个可点的入口:点谁,时间线就切去看谁。数据来自 `/ws/debug` 的
5
+ * `sessions` 帧;调试通道没挂时由 `/ws/sessions` 顶上(见 `index.ts`)。
6
+ *
7
+ * 卡面上的 `r-<role>` 是**服务端给的角色字符串**,不是前端认识的名单——各角色的
8
+ * 圆点配色写在 `styles.css` 里,前端这边一个角色名都不写死。
9
+ */
10
+
11
+ import type { ConsoleUi } from '../../../shared/client-panel.ts';
12
+ import type { SessionStat } from './protocol.ts';
13
+ import { S } from './strings.ts';
14
+
15
+ export interface SessionBandDeps {
16
+ ui: ConsoleUi;
17
+ /**
18
+ * 本次挂载的 signal,统一管理卡片监听,unmount 后解除。
19
+ */
20
+ signal: AbortSignal;
21
+ /** 点了某张卡。切不切、怎么切由调用方决定。 */
22
+ onPick(id: string, label: string): void;
23
+ }
24
+
25
+ export interface SessionBand {
26
+ el: HTMLElement;
27
+ /** 重画。`currentId` 决定哪张卡带选中框。 */
28
+ render(sessions: readonly SessionStat[], currentId: string): void;
29
+ }
30
+
31
+ export function createSessionBand(deps: SessionBandDeps): SessionBand {
32
+ const { ui } = deps;
33
+ const el = ui.h('div', 'sesscards');
34
+ el.title = S.sessionBandTitle;
35
+
36
+ return {
37
+ el,
38
+ render(sessions, currentId) {
39
+ while (el.children.length) el.children[0].remove();
40
+ for (const s of sessions) {
41
+ const running = s.endedAt == null;
42
+ const cls = [
43
+ 'sesscard',
44
+ running ? 'running' : '',
45
+ `r-${s.role}`,
46
+ s.id === currentId ? 'sel' : '',
47
+ ]
48
+ .filter((c) => c !== '')
49
+ .join(' ');
50
+ const card = ui.h('span', cls);
51
+ card.appendChild(ui.h('span', 'sdot'));
52
+ const info = ui.h('span');
53
+ info.appendChild(ui.h('b', null, s.label));
54
+ info.appendChild(ui.h('span', null, ` ↑${ui.fmt.count(s.promptTokens)} `));
55
+ info.appendChild(ui.h('span', 'shr', S.sessionCache(ui.fmt.percent(s.cacheHitRate))));
56
+ card.appendChild(info);
57
+ card.title = S.sessionCardTitle(
58
+ s.label,
59
+ s.id,
60
+ running,
61
+ running ? '' : ui.fmt.clock(s.endedAt),
62
+ s.promptTokens,
63
+ s.completionTokens,
64
+ s.cacheHitTokens,
65
+ );
66
+ card.addEventListener('click', () => deps.onPick(s.id, s.label), { signal: deps.signal });
67
+ el.appendChild(card);
68
+ }
69
+ },
70
+ };
71
+ }