@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.23

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 (256) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/agent-cli.js +4 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +1 -0
  6. package/dist/agent-endpoint.js +4 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +5 -0
  9. package/dist/agent-whats-new.js +16 -1
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.d.ts +7 -0
  12. package/dist/auth-api.js +8 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/codex-package.d.ts +30 -0
  15. package/dist/backend/codex-package.js +133 -0
  16. package/dist/backend/codex-package.js.map +1 -0
  17. package/dist/backend/codex.d.ts +2 -0
  18. package/dist/backend/codex.js +9 -2
  19. package/dist/backend/codex.js.map +1 -1
  20. package/dist/backend/kiro.d.ts +9 -1
  21. package/dist/backend/kiro.js +102 -1
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/opencode-permission.js +1 -1
  24. package/dist/backend/opencode-permission.js.map +1 -1
  25. package/dist/backend/opencode.js +2 -1
  26. package/dist/backend/opencode.js.map +1 -1
  27. package/dist/binary-probe.js +1 -1
  28. package/dist/bug-report-cli.js +1 -1
  29. package/dist/bundled-skills.d.ts +7 -3
  30. package/dist/bundled-skills.js +16 -7
  31. package/dist/bundled-skills.js.map +1 -1
  32. package/dist/channel/adapters/discord.js +3 -3
  33. package/dist/channel/adapters/discord.js.map +1 -1
  34. package/dist/channel/mcp-server.js +12 -4
  35. package/dist/channel/mcp-server.js.map +1 -1
  36. package/dist/channel/mcp-tools.d.ts +3 -1
  37. package/dist/channel/mcp-tools.js +36 -0
  38. package/dist/channel/mcp-tools.js.map +1 -1
  39. package/dist/child-process.d.ts +21 -0
  40. package/dist/child-process.js +54 -0
  41. package/dist/child-process.js.map +1 -0
  42. package/dist/cli.js +167 -182
  43. package/dist/cli.js.map +1 -1
  44. package/dist/command-table.d.ts +1 -1
  45. package/dist/command-table.js +1 -1
  46. package/dist/command-table.js.map +1 -1
  47. package/dist/config-validator.js +20 -1
  48. package/dist/config-validator.js.map +1 -1
  49. package/dist/config.js +3 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/daemon.d.ts +8 -0
  52. package/dist/daemon.js +31 -8
  53. package/dist/daemon.js.map +1 -1
  54. package/dist/darwin-memory.js +1 -1
  55. package/dist/doctor.d.ts +5 -0
  56. package/dist/doctor.js +42 -0
  57. package/dist/doctor.js.map +1 -1
  58. package/dist/export-import.js +1 -1
  59. package/dist/export-import.js.map +1 -1
  60. package/dist/fleet-level-config.d.ts +1 -1
  61. package/dist/fleet-level-config.js +2 -0
  62. package/dist/fleet-level-config.js.map +1 -1
  63. package/dist/fleet-manager.d.ts +45 -0
  64. package/dist/fleet-manager.js +301 -21
  65. package/dist/fleet-manager.js.map +1 -1
  66. package/dist/full-restart.d.ts +5 -5
  67. package/dist/full-restart.js +5 -5
  68. package/dist/full-restart.js.map +1 -1
  69. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  70. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
  71. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  72. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  73. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  74. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -3
  75. package/dist/general-knowledge/steering/core-rules.md +1 -1
  76. package/dist/instance-config-impact.js +10 -3
  77. package/dist/instance-config-impact.js.map +1 -1
  78. package/dist/instance-lifecycle.js +4 -4
  79. package/dist/locale.js +118 -4
  80. package/dist/locale.js.map +1 -1
  81. package/dist/login-flows.js +1 -1
  82. package/dist/login-shell.js +3 -1
  83. package/dist/login-shell.js.map +1 -1
  84. package/dist/ls-rows.js +1 -1
  85. package/dist/needs-you-hub.d.ts +98 -1
  86. package/dist/needs-you-hub.js +313 -44
  87. package/dist/needs-you-hub.js.map +1 -1
  88. package/dist/needs-you-live.d.ts +5 -2
  89. package/dist/needs-you-live.js +6 -3
  90. package/dist/needs-you-live.js.map +1 -1
  91. package/dist/needs-you.d.ts +58 -3
  92. package/dist/needs-you.js +36 -3
  93. package/dist/needs-you.js.map +1 -1
  94. package/dist/outbound-schemas.d.ts +3 -3
  95. package/dist/public-link-progress.d.ts +12 -1
  96. package/dist/public-link-progress.js +5 -10
  97. package/dist/public-link-progress.js.map +1 -1
  98. package/dist/public-tunnel-config.d.ts +5 -0
  99. package/dist/public-tunnel-config.js +45 -1
  100. package/dist/public-tunnel-config.js.map +1 -1
  101. package/dist/public-web-link.js +6 -5
  102. package/dist/public-web-link.js.map +1 -1
  103. package/dist/quickstart.js +14 -8
  104. package/dist/quickstart.js.map +1 -1
  105. package/dist/resource-report.js +1 -1
  106. package/dist/scheduler/task-board.d.ts +41 -1
  107. package/dist/scheduler/task-board.js +150 -4
  108. package/dist/scheduler/task-board.js.map +1 -1
  109. package/dist/sd-notify.js +2 -1
  110. package/dist/sd-notify.js.map +1 -1
  111. package/dist/service-installer.d.ts +69 -0
  112. package/dist/service-installer.js +181 -0
  113. package/dist/service-installer.js.map +1 -1
  114. package/dist/service-owner.d.ts +44 -0
  115. package/dist/service-owner.js +48 -0
  116. package/dist/service-owner.js.map +1 -0
  117. package/dist/settings-api.d.ts +4 -0
  118. package/dist/settings-api.js +21 -3
  119. package/dist/settings-api.js.map +1 -1
  120. package/dist/settings-change.js +85 -2
  121. package/dist/settings-change.js.map +1 -1
  122. package/dist/settings-effect.js +2 -0
  123. package/dist/settings-effect.js.map +1 -1
  124. package/dist/settings-http-confirmation.d.ts +2 -0
  125. package/dist/settings-http-confirmation.js +3 -2
  126. package/dist/settings-http-confirmation.js.map +1 -1
  127. package/dist/settings-request-capability.d.ts +4 -1
  128. package/dist/settings-request-capability.js +11 -3
  129. package/dist/settings-request-capability.js.map +1 -1
  130. package/dist/setup-host.js +1 -1
  131. package/dist/setup-wizard.js +1 -1
  132. package/dist/setup-wizard.js.map +1 -1
  133. package/dist/task-board-summary.d.ts +101 -0
  134. package/dist/task-board-summary.js +177 -0
  135. package/dist/task-board-summary.js.map +1 -0
  136. package/dist/task-board-waker.d.ts +8 -1
  137. package/dist/task-board-waker.js +15 -6
  138. package/dist/task-board-waker.js.map +1 -1
  139. package/dist/tmux-control.js +1 -1
  140. package/dist/tmux-control.js.map +1 -1
  141. package/dist/tmux-exec-helper.d.ts +5 -1
  142. package/dist/tmux-exec-helper.js +13 -21
  143. package/dist/tmux-exec-helper.js.map +1 -1
  144. package/dist/tmux-manager.js +1 -1
  145. package/dist/tmux-read.js +1 -1
  146. package/dist/tmux-read.js.map +1 -1
  147. package/dist/topic-commands.d.ts +3 -0
  148. package/dist/topic-commands.js +29 -18
  149. package/dist/topic-commands.js.map +1 -1
  150. package/dist/transcript-monitor.js +5 -0
  151. package/dist/transcript-monitor.js.map +1 -1
  152. package/dist/transcript-sources.d.ts +2 -0
  153. package/dist/transcript-sources.js +4 -0
  154. package/dist/transcript-sources.js.map +1 -1
  155. package/dist/tunnel/cloudflared-install.js +3 -1
  156. package/dist/tunnel/cloudflared-install.js.map +1 -1
  157. package/dist/tunnel/cloudflared.d.ts +1 -1
  158. package/dist/tunnel/cloudflared.js +51 -10
  159. package/dist/tunnel/cloudflared.js.map +1 -1
  160. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  161. package/dist/tunnel/named-diagnostics.js +52 -0
  162. package/dist/tunnel/named-diagnostics.js.map +1 -0
  163. package/dist/tunnel/types.d.ts +2 -1
  164. package/dist/tunnel/types.js.map +1 -1
  165. package/dist/types.d.ts +9 -0
  166. package/dist/ui/app-effects.js +10 -4
  167. package/dist/ui/chat-strings.js +3 -0
  168. package/dist/ui/chat-thread.js +14 -7
  169. package/dist/ui/effect-trial-offer.js +2 -31
  170. package/dist/ui/effects-waker.js +2 -70
  171. package/dist/ui/fleet-task-card.js +5 -5
  172. package/dist/ui/fleet-task-common.js +41 -7
  173. package/dist/ui/fleet-task-work.js +107 -0
  174. package/dist/ui/fleet-tasks.js +46 -12
  175. package/dist/ui/image-lightbox.js +39 -1
  176. package/dist/ui/image-zoom.js +242 -0
  177. package/dist/ui/panel-chat.js +24 -13
  178. package/dist/ui/panel-details.js +2 -0
  179. package/dist/ui/panel-fleet.js +6 -2
  180. package/dist/ui/panel-needs.js +21 -6
  181. package/dist/ui/panel-settings.js +35 -13
  182. package/dist/ui/preview-panel.js +2 -1
  183. package/dist/ui/search-dialog.js +2 -1
  184. package/dist/ui/settings-appearance.js +33 -8
  185. package/dist/ui/settings-dialogs.js +33 -17
  186. package/dist/ui/settings-discord.js +6 -2
  187. package/dist/ui/settings-guests.js +2 -1
  188. package/dist/ui/settings-model.js +2 -11
  189. package/dist/ui/settings-mods.js +6 -3
  190. package/dist/ui/settings-strings.js +2 -0
  191. package/dist/ui/settings-token.js +3 -1
  192. package/dist/ui/settings-web-authority.js +6 -3
  193. package/dist/ui/settings-wizard.js +13 -7
  194. package/dist/ui/shared/app-i18n.js +2 -2
  195. package/dist/ui/shared/app-needs.js +2 -2
  196. package/dist/ui/shared/app-store.js +2 -2
  197. package/dist/ui/shared/app.css +80 -9
  198. package/dist/ui/shared/app.js +5 -1
  199. package/dist/ui/shared/effect-app-runtime.js +7 -5
  200. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  201. package/dist/ui/shared/effect-cursor.js +3 -2
  202. package/dist/ui/shared/effect-display-metadata.js +39 -0
  203. package/dist/ui/shared/effect-registry.js +4 -2
  204. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  205. package/dist/ui/shared/effect-stock-trust.js +1 -1
  206. package/dist/ui/shared/feature-flags.js +127 -0
  207. package/dist/ui/shared/identity-color.js +6 -3
  208. package/dist/ui/shared/instance-nav.js +3 -2
  209. package/dist/ui/shared/text-undo.js +210 -0
  210. package/dist/ui/shared/ui-dialog.js +4 -3
  211. package/dist/ui/shared/ui-icons.js +2 -0
  212. package/dist/update-dispatch.js +1 -1
  213. package/dist/update-launch.d.ts +1 -1
  214. package/dist/update-launch.js +1 -2
  215. package/dist/update-launch.js.map +1 -1
  216. package/dist/usage/providers.js +1 -1
  217. package/dist/usage/providers.js.map +1 -1
  218. package/dist/view-api.js +1 -1
  219. package/dist/web-api.d.ts +6 -0
  220. package/dist/web-api.js +152 -14
  221. package/dist/web-api.js.map +1 -1
  222. package/dist/web-appearance-tools.d.ts +1 -0
  223. package/dist/web-appearance-tools.js +7 -3
  224. package/dist/web-appearance-tools.js.map +1 -1
  225. package/dist/web-appearance.d.ts +4 -10
  226. package/dist/web-appearance.js +4 -3
  227. package/dist/web-appearance.js.map +1 -1
  228. package/dist/web-chat-history.d.ts +14 -0
  229. package/dist/web-chat-history.js +26 -1
  230. package/dist/web-chat-history.js.map +1 -1
  231. package/dist/web-experimental-http.d.ts +2 -0
  232. package/dist/web-experimental-http.js +12 -0
  233. package/dist/web-experimental-http.js.map +1 -0
  234. package/dist/web-experimental.d.ts +11 -0
  235. package/dist/web-experimental.js +49 -0
  236. package/dist/web-experimental.js.map +1 -0
  237. package/dist/web-guest-access.d.ts +1 -0
  238. package/dist/web-guest-access.js +4 -2
  239. package/dist/web-guest-access.js.map +1 -1
  240. package/dist/web-mod-store.d.ts +2 -0
  241. package/dist/web-mod-store.js +10 -0
  242. package/dist/web-mod-store.js.map +1 -1
  243. package/dist/web-preview.d.ts +2 -0
  244. package/dist/web-preview.js +2 -0
  245. package/dist/web-preview.js.map +1 -1
  246. package/dist/web-settings-patch.d.ts +2 -1
  247. package/dist/web-settings-patch.js +5 -1
  248. package/dist/web-settings-patch.js.map +1 -1
  249. package/dist/web-settings-tools.d.ts +1 -0
  250. package/dist/web-settings-tools.js +14 -10
  251. package/dist/web-settings-tools.js.map +1 -1
  252. package/dist/web-terminal.js +1 -1
  253. package/dist/web-theme-store.d.ts +2 -2
  254. package/dist/web-theme-store.js +6 -3
  255. package/dist/web-theme-store.js.map +1 -1
  256. package/package.json +1 -1
@@ -0,0 +1,52 @@
1
+ /** #2344: private vendor text is evidence for a closed reason, never a user/log message. */
2
+ import { redactDiagnostics } from "../diagnostic-redaction.js";
3
+ export const NAMED_TUNNEL_FAILURE_MESSAGES = {
4
+ "named-auth-failed": "Named tunnel credentials were refused; copy only the connector token from Cloudflare and set it again.",
5
+ "named-route-missing": "The named hostname did not reach a published application; check this tunnel's hostname, DNS and published application route.",
6
+ "named-hostname-mismatch": "The named hostname answered with a different page; check that its published application points to AgEnD's origin URL.",
7
+ "named-origin-unreachable": "Cloudflare could not reach the local origin; check the published application's origin URL and port.",
8
+ "named-connection-failed": "The named tunnel could not connect to Cloudflare; check network, DNS and firewall access, then retry.",
9
+ };
10
+ export function namedTunnelFailureMessage(kind) {
11
+ return Object.hasOwn(NAMED_TUNNEL_FAILURE_MESSAGES, kind) ? NAMED_TUNNEL_FAILURE_MESSAGES[kind] : undefined;
12
+ }
13
+ const CONNECTION_CODES = new Set(["EAI_AGAIN", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET"]);
14
+ /** Node fetch wraps DNS/connect failures in cause; AggregateError can contain several address attempts. */
15
+ export function classifyNamedTunnelProbeError(error, token) {
16
+ const pending = [error], seen = new Set(), messages = [];
17
+ let connection = false;
18
+ // Bound both nested errors and text; cycles or a large address list must not turn a failed probe into work.
19
+ while (pending.length && seen.size < 8) {
20
+ const next = pending.shift();
21
+ if (!next || typeof next !== "object" || seen.has(next))
22
+ continue;
23
+ seen.add(next);
24
+ const node = next;
25
+ if (node.code === "ENOTFOUND")
26
+ return "named-route-missing";
27
+ if (typeof node.code === "string" && CONNECTION_CODES.has(node.code))
28
+ connection = true;
29
+ if (typeof node.message === "string")
30
+ messages.push(node.message.slice(0, 8 * 1024));
31
+ if (node.cause)
32
+ pending.push(node.cause);
33
+ if (Array.isArray(node.errors))
34
+ pending.push(...node.errors.slice(0, 8));
35
+ }
36
+ return connection ? "named-connection-failed" : classifyNamedTunnelOutput(messages.join("\n"), token);
37
+ }
38
+ /** Exact configured credentials and the shared redactor's credential shapes are removed before interpretation. */
39
+ export function classifyNamedTunnelOutput(output, token) {
40
+ const exactRemoved = token ? output.split(token).join("[REDACTED]") : output;
41
+ const clean = redactDiagnostics("cloudflared diagnostics: " + exactRemoved.slice(-64 * 1024), { keepIdentifiers: true });
42
+ if (/\b(?:invalid|unauthorized|unauthorised|expired|revoked)\b.{0,100}\b(?:token|credentials?|tunnel)\b|\b(?:token|credentials?)\b.{0,100}\b(?:not valid|invalid|unauthorized|unauthorised|expired|revoked)\b|\bauthentication (?:failed|error)\b/i.test(clean))
43
+ return "named-auth-failed";
44
+ if (/\b(?:no|missing) (?:ingress|published application) (?:rules?|routes?)\b|\bno ingress rules\b/i.test(clean))
45
+ return "named-route-missing";
46
+ if (/\bunable to reach the origin service\b|\b(?:origin|127\.0\.0\.1|localhost)\b.{0,160}\b(?:connection refused|ECONNREFUSED)\b/i.test(clean))
47
+ return "named-origin-unreachable";
48
+ if (/\b(?:failed to (?:dial|connect|resolve)|unable to (?:dial|connect|resolve)|no such host|network is unreachable|i\/o timeout|connection timed out|connect timeout|context deadline exceeded|EAI_AGAIN|ENOTFOUND|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENETUNREACH|EHOSTUNREACH)\b/i.test(clean))
49
+ return "named-connection-failed";
50
+ return undefined;
51
+ }
52
+ //# sourceMappingURL=named-diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"named-diagnostics.js","sourceRoot":"","sources":["../../src/tunnel/named-diagnostics.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAC5F,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,CAAC,MAAM,6BAA6B,GAAG;IAC3C,mBAAmB,EAAE,wGAAwG;IAC7H,qBAAqB,EAAE,8HAA8H;IACrJ,yBAAyB,EAAE,uHAAuH;IAClJ,0BAA0B,EAAE,qGAAqG;IACjI,yBAAyB,EAAE,uGAAuG;CAC1H,CAAC;AAEX,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,OAAO,MAAM,CAAC,MAAM,CAAC,6BAA6B,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,IAA8B,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACxI,CAAC;AAED,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,yBAAyB,EAAE,gBAAgB,CAAC,CAAC,CAAC;AAEvK,2GAA2G;AAC3G,MAAM,UAAU,6BAA6B,CAAC,KAAc,EAAE,KAAa;IACzE,MAAM,OAAO,GAAc,CAAC,KAAK,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,EAAU,EAAE,QAAQ,GAAa,EAAE,CAAC;IACtF,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,4GAA4G;IAC5G,OAAO,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAClE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,MAAM,IAAI,GAAG,IAAgF,CAAC;QAC9F,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO,qBAAqB,CAAC;QAC5D,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,UAAU,GAAG,IAAI,CAAC;QACxF,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;YAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QACrF,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,UAAU,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,yBAAyB,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;AACxG,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,yBAAyB,CAAC,MAAc,EAAE,KAAa;IACrE,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7E,MAAM,KAAK,GAAG,iBAAiB,CAAC,2BAA2B,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC,CAAC;IACzH,IAAI,+OAA+O,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,mBAAmB,CAAC;IAC5R,IAAI,+FAA+F,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,qBAAqB,CAAC;IAC9I,IAAI,8HAA8H,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,0BAA0B,CAAC;IAClL,IAAI,+QAA+Q,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,yBAAyB,CAAC;IAClU,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -12,9 +12,10 @@
12
12
  * serve, and `docs/design/web-terminal-tunnel-provider.zh-TW.md` (sol) for the
13
13
  * original provider design.
14
14
  */
15
+ import type { NamedTunnelFailureKind } from "./named-diagnostics.js";
15
16
  export type TunnelVisibility = "public" | "tailnet" | "loopback" | "manual";
16
17
  /** Why a provider cannot run, in bounded terms — never a vendor error string. */
17
- export type TunnelErrorKind = "binary-missing" | "binary-not-executable" | "not-logged-in" | "spawn-failed" | "transport-failed" | "no-url"
18
+ export type TunnelErrorKind = NamedTunnelFailureKind | "binary-missing" | "binary-not-executable" | "not-logged-in" | "spawn-failed" | "transport-failed" | "no-url"
18
19
  /** #1767: Cloudflare refused to provision a quick tunnel with HTTP 429 (rate limited for this network). */
19
20
  | "rate-limited"
20
21
  /** #1767: Cloudflare refused to provision a quick tunnel with another HTTP error. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tunnel/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA2GH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAE9B;IAGA;IAJX,YACW,SAA0B,EACnC,OAAe;IACf,6EAA6E;IACpE,WAA+E;QAExF,KAAK,CAAC,OAAO,CAAC,CAAC;QALN,cAAS,GAAT,SAAS,CAAiB;QAG1B,gBAAW,GAAX,WAAW,CAAoE;QAGxF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AACjD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tunnel/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AA8GH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAE9B;IAGA;IAJX,YACW,SAA0B,EACnC,OAAe;IACf,6EAA6E;IACpE,WAA+E;QAExF,KAAK,CAAC,OAAO,CAAC,CAAC;QALN,cAAS,GAAT,SAAS,CAAiB;QAG1B,gBAAW,GAAX,WAAW,CAAoE;QAGxF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AACjD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC"}
package/dist/types.d.ts CHANGED
@@ -274,6 +274,11 @@ export interface FleetDefaults extends Partial<InstanceConfig> {
274
274
  daily_summary?: DailySummaryConfig;
275
275
  /** Daily General-topic tips and update-completion tip. Default: true. */
276
276
  tips?: boolean;
277
+ /** #2246 step 3: the task board's fleet-level settings. */
278
+ task_board?: {
279
+ /** The daily summary's time in the fleet's zone ("HH:MM", 24 h), or "off". Default "09:00". */
280
+ summary_at?: string;
281
+ };
277
282
  webhooks?: WebhookConfig[];
278
283
  }
279
284
  export type RotationSnapshotEvent = {
@@ -341,6 +346,10 @@ export interface FleetTemplate {
341
346
  }
342
347
  /** Web UI feature toggles (fleet.yaml `web:` section). */
343
348
  export interface WebConfig {
349
+ /** Canvas Worker effects are experimental and off unless explicitly enabled. Ambient effects are independent. */
350
+ experimental?: {
351
+ canvas_worker?: boolean;
352
+ };
344
353
  /** Only menu availability; every exposure requires explicit admin consent. */
345
354
  public_link?: {
346
355
  allow_public?: boolean;
@@ -5,6 +5,7 @@ import { navStore } from "/assets/app-nav.js";
5
5
  import { onPrefsChanged } from "/assets/app-prefs.js";
6
6
  import { resolvedMode, themesStore, themesShown } from "/assets/app-themes.js";
7
7
  import { ownerCssLifecycle, safeMode } from "/assets/owner-css.js";
8
+ import { canvasWorkerEnabled } from "/assets/feature-flags.js";
8
9
  import { constrainedDevice } from "/assets/effect-policy.js";
9
10
  import { createAppEffectRuntime } from "/assets/effect-app-runtime.js";
10
11
  import { effectBootstrapReady } from "/assets/effect-frame.js";
@@ -19,6 +20,7 @@ export function getAppEffects(host = window) { const value = owners.get(host); r
19
20
 
20
21
  /** Boot once with the page's Chat store, including when the first visible panel is Fleet. */
21
22
  export function startAppEffects({ store, window: host = window, factories = {} } = {}) {
23
+ if (!canvasWorkerEnabled()) return null;
22
24
  const existing = owners.get(host);
23
25
  if (existing) { if (!existing.dead) existing.bindStore(store); return getAppEffects(host); }
24
26
  const playbackIdentity = effectPlaybackIdentity(host), ownerIdentity = chatEffectIdentity(host);
@@ -26,7 +28,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
26
28
  const doc = host.document, releases = [], ended = new Set(), updates = new Set(), trialUpdates = new Set(); let replyOff = null, replyStore = null, regions = null, cursor = null, cursorStamp = null, trialControl = null, trialIntent = null, trialHint = ownerIdentity ? null : false, trialSerial = 0, trialStamp = null, chatBinding = null;
27
29
  const safe = fn => { try { return fn(); } catch { return null; } };
28
30
  const hold = off => { if (entry.dead) safe(() => off?.()); else releases.push(off); };
29
- const entry = { dead: false, api: null,
31
+ const entry = { dead: false, api: null, disposed: null,
30
32
  cursorSnapshot: () => cursor?.snapshot() ?? { status: "unavailable", id: null },
31
33
  gates: () => context(),
32
34
  subscribe(fn) { if (typeof fn !== "function" || entry.dead) return () => {}; updates.add(fn); return () => updates.delete(fn); },
@@ -74,11 +76,14 @@ export function startAppEffects({ store, window: host = window, factories = {} }
74
76
  if (!handle) chatBinding = null;
75
77
  return handle;
76
78
  },
77
- dispose() {
78
- if (entry.dead) return; entry.dead = true;
79
- safe(() => trialControl?.dispose()); safe(() => entry.api?.dispose()); safe(() => replyOff?.()); replyOff = null; replyStore = null;
79
+ dispose(restartable = false) {
80
+ if (entry.dead) return entry.disposed; entry.dead = true;
81
+ if (restartable && owners.get(host) === entry) owners.delete(host);
82
+ safe(() => trialControl?.dispose()); const coreDisposed = safe(() => entry.api?.dispose(restartable === true)); safe(() => replyOff?.()); replyOff = null; replyStore = null;
80
83
  for (const off of releases.splice(0)) safe(off);
81
84
  updates.clear(); trialUpdates.clear(); const listeners = [...ended]; ended.clear(); for (const fn of listeners) safe(fn);
85
+ entry.disposed = Promise.all([coreDisposed, safe(() => cursor?.whenDisposed?.())]);
86
+ return entry.disposed;
82
87
  },
83
88
  };
84
89
  owners.set(host, entry); // Reentrant setup never constructs another owner.
@@ -210,6 +215,7 @@ export function startAppEffects({ store, window: host = window, factories = {} }
210
215
  hold(navStore.subscribe(value => { if (entry.dead || value.seq === navSeq) return; navSeq = value.seq; trialControl?.stop(); chatBinding = null; entry.api.detachChat(); syncCursor(); }));
211
216
  hold(appStore.subscribe(value => {
212
217
  if (entry.dead) return;
218
+ if (!canvasWorkerEnabled()) { entry.dispose(true); return; }
213
219
  if (value.connection === "ended") { entry.dispose(); return; }
214
220
  const guestNext = guestChatVersion(value), chatChanged = value.themesRev !== revision || guestNext !== guestRevision, ownerChanged = value.prefsRev !== prefsRevision;
215
221
  revision = value.themesRev; prefsRevision = value.prefsRev; guestRevision = guestNext;
@@ -72,6 +72,8 @@ register("chat", {
72
72
  pvPage: "Page", pvCode: "Code", pvReload: "Reload", pvClose: "Close the panel", pvBack: "Back to the chat", pvResize: "Resize the preview panel",
73
73
  pvNewer: "A newer version is in the reply from {0}.", pvNewerShow: "Show it", pvPanelIdle: "Click Preview to run it here.",
74
74
  lbLabel: "Image: {0}", lbLabelNoName: "Image", lbClose: "Close", lbOpenOriginal: "Open original", lbPrev: "Previous image", lbNext: "Next image", lbCount: "{0} / {1}",
75
+ // #2333: zoom in the lightbox.
76
+ lbZoomIn: "Zoom in (+)", lbZoomOut: "Zoom out (−)", lbZoomLevel: "{0}% — click for fit or actual size (0 / 1)", lbFitWidth: "Fit width (W)",
75
77
  // #1554: preview.js's words (its English is the fallback there), and the server's reasons by code.
76
78
  pvWhyOptin: "HTML previews are off. Allow them from this card's ⋯ menu or Settings → Preferences.",
77
79
  pvWhyOptinPanel: "HTML previews are off.",
@@ -209,6 +211,7 @@ register("chat", {
209
211
  pvPage: "頁面", pvCode: "程式碼", pvReload: "重新載入", pvClose: "關閉面板", pvBack: "回到對話", pvResize: "調整預覽面板寬度",
210
212
  pvNewer: "{0} 的回覆裡有較新的版本。", pvNewerShow: "顯示", pvPanelIdle: "按「預覽」在這裡執行。",
211
213
  lbLabel: "圖片:{0}", lbLabelNoName: "圖片", lbClose: "關閉", lbOpenOriginal: "開啟原圖", lbPrev: "上一張", lbNext: "下一張", lbCount: "{0} / {1}",
214
+ lbZoomIn: "放大(+)", lbZoomOut: "縮小(−)", lbZoomLevel: "{0}%:點一下切換「符合視窗」與「實際大小」(0 / 1)", lbFitWidth: "符合寬度(W)",
212
215
  pvWhyOptin: "還沒允許 HTML 預覽。可從這張卡片的 ⋯ 選單或「設定 → 偏好設定」允許。",
213
216
  pvWhyOptinPanel: "還沒允許 HTML 預覽。",
214
217
  pvWhyNever: "這台裝置在這個工作階段已關閉預覽。",
@@ -8,7 +8,7 @@
8
8
  // Unrelated updates (a new message, another message's tick) stop nothing (#1306 §6.2, #1408 §4).
9
9
  import "./chat-render.js";
10
10
  import "./preview.js";
11
- import { effectTrialMessage } from "./effect-trial-offer.js";
11
+ import { canvasWorkerEnabled, onCanvasWorkerChange, onCanvasMetadataChange, readCanvasTrialMessage } from "/assets/feature-flags.js";
12
12
  import { loadHtmlAttachment } from "./html-attachment.js";
13
13
  import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
14
14
  import { textLinkSegments } from "/assets/app-route.js";
@@ -132,8 +132,8 @@ export function createThread(list, scroller, opts) {
132
132
  // #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
133
133
  // is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
134
134
  const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
135
- const trial = effectTrialMessage(x);
136
- if (trial) return trialHtml(trial, time, x);
135
+ const trial = readCanvasTrialMessage(x);
136
+ if (trial) return canvasWorkerEnabled() ? trialHtml(trial, time, x) : "";
137
137
  if (x.role === "status") return statusHtml(x, time);
138
138
  const u = opts.isUser(x);
139
139
  // renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
@@ -186,7 +186,8 @@ export function createThread(list, scroller, opts) {
186
186
  }
187
187
  function armTrials() {
188
188
  if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
189
- const now = trialNow(), next = msgs.map(x => effectTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
189
+ if (!canvasWorkerEnabled()) return;
190
+ const now = trialNow(), next = msgs.map(x => readCanvasTrialMessage(x)).filter(x => x && x.expiresAt > now).reduce((n, x) => Math.min(n, x.expiresAt), Infinity);
190
191
  if (Number.isFinite(next)) trialTimer = setTimeout(() => { trialTimer = null; render(msgs); }, Math.max(1, Math.min(2147483647, next - now)));
191
192
  }
192
193
  /**
@@ -649,7 +650,11 @@ export function createThread(list, scroller, opts) {
649
650
  }
650
651
  /** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
651
652
  function draw(drawOpts) {
652
- const shown = msgs.slice(startIndex());
653
+ const shown = msgs.slice(startIndex()).filter(message => {
654
+ // Volatile host offer records are hidden off and while their on-only parser is loading.
655
+ const hostOffer = message?.role === "status" && message.sender === "agend" && Object.hasOwn(message, "effectTrial") && message.effectTrial != null;
656
+ return !hostOffer || canvasWorkerEnabled() && readCanvasTrialMessage(message) !== undefined;
657
+ });
653
658
  const keep = new Set(shown.map(msgKey));
654
659
  for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
655
660
  let prev = null, added = 0;
@@ -764,7 +769,7 @@ export function createThread(list, scroller, opts) {
764
769
  if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
765
770
  } else if (act === "effectTrial") {
766
771
  // Only the current host-produced keyed card and a real local click can request admission. No token is read from DOM.
767
- const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = effectTrialMessage(row);
772
+ const key = el.dataset.arg, row = msgs.find(x => msgKey(x) === key), owner = nodes.get(key), node = owner?.node, offer = readCanvasTrialMessage(row);
768
773
  if (PUBLIC_LINK() || e.defaultPrevented || e.isTrusted !== true || owner?.trialButton !== el || el.className !== "btn btn-sm effect-trial-start" || !node?.contains(el) || el.closest(".effect-trial-offer")?.getAttribute("data-trust") !== "thread-control"
769
774
  || !offer || trialNow() >= offer.expiresAt) return;
770
775
  if (trialStatus(row) === "ready" && opts.canTryEffect?.(row) === true) opts.startEffectTrial?.(row);
@@ -816,8 +821,10 @@ export function createThread(list, scroller, opts) {
816
821
  if (ro) { ro.observe(list); ro.observe(sc); }
817
822
  }
818
823
  /** `own`: only this thread's previews stop (a thread let go while another chat is in view); else every one, as leaving. */
824
+ const redrawFeature = () => { if (!disposed && !parked) render(msgs); };
825
+ const featureOff = onCanvasWorkerChange(redrawFeature), metadataOff = onCanvasMetadataChange(redrawFeature);
819
826
  function dispose(own = false) {
820
- disposed = true;
827
+ disposed = true; featureOff(); metadataOff();
821
828
  if (!parked) watchReader(scroller, false);
822
829
  if (trialTimer !== null) clearTimeout(trialTimer); trialTimer = null;
823
830
  if (ro) ro.disconnect();
@@ -1,31 +1,2 @@
1
- /** #1629: closed host display metadata. Neither this object nor its token grants a trial read. */
2
- const KEYS = ["v", "offer", "id", "title", "expiresAt"], ID = /^[0-9a-f]{32}$/;
3
- const FORBIDDEN = /(?!\u200d)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
4
- const VISIBLE = /[^\p{M}\p{C}\p{Z}\u115f\u1160\u3164\uffa0\u2800]/u;
5
- let graphemes = null;
6
- export function checkEffectTrialOffer(value) {
7
- try {
8
- if (!value || ![Object.prototype, null].includes(Object.getPrototypeOf(value))) return null;
9
- const keys = Reflect.ownKeys(value);
10
- if (keys.length !== KEYS.length || keys.some(k => typeof k !== "string" || !KEYS.includes(k))) return null;
11
- const x = Object.create(null);
12
- for (const key of KEYS) {
13
- const d = Object.getOwnPropertyDescriptor(value, key);
14
- if (!d || !("value" in d) || !d.enumerable) return null;
15
- x[key] = d.value;
16
- }
17
- if (x.v !== 1 || typeof x.offer !== "string" || !ID.test(x.offer) || typeof x.id !== "string" || !ID.test(x.id)
18
- || typeof x.title !== "string" || x.title.length > 400 || x.title.normalize("NFC") !== x.title || x.title.trim() !== x.title
19
- || FORBIDDEN.test(x.title) || !VISIBLE.test(x.title) || !Number.isSafeInteger(x.expiresAt) || x.expiresAt < 0) return null;
20
- graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" });
21
- let count = 0; for (const _part of graphemes.segment(x.title)) if (++count > 40) return null;
22
- if (count < 1) return null;
23
- return Object.freeze({ v: 1, offer: x.offer, id: x.id, title: x.title, expiresAt: x.expiresAt });
24
- } catch { return null; }
25
- }
26
- /** Role and sender are host fields, never inferred from a message's text. Start must separately authorize. */
27
- export function effectTrialMessage(message) {
28
- if (!message || message.role !== "status" || message.sender !== "agend" || typeof message.instance !== "string"
29
- || message.instance.length < 1 || message.instance.length > 128) return null;
30
- return checkEffectTrialOffer(message.effectTrial);
31
- }
1
+ // On-only Canvas Worker display validation; ordinary chat boot imports no parser.
2
+ export { checkEffectTrialOffer, effectTrialMessage } from "/assets/effect-display-metadata.js";
@@ -1,70 +1,2 @@
1
- // #2193: the web effects runtime stays unloaded until there is something it could play for this viewer. Before that,
2
- // this module is all there is: a few store subscriptions comparing fields the page already has (no observer, listener,
3
- // timer, layer or request). The runtime itself (app-effects.js and everything it imports) is the reviewed code,
4
- // unchanged; it is imported and started here, once, the first time one of these holds:
5
- // - the open chat has Canvas Worker placements: its status row's effect_chat_v (the hash of its placements, taken
6
- // before the server's region filtering — so it can only wake for nothing, never miss something) is not the hash of
7
- // an empty list;
8
- // - the signed-in person has a personal effect slot chosen (fx.page/header/sidebar/box/cursor — app-prefs);
9
- // - an agent's "Try" offer is shown (wakeEffects() from the chat), or Settings saved a personal effect.
10
- // Once started it stays for the page: the runtime is one episode per window (a disposed one cannot start again).
11
- // Owner CSS is not the runtime's: owner-css.js is installed by the app shell and works with the runtime never loaded.
12
- import { appStore } from "/assets/app-store.js";
13
- import { navStore } from "/assets/app-nav.js";
14
- import { ownerEffectsChosen, onOwnerEffectsChanged } from "/assets/app-prefs.js";
15
-
16
- /** effect_chat_v of a chat with no placements: sha256(JSON.stringify([])), first 32 hex (fleet-manager's status row). */
17
- export const EMPTY_CHAT_EFFECTS = "4f53cda18c2baa0c0354bb5f9a3ecbe5";
18
-
19
- let mod = null, waking = null, chatStore = null, started = false;
20
- const awakeListeners = new Set();
21
-
22
- /** Whether `name`'s chat has Canvas Worker placements (as the status rows say). */
23
- export function chatHasEffects(name, app = appStore.get()) {
24
- const v = name ? app.instances?.find(i => i.name === name)?.effect_chat_v : undefined;
25
- return typeof v === "string" && /^[a-f0-9]{32}$/.test(v) && v !== EMPTY_CHAT_EFFECTS;
26
- }
27
-
28
- /** The running effects owner, or null while dormant (or when it could not start: as getAppEffects always was). */
29
- export function getAppEffects(host) { return mod ? mod.getAppEffects(host) : null; }
30
-
31
- /** Whether the runtime has been asked to start on this page. */
32
- export function effectsAwake() { return waking !== null; }
33
- /** Whether its module has loaded (started, or found it could not: getAppEffects() then says which). */
34
- export function effectsLoaded() { return mod !== null; }
35
-
36
- /** Called once the runtime has started (or immediately when it already has). Returns the unsubscribe. */
37
- export function onEffectsAwake(fn) {
38
- if (typeof fn !== "function") return () => {};
39
- if (mod) { try { fn(); } catch { /* a listener's error is its own */ } return () => {}; }
40
- awakeListeners.add(fn); return () => awakeListeners.delete(fn);
41
- }
42
-
43
- /** Load and start the runtime (once per page). Resolves to the owner, or null when it could not start. */
44
- export function wakeEffects() {
45
- if (waking) return waking;
46
- waking = import("./app-effects.js").then(m => {
47
- mod = m;
48
- const owner = chatStore ? m.startAppEffects({ store: chatStore }) : null;
49
- for (const fn of [...awakeListeners]) { try { fn(); } catch { /* the rest still hear */ } }
50
- awakeListeners.clear();
51
- return owner;
52
- }, () => null);
53
- return waking;
54
- }
55
-
56
- /** Wake when the open chat has placements or the person has a personal effect. */
57
- function check() {
58
- if (waking) return;
59
- const route = navStore.get().route;
60
- if ((route?.panel === "chat" && chatHasEffects(route.instance)) || ownerEffectsChosen()) void wakeEffects();
61
- }
62
-
63
- /** Boot (once per page, with the Chat store the runtime binds to). Starts nothing but the watch. */
64
- export function startEffectsWaker({ store }) {
65
- chatStore = store;
66
- if (started) return;
67
- started = true;
68
- appStore.subscribe(check); navStore.subscribe(check); onOwnerEffectsChanged(check);
69
- check();
70
- }
1
+ // Compatibility entry for opted-in effect consumers. Product boot uses the neutral feature policy.
2
+ export { EMPTY_CHAT_EFFECTS, chatHasEffects, getAppEffects, effectsAwake, effectsLoaded, onEffectsAwake, wakeEffects, startEffectsWaker } from "/assets/feature-flags.js";
@@ -8,7 +8,7 @@ import { appStore, useStore } from "/assets/app-store.js";
8
8
  import { Dialog } from "/assets/ui-dialog.js";
9
9
  import { Icon } from "/assets/ui-icons.js";
10
10
  import { toast } from "/assets/ui-toast.js";
11
- import { StatusPill, answerBlocked, askText, boardApi, currentWork, errorText, followsSubs, moveCard, notesForPeople, relTime, wrapUpText } from "./fleet-task-common.js";
11
+ import { StatusPill, Who, answerBlocked, askText, boardApi, currentWork, errorText, followsSubs, moveCard, notesForPeople, relTime, wrapUpText } from "./fleet-task-common.js";
12
12
 
13
13
  register("taskcard", {
14
14
  en: {
@@ -132,7 +132,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
132
132
  }
133
133
 
134
134
  if (missing || !detail) {
135
- return html`<${Dialog} title=${id} onClose=${onClose} wide><p class="muted">${missing ? t("taskcard.missing", id) : t("taskcard.loading")}</p></${Dialog}>`;
135
+ return html`<${Dialog} title=${id} onClose=${onClose} wide drawer><p class="muted">${missing ? t("taskcard.missing", id) : t("taskcard.loading")}</p></${Dialog}>`;
136
136
  }
137
137
 
138
138
  const { card, parent, children } = detail;
@@ -166,7 +166,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
166
166
  } catch { /* no clipboard */ }
167
167
  }
168
168
 
169
- return html`<${Dialog} title=${`${card.id} · ${t(`tasks.${card.kind}`)}`} onClose=${onClose} wide label=${`${card.id} ${card.title}`}>
169
+ return html`<${Dialog} title=${`${card.id} · ${t(`tasks.${card.kind}`)}`} onClose=${onClose} wide drawer label=${`${card.id} ${card.title}`}>
170
170
  <div class="tc">
171
171
  <div class="tc-head">
172
172
  ${writable ? html`<${InlineTitle} value=${card.title} version=${card.version} onSave=${(title, base) => save({ title }, base)} />` : html`<h3 class="tc-title">${card.title}</h3>`}
@@ -195,7 +195,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
195
195
 
196
196
  <dl class="tc-meta">
197
197
  <dt>${t("taskcard.assignee")}</dt>
198
- <dd><span class="tb-who">${card.assignee || t("taskcard.nobody")}</span>
198
+ <dd><${Who} name=${card.assignee} empty=${t("taskcard.nobody")} />
199
199
  ${writable ? html`<select class="tb-select" value="\u0000pick" aria-label=${t("taskcard.assignee")} onChange=${(e) => { const v = e.target.value; e.target.value = "\u0000pick"; void chooseAssignee(v); }}>
200
200
  <option value="\u0000pick" disabled hidden>${t("taskcard.change")}</option>
201
201
  ${card.assignee ? html`<option value="\u0000none">${t("taskcard.unassign")}</option>` : null}
@@ -207,7 +207,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
207
207
  if (!now) return html`<span class="muted">${card.status === "review" ? t("tasks.review") : t("taskcard.nowNone")}</span>`;
208
208
  return now.cards.map((c) => html`<button key=${c.id} type="button" class="tc-now" onClick=${() => onOpen(c.id)} title=${c.title}>
209
209
  <span class=${`tb-now-label st-${now.tone}`}>${t(now.label)}</span><span class="tb-code">${c.id}</span><span class="tc-child-title">${c.title}</span>
210
- <span class="tb-who">${c.assignee || t("tasks.nobody")}</span></button>`);
210
+ <${Who} name=${c.assignee} empty=${t("tasks.nobody")} /></button>`);
211
211
  })()}</dd>` : null}
212
212
  <dt>${t("taskcard.dispatcher")}</dt>
213
213
  <dd><span class="tb-who" title=${t("taskcard.dispatcherHint")}>${!card.dispatcher ? t("taskcard.dispatcherNone")
@@ -1,9 +1,10 @@
1
1
  // The task board's shared pieces (Fleet › Tasks): its words, the numbers a project shows, the requests, asking for a
2
- // reason, moving a card, and the small marks (status pill, progress bar). fleet-tasks.js draws the board and
2
+ // reason, moving a card, and the small marks (status pill, progress bar, who). fleet-tasks.js draws the board and
3
3
  // fleet-task-card.js one card; both use these. No style attribute anywhere (the CSP): bars are SVG rects.
4
4
  import { html, useEffect, useState } from "/assets/app-html.js";
5
5
  import { t, register, lang } from "/assets/app-i18n.js";
6
- import { createStore, useStore } from "/assets/app-store.js";
6
+ import { appStore, createStore, useStore } from "/assets/app-store.js";
7
+ import { identityOf, paintIdentity, useIdentityColors } from "/assets/identity-color.js";
7
8
  import { Dialog } from "/assets/ui-dialog.js";
8
9
  import { toast } from "/assets/ui-toast.js";
9
10
 
@@ -14,7 +15,8 @@ register("tasks", {
14
15
  sizeS: "Small", sizeM: "Medium", sizeL: "Large", sizeNone: "Not sized",
15
16
  projects: "Projects", noProject: "No project", noProjectDesc: "Tasks the fleet opened outside any project",
16
17
  newProject: "New project", newMain: "Main task", archived: "Archived", archive: "Archive", restore: "Restore",
17
- rename: "Rename or describe", projectName: "Name", projectDesc: "Description", create: "Create", save: "Save", cancel: "Cancel",
18
+ rename: "Edit project", projectName: "Name", projectDesc: "Description",
19
+ projectLead: "Lead instance", projectLeadNone: "None — General", projectLeadHint: "Cards of this project that wait for you (to confirm, or blocked) get their buttons in the lead's channel, unless an instance handed them out (then in its channel).", projectLeadGone: "{0} (no longer in the fleet)", projectLeadChip: "lead: {0}", create: "Create", save: "Save", cancel: "Cancel",
18
20
  projectCreated: "Project created", projectSaved: "Project saved",
19
21
  noProjects: "No projects yet", noProjectsHint: "A project groups related work — create one, then open main tasks in it.",
20
22
  mainCount: "{0} main tasks", mainOne: "1 main task", waiting: "{0} waiting for you", blockedN: "{0} blocked", lastActivity: "Last change {0}",
@@ -56,7 +58,7 @@ register("tasks", {
56
58
  "err.too_long": "{0} is too long: {1} characters, at most {2}", "err.state_lines": "State is at most {0} lines (it has {1})",
57
59
  "err.acceptance_empty": "An acceptance item cannot be empty", "err.acceptance_max": "At most {0} acceptance items",
58
60
  "err.acceptance_no_item": "There is no acceptance item {0} (there are {1})", "err.project_not_found": "There is no project “{0}”",
59
- "err.project_name_empty": "A project needs a name", "err.project_name_digits": "A project's name cannot be only digits",
61
+ "err.project_name_empty": "A project needs a name", "err.project_name_digits": "A project's name cannot be only digits", "err.lead_not_instance": "The lead must be one of the fleet's instances",
60
62
  "err.project_name_taken": "There is already a project named “{0}”", "err.project_archived": "Project “{0}” is archived: it takes no new main tasks",
61
63
  "err.card_not_found": "There is no card {0}", "err.title_empty": "A card needs a title", "err.scope_main_only": "Scope is for main tasks only",
62
64
  "err.blocked_reason_not_blocked": "Only a blocked card has a blocked reason", "err.block_reason": "Blocking needs a reason: who has to answer what",
@@ -72,6 +74,13 @@ register("tasks", {
72
74
  "note.review_waiting": "{0} “{1}”: every sub task is done — it waits for your confirmation",
73
75
  "note.back_to": "{0} has unfinished sub tasks again: it is back to {1}", "note.deps_done": "{0}'s dependencies are all done: it can start",
74
76
  "note.deps_unmet": "Note: {0} not done yet",
77
+ // #2246 step 3: who works on what (fleet-task-work.js)
78
+ instances: "Instances", instancesTip: "The fleet's instances and what each works on", instancesHead: "{0} instance(s) · {1} working",
79
+ workSentBack: "Sent back", workDoing: "In progress", workBlocked: "Blocked", workReview: "Waiting for you", workMain: "Owns",
80
+ workReady: "Can start", workWaiting: "Waiting on", workSubs: "sub tasks {0}/{1}", workClaimable: "nobody's: can be taken",
81
+ workWaitsFor: "waits for {0}", workNone: "Nothing on the board", workNewComments: "Comments it has not read yet",
82
+ workOutside: "Not in the fleet", workFailed: "Could not read who works on what: {0}", workJoin: ", ",
83
+ detailsBoard: "Task board",
75
84
  },
76
85
  "zh-TW": {
77
86
  open: "待辦", claimed: "進行中", blocked: "卡住", review: "待確認", done: "完成", cancelled: "取消",
@@ -79,7 +88,8 @@ register("tasks", {
79
88
  sizeS: "小", sizeM: "中", sizeL: "大", sizeNone: "未定",
80
89
  projects: "專案", noProject: "未分類", noProjectDesc: "fleet 在專案以外開的 task",
81
90
  newProject: "新增專案", newMain: "主任務", archived: "已封存", archive: "封存", restore: "恢復",
82
- rename: "改名或改說明", projectName: "名稱", projectDesc: "說明", create: "建立", save: "儲存", cancel: "取消",
91
+ rename: "編輯專案", projectName: "名稱", projectDesc: "說明",
92
+ projectLead: "負責的 instance", projectLeadNone: "無(General)", projectLeadHint: "這個專案裡等你處理的卡片(待確認或卡住)會在負責 instance 的頻道顯示按鈕;若卡片是某個 instance 派出的,則顯示在派出者的頻道。", projectLeadGone: "{0}(已不在 fleet 中)", projectLeadChip: "負責:{0}", create: "建立", save: "儲存", cancel: "取消",
83
93
  projectCreated: "專案已建立", projectSaved: "專案已儲存",
84
94
  noProjects: "還沒有專案", noProjectsHint: "專案是一組相關的工作:先建立專案,再在裡面開主任務。",
85
95
  mainCount: "{0} 個主任務", mainOne: "1 個主任務", waiting: "等你確認 {0}", blockedN: "卡住 {0}", lastActivity: "最後動態 {0}",
@@ -121,7 +131,7 @@ register("tasks", {
121
131
  "err.too_long": "{0}太長:{1} 字,上限 {2} 字", "err.state_lines": "目前狀態最多 {0} 行(現在 {1} 行)",
122
132
  "err.acceptance_empty": "驗收條件不能是空的", "err.acceptance_max": "驗收條件最多 {0} 項",
123
133
  "err.acceptance_no_item": "驗收條件沒有第 {0} 項(共 {1} 項)", "err.project_not_found": "找不到專案「{0}」",
124
- "err.project_name_empty": "專案名稱不能是空的", "err.project_name_digits": "專案名稱不能只有數字",
134
+ "err.project_name_empty": "專案名稱不能是空的", "err.project_name_digits": "專案名稱不能只有數字", "err.lead_not_instance": "負責人必須是 fleet 裡的 instance",
125
135
  "err.project_name_taken": "已經有專案叫「{0}」", "err.project_archived": "專案「{0}」已封存,不能再開主任務",
126
136
  "err.card_not_found": "找不到 {0}", "err.title_empty": "標題不能是空的", "err.scope_main_only": "範圍只用在主任務",
127
137
  "err.blocked_reason_not_blocked": "只有卡住的卡才有卡住原因", "err.block_reason": "卡住要寫原因:需要誰回答什麼",
@@ -137,6 +147,13 @@ register("tasks", {
137
147
  "note.review_waiting": "{0}「{1}」的子任務都完成了,等你確認",
138
148
  "note.back_to": "{0} 又有沒完成的子任務,已回到{1}", "note.deps_done": "{0} 的相依都完成了,可以開始",
139
149
  "note.deps_unmet": "提醒:{0} 還沒完成",
150
+ // #2246 step 3:誰在做什麼(fleet-task-work.js)
151
+ instances: "Instance", instancesTip: "fleet 有哪些 instance、各在做什麼", instancesHead: "{0} 個 · 工作中 {1}",
152
+ workSentBack: "被退回", workDoing: "進行中", workBlocked: "卡住", workReview: "待確認", workMain: "負責",
153
+ workReady: "可以開始", workWaiting: "等前置", workSubs: "子任務 {0}/{1}", workClaimable: "沒人負責,可領",
154
+ workWaitsFor: "等 {0}", workNone: "看板上沒有它的卡", workNewComments: "有它還沒看過的留言",
155
+ workOutside: "不在 fleet 裡", workFailed: "讀不到工作清單:{0}", workJoin: "、",
156
+ detailsBoard: "任務看板",
140
157
  },
141
158
  });
142
159
 
@@ -312,7 +329,9 @@ export const boardApi = {
312
329
  move: (id, to, input = {}) => boardCall("POST", `/ui/tasks/cards/${enc(id)}/move`, { to, ...input }),
313
330
  reorder: (id, after, before) => boardCall("POST", `/ui/tasks/cards/${enc(id)}/reorder`, { after, before }),
314
331
  search: (q) => boardCall("GET", `/ui/tasks/search?q=${enc(q)}`),
315
- createProject: (name, description) => boardCall("POST", "/ui/tasks/projects", { name, description }),
332
+ /** #2246 step 3: each instance's work (its `task mine`), or one instance's. */
333
+ work: (instance) => boardCall("GET", `/ui/tasks/work${instance ? `?instance=${enc(instance)}` : ""}`),
334
+ createProject: (name, description, leadInstance) => boardCall("POST", "/ui/tasks/projects", { name, description, ...(leadInstance ? { leadInstance } : {}) }),
316
335
  updateProject: (id, patch) => boardCall("POST", `/ui/tasks/projects/${id}`, patch),
317
336
  };
318
337
 
@@ -439,6 +458,21 @@ export function StatusPill({ status }) {
439
458
  return html`<span class=${`tb-pill st-${status}`}>${t(`tasks.${status}`)}</span>`;
440
459
  }
441
460
 
461
+ /**
462
+ * Who a card is with, as taskhall shows it: a round initial, then the name. An instance's initial takes its identity
463
+ * colour (#1958, the sidebar's), painted through the CSSOM; a person's, or anyone not in the fleet, stays grey.
464
+ */
465
+ export function Who({ name, title, empty = "—" }) {
466
+ const { instances } = useStore(appStore);
467
+ useIdentityColors();
468
+ if (!name) return html`<span class="tb-person none"><span class="tb-who">${empty}</span></span>`;
469
+ const it = (instances || []).find((i) => i.name === name);
470
+ const id = it ? identityOf(it) : null;
471
+ return html`<span class="tb-person" title=${title || name}>
472
+ <span class="tb-av idc" aria-hidden="true" ref=${(el) => paintIdentity(el, id)}>${initialOf(name)}</span><span class="tb-who">${name}</span></span>`;
473
+ }
474
+ const initialOf = (name) => (Array.from(name.trim())[0] || "?").toLocaleUpperCase();
475
+
442
476
  /** A per-device preference (localStorage may be unavailable: then nothing is remembered). */
443
477
  export function readStored(key, fallback) {
444
478
  try { const v = localStorage.getItem(key); return v === null ? fallback : v; } catch { return fallback; }
@@ -0,0 +1,107 @@
1
+ // #2246 step 3: who works on what on the task board — each instance's cards as its `task mine` reads them (GET
2
+ // /ui/tasks/work, the server's myWork): the board's Instance list, a drawer by team as taskhall's, and the block on an
3
+ // instance's Details page. A card opens on the board. No style attribute anywhere (the CSP).
4
+ import { html, useEffect, useState } from "/assets/app-html.js";
5
+ import { t } from "/assets/app-i18n.js";
6
+ import { appStore, useStore } from "/assets/app-store.js";
7
+ import { Dialog } from "/assets/ui-dialog.js";
8
+ import { Icon } from "/assets/ui-icons.js";
9
+ import { Skeleton } from "/assets/ui-states.js";
10
+ import { GroupMark, InstanceAvatar, dotLabel, dotState, groupKind, groupLabel, groupOf, namesOf, namesStore } from "/assets/instance-nav.js";
11
+ import { Who, boardApi, errorText } from "./fleet-task-common.js";
12
+
13
+ const LABEL = { back: "tasks.workSentBack", doing: "tasks.workDoing", blocked: "tasks.workBlocked", review: "tasks.workReview", main: "tasks.workMain", ready: "tasks.workReady", waiting: "tasks.workWaiting" };
14
+
15
+ /**
16
+ * An instance's work as lines, in taskhall's order: what it works on, is blocked on, waits on the person for, the main
17
+ * tasks it coordinates, what it can start, what waits for other cards. A card sent back to it says so instead.
18
+ */
19
+ export function workLines(w) {
20
+ const back = new Set(w.sentBack || []), unread = new Set(w.newComments || []);
21
+ const line = (kind, card, note = "") => ({ kind: kind !== "main" && back.has(card.id) ? "back" : kind, card, note, unread: unread.has(card.id) });
22
+ return [
23
+ ...(w.doing || []).map((c) => line("doing", c)),
24
+ ...(w.blocked || []).map((c) => line("blocked", c)),
25
+ ...(w.review || []).map((c) => line("review", c)),
26
+ ...(w.mains || []).map((c) => line("main", c, c.progress && c.progress.total ? t("tasks.workSubs", c.progress.done, c.progress.total) : "")),
27
+ ...(w.ready || []).map((c) => line("ready", c, c.assignee ? "" : t("tasks.workClaimable"))),
28
+ ...(w.waiting || []).map((c) => line("waiting", c, t("tasks.workWaitsFor", (c.unmetDeps || []).join(t("tasks.workJoin"))))),
29
+ ];
30
+ }
31
+
32
+ export function WorkLines({ lines, onOpen }) {
33
+ if (!lines.length) return html`<p class="muted tw-none">${t("tasks.workNone")}</p>`;
34
+ return html`<ul class="tw-lines">${lines.map((l) => html`<li key=${`${l.kind}:${l.card.id}`}>
35
+ <button type="button" class="tw-line" title=${`${l.card.id} ${l.card.title}`} onClick=${() => onOpen(l.card.id)}>
36
+ <span class=${`tw-kind k-${l.kind}`}>${t(LABEL[l.kind])}</span><span class="tb-code">${l.card.id}</span><span class="tw-title">${l.card.title}</span>
37
+ ${l.note ? html`<span class="muted tw-note">${l.note}</span>` : null}
38
+ ${l.unread ? html`<span class="tw-unread" title=${t("tasks.workNewComments")} aria-label=${t("tasks.workNewComments")}><${Icon} name="chat" size=${13} /></span>` : null}
39
+ </button></li>`)}</ul>`;
40
+ }
41
+
42
+ const OUTSIDE = "\u0000outside";
43
+ /** Groups as the sidebar orders them: teams, then tags, then the rest; names nobody in the fleet has last. */
44
+ const groupRank = (g) => (g === OUTSIDE ? 3 : groupKind(g) === "team" ? 0 : groupKind(g) === "tag" ? 1 : 2);
45
+
46
+ /** The board's Instance list: every instance by team, working ones first, each with its cards. Read again on each board change. */
47
+ export function InstancesDrawer({ tick, onClose, onOpen }) {
48
+ const { instances, exec, awaiting } = useStore(appStore);
49
+ useStore(namesStore); // the sidebar's Names choice: display name or instance name first
50
+ const [work, setWork] = useState(null);
51
+ const [error, setError] = useState("");
52
+ useEffect(() => {
53
+ let live = true;
54
+ boardApi.work().then((d) => { if (live) { setWork(d.instances || []); setError(""); } }, (e) => { if (live) setError(errorText(e)); });
55
+ return () => { live = false; };
56
+ }, [tick]);
57
+ const byName = new Map((instances || []).map((i) => [i.name, i]));
58
+ const rows = (work || []).map((w) => {
59
+ const it = byName.get(w.name);
60
+ return { w, it, state: it ? dotState(it, exec[w.name], awaiting[w.name]) : null, lines: workLines(w) };
61
+ });
62
+ // Working first, then those with cards, then the running ones; by name.
63
+ const rank = (r) => (r.state === "working" ? 0 : r.lines.length ? 1 : r.it && r.it.status === "running" ? 2 : 3);
64
+ rows.sort((a, b) => rank(a) - rank(b) || a.w.name.localeCompare(b.w.name));
65
+ const groups = new Map();
66
+ for (const r of rows) {
67
+ const g = r.it ? groupOf(r.it) : OUTSIDE;
68
+ if (!groups.has(g)) groups.set(g, []);
69
+ groups.get(g).push(r);
70
+ }
71
+ const ordered = [...groups].sort(([a], [b]) => groupRank(a) - groupRank(b));
72
+ const working = rows.filter((r) => r.state === "working").length;
73
+ return html`<${Dialog} title=${t("tasks.instances")} onClose=${onClose} wide drawer>
74
+ <div class="tw">
75
+ ${work ? html`<p class="muted tw-head">${t("tasks.instancesHead", rows.length, working)}</p>` : null}
76
+ ${error ? html`<p class="tw-error" role="alert">${t("tasks.workFailed", error)}</p>` : null}
77
+ ${!work && !error ? html`<${Skeleton} lines=${4} />` : null}
78
+ ${ordered.map(([g, list]) => html`<section key=${g} class="tw-group">
79
+ <h3 class="tw-group-head">${g === OUTSIDE ? t("tasks.workOutside") : html`<${GroupMark} group=${g} />${groupLabel(g)}`}<span class="tb-count">${list.length}</span></h3>
80
+ ${list.map((r) => html`<div key=${r.w.name} class="tw-inst">
81
+ <div class="tw-inst-head">
82
+ ${r.it ? html`<${InstanceAvatar} it=${r.it} state=${r.state} />` : html`<${Who} name=${r.w.name} />`}
83
+ ${r.it ? (() => { const n = namesOf(r.it); return html`<span class="tw-name" title=${r.w.name}>${n.primary}</span>${n.secondary ? html`<span class="muted tw-alt">${n.secondary}</span>` : null}`; })() : null}
84
+ ${r.state ? html`<span class="muted tw-state">${dotLabel(r.state)}</span>` : null}
85
+ </div>
86
+ ${r.it && r.it.description ? html`<p class="muted tw-desc">${r.it.description}</p>` : null}
87
+ <${WorkLines} lines=${r.lines} onOpen=${onOpen} />
88
+ </div>`)}
89
+ </section>`)}
90
+ </div>
91
+ </${Dialog}>`;
92
+ }
93
+
94
+ /** An instance's Details page: its cards, read once when the page opens (the page reads nothing recurring, #1374). */
95
+ export function InstanceBoardWork({ name, onOpen }) {
96
+ const [work, setWork] = useState(null);
97
+ const [error, setError] = useState("");
98
+ useEffect(() => {
99
+ let live = true;
100
+ setWork(null); setError("");
101
+ boardApi.work(name).then((d) => { if (live) setWork((d.instances || [])[0] || { name }); }, (e) => { if (live) setError(errorText(e)); });
102
+ return () => { live = false; };
103
+ }, [name]);
104
+ return html`<section class="card d-board"><h3>${t("tasks.detailsBoard")}</h3>
105
+ ${error ? html`<p class="note">${t("tasks.workFailed", error)}</p>` : !work ? html`<${Skeleton} lines=${2} />` : html`<${WorkLines} lines=${workLines(work)} onOpen=${onOpen} />`}
106
+ </section>`;
107
+ }