@felixdotgo/aidlc-kit 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/DEVELOPMENT.md +14 -13
  2. package/README.md +37 -43
  3. package/dist/assets/.agents/aidlc/docker/Dockerfile.claude +7 -0
  4. package/dist/assets/.agents/aidlc/docker/Dockerfile.codex +9 -0
  5. package/dist/assets/.agents/aidlc/docker/Dockerfile.opencode +8 -0
  6. package/dist/assets/.agents/aidlc/orchestrator.md +2 -2
  7. package/dist/assets/.agents/aidlc/phase-clarify.md +1 -1
  8. package/dist/assets/.agents/aidlc/phase-plan.md +2 -2
  9. package/dist/assets/.agents/aidlc/schemas/config.schema.json +2 -1
  10. package/dist/assets/.agents/aidlc/schemas/state.schema.json +2 -0
  11. package/dist/assets/.agents/aidlc/scripts/lib/assignment-store.mjs +94 -48
  12. package/dist/assets/.agents/aidlc/scripts/lib/context-runtime.mjs +5 -4
  13. package/dist/assets/.agents/aidlc/scripts/lib/loop-authority.mjs +261 -0
  14. package/dist/assets/.agents/aidlc/scripts/lib/loop-commit-message.mjs +45 -0
  15. package/dist/assets/.agents/aidlc/scripts/lib/loop-daemon.mjs +206 -0
  16. package/dist/assets/.agents/aidlc/scripts/lib/loop-drivers.mjs +100 -14
  17. package/dist/assets/.agents/aidlc/scripts/lib/loop-env.mjs +279 -0
  18. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-cli.mjs +105 -32
  19. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-remote.mjs +17 -1
  20. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet-supervisor.mjs +24 -7
  21. package/dist/assets/.agents/aidlc/scripts/lib/loop-fleet.mjs +42 -16
  22. package/dist/assets/.agents/aidlc/scripts/lib/loop-git.mjs +20 -6
  23. package/dist/assets/.agents/aidlc/scripts/lib/loop-holder.mjs +376 -0
  24. package/dist/assets/.agents/aidlc/scripts/lib/loop-intake.mjs +33 -7
  25. package/dist/assets/.agents/aidlc/scripts/lib/loop-lease.mjs +67 -61
  26. package/dist/assets/.agents/aidlc/scripts/lib/loop-leases.mjs +268 -0
  27. package/dist/assets/.agents/aidlc/scripts/lib/loop-local-workers.mjs +116 -28
  28. package/dist/assets/.agents/aidlc/scripts/lib/loop-runtime.mjs +114 -12
  29. package/dist/assets/.agents/aidlc/scripts/lib/loop-stream.mjs +117 -0
  30. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-local.mjs +16 -56
  31. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor-run-local.mjs +149 -6
  32. package/dist/assets/.agents/aidlc/scripts/lib/loop-supervisor.mjs +9 -1
  33. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-docker.mjs +44 -6
  34. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-image.mjs +64 -0
  35. package/dist/assets/.agents/aidlc/scripts/lib/loop-worker-log.mjs +158 -0
  36. package/dist/assets/.agents/aidlc/scripts/lib/repo-head.mjs +22 -0
  37. package/dist/assets/.agents/aidlc/scripts/lib/store.mjs +68 -2
  38. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime-migrate.mjs +75 -0
  39. package/dist/assets/.agents/aidlc/scripts/lib/task-runtime.mjs +97 -0
  40. package/dist/assets/.agents/aidlc/scripts/loop-entry.mjs +168 -73
  41. package/dist/assets/.agents/aidlc/scripts/loop-gate.mjs +19 -7
  42. package/dist/assets/.agents/aidlc/scripts/loop.cmd +101 -0
  43. package/dist/assets/.agents/aidlc/scripts/loop.mjs +2 -1
  44. package/dist/assets/.agents/aidlc/scripts/loop.sh +71 -0
  45. package/dist/assets/.agents/aidlc/scripts/remote-state.mjs +10 -4
  46. package/dist/assets/.agents/aidlc/scripts/state.mjs +16 -8
  47. package/dist/assets/.agents/aidlc/templates/design.md +9 -0
  48. package/dist/assets/.agents/aidlc/templates/intent.md +4 -0
  49. package/dist/assets/.agents/aidlc/templates/workplan.md +1 -1
  50. package/dist/assets/.agents/skills/aidlc-plan/SKILL.md +1 -1
  51. package/dist/assets/ui/bulma.min.css +3 -0
  52. package/dist/assets/ui/changes.js +83 -0
  53. package/dist/assets/ui/common.js +664 -0
  54. package/dist/assets/ui/dashboard.css +636 -0
  55. package/dist/assets/ui/dashboard.js +77 -0
  56. package/dist/assets/ui/detail.js +159 -0
  57. package/dist/assets/ui/live-log.js +332 -0
  58. package/dist/services/aidlc-coordination/Dockerfile +11 -3
  59. package/dist/services/aidlc-coordination/compose.yaml +12 -0
  60. package/dist/services/aidlc-coordination/package-lock.json +3 -1
  61. package/dist/services/aidlc-coordination/package.json +9 -4
  62. package/dist/services/aidlc-coordination/src/lifecycle-core.mjs +68 -2
  63. package/dist/services/aidlc-coordination/src/repository.mjs +66 -0
  64. package/dist/services/aidlc-coordination/src/server.mjs +9 -3
  65. package/dist/services/aidlc-coordination/test/fleet.test.mjs +21 -0
  66. package/dist/services/aidlc-coordination/test/server-health-auth.test.mjs +67 -0
  67. package/dist/src/adapters.d.ts +0 -3
  68. package/dist/src/adapters.js +3 -5
  69. package/dist/src/cli/argv.d.ts +12 -0
  70. package/dist/src/cli/argv.js +68 -0
  71. package/dist/src/cli/commands.d.ts +8 -0
  72. package/dist/src/cli/commands.js +159 -0
  73. package/dist/src/cli/loop-launcher.d.ts +47 -0
  74. package/dist/src/cli/loop-launcher.js +322 -0
  75. package/dist/src/cli/options.d.ts +25 -0
  76. package/dist/src/cli/options.js +32 -0
  77. package/dist/src/cli/prompt.d.ts +9 -0
  78. package/dist/src/cli/prompt.js +66 -0
  79. package/dist/src/cli.js +28 -191
  80. package/dist/src/gate.js +3 -2
  81. package/dist/src/installer.d.ts +14 -7
  82. package/dist/src/installer.js +42 -143
  83. package/dist/src/legacy.js +1 -0
  84. package/dist/src/managed-content.d.ts +7 -0
  85. package/dist/src/managed-content.js +15 -4
  86. package/dist/src/managed-planning.d.ts +65 -0
  87. package/dist/src/managed-planning.js +219 -0
  88. package/dist/src/mcp-setup.js +3 -1
  89. package/dist/src/model/config-contracts.d.ts +65 -0
  90. package/dist/src/model/config-contracts.js +1 -0
  91. package/dist/src/model/install-contracts.d.ts +29 -0
  92. package/dist/src/model/install-contracts.js +1 -0
  93. package/dist/src/model/lifecycle-enums.d.ts +13 -0
  94. package/dist/src/model/lifecycle-enums.js +1 -0
  95. package/dist/src/model/loop.d.ts +20 -0
  96. package/dist/src/model/loop.js +1 -0
  97. package/dist/src/model/manifest.d.ts +16 -0
  98. package/dist/src/model/manifest.js +1 -0
  99. package/dist/src/model/records.d.ts +142 -0
  100. package/dist/src/model/records.js +1 -0
  101. package/dist/src/model.d.ts +11 -264
  102. package/dist/src/model.js +1 -1
  103. package/dist/src/profiles.d.ts +2 -0
  104. package/dist/src/profiles.js +53 -9
  105. package/dist/src/state/archive-record.d.ts +9 -0
  106. package/dist/src/state/archive-record.js +26 -0
  107. package/dist/src/state/commands.d.ts +18 -0
  108. package/dist/src/state/commands.js +27 -0
  109. package/dist/src/state/domain.d.ts +9 -0
  110. package/dist/src/state/domain.js +9 -0
  111. package/dist/src/state/guards.d.ts +7 -0
  112. package/dist/src/state/guards.js +17 -0
  113. package/dist/src/state/io.d.ts +10 -0
  114. package/dist/src/state/io.js +35 -0
  115. package/dist/src/state/items.d.ts +3 -4
  116. package/dist/src/state/items.js +58 -57
  117. package/dist/src/state/lessons.js +14 -7
  118. package/dist/src/state/lifecycle.js +2 -2
  119. package/dist/src/state/lock.js +7 -6
  120. package/dist/src/state/memory.js +24 -12
  121. package/dist/src/state/{archive.js → migrate.js} +7 -7
  122. package/dist/src/state/partition.d.ts +15 -0
  123. package/dist/src/state/partition.js +90 -0
  124. package/dist/src/state/paths.d.ts +11 -0
  125. package/dist/src/state/paths.js +12 -0
  126. package/dist/src/state/render.js +9 -6
  127. package/dist/src/state/store.js +9 -15
  128. package/dist/src/state/transitions.js +9 -7
  129. package/dist/src/state/validate.js +55 -7
  130. package/dist/src/state.d.ts +16 -11
  131. package/dist/src/state.js +16 -11
  132. package/dist/src/task-ui.d.ts +33 -0
  133. package/dist/src/task-ui.js +33 -0
  134. package/dist/src/ui/assets.d.ts +14 -0
  135. package/dist/src/ui/assets.js +45 -0
  136. package/dist/src/ui/changes.d.ts +83 -0
  137. package/dist/src/ui/changes.js +498 -0
  138. package/dist/src/ui/containers.d.ts +71 -0
  139. package/dist/src/ui/containers.js +214 -0
  140. package/dist/src/ui/daemon.d.ts +75 -0
  141. package/dist/src/ui/daemon.js +232 -0
  142. package/dist/src/ui/data.d.ts +42 -0
  143. package/dist/src/ui/data.js +89 -0
  144. package/dist/src/ui/docs.d.ts +13 -0
  145. package/dist/src/ui/docs.js +39 -0
  146. package/dist/src/ui/files.d.ts +3 -0
  147. package/dist/src/ui/files.js +20 -0
  148. package/dist/src/ui/http.d.ts +14 -0
  149. package/dist/src/ui/http.js +80 -0
  150. package/dist/src/ui/live.d.ts +5 -0
  151. package/dist/src/ui/live.js +43 -0
  152. package/dist/src/ui/markdown.d.ts +2 -0
  153. package/dist/src/ui/markdown.js +127 -0
  154. package/dist/src/ui/options.d.ts +48 -0
  155. package/dist/src/ui/options.js +81 -0
  156. package/dist/src/ui/security.d.ts +34 -0
  157. package/dist/src/ui/security.js +38 -0
  158. package/dist/src/ui/server.d.ts +39 -0
  159. package/dist/src/ui/server.js +347 -0
  160. package/dist/src/ui/sse.d.ts +14 -0
  161. package/dist/src/ui/sse.js +281 -0
  162. package/dist/src/ui/views/board.d.ts +7 -0
  163. package/dist/src/ui/views/board.js +131 -0
  164. package/dist/src/ui/views/changes.d.ts +19 -0
  165. package/dist/src/ui/views/changes.js +154 -0
  166. package/dist/src/ui/views/components.d.ts +56 -0
  167. package/dist/src/ui/views/components.js +113 -0
  168. package/dist/src/ui/views/docs.d.ts +15 -0
  169. package/dist/src/ui/views/docs.js +64 -0
  170. package/dist/src/ui/views/layout.d.ts +44 -0
  171. package/dist/src/ui/views/layout.js +233 -0
  172. package/dist/src/ui/views/task.d.ts +25 -0
  173. package/dist/src/ui/views/task.js +320 -0
  174. package/dist/src/upgrade-board.js +10 -10
  175. package/dist/src/upgrade.d.ts +0 -1
  176. package/dist/src/upgrade.js +24 -85
  177. package/dist/src/workflow.d.ts +4 -0
  178. package/dist/src/workflow.js +36 -4
  179. package/docs/README.md +41 -22
  180. package/docs/architecture.md +101 -37
  181. package/docs/command-reference.md +289 -52
  182. package/docs/configuration.md +70 -30
  183. package/docs/coordination-service.md +91 -30
  184. package/docs/design-decisions.md +87 -0
  185. package/docs/development.md +124 -49
  186. package/docs/loop-local-run.md +228 -0
  187. package/docs/loop-runbook.md +214 -0
  188. package/docs/operating-workflow.md +55 -42
  189. package/docs/testing-and-release.md +72 -49
  190. package/docs/troubleshooting.md +221 -0
  191. package/package.json +4 -1
  192. package/dist/src/ledger.d.ts +0 -22
  193. package/dist/src/ledger.js +0 -119
  194. package/dist/src/state/shared.d.ts +0 -29
  195. package/dist/src/state/shared.js +0 -71
  196. package/dist/src/tracker.d.ts +0 -36
  197. package/dist/src/tracker.js +0 -44
  198. /package/dist/src/state/{archive.d.ts → migrate.d.ts} +0 -0
@@ -0,0 +1,56 @@
1
+ export declare const PHASES: readonly ["clarify", "plan", "build", "wrap", "done"];
2
+ export declare const summarize: (counts: Record<string, number> | undefined) => string;
3
+ export declare const GATE_LABELS: Record<string, string>;
4
+ export declare const PHASE_GATE: Record<string, string>;
5
+ export declare const STATUS_META: Record<string, {
6
+ label: string;
7
+ tone: string;
8
+ }>;
9
+ export declare const RISK_TONE: Record<string, string>;
10
+ export declare const RESULT_TONE: Record<string, string>;
11
+ export declare const DECISION_TONE: Record<string, string>;
12
+ export declare const ITEM_TONE: Record<string, string>;
13
+ export declare const gateLabel: (gate: string) => string;
14
+ export declare const pill: (label: string, tone?: string, title?: string, dot?: boolean) => string;
15
+ export declare const statusPill: (status: string, handedOff?: boolean) => string;
16
+ export declare const gatePill: (gate: string) => string;
17
+ /** Human feedback recorded at a gate and not yet followed by that gate's approval. */
18
+ export declare const feedbackPill: (count: number) => string;
19
+ export declare const riskPill: (risk: string) => string;
20
+ export declare const phaseSteps: (phase: string) => string;
21
+ /** Horizontal phase stepper shown above the artifacts (detail page and drawer). A pulse
22
+ * ring marks the current step while the task is live; `stalled` (handed off) suppresses it. */
23
+ export declare const phaseStepper: (phase: string, gate: string, status: string, stalled?: boolean) => string;
24
+ /** Server-rendered relative time; the absolute ISO value stays in the title. */
25
+ export declare const relativeTime: (iso: string | undefined) => string;
26
+ export interface GateViewData {
27
+ icon?: string;
28
+ title?: string;
29
+ gate?: string;
30
+ action?: string;
31
+ task?: {
32
+ id?: string;
33
+ title?: string;
34
+ status?: string;
35
+ risk?: string;
36
+ areas?: string[];
37
+ };
38
+ artifacts?: Array<{
39
+ name?: string;
40
+ path?: string;
41
+ }>;
42
+ decisions?: Record<string, number>;
43
+ execution?: Record<string, number>;
44
+ evidence?: Array<{
45
+ kind?: string;
46
+ result?: string;
47
+ source?: string;
48
+ }>;
49
+ diagnostics?: Array<{
50
+ level?: string;
51
+ code?: string;
52
+ message?: string;
53
+ }>;
54
+ }
55
+ /** `actions`: extra trusted HTML (e.g. a link to the task's changes) shown above the action callout. */
56
+ export declare const renderGateViewHtml: (view: unknown, actions?: string) => string;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Presentation helpers shared by the dashboard, task pages, and live regions:
3
+ * labels, pills, phase stepper, relative time, and the structured gate view.
4
+ */
5
+ import { escapeHtml } from "../http.js";
6
+ export const PHASES = ["clarify", "plan", "build", "wrap", "done"];
7
+ export const summarize = (counts) => Object.entries(counts ?? {}).map(([k, n]) => `${n} ${k}`).join(" · ") || "none";
8
+ /* ---------------------------------------------------------------------------
9
+ * Presentation helpers: labels, pills, phase stepper, relative time.
10
+ * Colour is never the only signal; every pill carries a text label.
11
+ * ------------------------------------------------------------------------- */
12
+ export const GATE_LABELS = {
13
+ G0_confirm: "Confirm requirements",
14
+ G1_review: "Approve plan",
15
+ G2_codereview: "Review implementation",
16
+ };
17
+ export const PHASE_GATE = { clarify: "G0_confirm", plan: "G1_review", build: "G2_codereview" };
18
+ export const STATUS_META = {
19
+ active: { label: "Active", tone: "is-info" },
20
+ blocked_on_user: { label: "Needs review", tone: "is-warning" },
21
+ paused: { label: "Paused", tone: "" },
22
+ done: { label: "Done", tone: "is-success" },
23
+ closed: { label: "Closed", tone: "is-danger" },
24
+ superseded: { label: "Superseded", tone: "is-danger" },
25
+ };
26
+ export const RISK_TONE = { low: "is-success", normal: "", high: "is-warning", regulated: "is-danger" };
27
+ export const RESULT_TONE = { pass: "is-success", fail: "is-danger", skip: "is-warning" };
28
+ export const DECISION_TONE = { approved: "is-success", changed: "is-info", dropped: "is-ghost", unresolved: "is-warning" };
29
+ export const ITEM_TONE = { done: "is-success", in_progress: "is-info", todo: "", deferred: "is-warning" };
30
+ export const gateLabel = (gate) => GATE_LABELS[gate] ?? gate;
31
+ export const pill = (label, tone = "", title = "", dot = true) => `<span class="ad-pill ${tone}"${title ? ` title="${escapeHtml(title)}"` : ""}>${dot ? '<span class="ad-dot"></span>' : ""}${escapeHtml(label)}</span>`;
32
+ export const statusPill = (status, handedOff = false) => {
33
+ if (handedOff)
34
+ return pill("Handed off", "is-danger", `status: ${status} · handed off`);
35
+ const meta = STATUS_META[status] ?? { label: status, tone: "" };
36
+ return pill(meta.label, meta.tone, `status: ${status}`);
37
+ };
38
+ export const gatePill = (gate) => gate && gate !== "none" ? pill(gateLabel(gate), "is-ghost", `gate: ${gate}`, false) : "";
39
+ /** Human feedback recorded at a gate and not yet followed by that gate's approval. */
40
+ export const feedbackPill = (count) => count > 0 ? `<span class="ad-pill is-info" data-feedback-pending="${count}" title="${count} human feedback item${count === 1 ? "" : "s"} not yet followed by an approval">✎ feedback pending${count > 1 ? ` · ${count}` : ""}</span>` : "";
41
+ export const riskPill = (risk) => pill(`${risk} risk`, RISK_TONE[risk] ?? "", `risk: ${risk}`);
42
+ export const phaseSteps = (phase) => {
43
+ const current = PHASES.indexOf(phase);
44
+ const done = phase === "done";
45
+ const bars = PHASES.map((name, index) => {
46
+ const state = done || index < current ? "is-complete" : index === current ? "is-current" : "";
47
+ return `<span class="${state}" title="${escapeHtml(name)}"></span>`;
48
+ }).join("");
49
+ return `<div class="ad-steps${done ? " is-done" : ""}" role="img" aria-label="Phase ${current + 1} of ${PHASES.length}: ${escapeHtml(phase)}">${bars}</div>`;
50
+ };
51
+ /** Statuses whose current step shows the "radioactive" pulse: the task is moving
52
+ * (`active`) or waiting for a human (`blocked_on_user`, pulsed in the warning tone). */
53
+ const PULSE_STATUSES = new Set(["active", "blocked_on_user"]);
54
+ /** Horizontal phase stepper shown above the artifacts (detail page and drawer). A pulse
55
+ * ring marks the current step while the task is live; `stalled` (handed off) suppresses it. */
56
+ export const phaseStepper = (phase, gate, status, stalled = false) => {
57
+ const current = PHASES.indexOf(phase);
58
+ const pulse = !stalled && phase !== "done" && PULSE_STATUSES.has(status)
59
+ ? `<span class="ad-pulse${status === "blocked_on_user" ? " is-waiting" : ""}" aria-hidden="true"></span>`
60
+ : "";
61
+ return `<ol class="ad-stepper" aria-label="Progress">${PHASES.map((name, index) => {
62
+ const complete = phase === "done" || index < current;
63
+ const isCurrent = !complete && index === current;
64
+ const phaseGate = PHASE_GATE[name];
65
+ const note = isCurrent && status === "blocked_on_user" && gate !== "none"
66
+ ? `waiting at ${gateLabel(gate)}`
67
+ : phaseGate ? `gate · ${gateLabel(phaseGate)}` : name === "done" ? "complete" : "no gate";
68
+ return `<li class="${complete ? "is-complete" : isCurrent ? "is-current" : ""}"${isCurrent ? ' aria-current="step"' : ""}><span class="ad-stepper-marker">${isCurrent ? pulse : ""}${complete ? "✓" : index + 1}</span><span class="ad-stepper-text"><strong>${escapeHtml(name)}</strong><small>${escapeHtml(note)}</small></span></li>`;
69
+ }).join("")}</ol>`;
70
+ };
71
+ /** Server-rendered relative time; the absolute ISO value stays in the title. */
72
+ export const relativeTime = (iso) => {
73
+ if (!iso)
74
+ return "";
75
+ const at = Date.parse(iso);
76
+ if (Number.isNaN(at))
77
+ return escapeHtml(iso);
78
+ const seconds = Math.max(0, Math.round((Date.now() - at) / 1000));
79
+ const text = seconds < 60 ? "just now"
80
+ : seconds < 3600 ? `${Math.floor(seconds / 60)}m ago`
81
+ : seconds < 86400 ? `${Math.floor(seconds / 3600)}h ago`
82
+ : `${Math.floor(seconds / 86400)}d ago`;
83
+ return `<time datetime="${escapeHtml(iso)}" title="${escapeHtml(iso)}">${text}</time>`;
84
+ };
85
+ /** `actions`: extra trusted HTML (e.g. a link to the task's changes) shown above the action callout. */
86
+ export const renderGateViewHtml = (view, actions = "") => {
87
+ const v = view;
88
+ const task = v.task ?? {};
89
+ const artifacts = v.artifacts ?? [];
90
+ const evidence = v.evidence ?? [];
91
+ const diagnostics = v.diagnostics ?? [];
92
+ const levelTone = (level) => level === "ERROR" ? "is-danger" : level === "WARN" ? "is-warning" : "is-info";
93
+ const cell = (label, value) => `<div class="ad-gate-cell"><small>${escapeHtml(label)}</small><span>${value}</span></div>`;
94
+ return `<section class="ad-panel is-warning" data-key="gate-view">
95
+ <header class="ad-panel-head"><span aria-hidden="true">${escapeHtml(String(v.icon ?? ""))}</span><h2>${escapeHtml(String(v.title ?? ""))}</h2><span class="ad-pill is-warning ad-panel-tools" title="gate">${escapeHtml(String(v.gate ?? ""))}</span></header>
96
+ <div class="ad-panel-body">
97
+ <div class="ad-gate-grid">
98
+ ${cell("Status", escapeHtml(task.status ?? ""))}
99
+ ${cell("Risk", escapeHtml(task.risk ?? ""))}
100
+ ${cell("Areas", escapeHtml((task.areas ?? []).join(", ")))}
101
+ ${cell("Decisions", escapeHtml(summarize(v.decisions)))}
102
+ ${cell("Execution", escapeHtml(summarize(v.execution)))}
103
+ </div>
104
+ <h3 class="ad-subhead">Review artifacts</h3>
105
+ ${artifacts.length ? `<ul class="ad-plain-list">${artifacts.map((a) => `<li><strong>${escapeHtml(a.name ?? "")}</strong><code>${escapeHtml(a.path ?? "")}</code></li>`).join("")}</ul>` : '<p class="has-text-grey">none</p>'}
106
+ <h3 class="ad-subhead">Verification evidence</h3>
107
+ ${evidence.length ? `<ul class="ad-plain-list">${evidence.map((e) => `<li>${pill(e.result ?? "", RESULT_TONE[e.result ?? ""] ?? "")} <strong>${escapeHtml(e.kind ?? "")}</strong> <span class="has-text-grey">${escapeHtml(e.source ?? "")}</span></li>`).join("")}</ul>` : '<p class="has-text-grey">none yet</p>'}
108
+ ${diagnostics.length ? `<h3 class="ad-subhead">Diagnostics</h3><ul class="ad-plain-list">${diagnostics.map((d) => `<li>${pill(d.level ?? "", levelTone(d.level))} <code>${escapeHtml(d.code ?? "")}</code> ${escapeHtml(d.message ?? "")}</li>`).join("")}</ul>` : ""}
109
+ ${actions}
110
+ <div class="ad-callout"><span aria-hidden="true">→</span><span>Action required: ${escapeHtml(String(v.action ?? ""))}</span></div>
111
+ </div>
112
+ </section>`;
113
+ };
@@ -0,0 +1,15 @@
1
+ import { type UiOptions } from "../options.js";
2
+ /** GitHub-style heading anchor, so the docs' own `#section` links resolve in-app. */
3
+ export declare const headingSlug: (text: string) => string;
4
+ export interface TocEntry {
5
+ level: number;
6
+ id: string;
7
+ text: string;
8
+ }
9
+ /** Markdown → HTML plus heading ids, in-app links, and GitHub-style callouts. */
10
+ export declare const renderDocHtml: (markdown: string) => {
11
+ html: string;
12
+ toc: TocEntry[];
13
+ };
14
+ /** The full docs page, or undefined for an unknown slug (the route answers 404). */
15
+ export declare const renderDocsPage: (options: UiOptions, slug: string) => Promise<string | undefined>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `/docs` and `/docs/<slug>`: the packaged guides rendered with the dashboard's markdown
3
+ * renderer, a page sidebar, and an on-page table of contents.
4
+ */
5
+ import { DOC_PAGES, readDoc } from "../docs.js";
6
+ import { readUiState } from "../data.js";
7
+ import { escapeHtml } from "../http.js";
8
+ import { markdownToHtml } from "../markdown.js";
9
+ import { uiMode } from "../options.js";
10
+ import { pageHead, pageScripts, renderHeader, renderModals, reviewQueue } from "./layout.js";
11
+ const REPO_DOCS = "https://github.com/felixdotgo/aidlc-kit/blob/main/docs/";
12
+ /** GitHub-style heading anchor, so the docs' own `#section` links resolve in-app. */
13
+ export const headingSlug = (text) => text.replace(/<[^>]+>/g, "").replace(/&[a-z]+;|&#\d+;/g, "").toLowerCase().replace(/[^\p{L}\p{N}\s_-]/gu, "").trim().replace(/\s/g, "-");
14
+ const CALLOUTS = { NOTE: "Note", TIP: "Tip", IMPORTANT: "Important", WARNING: "Warning", CAUTION: "Caution" };
15
+ /** Markdown → HTML plus heading ids, in-app links, and GitHub-style callouts. */
16
+ export const renderDocHtml = (markdown) => {
17
+ const toc = [];
18
+ const seen = new Map();
19
+ let html = markdownToHtml(markdown).replace(/<h([1-4])>([\s\S]*?)<\/h\1>/g, (_match, level, inner) => {
20
+ const base = headingSlug(inner) || "section";
21
+ const count = seen.get(base) ?? 0;
22
+ seen.set(base, count + 1);
23
+ const id = count ? `${base}-${count}` : base;
24
+ // `inner` is already escaped by the renderer; the tags are dropped, the entities kept as-is.
25
+ if (level === "2" || level === "3")
26
+ toc.push({ level: Number(level), id, text: inner.replace(/<[^>]+>/g, "") });
27
+ return `<h${level} id="${id}"><a class="ad-doc-anchor" href="#${id}" aria-hidden="true">#</a>${inner}</h${level}>`;
28
+ });
29
+ // Relative links between guides stay in the app; other repo docs open on GitHub.
30
+ html = html.replace(/href="(?:\.\/)?([A-Za-z0-9-]+)\.md(#[^"]*)?"/g, (_match, slug, hash = "") => DOC_PAGES.some((page) => page.slug === slug) ? `href="/docs/${slug}${hash}"` : `href="${REPO_DOCS}${slug}.md${hash}" target="_blank" rel="noopener"`);
31
+ html = html.replace(/<blockquote>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*/g, (_match, kind) => `<blockquote class="ad-doc-callout is-${kind.toLowerCase()}"><strong class="ad-doc-callout-title">${CALLOUTS[kind] ?? kind}</strong> `);
32
+ return { html, toc };
33
+ };
34
+ /** The full docs page, or undefined for an unknown slug (the route answers 404). */
35
+ export const renderDocsPage = async (options, slug) => {
36
+ const doc = readDoc(slug);
37
+ if (!doc)
38
+ return undefined;
39
+ const { html, toc } = renderDocHtml(doc.markdown);
40
+ const state = await readUiState(options);
41
+ const nav = DOC_PAGES.map((page) => `<li><a href="/docs/${page.slug}"${page.slug === slug ? ' class="is-active" aria-current="page"' : ""}>${escapeHtml(page.label)}</a></li>`).join("");
42
+ const contents = toc.length
43
+ ? `<p class="ad-docs-nav-title">On this page</p><ul class="ad-docs-toc">${toc.map((entry) => `<li class="is-level-${entry.level}"><a href="#${entry.id}">${entry.text}</a></li>`).join("")}</ul>`
44
+ : "";
45
+ return `${pageHead(`${doc.page.label} · Docs · Loop`)}
46
+ ${renderHeader(options.root, uiMode(options), reviewQueue(state))}
47
+ <main class="section ad-main">
48
+ <div class="container is-fluid">
49
+ <nav class="breadcrumb is-small mb-3" aria-label="breadcrumbs"><ul><li><a href="/">Loop</a></li><li><a href="/docs">Docs</a></li><li class="is-active"><a href="/docs/${doc.page.slug}" aria-current="page">${escapeHtml(doc.page.label)}</a></li></ul></nav>
50
+ <div class="ad-docs">
51
+ <aside class="ad-docs-nav" aria-label="Documentation">
52
+ <p class="ad-docs-nav-title">Guides</p>
53
+ <ul class="ad-docs-pages">${nav}</ul>
54
+ ${contents}
55
+ </aside>
56
+ <article class="ad-panel ad-docs-body"><div class="content artifact">${html}</div></article>
57
+ </div>
58
+ </div>
59
+ </main>
60
+ ${renderModals()}
61
+ ${pageScripts("common")}
62
+ </body>
63
+ </html>`;
64
+ };
@@ -0,0 +1,44 @@
1
+ import type { WorkflowState } from "../../model.js";
2
+ import { type ClientScript } from "../assets.js";
3
+ import type { UiAuthorityMode } from "../options.js";
4
+ /** `task` marks a task detail page: live updates refresh that task's regions. The
5
+ * inline head scripts apply the saved kanban/list view and the saved theme
6
+ * (`localStorage["aidlc-settings"].theme`, see `assets/ui/common.js`) before first
7
+ * paint; `system` leaves `data-theme` unset so the OS preference applies. */
8
+ export declare const pageHead: (title: string, task?: string) => string;
9
+ /** Classic `<script src>` tags in load order (`common` first; page scripts after it). */
10
+ export declare const pageScripts: (...names: ClientScript[]) => string;
11
+ /** Region names `/api/live` can render; anything else in `known` is ignored. */
12
+ export declare const LIVE_REGIONS: readonly ["notify", "daemon", "board", "detail", "drawer"];
13
+ export type LiveRegion = (typeof LIVE_REGIONS)[number];
14
+ export declare const regionHash: (html: string) => string;
15
+ /** Wrapper is `display: contents`, so it never affects layout (e.g. header flex items). */
16
+ export declare const liveRegion: (name: LiveRegion, html: string) => string;
17
+ /** Daemon state for the header: running/stopped, plus unresolved turn or poll
18
+ * errors (from the event log) while it runs, so failures are not buried in logs. */
19
+ export declare const renderDaemonPill: (root: string) => string;
20
+ /** A task waiting at a human gate (not handed off): shown as a header notification. */
21
+ export interface ReviewItem {
22
+ id: string;
23
+ title: string;
24
+ gate: string;
25
+ updatedAt?: string;
26
+ feedbackPending: number;
27
+ }
28
+ /** Every task waiting at a human gate, newest first. Handed-off tasks follow their
29
+ * handoff actions instead, and terminal (archived) tasks never wait at a gate. */
30
+ export declare const reviewQueue: (state: WorkflowState) => ReviewItem[];
31
+ /** Notifications shown at first; "Load more" adds this many at a time. The client
32
+ * reads it from `<body data-notify-page>` (see pageHead). */
33
+ export declare const NOTIFY_PAGE = 5;
34
+ export declare const notifyLimit: (raw: string | null) => number;
35
+ /** Header bell: a count badge plus a dropdown whose items open the task detail
36
+ * page (where gate feedback and approval live). Shows `limit` items and a
37
+ * "Load more" button for the rest. The panel's `hidden` and the toggle's
38
+ * `aria-expanded` are client state that live updates preserve. */
39
+ export declare const renderNotifications: (reviews: ReviewItem[], currentTask?: string, limit?: number) => string;
40
+ export declare const renderHeader: (root: string, mode?: UiAuthorityMode, reviews?: ReviewItem[], currentTask?: string) => string;
41
+ /** Settings dialog: client-side preferences stored in this browser (`aidlc-settings`,
42
+ * see `assets/ui/common.js` `aidlcSettings`). The form is read and written by the
43
+ * client; every control id is `settings-<key>`. */
44
+ export declare const renderModals: () => string;
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Page chrome shared by the dashboard and task pages: document head, live-region
3
+ * wrappers, header (daemon pill, review bell), dialogs, and client script tags.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { scriptTag, stylesheetTag } from "../assets.js";
7
+ import { daemonHealth, daemonStatus } from "../daemon.js";
8
+ import { pendingFeedback } from "../data.js";
9
+ import { escapeHtml } from "../http.js";
10
+ import { feedbackPill, gateLabel, pill, relativeTime } from "./components.js";
11
+ /** `task` marks a task detail page: live updates refresh that task's regions. The
12
+ * inline head scripts apply the saved kanban/list view and the saved theme
13
+ * (`localStorage["aidlc-settings"].theme`, see `assets/ui/common.js`) before first
14
+ * paint; `system` leaves `data-theme` unset so the OS preference applies. */
15
+ export const pageHead = (title, task = "") => `<!doctype html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="utf-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1">
20
+ <title>${escapeHtml(title)}</title>
21
+ <meta name="color-scheme" content="light dark">
22
+ ${stylesheetTag("bulma")}
23
+ ${stylesheetTag("dashboard")}
24
+ <script>try { document.documentElement.setAttribute("data-view", localStorage.getItem("aidlc-view") === "list" ? "list" : "kanban"); } catch (error) { /* storage disabled */ }</script>
25
+ <script>try { const theme = JSON.parse(localStorage.getItem("aidlc-settings") || "{}").theme; if (theme === "light" || theme === "dark") document.documentElement.setAttribute("data-theme", theme); } catch (error) { /* storage disabled or malformed */ }</script>
26
+ </head>
27
+ <body${task ? ` data-task="${escapeHtml(task)}"` : ""} data-notify-page="${NOTIFY_PAGE}">`;
28
+ /** Classic `<script src>` tags in load order (`common` first; page scripts after it). */
29
+ export const pageScripts = (...names) => names.map(scriptTag).join("\n");
30
+ /* ---------------------------------------------------------------------------
31
+ * Live regions: server-rendered fragments the client polls and morphs in place.
32
+ * Each region carries a content hash; `/api/live` returns only regions whose
33
+ * hash differs from the one the client already shows.
34
+ * ------------------------------------------------------------------------- */
35
+ /** Region names `/api/live` can render; anything else in `known` is ignored. */
36
+ export const LIVE_REGIONS = ["notify", "daemon", "board", "detail", "drawer"];
37
+ export const regionHash = (html) => createHash("sha1").update(html).digest("base64url").slice(0, 16);
38
+ /** Wrapper is `display: contents`, so it never affects layout (e.g. header flex items). */
39
+ export const liveRegion = (name, html) => `<div class="ad-live" data-live="${name}" data-live-hash="${regionHash(html)}">${html}</div>`;
40
+ /** Daemon state for the header: running/stopped, plus unresolved turn or poll
41
+ * errors (from the event log) while it runs, so failures are not buried in logs. */
42
+ export const renderDaemonPill = (root) => {
43
+ const daemon = daemonStatus(root);
44
+ if (!daemon.running)
45
+ return `<span class="ad-pill is-ghost" data-daemon="stopped" title="start it with: aidlc-kit loop start (or New task)"><span class="ad-dot"></span>daemon stopped</span>`;
46
+ const errors = daemonHealth(root);
47
+ if (!errors.length)
48
+ return `<span class="ad-pill is-success is-live" data-daemon="running" title="started ${escapeHtml(daemon.startedAt ?? "")}"><span class="ad-dot"></span>daemon running · pid ${daemon.pid}</span>`;
49
+ const describe = (error) => `${error.taskId ? `${error.taskId}: ` : "poll: "}${error.message}${error.retryInMs ? ` (retry in ${Math.round(error.retryInMs / 1000)}s)` : ""}`;
50
+ const title = `daemon running · pid ${daemon.pid}\n${errors.map(describe).join("\n")}`;
51
+ const label = `<span class="ad-dot"></span>daemon · ${errors.length} error${errors.length === 1 ? "" : "s"}`;
52
+ const only = errors.length === 1 ? errors[0]?.taskId : undefined;
53
+ return only
54
+ ? `<a class="ad-pill is-warning is-live" data-daemon="error" href="/tasks/${encodeURIComponent(only)}" title="${escapeHtml(title)}">${label}</a>`
55
+ : `<span class="ad-pill is-warning is-live" data-daemon="error" title="${escapeHtml(title)}">${label}</span>`;
56
+ };
57
+ /** Every task waiting at a human gate, newest first. Handed-off tasks follow their
58
+ * handoff actions instead, and terminal (archived) tasks never wait at a gate. */
59
+ export const reviewQueue = (state) => Object.values(state.tasks)
60
+ .filter((task) => task.status === "blocked_on_user" && task.gate !== "none" && !task.handoff)
61
+ .map((task) => ({ id: task.id, title: task.title, gate: task.gate, updatedAt: task.updatedAt, feedbackPending: pendingFeedback(task).length }))
62
+ .sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""));
63
+ /** Notifications shown at first; "Load more" adds this many at a time. The client
64
+ * reads it from `<body data-notify-page>` (see pageHead). */
65
+ export const NOTIFY_PAGE = 5;
66
+ const NOTIFY_MAX = 100;
67
+ export const notifyLimit = (raw) => {
68
+ const value = Number(raw);
69
+ return Number.isSafeInteger(value) && value > 0 ? Math.min(NOTIFY_MAX, Math.max(NOTIFY_PAGE, value)) : NOTIFY_PAGE;
70
+ };
71
+ /** Header bell: a count badge plus a dropdown whose items open the task detail
72
+ * page (where gate feedback and approval live). Shows `limit` items and a
73
+ * "Load more" button for the rest. The panel's `hidden` and the toggle's
74
+ * `aria-expanded` are client state that live updates preserve. */
75
+ export const renderNotifications = (reviews, currentTask = "", limit = NOTIFY_PAGE) => {
76
+ const count = reviews.length;
77
+ const shown = reviews.slice(0, limit);
78
+ const remaining = count - shown.length;
79
+ const label = count ? `${count} task${count === 1 ? "" : "s"} waiting for your review` : "Nothing waiting for your review";
80
+ const items = count
81
+ ? `<ul class="ad-notify-list">${shown.map((task) => `<li data-key="review:${escapeHtml(task.id)}"><a class="ad-notify-item" href="/tasks/${encodeURIComponent(task.id)}" data-review-item="${escapeHtml(task.id)}"${task.id === currentTask ? ' aria-current="page"' : ""}>
82
+ <span class="ad-notify-row">${pill(gateLabel(task.gate), "is-warning", `gate: ${task.gate}`, false)}${task.feedbackPending ? feedbackPill(task.feedbackPending) : ""}<span class="ad-notify-time">${relativeTime(task.updatedAt)}</span></span>
83
+ <span class="ad-notify-title">${escapeHtml(task.title)}</span>
84
+ <code class="ad-notify-id">${escapeHtml(task.id)}</code>
85
+ </a></li>`).join("")}</ul>${remaining > 0 ? `<button type="button" class="ad-notify-more" data-notify-more>Load more <span class="has-text-grey">(${remaining} more)</span></button>` : ""}`
86
+ : '<p class="ad-notify-empty">Nothing waiting for your review.</p>';
87
+ return `<div class="ad-notify" data-notify>
88
+ <button type="button" class="button is-small ad-notify-toggle${count ? " has-reviews" : ""}" id="notify-toggle" aria-haspopup="true" aria-expanded="false" aria-controls="notify-panel" aria-label="${escapeHtml(label)}" title="${escapeHtml(label)}" data-live-preserve="aria-expanded">
89
+ <svg aria-hidden="true" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 8a5 5 0 0 1 10 0c0 4 1.5 5.5 1.5 5.5h-13S5 12 5 8Z"/><path d="M8.5 16.5a1.6 1.6 0 0 0 3 0"/></svg>
90
+ ${count ? `<span class="ad-notify-badge" data-review-count="${count}">${count}</span>` : ""}
91
+ </button>
92
+ <div class="ad-notify-panel" id="notify-panel" role="region" aria-label="Needs your review" hidden data-live-preserve="hidden">
93
+ <header class="ad-notify-head"><strong>Needs your review</strong><span class="ad-count">${count}</span></header>
94
+ ${items}
95
+ </div>
96
+ </div>`;
97
+ };
98
+ export const renderHeader = (root, mode = "local", reviews = [], currentTask = "") => `<header class="ad-header">
99
+ <div class="container is-fluid ad-header-inner">
100
+ <a class="ad-brand" href="/" title="Loop · AI-DLC dashboard"><span class="ad-logo" aria-hidden="true">L</span>Loop</a>
101
+ <span class="ad-pill ${mode === "fleet" ? "is-link" : "is-ghost"}" data-authority="${mode}" title="${mode === "fleet" ? "Canonical state lives in the coordination service" : "Canonical state lives in this checkout"}">authority · ${mode}</span>
102
+ ${liveRegion("daemon", renderDaemonPill(root))}
103
+ <span id="live-status" class="ad-pill is-warning" role="status" title="Live updates paused: the dashboard server is unreachable; retrying" hidden><span class="ad-dot"></span>reconnecting…</span>
104
+ <div class="ad-header-actions">
105
+ ${liveRegion("notify", renderNotifications(reviews, currentTask))}
106
+ <a class="button is-small" href="/docs" title="Guides: setup, configuration, commands, troubleshooting">?<span class="ad-hide-mobile ml-1">Docs</span></a>
107
+ <button class="button is-small" data-open="settings-modal" title="Settings">⚙<span class="ad-hide-mobile ml-1">Settings</span></button>
108
+ <button class="button is-small is-primary" data-open="new-task-drawer">+ New task<kbd class="ad-hide-mobile">N</kbd></button>
109
+ </div>
110
+ </div>
111
+ </header>`;
112
+ /** Settings dialog: client-side preferences stored in this browser (`aidlc-settings`,
113
+ * see `assets/ui/common.js` `aidlcSettings`). The form is read and written by the
114
+ * client; every control id is `settings-<key>`. */
115
+ export const renderModals = () => `
116
+ <div id="settings-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="settings-title">
117
+ <div class="modal-background" data-close></div>
118
+ <div class="modal-card">
119
+ <header class="modal-card-head">
120
+ <p class="modal-card-title" id="settings-title">Settings</p>
121
+ <button class="delete" aria-label="close" data-close></button>
122
+ </header>
123
+ <section class="modal-card-body">
124
+ <form id="settings-form" class="ad-settings" novalidate>
125
+ <fieldset class="ad-settings-group">
126
+ <legend class="label">Appearance</legend>
127
+ <div class="field">
128
+ <div class="control ad-settings-radios" role="radiogroup" aria-label="Theme">
129
+ <label class="radio"><input type="radio" name="settings-theme" id="settings-theme-system" value="system" data-autofocus> System</label>
130
+ <label class="radio"><input type="radio" name="settings-theme" id="settings-theme-light" value="light"> Light</label>
131
+ <label class="radio"><input type="radio" name="settings-theme" id="settings-theme-dark" value="dark"> Dark</label>
132
+ </div>
133
+ <p class="help">System follows the operating system's light/dark preference.</p>
134
+ </div>
135
+ </fieldset>
136
+ <fieldset class="ad-settings-group">
137
+ <legend class="label">Live updates</legend>
138
+ <div class="field">
139
+ <label class="label is-small" for="settings-livePoll">Refresh every</label>
140
+ <div class="control"><div class="select is-small"><select id="settings-livePoll"><option value="1500">1.5 seconds</option><option value="3000">3 seconds</option><option value="0">Paused</option></select></div></div>
141
+ <p class="help">How often the page asks the server for changes. Paused keeps the page as it is until you reload or change this.</p>
142
+ </div>
143
+ <div class="field">
144
+ <label class="checkbox"><input type="checkbox" id="settings-notify"> Browser notification when a task waits for your review</label>
145
+ <p class="help" id="settings-notify-state"></p>
146
+ </div>
147
+ </fieldset>
148
+ <fieldset class="ad-settings-group">
149
+ <legend class="label">Live log</legend>
150
+ <div class="field">
151
+ <label class="label is-small" for="settings-logFilter">Default filter</label>
152
+ <div class="control"><div class="select is-small"><select id="settings-logFilter"><option value="important">Important</option><option value="all">All events</option></select></div></div>
153
+ </div>
154
+ <div class="field">
155
+ <label class="checkbox"><input type="checkbox" id="settings-logRaw"> Show raw JSON by default</label>
156
+ <p class="help">Used when a page has no log preference of its own yet.</p>
157
+ </div>
158
+ </fieldset>
159
+ <fieldset class="ad-settings-group">
160
+ <legend class="label">Code changes</legend>
161
+ <div class="field">
162
+ <label class="checkbox"><input type="checkbox" id="settings-diffWrap"> Wrap long lines</label>
163
+ </div>
164
+ <div class="field">
165
+ <label class="checkbox"><input type="checkbox" id="settings-diffIgnoreWs"> Ignore whitespace changes</label>
166
+ </div>
167
+ </fieldset>
168
+ <fieldset class="ad-settings-group">
169
+ <legend class="label">Containers</legend>
170
+ <div class="field">
171
+ <label class="label is-small" for="settings-logTailKb">Container log tail</label>
172
+ <div class="control"><div class="select is-small"><select id="settings-logTailKb"><option value="64">64 KiB</option><option value="256">256 KiB</option><option value="1024">1024 KiB</option></select></div></div>
173
+ </div>
174
+ </fieldset>
175
+ <fieldset class="ad-settings-group">
176
+ <legend class="label">Board</legend>
177
+ <div class="field">
178
+ <label class="label is-small" for="settings-openCards">Clicking a card opens</label>
179
+ <div class="control"><div class="select is-small"><select id="settings-openCards"><option value="panel">The side panel</option><option value="page">The task page</option></select></div></div>
180
+ <p class="help">Ctrl/Cmd-click and middle-click always open the task page in a new tab; shift-click in this tab.</p>
181
+ </div>
182
+ </fieldset>
183
+ </form>
184
+ </section>
185
+ <footer class="modal-card-foot">
186
+ <span class="ad-foot-start">Stored in this browser only</span>
187
+ <button type="button" id="settings-reset" class="button is-light">Reset to defaults</button>
188
+ <button type="button" id="settings-save" class="button is-primary">Save</button>
189
+ </footer>
190
+ </div>
191
+ </div>
192
+ <div id="new-task-drawer" class="ad-drawer is-compose" role="dialog" aria-modal="true" aria-labelledby="new-task-title" hidden>
193
+ <div class="ad-drawer-backdrop" data-drawer-close></div>
194
+ <aside class="ad-drawer-panel" tabindex="-1">
195
+ <header class="ad-drawer-head">
196
+ <h2 id="new-task-title" class="ad-drawer-heading">New task</h2>
197
+ <button type="button" class="delete" aria-label="Close the new task panel" data-drawer-close></button>
198
+ </header>
199
+ <div class="ad-drawer-body">
200
+ <p class="ad-compose-intro">Describe the feature, bug fix, or change. The first line becomes the task title; the loop creates the task and starts the daemon if it is not running.</p>
201
+ <div class="ad-editor" data-editor>
202
+ <div class="ad-editor-bar">
203
+ <div class="ad-editor-tabs" role="tablist" aria-label="Editor mode">
204
+ <button type="button" role="tab" id="new-task-tab-write" aria-selected="true" aria-controls="new-task-requirement" data-editor-tab="write">Write</button>
205
+ <button type="button" role="tab" id="new-task-tab-preview" aria-selected="false" aria-controls="new-task-preview" data-editor-tab="preview">Preview</button>
206
+ </div>
207
+ <div class="ad-editor-tools" role="toolbar" aria-label="Formatting"><button type="button" class="ad-editor-tool" data-md="heading" title="Heading" aria-label="Heading">H</button><button type="button" class="ad-editor-tool" data-md="bold" title="Bold (Ctrl+B)" aria-label="Bold"><b>B</b></button><button type="button" class="ad-editor-tool" data-md="italic" title="Italic (Ctrl+I)" aria-label="Italic"><i>I</i></button><button type="button" class="ad-editor-tool" data-md="code" title="Inline code" aria-label="Inline code">&lt;/&gt;</button><button type="button" class="ad-editor-tool" data-md="codeblock" title="Code block" aria-label="Code block">{ }</button><button type="button" class="ad-editor-tool" data-md="ul" title="Bulleted list" aria-label="Bulleted list">•≡</button><button type="button" class="ad-editor-tool" data-md="ol" title="Numbered list" aria-label="Numbered list">1≡</button><button type="button" class="ad-editor-tool" data-md="quote" title="Quote" aria-label="Quote">❝</button><button type="button" class="ad-editor-tool" data-md="link" title="Link (Ctrl+K)" aria-label="Link">🔗</button></div>
208
+ </div>
209
+ <label class="is-sr-only" for="new-task-requirement">Requirement (Markdown)</label>
210
+ <textarea id="new-task-requirement" class="ad-editor-input" rows="16" spellcheck="true" placeholder="Short title on the first line&#10;&#10;What should change, why, and how to tell it works. Markdown is supported." data-autofocus></textarea>
211
+ <div id="new-task-preview" class="ad-editor-preview content artifact" role="tabpanel" aria-labelledby="new-task-tab-preview" hidden></div>
212
+ </div>
213
+ <div class="ad-field-hint"><span>Markdown · <kbd>Ctrl</kbd>+<kbd>B</kbd> <kbd>I</kbd> <kbd>K</kbd> · the draft is kept in this browser</span><span id="new-task-count">0 / 8192 bytes</span></div>
214
+ </div>
215
+ <footer class="ad-drawer-foot">
216
+ <span class="ad-foot-start"><kbd>Ctrl</kbd>+<kbd>Enter</kbd> to create</span>
217
+ <button type="button" class="button" data-drawer-close>Cancel</button>
218
+ <button type="button" id="new-task-submit" class="button is-primary">Create task</button>
219
+ </footer>
220
+ </aside>
221
+ </div>
222
+ <div id="task-drawer" class="ad-drawer" role="dialog" aria-modal="true" aria-labelledby="task-drawer-title" hidden>
223
+ <div class="ad-drawer-backdrop" data-drawer-close></div>
224
+ <aside class="ad-drawer-panel" tabindex="-1">
225
+ <header class="ad-drawer-head">
226
+ <button type="button" id="task-drawer-title" class="ad-copy ad-drawer-id" data-copy="" title="Copy task ID"><code>Task</code><svg aria-hidden="true" viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5" y="5" width="8.5" height="8.5" rx="1.5"/><path d="M10.5 5V3.5A1.5 1.5 0 0 0 9 2H3.5A1.5 1.5 0 0 0 2 3.5V9a1.5 1.5 0 0 0 1.5 1.5H5"/></svg></button>
227
+ <a id="task-drawer-open" class="button is-small" href="#" target="_blank" rel="noopener">Open full page ↗</a>
228
+ <button type="button" class="delete" aria-label="Close task panel" data-drawer-close></button>
229
+ </header>
230
+ <div class="ad-drawer-body" id="task-drawer-body" data-live="drawer" data-live-hash=""><p class="has-text-grey">Loading…</p></div>
231
+ </aside>
232
+ </div>
233
+ <div id="toasts" class="ad-toasts" aria-live="polite"></div>`;
@@ -0,0 +1,25 @@
1
+ import type { TaskState, WorkflowState } from "../../model.js";
2
+ import { type ContainerEntry } from "../containers.js";
3
+ import { type UiOptions } from "../options.js";
4
+ export declare const DRAWER_LOG_LINES = 20;
5
+ /** Task id as a copy-to-clipboard button (handled by the delegated \`[data-copy]\` click in common.js). */
6
+ export declare const copyIdButton: (id: string) => string;
7
+ /** `aidlc-worker-3f2a1b9c-…` → `3f2a1b9c`; names without a uuid are shown whole. */
8
+ export declare const shortContainerName: (name: string) => string;
9
+ /** `endedAt - startedAt` as `4s`, `3m 12s`, or `1h 05m`; empty when either timestamp is unusable. */
10
+ export declare const durationText: (startedAt: string | undefined, endedAt: string | undefined) => string;
11
+ /** The Containers panel: a table of the task's recorded worker containers (newest first) and the log viewer. */
12
+ export declare const renderContainersPanel: (entries: ContainerEntry[], docker: "ok" | "unavailable") => string;
13
+ /**
14
+ * Task detail body shared by the full page (`detail` live region), the side
15
+ * panel (`fragment`, `drawer` live region), and `/api/live`. Undefined when the
16
+ * task does not exist. Keyed panels (`id`/`data-key`) let the client morph them
17
+ * in place: open artifacts, typed feedback, and the live log survive updates.
18
+ */
19
+ export declare const renderTaskBody: (options: UiOptions, state: WorkflowState, id: string, fragment: boolean, maxAgeMs?: number) => Promise<{
20
+ task: TaskState;
21
+ html: string;
22
+ } | undefined>;
23
+ export declare const taskNotFound: (options: UiOptions, id: string) => string;
24
+ export declare const drawerNotFound: (id: string) => string;
25
+ export declare const renderTaskDetail: (options: UiOptions, id: string, fragment?: boolean) => Promise<string>;