@zq-silk/yui 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/cli/commandCatalog.js +294 -286
  2. package/dist/cli/commandDiscovery.js +69 -0
  3. package/dist/cli/completion.js +22 -227
  4. package/dist/cli/dynamicCompletion.js +12 -6
  5. package/dist/cli/invocationAuthority.js +2 -2
  6. package/dist/cli/invocationRouter.js +18 -7
  7. package/dist/cli.js +71 -23
  8. package/dist/commands/capabilityCommands.js +5 -5
  9. package/dist/commands/globalRoleCommands.js +2 -1
  10. package/dist/commands/taskCommands.js +11 -0
  11. package/dist/context/taskContext.js +6 -2
  12. package/dist/kernel/accessAssessment.js +1 -0
  13. package/dist/kernel/builtinCapabilities.js +28 -1
  14. package/dist/kernel/capabilityRegistry.js +64 -19
  15. package/dist/nativeAgent/agent.js +173 -0
  16. package/dist/nativeAgent/contracts.js +1 -0
  17. package/dist/nativeAgent/demo.js +29 -0
  18. package/dist/nativeAgent/index.js +3 -0
  19. package/dist/nativeAgent/mockProvider.js +43 -0
  20. package/dist/nativeAgent/textTools.js +156 -0
  21. package/dist/nativeAgent/validation.js +95 -0
  22. package/dist/plugins/pluginService.js +98 -8
  23. package/dist/resources/projectResourceService.js +23 -1
  24. package/dist/runtime/managedIdentity.js +6 -0
  25. package/dist/surface/surfaceContributions.js +3 -0
  26. package/dist/task/taskAuthority.js +5 -10
  27. package/dist/web/assets/assetManifest.js +38 -22
  28. package/dist/web/assets/client/api.js +115 -0
  29. package/dist/web/assets/client/app.js +555 -863
  30. package/dist/web/assets/client/components.js +323 -897
  31. package/dist/web/assets/client/detail.js +333 -0
  32. package/dist/web/assets/client/dock.js +225 -0
  33. package/dist/web/assets/client/dom.js +55 -3
  34. package/dist/web/assets/client/evidence.js +172 -0
  35. package/dist/web/assets/client/format.js +25 -14
  36. package/dist/web/assets/client/forms.js +230 -0
  37. package/dist/web/assets/client/i18n.js +1020 -791
  38. package/dist/web/assets/client/overview.js +115 -0
  39. package/dist/web/assets/client/records.js +63 -0
  40. package/dist/web/assets/client/sections.js +313 -0
  41. package/dist/web/assets/client/sidebar.js +117 -0
  42. package/dist/web/assets/client/theme.js +61 -22
  43. package/dist/web/assets/icons.js +39 -0
  44. package/dist/web/assets/shell.js +124 -118
  45. package/dist/web/assets/styles/base.js +41 -0
  46. package/dist/web/assets/styles/components.js +167 -0
  47. package/dist/web/assets/styles/layout.js +49 -77
  48. package/dist/web/assets/styles/markdown.js +16 -24
  49. package/dist/web/assets/styles/responsive.js +33 -44
  50. package/dist/web/assets/styles/tokens.js +65 -89
  51. package/dist/web/assets/styles/views.js +289 -0
  52. package/dist/web/webServer.js +2 -1
  53. package/docs/release-workflow.md +23 -0
  54. package/docs/release-workflow.zh-CN.md +16 -0
  55. package/package.json +1 -1
  56. package/skills/yui-runtime/SKILL.md +26 -0
  57. package/dist/web/assets/client/taskSummary.js +0 -350
  58. package/dist/web/assets/client/taskSurface.js +0 -616
  59. package/dist/web/assets/client/view.js +0 -608
  60. package/dist/web/assets/styles/cards.js +0 -247
  61. package/dist/web/assets/styles/widgets.js +0 -168
@@ -0,0 +1,172 @@
1
+ export const EVIDENCE_SCRIPT = String.raw `
2
+ // Results and evidence. Files are read on request at a fixed revision; the
3
+ // selected revision survives refreshes. Reading never accepts, publishes or
4
+ // executes a result.
5
+ import { h, icon, clear } from "/assets/js/dom.js";
6
+ import { formatBytes } from "/assets/js/format.js";
7
+ import { card, button, note, mono, emptyState, richText, badge, observabilityMetricCard } from "/assets/js/components.js";
8
+
9
+ export function renderEvidence(container, data, t, locale, actions) {
10
+ const state = data.viewState;
11
+ const files = card({ title: t("evidence.files"), icon: "file", hint: t("evidence.filesHint") });
12
+ const list = h("div.file-list");
13
+ const viewer = h("div.file-viewer");
14
+ const load = button(state.artifactList ? t("evidence.reload") : t("evidence.load"), { icon: "refresh", variant: "ghost" });
15
+ files.querySelector(".card-head").append(h("div.card-actions", null, load));
16
+ let readSequence = 0;
17
+ async function read(path, commit) {
18
+ const sequence = ++readSequence;
19
+ viewer.dataset.reading = "true";
20
+ clear(viewer);
21
+ viewer.append(h("p.faint", null, t("evidence.reading")));
22
+ try {
23
+ const result = await actions.readArtifact(data.task.id, path, commit);
24
+ if (sequence !== readSequence) return;
25
+ state.artifact = result;
26
+ drawViewer();
27
+ } catch (error) {
28
+ if (sequence === readSequence) { clear(viewer); viewer.append(note(t("evidence.unavailable") + " " + error.message, "bad")); }
29
+ } finally { if (sequence === readSequence) viewer.dataset.reading = "false"; }
30
+ }
31
+ function drawViewer() {
32
+ clear(viewer);
33
+ const value = state.artifact;
34
+ if (!value) return;
35
+ const ref = "git:" + value.commit + ":" + value.relativePath;
36
+ const copy = button(t("evidence.copy"), { icon: "copy", variant: "ghost" });
37
+ copy.addEventListener("click", async function () {
38
+ try { await navigator.clipboard.writeText(data.task.id + " " + ref); copy.lastChild.textContent = t("evidence.copied"); }
39
+ catch { copy.lastChild.textContent = t("evidence.copyManual"); }
40
+ });
41
+ viewer.append(h("header.viewer-head", null, icon("file"), h("strong", null, value.relativePath), h("span.spacer"), copy));
42
+ viewer.append(h("p.viewer-ref", null, mono(ref)));
43
+ viewer.append(h("p.faint.small", null, t("evidence.fixedNote")));
44
+ const content = h("pre.code-block.artifact-text", { tabIndex: 0, "aria-label": t("evidence.fixedText") }, value.content);
45
+ viewer.append(content);
46
+ }
47
+ function drawList() {
48
+ clear(list);
49
+ const current = state.artifactList;
50
+ if (!current) return;
51
+ if (state.artifact && current.commit !== state.artifact.commit) list.append(note(t("evidence.differentRevision")));
52
+ if (!current.entries.length) { list.append(emptyState(t("evidence.noFiles"), "file")); return; }
53
+ current.entries.forEach(function (entry) {
54
+ list.append(h("button.file-row", {
55
+ type: "button",
56
+ "aria-current": String(!!state.artifact && state.artifact.relativePath === entry.relativePath && state.artifact.commit === current.commit),
57
+ onclick: function () { read(entry.relativePath, current.commit); }
58
+ }, icon("file", "icon-sm"), h("span.file-name", null, entry.relativePath), h("span.faint.small", null, formatBytes(entry.size))));
59
+ });
60
+ }
61
+ load.addEventListener("click", async function () {
62
+ load.disabled = true;
63
+ list.dataset.reading = "true";
64
+ try {
65
+ state.artifactList = await actions.artifacts(data.task.id);
66
+ drawList();
67
+ } catch (error) {
68
+ clear(list);
69
+ list.append(note(t("evidence.listUnavailable") + " " + error.message, "bad"));
70
+ } finally { load.disabled = false; list.dataset.reading = "false"; }
71
+ });
72
+ // Explicit frozen references stay attached to their original revision.
73
+ const sources = [data.task.completionArtifactRefs || [],
74
+ data.core.records.filter(function (entry) { return !entry.omitted && ["task-brief", "task-decision"].includes(entry.ref.store); })
75
+ .map(function (entry) { return JSON.stringify(entry.value); })].flat().join(" ");
76
+ const refs = Array.from(sources.matchAll(/git:([0-9a-f]{40}):([^\s"\\]+?)(?=[\s"\\]|$)/g));
77
+ const seen = new Set();
78
+ const referenced = h("div.file-list");
79
+ refs.forEach(function (match) {
80
+ if (seen.has(match[0])) return;
81
+ seen.add(match[0]);
82
+ referenced.append(h("button.file-row", { type: "button", onclick: function () { read(match[2], match[1]); } },
83
+ icon("flag", "icon-sm"), h("span.file-name", null, match[2]), badge(t("evidence.referenced"), "accent")));
84
+ });
85
+ if (!state.artifactList && !seen.size) files.body.append(emptyState(t("evidence.notLoaded"), "file"));
86
+ files.body.append(referenced, list, viewer);
87
+ drawList();
88
+ drawViewer();
89
+ container.append(files);
90
+
91
+ const delivery = card({ title: t("evidence.delivery"), icon: "layers", hint: t("evidence.deliveryHint") });
92
+ const output = h("div.stack");
93
+ const open = button(t("evidence.readDelivery"), { icon: "eye", variant: "ghost" });
94
+ delivery.querySelector(".card-head").append(h("div.card-actions", null, open));
95
+ delivery.body.append(output);
96
+ open.addEventListener("click", function () {
97
+ open.disabled = true;
98
+ drawDelivery(output, data, t, actions).finally(function () { open.disabled = false; });
99
+ });
100
+ container.append(delivery);
101
+ }
102
+
103
+ async function drawDelivery(output, data, t, actions) {
104
+ clear(output);
105
+ const runtime = data.runtimeStatus === "available" ? data.runtime : null;
106
+ if (!runtime) output.append(note(t("evidence.observationUnavailable")));
107
+ const remote = runtime && runtime.remoteDelivery;
108
+ if (remote) {
109
+ output.append(h("h4.sub-head", null, t("evidence.remote"), badge(t("remoteDelivery." + remote.status, remote.status), remote.status === "merged" ? "ok" : "idle")));
110
+ remote.projects.forEach(function (project) {
111
+ output.append(h("article.record", null,
112
+ h("header.record-head", null, h("strong", null, project.directory), h("span.faint.small", null, project.coverage)),
113
+ h("p.small", null, project.reason),
114
+ h("p.faint.small", null, t("evidence.expectedCommit") + " ", mono(project.expectedLocalCommit || t("common.unknown"))),
115
+ h("p.faint.small", null, "PR/MR · " + (project.state || "none") + " · " + t("evidence.verification") + " " + (project.verification || "unverified")),
116
+ project.adoption ? h("p.faint.small", null, t("evidence.adoption") + " ", mono(project.adoption.id)) : null));
117
+ });
118
+ }
119
+ const original = h("div.stack", null, h("p.faint", null, t("evidence.readingOriginal")));
120
+ output.append(original);
121
+ try {
122
+ const evidence = await actions.evidence(data.task.id);
123
+ clear(original);
124
+ original.append(h("h4.sub-head", null, t("evidence.integrations")));
125
+ if (!evidence.integrations.length) original.append(note(t("evidence.noIntegrations")));
126
+ evidence.integrations.forEach(function (attempt) {
127
+ const element = h("article.record", null,
128
+ h("header.record-head", null, mono(attempt.id), h("span.spacer"), badge(attempt.status, attempt.status === "succeeded" ? "ok" : "idle")),
129
+ h("p.faint.small", null, t("evidence.candidate") + " ", mono(attempt.candidateCommit || t("evidence.noCandidate"))));
130
+ if (!attempt.checks || !attempt.checks.length) element.append(h("p.faint.small", null, t("evidence.noChecks")));
131
+ (attempt.checks || []).forEach(function (check) {
132
+ element.append(h("p.small", null, check.name + " · " + check.outcome));
133
+ if (check.details) element.append(h("p.faint.small", null, check.details));
134
+ });
135
+ original.append(element);
136
+ });
137
+ original.append(h("h4.sub-head", null, t("evidence.reviews")));
138
+ if (!evidence.reviews.length) original.append(note(t("evidence.noReviews")));
139
+ evidence.reviews.forEach(function (round) {
140
+ const element = h("article.record", null,
141
+ h("header.record-head", null, mono(round.id), h("span.spacer"), badge(round.status, round.status === "completed" ? "ok" : "idle")),
142
+ h("p.faint.small.mono-line", null, round.taskCandidate
143
+ ? round.taskCandidate.projects.map(function (p) { return p.projectId + " · " + p.commit; }).join(" ")
144
+ : [round.workItemId, round.candidateId, round.reviewBaseCommit].filter(Boolean).join(" · ")));
145
+ if (round.reviewerRunId) {
146
+ const report = button(t("evidence.readReport"), { icon: "eye", variant: "link" });
147
+ report.addEventListener("click", async function () {
148
+ report.disabled = true;
149
+ try {
150
+ const source = await actions.inspect(data.task.id, { store: "run", refId: round.reviewerRunId });
151
+ element.append(richText(round.reviewerRunId, (source.value.result && source.value.result.output) || t("evidence.noReport"), t));
152
+ report.remove();
153
+ } catch (error) { element.append(note(error.message, "bad")); report.disabled = false; }
154
+ });
155
+ element.append(report);
156
+ }
157
+ original.append(element);
158
+ });
159
+ original.append(h("h4.sub-head", null, t("evidence.workspaces")));
160
+ if (!evidence.workspaces.length) original.append(note(t("evidence.noWorkspaces")));
161
+ evidence.workspaces.forEach(function (workspace) {
162
+ original.append(h("p.small.mono-line", null, workspace.owner.type + " · " + workspace.root));
163
+ });
164
+ original.append(note(t("evidence.cleanupNote")), h("pre.code-block", null, "yui task archive-preflight " + data.task.id + " --integrated"));
165
+ } catch (error) {
166
+ clear(original);
167
+ original.append(note(t("evidence.originalUnavailable") + " " + error.message, "bad"));
168
+ }
169
+ const metrics = runtime && observabilityMetricCard(runtime.observability, t);
170
+ if (metrics) output.append(h("h4.sub-head", null, t("evidence.usage")), metrics);
171
+ }
172
+ `;
@@ -1,20 +1,13 @@
1
- export const FORMAT_SCRIPT = `
2
- // Date / time formatting helpers shared by cards and views.
1
+ export const FORMAT_SCRIPT = String.raw `
2
+ // Date / number formatting shared by every view.
3
3
  export function relativeTime(iso, locale, t) {
4
4
  const seconds = Math.round((Date.parse(iso) - Date.now()) / 1000);
5
- if (!Number.isFinite(seconds) || Math.abs(seconds) < 45) return t("time.justNow");
6
- const units = [
7
- ["year", 31536000],
8
- ["month", 2592000],
9
- ["day", 86400],
10
- ["hour", 3600],
11
- ["minute", 60]
12
- ];
5
+ if (!Number.isFinite(seconds)) return "—";
6
+ if (Math.abs(seconds) < 45) return t("time.justNow");
7
+ const units = [["year", 31536000], ["month", 2592000], ["day", 86400], ["hour", 3600], ["minute", 60]];
13
8
  const selected = units.find(function (entry) { return Math.abs(seconds) >= entry[1]; }) || ["second", 1];
14
- return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(
15
- Math.round(seconds / selected[1]),
16
- selected[0]
17
- );
9
+ return new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "short" })
10
+ .format(Math.round(seconds / selected[1]), selected[0]);
18
11
  }
19
12
 
20
13
  export function formatDateTime(iso, locale) {
@@ -22,6 +15,24 @@ export function formatDateTime(iso, locale) {
22
15
  return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(new Date(iso));
23
16
  }
24
17
 
18
+ export function formatClock(iso, locale) {
19
+ if (!iso || !Number.isFinite(Date.parse(iso))) return "—";
20
+ return new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(new Date(iso));
21
+ }
22
+
23
+ export function formatShort(iso, locale) {
24
+ if (!iso || !Number.isFinite(Date.parse(iso))) return "";
25
+ return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })
26
+ .format(new Date(iso));
27
+ }
28
+
29
+ export function formatBytes(value) {
30
+ if (!Number.isFinite(value)) return "—";
31
+ if (value < 1024) return value + " B";
32
+ if (value < 1024 * 1024) return (value / 1024).toFixed(1) + " KB";
33
+ return (value / 1024 / 1024).toFixed(1) + " MB";
34
+ }
35
+
25
36
  export function byNewest(left, right) {
26
37
  return Date.parse(right.updatedAt || right.createdAt) - Date.parse(left.updatedAt || left.createdAt);
27
38
  }
@@ -0,0 +1,230 @@
1
+ export const FORMS_SCRIPT = String.raw `
2
+ // Write forms. Each submission carries a fresh requestId, marks itself unsent
3
+ // until a receipt settles, and reports an unknown outcome as unknown: nothing
4
+ // here ever replays a write automatically.
5
+ import { h, icon, clear } from "/assets/js/dom.js";
6
+ import { button } from "/assets/js/components.js";
7
+
8
+ function receiptLine(text, state) {
9
+ const line = h("p.receipt", { role: "status" }, text);
10
+ if (state) line.dataset.state = state;
11
+ return line;
12
+ }
13
+ function setReceipt(line, text, state) {
14
+ line.textContent = text;
15
+ line.dataset.state = state || "";
16
+ }
17
+
18
+ function segmented(name, options, value, onChange) {
19
+ const group = h("div.seg.seg-sm", { role: "radiogroup" });
20
+ options.forEach(function (option) {
21
+ const item = h("button.seg-btn", {
22
+ type: "button", role: "radio", title: option.title || null,
23
+ "aria-checked": String(option.value === value),
24
+ dataset: { value: option.value },
25
+ onclick: function () {
26
+ group.querySelectorAll(".seg-btn").forEach(function (other) { other.setAttribute("aria-checked", String(other === item)); });
27
+ onChange(option.value);
28
+ }
29
+ }, option.label);
30
+ group.append(item);
31
+ });
32
+ group.dataset.name = name;
33
+ return group;
34
+ }
35
+
36
+ export function messageComposer(task, t, actions) {
37
+ let intent = "discuss";
38
+ const form = h("form.composer");
39
+ const message = h("textarea.composer-input", {
40
+ required: true, maxLength: 8000, rows: 3, placeholder: t("composer.placeholder"),
41
+ "aria-label": t("composer.label")
42
+ });
43
+ const open = ["active", "draft"].includes(task.status);
44
+ const send = h("button.btn.btn-primary.composer-send", { type: "submit", disabled: !open, title: t("composer.sendHint") },
45
+ icon("send"), h("span", null, t("composer.send")));
46
+ const intents = segmented("intent", [
47
+ { value: "discuss", label: t("intent.discuss"), title: t("intent.discuss.hint") },
48
+ { value: "record", label: t("intent.record"), title: t("intent.record.hint") },
49
+ { value: "develop", label: t("intent.develop"), title: t("intent.develop.hint") }
50
+ ], intent, function (value) { intent = value; });
51
+ const receipt = receiptLine(open ? t("receipt.notSubmitted") : t("composer.closed"));
52
+ const facets = h("div.facets");
53
+ form.append(h("div.composer-box", null, message, h("div.composer-bar", null, intents, h("span.spacer"), send)), receipt, facets);
54
+ form.addEventListener("input", function () { form.dataset.unsent = message.value ? "true" : "false"; });
55
+ message.addEventListener("keydown", function (event) {
56
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { event.preventDefault(); form.requestSubmit(); }
57
+ });
58
+ form.addEventListener("submit", async function (event) {
59
+ event.preventDefault();
60
+ if (send.disabled || !message.value.trim()) return;
61
+ send.disabled = true;
62
+ form.dataset.unsent = "true";
63
+ const requestId = crypto.randomUUID();
64
+ clear(facets);
65
+ setReceipt(receipt, t("receipt.waiting") + " · " + requestId, "pending");
66
+ try {
67
+ // requestId is the submission key: the same key retried returns the
68
+ // original Message and routing, never a second submission.
69
+ const result = await actions.sendMessage(task.id, message.value, requestId, intent);
70
+ setReceipt(receipt, t("receipt.saved") + " · " + result.record.id, "ok");
71
+ renderSubmissionFacets(facets, result.submission, t);
72
+ message.value = "";
73
+ form.dataset.unsent = "false";
74
+ send.disabled = false;
75
+ if (actions.afterWrite) actions.afterWrite();
76
+ } catch (error) {
77
+ if (error.disposition === "not-submitted") {
78
+ setReceipt(receipt, t("receipt.notSubmittedBecause") + " " + error.message, "bad");
79
+ send.disabled = false;
80
+ return;
81
+ }
82
+ setReceipt(receipt, t("receipt.unknownMessage") + " · " + requestId, "warn");
83
+ }
84
+ });
85
+ return form;
86
+ }
87
+
88
+ // Each submission facet is reported on its own line, never collapsed into a
89
+ // single "started". A missing facet block is a contract violation and is
90
+ // reported as such rather than reinterpreted.
91
+ export function renderSubmissionFacets(container, submission, t) {
92
+ clear(container);
93
+ if (!submission) { container.append(h("p.faint.small", null, t("facet.missing"))); return; }
94
+ const rows = [
95
+ ["facet.phase", "phase." + submission.phase, submission.phase],
96
+ ["facet.planning", "planningFacet." + submission.planning, submission.planning],
97
+ ["facet.activation", "activation." + submission.activation, submission.activation],
98
+ ["facet.delivery", "deliveryFacet." + submission.delivery, submission.delivery]
99
+ ];
100
+ container.append(h("dl.kv.kv-compact", null, rows.map(function (row) {
101
+ return [h("dt", null, t(row[0])), h("dd", null, t(row[1], row[2]))];
102
+ })));
103
+ const step = submission.nextStep;
104
+ if (!step) return;
105
+ const text = step.kind === "activate-manually" ? t("next.activateManually").replace("{task}", step.taskId)
106
+ : step.kind === "start-execution" ? t("next.startExecution").replace("{task}", step.taskId)
107
+ : step.kind === "await-pending-activation" ? t("next.awaitActivation").replace("{ref}", step.activationRef)
108
+ : step.kind === "resolve-failed-activation" ? t("next.resolveFailed").replace("{failure}", step.failure).replace("{ref}", step.activationRef)
109
+ : "";
110
+ if (text) container.append(h("p.next-step", { dataset: { nextStep: step.kind } }, icon("flag", "icon-sm"), h("span", null, text)));
111
+ }
112
+
113
+ // Queue / steer / interrupt — the same three application-layer actions the
114
+ // CLI drives. Steer and interrupt name an exact current Turn; a mismatch is a
115
+ // visible failure receipt, never a silent downgrade to a queue.
116
+ export function controlForm(task, t, actions, roleNames) {
117
+ let kind = "queue";
118
+ const form = h("form.stack");
119
+ const role = h("input", { list: "control-roles-" + task.id, placeholder: t("control.rolePlaceholder") });
120
+ const roles = h("datalist", { id: "control-roles-" + task.id }, (roleNames || []).map(function (name) { return h("option", { value: name }); }));
121
+ const body = h("textarea", { maxLength: 8000, rows: 3 });
122
+ const expected = h("input.mono", { placeholder: t("control.expectedPlaceholder") });
123
+ const workItem = h("input.mono");
124
+ const then = h("input.mono", { placeholder: t("control.thenPlaceholder") });
125
+ const field = function (text, control, hint) {
126
+ return h("label.field", null, h("span", null, text), control, hint ? h("small.faint", null, hint) : null);
127
+ };
128
+ const roleField = field(t("control.role"), role);
129
+ const bodyField = field(t("control.message"), body);
130
+ const expectedField = field(t("control.expectedTurn"), expected, t("control.expectedHint"));
131
+ const workItemField = field(t("control.workItem"), workItem);
132
+ const thenField = field(t("control.then"), then);
133
+ const open = ["active", "draft"].includes(task.status);
134
+ const submit = button(t("control.submit"), { type: "submit", variant: "primary", icon: "send", disabled: !open });
135
+ const receipt = receiptLine(t("receipt.notSubmitted"));
136
+ function apply() {
137
+ bodyField.hidden = kind === "interrupt";
138
+ expectedField.hidden = kind === "queue";
139
+ thenField.hidden = kind !== "interrupt";
140
+ workItemField.hidden = kind === "interrupt";
141
+ role.required = kind !== "queue";
142
+ body.required = kind !== "interrupt";
143
+ expected.required = kind !== "queue";
144
+ }
145
+ const actionsSeg = segmented("action", [
146
+ { value: "queue", label: t("control.queue"), title: t("control.queue.hint") },
147
+ { value: "steer", label: t("control.steer"), title: t("control.steer.hint") },
148
+ { value: "interrupt", label: t("control.interrupt"), title: t("control.interrupt.hint") }
149
+ ], kind, function (value) { kind = value; apply(); });
150
+ form.append(h("div.field", null, h("span", null, t("control.action")), actionsSeg),
151
+ h("div.field-row", null, roleField, workItemField), roles, bodyField, expectedField, thenField,
152
+ h("div.form-actions", null, submit), receipt);
153
+ apply();
154
+ form.addEventListener("input", function () { form.dataset.unsent = "true"; });
155
+ form.addEventListener("submit", async function (event) {
156
+ event.preventDefault();
157
+ if (submit.disabled) return;
158
+ submit.disabled = true;
159
+ form.dataset.unsent = "true";
160
+ const requestId = crypto.randomUUID();
161
+ const payload = { action: kind, requestId };
162
+ if (kind === "interrupt") {
163
+ payload.role = role.value.trim();
164
+ payload.expectedTarget = expected.value.trim();
165
+ if (then.value.trim()) payload.thenMessage = then.value.trim();
166
+ } else {
167
+ payload.body = body.value;
168
+ if (kind === "steer") { payload.to = role.value.trim(); payload.expectedTarget = expected.value.trim(); }
169
+ else if (role.value.trim()) payload.to = role.value.trim();
170
+ if (workItem.value.trim()) payload.workItem = workItem.value.trim();
171
+ }
172
+ setReceipt(receipt, t("receipt.waiting") + " · " + requestId, "pending");
173
+ try {
174
+ // The receipt states exactly what settled; none of these states implies
175
+ // the Provider executed the input.
176
+ const result = await actions.control(task.id, payload);
177
+ const settled = result.steer || result.interrupt || result.delivery || { state: result.disposition };
178
+ setReceipt(receipt, t("receipt.receipt") + " " + result.action + " · " + settled.state
179
+ + (settled.outcome ? " · " + settled.outcome : "") + (settled.detail ? " · " + settled.detail : "")
180
+ + (result.record ? " · " + result.record.id : ""), "ok");
181
+ form.dataset.unsent = "false";
182
+ submit.disabled = false;
183
+ if (actions.afterWrite) actions.afterWrite();
184
+ } catch (error) {
185
+ if (error.disposition === "not-submitted") {
186
+ setReceipt(receipt, t("receipt.notSubmittedBecause") + " " + error.message, "bad");
187
+ submit.disabled = false;
188
+ return;
189
+ }
190
+ setReceipt(receipt, t("receipt.unknownControl") + " · " + requestId, "warn");
191
+ }
192
+ });
193
+ return form;
194
+ }
195
+
196
+ export function titleForm(task, t, actions) {
197
+ const form = h("form.inline-form");
198
+ const archived = task.status === "archived";
199
+ const input = h("input", { value: task.title, required: true, maxLength: 500, disabled: archived, "aria-label": t("details.title") });
200
+ const save = button(t("actions.save"), { type: "submit", variant: "primary", disabled: archived });
201
+ const receipt = receiptLine(t("receipt.notSubmitted"));
202
+ form.append(h("div.inline-row", null, input, save), receipt);
203
+ form.addEventListener("input", function () { form.dataset.unsent = input.value !== task.title ? "true" : "false"; });
204
+ form.addEventListener("submit", async function (event) {
205
+ event.preventDefault();
206
+ if (save.disabled) return;
207
+ save.disabled = true;
208
+ form.dataset.unsent = "true";
209
+ const requestId = crypto.randomUUID();
210
+ setReceipt(receipt, t("receipt.waiting") + " · " + requestId, "pending");
211
+ try {
212
+ const result = await actions.updateTask(task.id, { title: input.value }, requestId);
213
+ setReceipt(receipt, t("receipt.savedRevision") + " " + result.revision, "ok");
214
+ input.value = result.record.title;
215
+ form.dataset.unsent = "false";
216
+ save.disabled = false;
217
+ if (actions.afterWrite) actions.afterWrite();
218
+ } catch (error) {
219
+ if (error.disposition === "not-submitted") {
220
+ setReceipt(receipt, t("receipt.notSavedBecause") + " " + error.message, "bad");
221
+ save.disabled = false;
222
+ return;
223
+ }
224
+ // A lost response must never automatically replay a write.
225
+ setReceipt(receipt, t("receipt.unknownSave") + " · " + requestId, "warn");
226
+ }
227
+ });
228
+ return form;
229
+ }
230
+ `;