@songsid/agend 2.3.0-beta.21 → 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 (300) 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 +9 -0
  6. package/dist/agent-endpoint.js +15 -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 +10 -3
  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/grok.js +56 -3
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +9 -1
  23. package/dist/backend/kiro.js +102 -1
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/opencode-permission.js +1 -1
  26. package/dist/backend/opencode-permission.js.map +1 -1
  27. package/dist/backend/opencode.js +2 -1
  28. package/dist/backend/opencode.js.map +1 -1
  29. package/dist/binary-probe.js +1 -1
  30. package/dist/bug-report-cli.js +1 -1
  31. package/dist/bundled-skills.d.ts +7 -3
  32. package/dist/bundled-skills.js +16 -7
  33. package/dist/bundled-skills.js.map +1 -1
  34. package/dist/channel/adapters/discord.js +3 -3
  35. package/dist/channel/adapters/discord.js.map +1 -1
  36. package/dist/channel/mcp-server.js +12 -4
  37. package/dist/channel/mcp-server.js.map +1 -1
  38. package/dist/channel/mcp-tools.d.ts +3 -1
  39. package/dist/channel/mcp-tools.js +38 -2
  40. package/dist/channel/mcp-tools.js.map +1 -1
  41. package/dist/child-process.d.ts +21 -0
  42. package/dist/child-process.js +54 -0
  43. package/dist/child-process.js.map +1 -0
  44. package/dist/cli.js +167 -182
  45. package/dist/cli.js.map +1 -1
  46. package/dist/command-table.d.ts +1 -1
  47. package/dist/command-table.js +1 -1
  48. package/dist/command-table.js.map +1 -1
  49. package/dist/config-validator.js +28 -2
  50. package/dist/config-validator.js.map +1 -1
  51. package/dist/config.d.ts +2 -2
  52. package/dist/config.js +16 -2
  53. package/dist/config.js.map +1 -1
  54. package/dist/daemon.d.ts +14 -0
  55. package/dist/daemon.js +45 -8
  56. package/dist/daemon.js.map +1 -1
  57. package/dist/darwin-memory.js +1 -1
  58. package/dist/doctor.d.ts +5 -0
  59. package/dist/doctor.js +42 -0
  60. package/dist/doctor.js.map +1 -1
  61. package/dist/export-import.js +1 -1
  62. package/dist/export-import.js.map +1 -1
  63. package/dist/fleet-level-config.d.ts +1 -1
  64. package/dist/fleet-level-config.js +2 -0
  65. package/dist/fleet-level-config.js.map +1 -1
  66. package/dist/fleet-manager.d.ts +73 -2
  67. package/dist/fleet-manager.js +400 -33
  68. package/dist/fleet-manager.js.map +1 -1
  69. package/dist/full-restart.d.ts +5 -5
  70. package/dist/full-restart.js +5 -5
  71. package/dist/full-restart.js.map +1 -1
  72. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  73. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +8 -9
  74. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  75. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  76. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  77. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +19 -4
  78. package/dist/general-knowledge/steering/core-rules.md +1 -1
  79. package/dist/instance-config-impact.js +13 -3
  80. package/dist/instance-config-impact.js.map +1 -1
  81. package/dist/instance-lifecycle.js +6 -6
  82. package/dist/instance-lifecycle.js.map +1 -1
  83. package/dist/instructions.js +2 -1
  84. package/dist/instructions.js.map +1 -1
  85. package/dist/locale.js +137 -10
  86. package/dist/locale.js.map +1 -1
  87. package/dist/login-flows.js +1 -1
  88. package/dist/login-shell.js +3 -1
  89. package/dist/login-shell.js.map +1 -1
  90. package/dist/ls-rows.js +1 -1
  91. package/dist/needs-you-hub.d.ts +98 -1
  92. package/dist/needs-you-hub.js +313 -44
  93. package/dist/needs-you-hub.js.map +1 -1
  94. package/dist/needs-you-live.d.ts +5 -2
  95. package/dist/needs-you-live.js +6 -3
  96. package/dist/needs-you-live.js.map +1 -1
  97. package/dist/needs-you.d.ts +58 -3
  98. package/dist/needs-you.js +36 -3
  99. package/dist/needs-you.js.map +1 -1
  100. package/dist/outbound-handlers.js +25 -10
  101. package/dist/outbound-handlers.js.map +1 -1
  102. package/dist/outbound-schemas.d.ts +3 -3
  103. package/dist/provider-secret-registry.d.ts +5 -3
  104. package/dist/provider-secret-registry.js +8 -2
  105. package/dist/provider-secret-registry.js.map +1 -1
  106. package/dist/public-link-progress.d.ts +12 -1
  107. package/dist/public-link-progress.js +5 -10
  108. package/dist/public-link-progress.js.map +1 -1
  109. package/dist/public-tunnel-config.d.ts +11 -0
  110. package/dist/public-tunnel-config.js +68 -0
  111. package/dist/public-tunnel-config.js.map +1 -0
  112. package/dist/public-web-gateway.d.ts +2 -0
  113. package/dist/public-web-gateway.js +3 -2
  114. package/dist/public-web-gateway.js.map +1 -1
  115. package/dist/public-web-link.d.ts +13 -2
  116. package/dist/public-web-link.js +37 -10
  117. package/dist/public-web-link.js.map +1 -1
  118. package/dist/quickstart.js +14 -8
  119. package/dist/quickstart.js.map +1 -1
  120. package/dist/resource-report.js +1 -1
  121. package/dist/scheduler/task-board.d.ts +41 -1
  122. package/dist/scheduler/task-board.js +150 -4
  123. package/dist/scheduler/task-board.js.map +1 -1
  124. package/dist/sd-notify.js +2 -1
  125. package/dist/sd-notify.js.map +1 -1
  126. package/dist/secret-store.d.ts +2 -0
  127. package/dist/secret-store.js +21 -0
  128. package/dist/secret-store.js.map +1 -1
  129. package/dist/service-installer.d.ts +69 -0
  130. package/dist/service-installer.js +181 -0
  131. package/dist/service-installer.js.map +1 -1
  132. package/dist/service-owner.d.ts +44 -0
  133. package/dist/service-owner.js +48 -0
  134. package/dist/service-owner.js.map +1 -0
  135. package/dist/settings-api.d.ts +5 -0
  136. package/dist/settings-api.js +32 -10
  137. package/dist/settings-api.js.map +1 -1
  138. package/dist/settings-change.js +86 -3
  139. package/dist/settings-change.js.map +1 -1
  140. package/dist/settings-effect.js +6 -1
  141. package/dist/settings-effect.js.map +1 -1
  142. package/dist/settings-http-confirmation.d.ts +2 -0
  143. package/dist/settings-http-confirmation.js +4 -3
  144. package/dist/settings-http-confirmation.js.map +1 -1
  145. package/dist/settings-request-capability.d.ts +4 -1
  146. package/dist/settings-request-capability.js +11 -3
  147. package/dist/settings-request-capability.js.map +1 -1
  148. package/dist/setup-host.js +1 -1
  149. package/dist/setup-wizard.js +1 -1
  150. package/dist/setup-wizard.js.map +1 -1
  151. package/dist/task-board-summary.d.ts +101 -0
  152. package/dist/task-board-summary.js +177 -0
  153. package/dist/task-board-summary.js.map +1 -0
  154. package/dist/task-board-waker.d.ts +8 -1
  155. package/dist/task-board-waker.js +15 -6
  156. package/dist/task-board-waker.js.map +1 -1
  157. package/dist/tmux-control.js +1 -1
  158. package/dist/tmux-control.js.map +1 -1
  159. package/dist/tmux-exec-helper.d.ts +5 -1
  160. package/dist/tmux-exec-helper.js +13 -21
  161. package/dist/tmux-exec-helper.js.map +1 -1
  162. package/dist/tmux-manager.js +1 -1
  163. package/dist/tmux-read.js +1 -1
  164. package/dist/tmux-read.js.map +1 -1
  165. package/dist/topic-commands.d.ts +3 -0
  166. package/dist/topic-commands.js +29 -18
  167. package/dist/topic-commands.js.map +1 -1
  168. package/dist/transcript-monitor.js +5 -0
  169. package/dist/transcript-monitor.js.map +1 -1
  170. package/dist/transcript-sources.d.ts +2 -0
  171. package/dist/transcript-sources.js +4 -0
  172. package/dist/transcript-sources.js.map +1 -1
  173. package/dist/tunnel/cloudflared-install.js +3 -1
  174. package/dist/tunnel/cloudflared-install.js.map +1 -1
  175. package/dist/tunnel/cloudflared.d.ts +7 -16
  176. package/dist/tunnel/cloudflared.js +65 -13
  177. package/dist/tunnel/cloudflared.js.map +1 -1
  178. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  179. package/dist/tunnel/named-diagnostics.js +52 -0
  180. package/dist/tunnel/named-diagnostics.js.map +1 -0
  181. package/dist/tunnel/types.d.ts +2 -1
  182. package/dist/tunnel/types.js.map +1 -1
  183. package/dist/types.d.ts +12 -0
  184. package/dist/ui/app-effects.js +10 -4
  185. package/dist/ui/app.html +2 -0
  186. package/dist/ui/chat-render.js +39 -0
  187. package/dist/ui/chat-store.js +8 -0
  188. package/dist/ui/chat-strings.js +5 -2
  189. package/dist/ui/chat-thread.js +15 -8
  190. package/dist/ui/effect-trial-offer.js +2 -31
  191. package/dist/ui/effects-waker.js +2 -70
  192. package/dist/ui/fleet-canvas.js +14 -6
  193. package/dist/ui/fleet-route-scene.js +86 -21
  194. package/dist/ui/fleet-task-card.js +5 -5
  195. package/dist/ui/fleet-task-common.js +41 -7
  196. package/dist/ui/fleet-task-work.js +107 -0
  197. package/dist/ui/fleet-tasks.js +46 -12
  198. package/dist/ui/fleet-traffic.js +57 -10
  199. package/dist/ui/icons/icon-192.png +0 -0
  200. package/dist/ui/icons/icon-512.png +0 -0
  201. package/dist/ui/image-lightbox.js +39 -1
  202. package/dist/ui/image-zoom.js +242 -0
  203. package/dist/ui/panel-chat.js +24 -13
  204. package/dist/ui/panel-details.js +2 -0
  205. package/dist/ui/panel-fleet.js +6 -2
  206. package/dist/ui/panel-needs.js +21 -6
  207. package/dist/ui/panel-settings.js +90 -15
  208. package/dist/ui/preview-panel.js +2 -1
  209. package/dist/ui/search-dialog.js +2 -1
  210. package/dist/ui/settings-appearance.js +159 -9
  211. package/dist/ui/settings-dialogs.js +33 -17
  212. package/dist/ui/settings-discord.js +6 -2
  213. package/dist/ui/settings-guests.js +2 -1
  214. package/dist/ui/settings-learn.js +1 -0
  215. package/dist/ui/settings-model.js +2 -11
  216. package/dist/ui/settings-mods.js +6 -3
  217. package/dist/ui/settings-strings.js +80 -0
  218. package/dist/ui/settings-token.js +3 -1
  219. package/dist/ui/settings-web-authority.js +6 -3
  220. package/dist/ui/settings-wizard.js +13 -7
  221. package/dist/ui/shared/app-i18n.js +2 -2
  222. package/dist/ui/shared/app-needs.js +2 -2
  223. package/dist/ui/shared/app-store.js +2 -2
  224. package/dist/ui/shared/app-stream.js +2 -1
  225. package/dist/ui/shared/app.css +125 -28
  226. package/dist/ui/shared/app.js +5 -1
  227. package/dist/ui/shared/effect-app-runtime.js +7 -5
  228. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  229. package/dist/ui/shared/effect-cursor.js +3 -2
  230. package/dist/ui/shared/effect-display-metadata.js +39 -0
  231. package/dist/ui/shared/effect-registry.js +4 -2
  232. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  233. package/dist/ui/shared/effect-stock-trust.js +1 -1
  234. package/dist/ui/shared/feature-flags.js +127 -0
  235. package/dist/ui/shared/identity-color.js +6 -3
  236. package/dist/ui/shared/instance-nav.js +3 -2
  237. package/dist/ui/shared/text-undo.js +210 -0
  238. package/dist/ui/shared/ui-dialog.js +4 -3
  239. package/dist/ui/shared/ui-icons.js +2 -0
  240. package/dist/ui/signin.html +2 -0
  241. package/dist/update-dispatch.js +1 -1
  242. package/dist/update-launch.d.ts +1 -1
  243. package/dist/update-launch.js +1 -2
  244. package/dist/update-launch.js.map +1 -1
  245. package/dist/usage/providers.js +1 -1
  246. package/dist/usage/providers.js.map +1 -1
  247. package/dist/view-api.js +1 -1
  248. package/dist/web-api.d.ts +12 -2
  249. package/dist/web-api.js +266 -18
  250. package/dist/web-api.js.map +1 -1
  251. package/dist/web-app-icon-worker.d.ts +1 -0
  252. package/dist/web-app-icon-worker.js +15 -0
  253. package/dist/web-app-icon-worker.js.map +1 -0
  254. package/dist/web-app-icon.d.ts +107 -0
  255. package/dist/web-app-icon.js +256 -0
  256. package/dist/web-app-icon.js.map +1 -0
  257. package/dist/web-appearance-tools.d.ts +1 -0
  258. package/dist/web-appearance-tools.js +7 -3
  259. package/dist/web-appearance-tools.js.map +1 -1
  260. package/dist/web-appearance.d.ts +4 -10
  261. package/dist/web-appearance.js +4 -3
  262. package/dist/web-appearance.js.map +1 -1
  263. package/dist/web-chat-history.d.ts +37 -0
  264. package/dist/web-chat-history.js +57 -2
  265. package/dist/web-chat-history.js.map +1 -1
  266. package/dist/web-experimental-http.d.ts +2 -0
  267. package/dist/web-experimental-http.js +12 -0
  268. package/dist/web-experimental-http.js.map +1 -0
  269. package/dist/web-experimental.d.ts +11 -0
  270. package/dist/web-experimental.js +49 -0
  271. package/dist/web-experimental.js.map +1 -0
  272. package/dist/web-guest-access.d.ts +1 -0
  273. package/dist/web-guest-access.js +11 -3
  274. package/dist/web-guest-access.js.map +1 -1
  275. package/dist/web-icons.d.ts +33 -1
  276. package/dist/web-icons.js +83 -11
  277. package/dist/web-icons.js.map +1 -1
  278. package/dist/web-mod-store.d.ts +2 -0
  279. package/dist/web-mod-store.js +10 -0
  280. package/dist/web-mod-store.js.map +1 -1
  281. package/dist/web-png.d.ts +29 -0
  282. package/dist/web-png.js +321 -0
  283. package/dist/web-png.js.map +1 -0
  284. package/dist/web-preview.d.ts +2 -0
  285. package/dist/web-preview.js +2 -0
  286. package/dist/web-preview.js.map +1 -1
  287. package/dist/web-reactions.d.ts +34 -0
  288. package/dist/web-reactions.js +47 -0
  289. package/dist/web-reactions.js.map +1 -0
  290. package/dist/web-settings-patch.d.ts +2 -1
  291. package/dist/web-settings-patch.js +5 -1
  292. package/dist/web-settings-patch.js.map +1 -1
  293. package/dist/web-settings-tools.d.ts +1 -0
  294. package/dist/web-settings-tools.js +14 -10
  295. package/dist/web-settings-tools.js.map +1 -1
  296. package/dist/web-terminal.js +1 -1
  297. package/dist/web-theme-store.d.ts +2 -2
  298. package/dist/web-theme-store.js +6 -3
  299. package/dist/web-theme-store.js.map +1 -1
  300. package/package.json +1 -1
@@ -28,7 +28,7 @@ import { avatarOf, backendLabel, firstCharacter, namesOf } from "/assets/instanc
28
28
  import { identityClasses, identityOf, paintIdentity, useIdentityColors } from "/assets/identity-color.js";
29
29
  import { buildOrgView, OrgChart, OrgSummary } from "./fleet-org.js";
30
30
  import { OC, REST_KEY, SUM_LINE, canvasLayout, openingRegion, summaryRows } from "./fleet-canvas-layout.js";
31
- import { createTrafficStore, FAR_K, POLL_MS, sendingBy, trafficEdges } from "./fleet-traffic.js";
31
+ import { createTrafficStore, FAR_K, HEAD, POLL_MS, sendingBy, trafficEdges } from "./fleet-traffic.js";
32
32
  import { routeScene } from "./fleet-route-scene.js";
33
33
  import { createRouteClient } from "./fleet-route.js";
34
34
 
@@ -207,7 +207,8 @@ function TrafficLayer({ edges, q }) {
207
207
  const dim = q && !e.names.some((n) => n.toLowerCase().includes(q));
208
208
  return html`<g key=${e.key} class=${`oc-fl${dim ? " dim" : ""}`} data-routed=${e.routed ? "1" : undefined}>
209
209
  <path class="oc-flow-halo" d=${e.d} />
210
- <path class=${`oc-flow h-${e.hue} ph-${e.phase}`} d=${e.d} marker-start=${`url(#oc-src-${MARKER(e)})`} marker-end=${`url(#oc-ah-${MARKER(e)})`} />
210
+ <path class=${`oc-flow h-${e.hue} ph-${e.phase}`} d=${e.d} stroke-width=${e.w} marker-start=${`url(#oc-src-${MARKER(e)})`}
211
+ marker-end=${e.head ? `url(#oc-ah-${MARKER(e)})` : undefined} />
211
212
  ${e.phase === "live" && !still ? html`<circle class=${`oc-pulse h-${e.hue}`} r="4"><animateMotion dur=${`${Math.max(1.6, e.pts.length * 0.3)}s`} repeatCount="indefinite" path=${e.d} /></circle>` : null}
212
213
  ${e.phase === "return" && !still ? html`<circle class=${`oc-pulse h-${e.hue}`} r="5"><animateMotion dur="1.6s" repeatCount="1" fill="freeze" path=${e.d} /></circle>` : null}
213
214
  ${e.badge ? html`<g class="oc-badge" transform=${`translate(${e.badge.x - 14} ${e.badge.y - 10})`}><rect width="28" height="20" rx="10" /><text x="14" y="14" text-anchor="middle">${`×${e.n}`}</text></g>` : null}
@@ -215,6 +216,13 @@ function TrafficLayer({ edges, q }) {
215
216
  })}</g>`;
216
217
  }
217
218
  const ARROWS = ["task", "send", "report", "broadcast", "team", "fading"];
219
+ /**
220
+ * The head (#2329), in a 10-unit box that is HEAD.LEN stroke widths each way: a slim swept triangle HEAD.WIDE across,
221
+ * its tip at x 10; the path ends HEAD.BACK stroke widths before the tip (refX), so the line's stroke ends inside it.
222
+ */
223
+ const HEAD_HALF = (5 * HEAD.WIDE) / HEAD.LEN;
224
+ const HEAD_D = `M0 ${5 - HEAD_HALF} L10 5 L0 ${5 + HEAD_HALF} L2.5 5 z`;
225
+ const HEAD_REF_X = 10 - (10 * HEAD.BACK) / HEAD.LEN;
218
226
 
219
227
  /**
220
228
  * The key to the lines (#2052): a **Key** toggle in the toolbar; open, its rows are a strip between the toolbar and the
@@ -439,10 +447,10 @@ function Canvas({ view, folded, onFold, q, jump, traffic }) {
439
447
  <svg ref=${svgRef} class="oc-svg" role="application" tabindex="0" aria-label=${tc("label")} onWheel=${onWheel} onPointerDown=${onPointerDown}
440
448
  onPointerMove=${onPointerMove} onPointerUp=${onPointerUp} onPointerCancel=${onPointerUp} onKeyDown=${onKeyDown} onFocusIn=${onFocusIn} onClickCapture=${onClickCapture}>
441
449
  <defs><clipPath id="oc-clip-av" clipPathUnits="objectBoundingBox"><circle cx="0.5" cy="0.5" r="0.5" /></clipPath>
442
- ${ARROWS.map((a) => html`<marker key=${a} id=${`oc-ah-${a}`} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto">
443
- <path d="M0 0 L10 5 L0 10 z" class=${`oc-ah h-${a}`} /></marker>`)}
444
- ${ARROWS.map((a) => html`<marker key=${`s-${a}`} id=${`oc-src-${a}`} viewBox="0 0 10 10" refX="5" refY="5" markerWidth="5" markerHeight="5">
445
- <circle cx="5" cy="5" r="4" class=${`oc-ah h-${a}`} /></marker>`)}</defs>
450
+ ${ARROWS.map((a) => html`<marker key=${a} id=${`oc-ah-${a}`} viewBox="0 0 10 10" refX=${HEAD_REF_X} refY="5" markerWidth=${HEAD.LEN} markerHeight=${HEAD.LEN} orient="auto">
451
+ <path d=${HEAD_D} class=${`oc-ah h-${a}`} /></marker>`)}
452
+ ${ARROWS.map((a) => html`<marker key=${`s-${a}`} id=${`oc-src-${a}`} viewBox="0 0 10 10" refX="5" refY="5" markerWidth=${HEAD.DOT} markerHeight=${HEAD.DOT}>
453
+ <circle cx="5" cy="5" r="5" class=${`oc-ah h-${a}`} /></marker>`)}</defs>
446
454
  <g ref=${worldRef} class="oc-world">
447
455
  <g class="oc-tree">${L.tree.map((d, i) => html`<path key=${i} d=${d} />`)}</g>
448
456
  <g class="oc-teams">${L.boxes.map((b) => html`<${TeamBox} key=${b.key} b=${b} q=${q} />`)}</g>
@@ -7,7 +7,10 @@
7
7
  // the box's padding and the gutters between its rows and columns (L.pad, L.gap: wide enough for a few lines each)
8
8
  // — are the only doors, so a line crosses a border at right angles, through a door, and never runs along it.
9
9
  // - Ends: pins on each card's sides (several on the long sides, one on the short), facing out; a line ends on a card,
10
- // never on a box. A card with more lines than pins gets more, so two lines never start at one point.
10
+ // never on a box. A card with more lines than pins gets more, so two lines never start at one point. A short side
11
+ // facing a neighbour across less than ROUTE.SIDE (a column gutter, a phone's box padding) has no pin (#2329): a line
12
+ // into it would turn in that narrow gap with no room left for its arrowhead. libavoid turns a line into its pin right
13
+ // at the card's buffer, which leaves a head no straight run: lengthen() moves that turn out where there is room.
11
14
  // - Separation: libavoid nudges lines that share a run ROUTE.NUDGE apart; the same two ends and kind are one line (×N)
12
15
  // before they get here (fleet-traffic.js).
13
16
  // A route that comes back otherwise (not orthogonal, not ending on its two shapes) is not drawn: that line keeps the
@@ -15,10 +18,13 @@
15
18
  import { OC } from "./fleet-canvas-layout.js";
16
19
 
17
20
  /** Buffer around every obstacle, the distance nudging keeps between lines, the cost of a bend, pins on a long side. */
18
- export const ROUTE = { BUF: 5, NUDGE: 8, SEG: 60, PINS: 4, MAX_PINS: 64, MAX_POINTS: 64, MAX_SHAPES: 600, MAX_CONNS: 120 };
21
+ export const ROUTE = { BUF: 5, NUDGE: 8, SEG: 60, PINS: 4, MAX_PINS: 64, MAX_POINTS: 64, MAX_SHAPES: 600, MAX_CONNS: 120, SIDE: 30, RUN: 16 };
19
22
  /** libavoid's ConnDirFlags: the way a line leaves a pin. */
20
23
  const UP = 1, DOWN = 2, LEFT = 4, RIGHT = 8;
21
- const PINS_PER_CARD = ROUTE.PINS * 2 + 2;
24
+ /** A shape's `sides`: which short sides have a pin (absent: both). */
25
+ const SIDE_L = 1, SIDE_R = 2;
26
+ const sidesOf = (sh) => sh.sides ?? SIDE_L | SIDE_R;
27
+ const pinsOn = (sh) => ROUTE.PINS * 2 + ((sidesOf(sh) & SIDE_L) ? 1 : 0) + ((sidesOf(sh) & SIDE_R) ? 1 : 0);
22
28
 
23
29
  /** The shape a line's end is on: a card (General's or a member's) or a folded team's box. Null: not routed here. */
24
30
  function endShape(end) {
@@ -30,8 +36,9 @@ function endShape(end) {
30
36
 
31
37
  /**
32
38
  * The scene for layout `L` and the lines `lines` ([{ key, a, z }], a and z ends as fleet-traffic.js endOf gives them):
33
- * { shapes: [{ x, y, w, h, pins }], conns: [{ key, a, z }], boxes: [{ x, y, w, h }] } — `pins` 0 none (an obstacle
34
- * only), 1 the usual pins, more: that many on each long side (a busy card); every pin takes one line; `a`/`z` index
39
+ * { shapes: [{ x, y, w, h, pins, sides? }], conns: [{ key, a, z }], boxes: [{ x, y, w, h }] } — `pins` 0 none (an
40
+ * obstacle only), 1 the usual pins, more: that many on each long side (a busy card); `sides` the short sides with a pin
41
+ * (1 left, 2 right; absent: both); every pin takes one line; `a`/`z` index
35
42
  * shapes; `boxes` the open teams' outlines (separate() keeps lines off them). A line whose end is not a card or a folded box is left out (it keeps its lane).
36
43
  */
37
44
  export function routeScene(L, lines) {
@@ -57,9 +64,22 @@ export function routeScene(L, lines) {
57
64
  conns.push({ key: ln.key, a: ia, z: iz });
58
65
  for (const i of [ia, iz]) demand.set(i, (demand.get(i) || 0) + 1);
59
66
  }
67
+ // A short side with a pin only where a line has room to turn into it with its head: ROUTE.SIDE to the nearest shape
68
+ // beside it (a neighbour card, a box's wall piece).
69
+ for (const sh of shapes) {
70
+ if (!sh.pins) continue;
71
+ let left = Infinity, right = Infinity;
72
+ for (const o of shapes) {
73
+ if (o === sh || o.y >= sh.y + sh.h || o.y + o.h <= sh.y) continue;
74
+ if (o.x + o.w <= sh.x) left = Math.min(left, sh.x - o.x - o.w);
75
+ if (o.x >= sh.x + sh.w) right = Math.min(right, o.x - sh.x - sh.w);
76
+ }
77
+ const sides = (left >= ROUTE.SIDE ? SIDE_L : 0) | (right >= ROUTE.SIDE ? SIDE_R : 0);
78
+ if (sides !== (SIDE_L | SIDE_R)) sh.sides = sides;
79
+ }
60
80
  // A line each: a shape with more lines than pins gets more pins on its long sides (two lines never leave one point:
61
81
  // libavoid cannot nudge apart runs that start at the same point).
62
- for (const [i, n] of demand) if (n > PINS_PER_CARD) shapes[i].pins = Math.min(ROUTE.MAX_PINS, Math.ceil((n - 2) / 2));
82
+ for (const [i, n] of demand) if (n > pinsOn(shapes[i])) shapes[i].pins = Math.min(ROUTE.MAX_PINS, Math.ceil((n - (pinsOn(shapes[i]) - ROUTE.PINS * 2)) / 2));
63
83
  return { shapes, conns, boxes };
64
84
  }
65
85
 
@@ -69,7 +89,8 @@ export function sceneOk(s) {
69
89
  || s.boxes.length > ROUTE.MAX_SHAPES) return null;
70
90
  const num = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) < 1e6;
71
91
  for (const b of s.boxes) if (!b || ![b.x, b.y, b.w, b.h].every(num) || b.w <= 0 || b.h <= 0) return null;
72
- for (const sh of s.shapes) if (!sh || ![sh.x, sh.y, sh.w, sh.h].every(num) || sh.w <= 0 || sh.h <= 0 || !Number.isInteger(sh.pins) || sh.pins < 0 || sh.pins > ROUTE.MAX_PINS) return null;
92
+ for (const sh of s.shapes) if (!sh || ![sh.x, sh.y, sh.w, sh.h].every(num) || sh.w <= 0 || sh.h <= 0 || !Number.isInteger(sh.pins) || sh.pins < 0 || sh.pins > ROUTE.MAX_PINS
93
+ || (sh.sides !== undefined && (!Number.isInteger(sh.sides) || sh.sides < 0 || sh.sides > (SIDE_L | SIDE_R)))) return null;
73
94
  for (const c of s.conns) {
74
95
  if (!c || typeof c.key !== "string" || c.key.length > 2048) return null;
75
96
  for (const i of [c.a, c.z]) if (!Number.isInteger(i) || i < 0 || i >= s.shapes.length || !s.shapes[i].pins) return null;
@@ -97,15 +118,8 @@ export function answerRoute(A, m) {
97
118
  return { type: "routes", id: m.id, routes: scene ? [...routeWith(A, scene)] : null };
98
119
  }
99
120
 
100
- /**
101
- * libavoid nudges lines that share a run apart, but not always: in a channel shared by many lines it packs them closer
102
- * than ROUTE.NUDGE, and lines fanning out of one card can come back on the very same run. This pass moves the middle
103
- * segments of such lines (never a line's first or last, which end on a pin) sideways to the nearest place at least
104
- * SEPARATE from every parallel line along them — only where the moved segment and the two it joins stay clear of
105
- * every shape's buffer and keep their direction. What cannot move stays as libavoid left it.
106
- */
107
- export const SEPARATE = 6;
108
- export function separate(routes, scene) {
121
+ /** What separate() and lengthen() share, on `scene` and the routes' points `pts` (which they move in place). */
122
+ function mover(scene, pts) {
109
123
  const B = ROUTE.BUF - 0.5, OVER = 8;
110
124
  const segsOf = (r, p) => p.slice(1).map((q, i) => ({ r, i, h: p[i].y === q.y }));
111
125
  const coord = (p, s) => (s.h ? p[s.i].y : p[s.i].x);
@@ -120,9 +134,7 @@ export function separate(routes, scene) {
120
134
  && (scene.boxes ?? []).every((bx) => (h ? [bx.y, bx.y + bx.h].every((v) => Math.abs(v - y0) >= SEPARATE + 1) || Math.min(x1, bx.x + bx.w) - Math.max(x0, bx.x) <= 12
121
135
  : [bx.x, bx.x + bx.w].every((v) => Math.abs(v - x0) >= SEPARATE + 1) || Math.min(y1, bx.y + bx.h) - Math.max(y0, bx.y) <= 12));
122
136
  };
123
- const keys = [...routes.keys()];
124
137
  const connOf = new Map(scene.conns.map((c) => [c.key, c]));
125
- const pts = keys.map((k) => routes.get(k).map((p) => ({ x: p.x, y: p.y })));
126
138
  /** The segments of other lines (and this one's) parallel to segment s of line r, overlapping it, closer than SEPARATE. */
127
139
  const near = (r, s, c, lo, hi) => {
128
140
  const out = [];
@@ -131,8 +143,23 @@ export function separate(routes, scene) {
131
143
  const [l2, h2] = span(p, t);
132
144
  if (Math.min(hi, h2) - Math.max(lo, l2) > OVER) out.push(coord(p, t));
133
145
  }));
134
- return out.filter((c2) => Math.abs(c2 - c) < SEPARATE);
146
+ return out.filter((c2) => Math.abs(c2 - c) < SEPARATE - 1e-6); // libavoid's points carry float noise
135
147
  };
148
+ return { segsOf, coord, span, clear, near, connOf };
149
+ }
150
+
151
+ /**
152
+ * libavoid nudges lines that share a run apart, but not always: in a channel shared by many lines it packs them closer
153
+ * than ROUTE.NUDGE, and lines fanning out of one card can come back on the very same run. This pass moves the middle
154
+ * segments of such lines (never a line's first or last, which end on a pin) sideways to the nearest place at least
155
+ * SEPARATE from every parallel line along them — only where the moved segment and the two it joins stay clear of
156
+ * every shape's buffer and keep their direction. What cannot move stays as libavoid left it.
157
+ */
158
+ export const SEPARATE = 6;
159
+ export function separate(routes, scene) {
160
+ const keys = [...routes.keys()];
161
+ const pts = keys.map((k) => routes.get(k).map((p) => ({ x: p.x, y: p.y })));
162
+ const { segsOf, coord, span, clear, near, connOf } = mover(scene, pts);
136
163
  for (let pass = 0; pass < 8; pass++) {
137
164
  let moved = false;
138
165
  pts.forEach((p, r) => segsOf(r, p).forEach((s) => {
@@ -163,6 +190,43 @@ export function separate(routes, scene) {
163
190
  return new Map(keys.map((k, i) => [k, pts[i]]));
164
191
  }
165
192
 
193
+ /**
194
+ * #2329: room for each line's arrowhead. libavoid turns a line into its last pin right at the card's buffer, so the
195
+ * last run (where the head goes) is a few px long. This pass moves the turn before it out from the card until the last
196
+ * run is ROUTE.RUN long (or up to 3 × SEPARATE more; where none fits, the longest that does): only where the moved segment and the two it joins stay clear of
197
+ * every shape's buffer, at least SEPARATE from every parallel line, and the run before the turn keeps its direction and
198
+ * 4 px. A line whose turn cannot move keeps its short run (fleet-traffic.js draws it with no head).
199
+ */
200
+ export function lengthen(routes, scene) {
201
+ const keys = [...routes.keys()];
202
+ const pts = keys.map((k) => routes.get(k).map((p) => ({ x: p.x, y: p.y })));
203
+ const { coord, span, clear, near, connOf } = mover(scene, pts);
204
+ pts.forEach((p, r) => {
205
+ const n = p.length;
206
+ if (n < 4) return; // the turn's other end is the first pin
207
+ const z = p[n - 1], y = p[n - 2], x = p[n - 3], w = p[n - 4], h = y.y === z.y;
208
+ const at = (q) => (h ? q.x : q.y);
209
+ const u = Math.sign(at(y) - at(z)); // out from the card
210
+ if (!u || Math.abs(at(y) - at(z)) >= ROUTE.RUN) return;
211
+ const c0 = connOf.get(keys[r]);
212
+ const run = Math.abs(at(y) - at(z)), tries = [];
213
+ for (let t = ROUTE.RUN; t <= ROUTE.RUN + 3 * SEPARATE; t++) tries.push(t);
214
+ for (let t = ROUTE.RUN - 1; t > run; t--) tries.push(t); // short of RUN (a phone's gutter): the longest that fits
215
+ for (const t of tries) {
216
+ const c2 = at(z) + u * t;
217
+ const a = h ? { x: c2, y: x.y } : { x: x.x, y: c2 }, b = h ? { x: c2, y: y.y } : { x: y.x, y: c2 };
218
+ if (Math.sign(at(a) - at(w)) !== Math.sign(at(x) - at(w)) || Math.abs(at(a) - at(w)) < 4) continue;
219
+ if (!clear(a, b) || !clear(w, a, n - 4 === 0 && c0 ? c0.a : -1) || !clear(b, z, c0 ? c0.z : -1)) continue;
220
+ const old = [p[n - 3], p[n - 2]];
221
+ p[n - 3] = a; p[n - 2] = b;
222
+ const ok = [n - 4, n - 3, n - 2].every((i) => { const s = { r, i, h: i === n - 3 ? !h : h }; const [lo, hi] = span(p, s); return !near(r, s, coord(p, s), lo, hi).length; });
223
+ if (ok) return;
224
+ [p[n - 3], p[n - 2]] = old;
225
+ }
226
+ });
227
+ return new Map(keys.map((k, i) => [k, pts[i]]));
228
+ }
229
+
166
230
  /**
167
231
  * Route `scene` with libavoid instance `A` (libavoid-js 0.4.5's: its constants are numbers on `A`). A Map of
168
232
  * connection key → points, for the routes that pass routeOk only.
@@ -186,7 +250,8 @@ export function routeWith(A, scene) {
186
250
  const k = s.pins === 1 ? ROUTE.PINS : s.pins;
187
251
  const pin = (x, y, dir) => new A.ShapeConnectionPin(ref, 1, x, y, true, 0, dir).setExclusive(true);
188
252
  for (let i = 1; i <= k; i++) { pin(i / (k + 1), 0, UP); pin(i / (k + 1), 1, DOWN); }
189
- pin(0, 0.5, LEFT); pin(1, 0.5, RIGHT);
253
+ if (sidesOf(s) & SIDE_L) pin(0, 0.5, LEFT);
254
+ if (sidesOf(s) & SIDE_R) pin(1, 0.5, RIGHT);
190
255
  }
191
256
  return ref;
192
257
  });
@@ -200,7 +265,7 @@ export function routeWith(A, scene) {
200
265
  const ok = routeOk(pts, scene, c);
201
266
  if (ok) out.set(c.key, ok);
202
267
  });
203
- return ROUTE.SEPARATE === false ? out : separate(out, scene);
268
+ return ROUTE.SEPARATE === false ? out : lengthen(separate(out, scene), scene);
204
269
  } finally {
205
270
  A.destroy(r);
206
271
  }
@@ -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
+ }