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,125 @@
1
+ /** 渲染贡献方自报的四态灯;导航、World 总览与页头共用非重叠轮询,隐藏页面跳过取数。 */
2
+
3
+ import { get } from '../core/api.ts';
4
+ import { toDisposable, type Disposable } from '../../shared/client-panel.ts';
5
+ import {
6
+ CONSOLE_LAMPS_ROUTE,
7
+ type ConsoleLamp,
8
+ type ConsoleLampsResponse,
9
+ } from '../../shared/console-protocol.ts';
10
+ import { h } from './dom.ts';
11
+ import { S } from './strings.ts';
12
+
13
+ /** 灯多久重取一次。它标的是"这一刻",慢一拍就成了另一个时刻的事实。 */
14
+ const LAMP_POLL_MS = 500;
15
+
16
+ /** state → class。`offline` 不加修饰,就是 `.navdot` 本体那个灰。 */
17
+ const LAMP_CLASS: Record<ConsoleLamp['state'], string> = {
18
+ online: 'navdot on',
19
+ loading: 'navdot warn',
20
+ error: 'navdot bad',
21
+ offline: 'navdot',
22
+ };
23
+
24
+ /** 悬停时的状态词。 */
25
+ const LAMP_WORD: Record<ConsoleLamp['state'], string> = {
26
+ online: S.lamp.online,
27
+ loading: S.lamp.loading,
28
+ error: S.lamp.error,
29
+ offline: S.lamp.offline,
30
+ };
31
+
32
+ /**
33
+ * 一颗灯的悬停说明:链路名在最前面。
34
+ *
35
+ * 导航上只有这一处读得到 `label`,所以它必须先回答"这是谁",再回答"它怎么了"
36
+ * ——一排七个同样大小的点,颜色说得清严重程度,说不清是哪条链路。
37
+ */
38
+ function lampTitle(lamp: ConsoleLamp): string {
39
+ const head = `${lamp.label} ${LAMP_WORD[lamp.state]}`;
40
+ return lamp.hint ? `${head} · ${lamp.hint}` : head;
41
+ }
42
+
43
+ function paintDot(el: HTMLElement, lamp: ConsoleLamp): void {
44
+ el.className = LAMP_CLASS[lamp.state];
45
+ el.title = lampTitle(lamp);
46
+ el.setAttribute('aria-label', el.title);
47
+ }
48
+
49
+ function makeDot(doc: Document, lamp: ConsoleLamp): HTMLSpanElement {
50
+ const el = h(doc, 'span', 'navdot');
51
+ el.setAttribute('role', 'img');
52
+ paintDot(el, lamp);
53
+ return el;
54
+ }
55
+
56
+ /**
57
+ * 把一排灯画进既有容器。**空数组就是藏起来的空容器**,不画占位灰点:灰是"这条
58
+ * 链路关着",拿它当"没报灯"讲,等于替 provider 说了一句它没说过的话。
59
+ *
60
+ * 数目没变就地改颜色,变了才重排子节点——这是每半秒一次的操作,没必要每次重建。
61
+ */
62
+ export function paintLamps(host: HTMLElement, lamps: readonly ConsoleLamp[]): void {
63
+ const dots = [...host.children] as HTMLElement[];
64
+ if (dots.length === lamps.length) {
65
+ lamps.forEach((lamp, i) => paintDot(dots[i], lamp));
66
+ } else {
67
+ host.replaceChildren(...lamps.map((lamp) => makeDot(host.ownerDocument, lamp)));
68
+ }
69
+ host.hidden = lamps.length === 0;
70
+ }
71
+
72
+ /** 新造一排灯。给不出灯就得到一个藏起来的空容器(之后轮询可以点亮它)。 */
73
+ export function lampRow(doc: Document, lamps: readonly ConsoleLamp[]): HTMLSpanElement {
74
+ const host = h(doc, 'span', 'navlamps');
75
+ paintLamps(host, lamps);
76
+ return host;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // 取数
81
+ // ---------------------------------------------------------------------------
82
+
83
+ type LampListener = (lamps: Record<string, ConsoleLamp[]>) => void;
84
+
85
+ const listeners = new Set<LampListener>();
86
+ let timer: ReturnType<typeof setInterval> | null = null;
87
+ let doc: Document | null = null;
88
+ /** 是否有尚未完成的轮询请求。 */
89
+ let pending = false;
90
+
91
+ function tick(): void {
92
+ if (pending || doc?.hidden !== false) return; // 后台标签页里没人看那排灯
93
+ pending = true;
94
+ void get<ConsoleLampsResponse>(CONSOLE_LAMPS_ROUTE)
95
+ .then(
96
+ (out) => {
97
+ const lamps = out?.lamps ?? {};
98
+ for (const cb of [...listeners]) cb(lamps);
99
+ },
100
+ () => { /* 取不到灯不等于灯灭了:留上一拍的读数 */ },
101
+ )
102
+ .finally(() => { pending = false; });
103
+ }
104
+
105
+ /**
106
+ * 订阅状态灯。第一个订阅者把轮询打开,最后一个走掉时关掉——控制台整页没有一颗
107
+ * 灯在看的时候,不该还有一个定时器在敲后端。
108
+ *
109
+ * `ownerDoc` 只用来问"页面还看得见吗",第一个订阅者带进来的那份作数。
110
+ */
111
+ export function subscribeLamps(ownerDoc: Document, cb: LampListener): Disposable {
112
+ listeners.add(cb);
113
+ if (timer === null) {
114
+ doc = ownerDoc;
115
+ timer = setInterval(tick, LAMP_POLL_MS);
116
+ }
117
+ return toDisposable(() => {
118
+ listeners.delete(cb);
119
+ if (listeners.size > 0 || timer === null) return;
120
+ clearInterval(timer);
121
+ timer = null;
122
+ doc = null;
123
+ pending = false;
124
+ });
125
+ }
@@ -0,0 +1,98 @@
1
+ /** 有行数上限的滚动日志;超出时删除最早行。仅在贴底时随新增内容滚动,上翻后暂停,返回底部后恢复。 */
2
+
3
+ import type {
4
+ ConsoleLog,
5
+ ConsoleLogOptions,
6
+ ConsoleLogTone,
7
+ } from '../../shared/client-panel.ts';
8
+ import { h } from './dom.ts';
9
+
10
+ /** 缺省上限。四百行足够翻一阵子,又不至于让布局开始喘。 */
11
+ const LOG_MAX_LINES = 400;
12
+
13
+ /** 缺省的粘滞容差(像素)。 */
14
+ export const LOG_STICK_PX = 24;
15
+
16
+ /** 缺省高度。给了限高才会滚,粘滞也才有意义。 */
17
+ const LOG_MAX_HEIGHT = '240px';
18
+
19
+ /** scrollTop 可能含小数,使用容差判断是否贴底。未溢出或缺少有效读数时返回 true;负容差按 0 处理。 */
20
+ export function shouldStick(
21
+ scrollTop: number,
22
+ scrollHeight: number,
23
+ clientHeight: number,
24
+ threshold: number = LOG_STICK_PX,
25
+ ): boolean {
26
+ const rest = scrollHeight - clientHeight - scrollTop;
27
+ if (!Number.isFinite(rest)) return true;
28
+ return rest <= Math.max(0, threshold);
29
+ }
30
+
31
+ /** tone → class。`plain` 与不给一样落中性 `.logline`。 */
32
+ function lineClass(tone?: ConsoleLogTone): string {
33
+ return tone && tone !== 'plain' ? 'logline ' + tone : 'logline';
34
+ }
35
+
36
+ export function log(doc: Document, signal: AbortSignal, opts?: ConsoleLogOptions): ConsoleLog {
37
+ const o = opts ?? {};
38
+ const max = Math.max(1, Math.floor(o.max ?? LOG_MAX_LINES));
39
+ const threshold = o.stickThreshold ?? LOG_STICK_PX;
40
+ const el = h(doc, 'div', o.variant === 'conversation' ? 'logview conversation' : 'logview');
41
+ el.setAttribute('style', 'max-height:' + (o.maxHeight ?? LOG_MAX_HEIGHT));
42
+
43
+ // 空态是一个独立节点、且在第一行进来时就摘掉:留着它当孩子的话,环形裁剪
44
+ // 从头摘的第一个就是它,行数从此少算一行。
45
+ let empty: HTMLElement | null = null;
46
+ const showEmpty = (): void => {
47
+ if (!o.empty || empty) return;
48
+ empty = h(doc, 'div', 'placeholder', o.empty);
49
+ el.appendChild(empty);
50
+ };
51
+ const hideEmpty = (): void => {
52
+ empty?.remove();
53
+ empty = null;
54
+ };
55
+ showEmpty();
56
+
57
+ // 初值 true:刚建出来的日志本来就在底部,第一批行必须跟着走
58
+ let stuck = true;
59
+ const scrollToEnd = (): void => {
60
+ el.scrollTop = el.scrollHeight;
61
+ stuck = true;
62
+ };
63
+ // 唯一的一条监听,随面板 signal 摘。程序化地设 scrollTop 也会派发 scroll,
64
+ // 那一发照样走这里重算——算出来仍是"贴着底",所以不必特意去屏蔽它。
65
+ el.addEventListener(
66
+ 'scroll',
67
+ () => {
68
+ stuck = shouldStick(el.scrollTop, el.scrollHeight, el.clientHeight, threshold);
69
+ },
70
+ { signal },
71
+ );
72
+
73
+ return {
74
+ el,
75
+ append(line, tone) {
76
+ hideEmpty();
77
+ const row = h(doc, 'div', lineClass(tone), line);
78
+ el.appendChild(row);
79
+ while (el.children.length > max) el.children[0].remove();
80
+ if (stuck) scrollToEnd();
81
+ return row;
82
+ },
83
+ clear() {
84
+ while (el.children.length) el.children[0].remove();
85
+ empty = null;
86
+ showEmpty();
87
+ // 清空之后重新粘住:一个空的日志框谈不上"用户正在往上翻"
88
+ stuck = true;
89
+ },
90
+ get count(): number {
91
+ return empty ? 0 : el.children.length;
92
+ },
93
+ get stuck(): boolean {
94
+ return stuck;
95
+ },
96
+ scrollToEnd,
97
+ };
98
+ }
@@ -0,0 +1,184 @@
1
+ /** 浮层挂载到调用方提供的 host,并随面板 signal 关闭。confirm 取消或 abort 时返回 false;abort 后不显示新浮层。 */
2
+
3
+ import type { Disposable } from '../../shared/client-panel.ts';
4
+ import { h } from './dom.ts';
5
+ import { button, rowbar } from './sheet.ts';
6
+ import { S } from './strings.ts';
7
+
8
+ /** 浮层的生存环境:文档、挂载点、面板生命周期。 */
9
+ export interface OverlayEnv {
10
+ doc: Document;
11
+ /** 浮层挂在这里。**不能是面板的 `root`**——那个在 unmount 时会被清空。 */
12
+ host: HTMLElement;
13
+ /** 面板的 `ctx.signal`。abort = 面板没了,浮层必须跟着消失。 */
14
+ signal: AbortSignal;
15
+ }
16
+
17
+ /**
18
+ * 一层浮层。`close()` 幂等。
19
+ * `dispose` 与 `close` 是同一个函数:对外(扩展手里的 `Disposable`)叫 dispose,
20
+ * 对内叫 close——不为了统一名字去改契约里那个通用的资源名。
21
+ */
22
+ export interface Overlay extends Disposable {
23
+ el: HTMLElement;
24
+ close(): void;
25
+ /** 本层的生命周期:`close()` 即 abort。层内的监听全挂它。 */
26
+ signal: AbortSignal;
27
+ }
28
+
29
+ /** 面板已经 abort 之后的调用返回它:拿到的是个不会爆的空句柄。 */
30
+ export const noopDisposable: Disposable = { dispose() {} };
31
+
32
+ /**
33
+ * 铺一层浮层:进 DOM,接上"面板 abort 就关"。
34
+ *
35
+ * 面板那条 abort 监听自己也挂着本层的 signal(`once` + `signal`),所以浮层先被关掉时
36
+ * 它当场摘掉——不会在面板的 signal 上攒一串已经没用的闭包(那是个真泄漏:一个面板
37
+ * 弹一百次 toast 就攒一百条)。
38
+ */
39
+ function openLayer(env: OverlayEnv, el: HTMLElement, onClose?: () => void): Overlay {
40
+ const ac = new AbortController();
41
+ const close = (): void => {
42
+ if (ac.signal.aborted) return;
43
+ ac.abort();
44
+ el.remove();
45
+ onClose?.();
46
+ };
47
+ env.signal.addEventListener('abort', close, { once: true, signal: ac.signal });
48
+ env.host.appendChild(el);
49
+ return { el, close, signal: ac.signal, dispose: close };
50
+ }
51
+
52
+ /**
53
+ * 铺一层 `.modal` 遮罩,接好"点遮罩 / 按 Esc 关闭"。
54
+ * `onClose` 只会被调一次;重复调用 `close()` 是安全的。
55
+ */
56
+ function openModal(env: OverlayEnv, onClose: () => void): Overlay {
57
+ const el = h(env.doc, 'div', 'modal');
58
+ el.setAttribute('role', 'dialog');
59
+ el.setAttribute('aria-modal', 'true');
60
+ const layer = openLayer(env, el, onClose);
61
+
62
+ env.doc.addEventListener(
63
+ 'keydown',
64
+ (ev: KeyboardEvent) => {
65
+ if (ev.key === 'Escape') layer.close();
66
+ },
67
+ { signal: layer.signal },
68
+ );
69
+ // 按下位置决定是否关闭,卡片内开始的拖选不关闭弹窗。
70
+ el.addEventListener(
71
+ 'mousedown',
72
+ (ev: MouseEvent) => {
73
+ if (ev.target === el) layer.close();
74
+ },
75
+ { signal: layer.signal },
76
+ );
77
+ return layer;
78
+ }
79
+
80
+ /** toast 停留多久(与 `@keyframes toast` 的 1.9s 对齐,留一点余量再摘节点)。 */
81
+ const TOAST_MS = 2000;
82
+
83
+ /**
84
+ * 瞬时提示。`prev` 是上一条 toast(同一个 ui 实例只留一条,后来的顶掉前面的,
85
+ * 与既有前端 `sendToast` 的行为一致)。返回新的一层,面板 abort 后返回 `null`。
86
+ */
87
+ export function toast(
88
+ env: OverlayEnv,
89
+ text: string,
90
+ tone: 'ok' | 'bad' | undefined,
91
+ prev: Overlay | null,
92
+ ): Overlay | null {
93
+ prev?.close();
94
+ if (env.signal.aborted) return null;
95
+ const el = h(env.doc, 'div', tone === 'bad' ? 'toast bad' : 'toast', text);
96
+ const layer = openLayer(env, el);
97
+ const timer = setTimeout(() => layer.close(), TOAST_MS);
98
+ // 提前关掉(被下一条顶掉、或面板卸载)时把定时器也撤了,免得留一个空转的回调
99
+ layer.signal.addEventListener('abort', () => clearTimeout(timer), { once: true });
100
+ return layer;
101
+ }
102
+
103
+ /** 模态确认。resolve 之后 DOM 与监听都已清干净。 */
104
+ export function confirm(
105
+ env: OverlayEnv,
106
+ opts: { title: string; body?: string; danger?: boolean },
107
+ ): Promise<boolean> {
108
+ // 面板已经卸了:不弹窗,直接按"没答应"收场
109
+ if (env.signal.aborted) return Promise.resolve(false);
110
+ return new Promise<boolean>((resolve) => {
111
+ // 默认答案就是 false,所以**任何**关闭路径(取消键 / Esc / 点遮罩 / 面板 abort)
112
+ // 都会 resolve false,不必各自记得去 settle。
113
+ let answer = false;
114
+ const modal = openModal(env, () => resolve(answer));
115
+ const settle = (v: boolean): void => {
116
+ answer = v;
117
+ modal.close();
118
+ };
119
+
120
+ const doc = env.doc;
121
+ const card = h(doc, 'div', 'modalcard');
122
+ card.setAttribute('style', 'width:min(460px,92vw)');
123
+ const head = h(doc, 'div', 'modalhead');
124
+ head.appendChild(h(doc, 'span', 'modaltitle', opts.title));
125
+ const body = h(doc, 'div', 'modalbody');
126
+ if (opts.body) body.appendChild(h(doc, 'div', null, opts.body));
127
+
128
+ const bar = rowbar(doc);
129
+ bar.appendChild(h(doc, 'span', 'grow'));
130
+ bar.appendChild(
131
+ button(doc, modal.signal, S.cancel, { size: 'sm', onClick: () => settle(false) }),
132
+ );
133
+ const ok = button(doc, modal.signal, opts.danger ? S.proceedAnyway : S.confirm, {
134
+ size: 'sm',
135
+ variant: opts.danger ? 'danger' : 'primary',
136
+ onClick: () => settle(true),
137
+ });
138
+ bar.appendChild(ok);
139
+ body.appendChild(bar);
140
+
141
+ card.append(head, body);
142
+ modal.el.appendChild(card);
143
+ ok.focus?.();
144
+ });
145
+ }
146
+
147
+ /** busy 不响应 Esc、遮罩或关闭按钮;由返回的 Disposable 或面板 abort 关闭。 */
148
+ export function busy(env: OverlayEnv, title: string, text?: string): Disposable {
149
+ if (env.signal.aborted) return noopDisposable;
150
+ const doc = env.doc;
151
+ const el = h(doc, 'div', 'modal busy');
152
+ el.setAttribute('role', 'dialog');
153
+ el.setAttribute('aria-modal', 'true');
154
+ el.setAttribute('aria-busy', 'true');
155
+ const layer = openLayer(env, el);
156
+ const card = h(doc, 'div', 'modalcard');
157
+ card.setAttribute('style', 'width:min(460px,92vw)');
158
+ const head = h(doc, 'div', 'modalhead');
159
+ head.appendChild(h(doc, 'span', 'modaltitle', title));
160
+ const body = h(doc, 'div', 'modalbody');
161
+ if (text) body.appendChild(h(doc, 'div', 'busytext', text));
162
+ card.append(head, body);
163
+ el.appendChild(card);
164
+ return layer;
165
+ }
166
+
167
+ /** 可关闭的抽屉;字符串正文显示为 pre.mono,节点正文直接挂入 modalbody。 */
168
+ export function drawer(env: OverlayEnv, title: string, body: string | HTMLElement): Disposable {
169
+ if (env.signal.aborted) return noopDisposable;
170
+ const doc = env.doc;
171
+ const modal = openModal(env, () => {});
172
+ const card = h(doc, 'div', 'modalcard');
173
+ const head = h(doc, 'div', 'modalhead');
174
+ head.append(
175
+ h(doc, 'span', 'modaltitle', title),
176
+ button(doc, modal.signal, S.close, { size: 'sm', onClick: () => modal.close() }),
177
+ );
178
+ const bodyEl = h(doc, 'div', 'modalbody');
179
+ // 字符串走 pre.mono 保持现状观感;节点直接进,抽屉不必知道里面是什么
180
+ bodyEl.appendChild(typeof body === 'string' ? h(doc, 'pre', 'mono', body) : body);
181
+ card.append(head, bodyEl);
182
+ modal.el.appendChild(card);
183
+ return modal;
184
+ }
@@ -0,0 +1,8 @@
1
+ import type { ConsoleUi } from '../../shared/client-panel.ts';
2
+
3
+ export function pageIntro(ui: Pick<ConsoleUi, 'h'>, title: string, description?: string): HTMLElement {
4
+ const intro = ui.h('header', 'featureintro');
5
+ intro.appendChild(ui.h('h1', 'pagetitle', title));
6
+ if (description) intro.appendChild(ui.h('p', 'pagedesc', description));
7
+ return intro;
8
+ }