@zq-silk/yui 2.2.0 → 2.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 (51) hide show
  1. package/README.md +4 -2
  2. package/dist/cli/commandCatalog.js +8 -3
  3. package/dist/cli/interactionPolicy.js +4 -0
  4. package/dist/commands/taskCommands.js +5 -7
  5. package/dist/commands/taskIntegrationCommands.js +5 -16
  6. package/dist/controlPlaneCli.js +28 -21
  7. package/dist/controller/clientRuntime.js +3 -2
  8. package/dist/integration/gitIntegrationService.js +8 -67
  9. package/dist/job/jobCleanupInspection.js +67 -0
  10. package/dist/repository/gitWorkspace.js +41 -12
  11. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  12. package/dist/repository/taskWorkspacePreparer.js +39 -43
  13. package/dist/repository/workspaceCleanupInspection.js +88 -7
  14. package/dist/resources/resourceRegistrar.js +16 -10
  15. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  16. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  17. package/dist/task/archiveDiagnostics.js +26 -8
  18. package/dist/task/archivePreflight.js +26 -9
  19. package/dist/task/leaderArchive.js +9 -15
  20. package/dist/web/assets/client/api.js +8 -0
  21. package/dist/web/assets/client/app.js +10 -6
  22. package/dist/web/assets/client/components.js +95 -36
  23. package/dist/web/assets/client/detail.js +110 -64
  24. package/dist/web/assets/client/dock.js +95 -11
  25. package/dist/web/assets/client/evidence.js +286 -94
  26. package/dist/web/assets/client/i18n.js +178 -62
  27. package/dist/web/assets/client/overview.js +9 -13
  28. package/dist/web/assets/client/records.js +142 -7
  29. package/dist/web/assets/client/sections.js +234 -198
  30. package/dist/web/assets/icons.js +4 -0
  31. package/dist/web/assets/styles/components.js +64 -17
  32. package/dist/web/assets/styles/responsive.js +7 -5
  33. package/dist/web/assets/styles/tokens.js +1 -1
  34. package/dist/web/assets/styles/views.js +48 -31
  35. package/dist/web/webServer.js +12 -1
  36. package/dist/web/webTaskSurface.js +8 -1
  37. package/docs/observability/README.md +13 -3
  38. package/docs/observability/README.zh-CN.md +10 -3
  39. package/docs/release-workflow.md +25 -0
  40. package/docs/release-workflow.zh-CN.md +19 -0
  41. package/docs/task-delivery.md +36 -8
  42. package/docs/task-delivery.zh-CN.md +24 -7
  43. package/i18n/README.zh-CN.md +3 -2
  44. package/package.json +1 -1
  45. package/skills/yui-leader/references/execution.md +2 -1
  46. package/skills/yui-leader/references/integration.md +20 -0
  47. package/skills/yui-operator/SKILL.md +4 -0
  48. package/skills/yui-reviewer/SKILL.md +3 -0
  49. package/skills/yui-runtime/SKILL.md +4 -0
  50. package/skills/yui-runtime/references/publication.md +20 -4
  51. package/skills/yui-worker/SKILL.md +3 -0
@@ -8,7 +8,7 @@ import { FitAddon } from "/assets/vendor/addon-fit.mjs";
8
8
  import { h, icon, clear } from "/assets/js/dom.js";
9
9
  import { formatShort } from "/assets/js/format.js";
10
10
  import { label, chip, emptyState, richText, dot } from "/assets/js/components.js";
11
- import { entriesOf, recordCard, recordReader } from "/assets/js/records.js";
11
+ import { entriesOf, totalOf, readExact, recordCard } from "/assets/js/records.js";
12
12
  import { messageComposer } from "/assets/js/forms.js";
13
13
  import { pageToken } from "/assets/js/api.js";
14
14
 
@@ -31,6 +31,7 @@ export function renderDiscussion(host, data, t, locale, actions) {
31
31
  feed = host.querySelector(".feed");
32
32
  }
33
33
  host.dataset.taskId = task.id;
34
+ latest.set(task.id, { data: data, t: t, locale: locale, actions: actions });
34
35
  if (!feed.querySelector('[data-reading="true"]')) drawFeed(feed, data, t, locale, actions);
35
36
  feed.scrollTop = atEnd ? feed.scrollHeight : previousScroll;
36
37
  if (!keepComposer) {
@@ -44,21 +45,92 @@ export function discussionHasUnsent(host) {
44
45
  return !!host.querySelector('[data-unsent="true"]');
45
46
  }
46
47
 
48
+ // The Context read carries the newest messages as summaries; each is read
49
+ // exactly once per digest (kept in the Task's view state) and drawn in full,
50
+ // so a message needs no separate raw-record reader.
51
+ // Older messages are listed on request, newest first, and read the same way.
52
+ const older = new Map();
53
+ const failedReads = new Set();
54
+ const latest = new Map();
55
+
56
+ function readKey(entry) { return entry.ref.refId + "@" + entry.ref.digest; }
57
+
58
+ function exactValue(entry, cache) {
59
+ if (!entry.omitted) return entry.value;
60
+ const hit = cache[entry.ref.store + "/" + entry.ref.refId];
61
+ return hit && hit.digest === entry.ref.digest && hit.result ? hit.result.value : null;
62
+ }
63
+
64
+ function loadOlder(data, actions, state, shownIds) {
65
+ state.pending = true;
66
+ state.error = null;
67
+ let rounds = 0;
68
+ function next() {
69
+ return actions.list(data.task.id, "task-message", { limit: 20, cursor: state.cursor }).then(function (page) {
70
+ const known = new Set(state.items.map(function (item) { return item.ref.refId; }));
71
+ const fresh = page.items.filter(function (item) { return !known.has(item.ref.refId) && !shownIds.has(item.ref.refId); });
72
+ state.items = state.items.concat(fresh);
73
+ state.cursor = page.nextCursor;
74
+ state.done = !page.nextCursor;
75
+ rounds += 1;
76
+ if (!fresh.length && page.nextCursor && rounds < 10) return next();
77
+ });
78
+ }
79
+ // A listing that moved invalidates its continuation; the next request
80
+ // starts again from the newest page and skips what is already shown.
81
+ return next().catch(function (error) { state.error = error.message; state.cursor = null; })
82
+ .then(function () { state.pending = false; });
83
+ }
84
+
47
85
  function drawFeed(feed, data, t, locale, actions) {
48
86
  clear(feed);
49
- const core = data.core;
50
- const entries = entriesOf(core, "task-message").slice().sort(function (a, b) {
51
- return Date.parse((a.value && a.value.createdAt) || 0) - Date.parse((b.value && b.value.createdAt) || 0);
87
+ const task = data.task;
88
+ const cache = data.viewState.exact = data.viewState.exact || {};
89
+ const contextEntries = entriesOf(data.core, "task-message");
90
+ const shownIds = new Set(contextEntries.map(function (entry) { return entry.ref.refId; }));
91
+ if (!older.has(task.id)) older.set(task.id, { items: [], cursor: null, done: false, pending: false, error: null });
92
+ const state = older.get(task.id);
93
+ const olderItems = state.items.filter(function (item) { return !shownIds.has(item.ref.refId); })
94
+ .sort(function (a, b) { return Date.parse(a.createdAt || 0) - Date.parse(b.createdAt || 0); });
95
+ // Context lists messages newest first; the feed reads oldest first.
96
+ const rows = olderItems.map(function (item) { return { entry: { ref: item.ref, summary: item.summary, omitted: true }, at: item.createdAt }; })
97
+ .concat(contextEntries.slice().reverse().map(function (entry) { return { entry: entry, at: null }; }));
98
+ const reads = [];
99
+ rows.forEach(function (row) {
100
+ row.value = exactValue(row.entry, cache);
101
+ if (!row.value && !failedReads.has(readKey(row.entry))) {
102
+ row.reading = true;
103
+ reads.push(readExact(row.entry, task.id, actions, cache).catch(function () { failedReads.add(readKey(row.entry)); }));
104
+ }
105
+ row.at = (row.value && row.value.createdAt) || row.at;
52
106
  });
53
- if (core.omitted.records) feed.append(h("p.feed-note", null, t("discussion.omitted")));
54
- if (!entries.length) {
107
+ if (rows.every(function (row) { return row.at; })) rows.sort(function (a, b) { return Date.parse(a.at) - Date.parse(b.at); });
108
+
109
+ const remaining = totalOf(data.core, "task-message") - shownIds.size - olderItems.length;
110
+ if (actions.list && remaining > 0 && !state.done) {
111
+ const more = h("button.show-more.feed-older", { type: "button", disabled: state.pending },
112
+ state.pending ? t("discussion.olderLoading") : t("discussion.older").replace("{count}", String(remaining)));
113
+ more.addEventListener("click", function () {
114
+ more.disabled = true;
115
+ more.textContent = t("discussion.olderLoading");
116
+ loadOlder(data, actions, state, shownIds).then(function () { redraw(feed, task.id, true); });
117
+ });
118
+ feed.append(more);
119
+ }
120
+ if (state.error) feed.append(h("p.feed-note", null, t("records.unavailable") + " " + state.error));
121
+ if (!rows.length) {
55
122
  feed.append(emptyState(t("discussion.empty"), "chat"));
56
123
  return;
57
124
  }
58
125
  let lastDay = "";
59
- entries.forEach(function (entry) {
60
- if (entry.omitted) { feed.append(recordCard(entry, data.task.id, t, actions, { compact: true })); return; }
61
- const value = entry.value;
126
+ rows.forEach(function (row) {
127
+ if (!row.value) {
128
+ const placeholder = recordCard(row.entry, task.id, t, actions, { compact: true });
129
+ if (row.reading) placeholder.dataset.reading = "true";
130
+ feed.append(placeholder);
131
+ return;
132
+ }
133
+ const value = row.value;
62
134
  const day = value.createdAt ? new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(new Date(value.createdAt)) : "";
63
135
  if (day && day !== lastDay) { feed.append(h("div.feed-day", null, h("span", null, day))); lastDay = day; }
64
136
  const type = (value.author && value.author.type) || "system";
@@ -69,12 +141,24 @@ function drawFeed(feed, data, t, locale, actions) {
69
141
  h("header.msg-head", null, h("strong", null, author),
70
142
  value.intent ? chip(label(t, "intent", value.intent)) : null,
71
143
  value.kind === "role-result" ? chip(label(t, "messageKind", value.kind)) : null,
72
- h("time", { dateTime: value.createdAt || "" }, formatShort(value.createdAt, locale))),
144
+ h("time", { dateTime: value.createdAt || "", title: row.entry.ref.refId }, formatShort(value.createdAt, locale))),
73
145
  h("div.msg-bubble", null, richText(null, value.body || "", t, { threshold: 900 })));
74
- main.append(recordReader(entry, data.task.id, t, actions));
75
146
  message.append(main);
76
147
  feed.append(message);
77
148
  });
149
+ if (reads.length) Promise.all(reads).then(function () { redraw(feed, task.id, false); });
150
+ }
151
+
152
+ // Redraw after a read without losing the reader's place: the bottom stays
153
+ // pinned, and prepended older messages keep the current message in view.
154
+ function redraw(feed, taskId, prepend) {
155
+ const host = feed.closest("[data-task-id]");
156
+ const current = latest.get(taskId);
157
+ if (!feed.isConnected || !host || host.dataset.taskId !== taskId || !current) return;
158
+ const atEnd = feed.scrollHeight - feed.clientHeight - feed.scrollTop < 40;
159
+ const fromBottom = feed.scrollHeight - feed.scrollTop;
160
+ drawFeed(feed, current.data, current.t, current.locale, current.actions);
161
+ feed.scrollTop = atEnd && !prepend ? feed.scrollHeight : feed.scrollHeight - fromBottom;
78
162
  }
79
163
 
80
164
  // --- Live native Session ----------------------------------------------------------
@@ -1,18 +1,290 @@
1
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.
2
+ // The Delivery tab: work items, reviews, integration and remote delivery,
3
+ // saved files and retained workspaces. Opening the tab reads the original
4
+ // evidence once per Context cursor; reading never accepts, publishes,
5
+ // executes or cleans anything.
5
6
  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";
7
+ import { formatBytes, formatClock } from "/assets/js/format.js";
8
+ import {
9
+ card, listCard, setCardCount, button, note, mono, chip, emptyState, richText, badge, statusBadge, label, tone, dot,
10
+ timeTag, disclosure, workItemCard, workItemBody, reviewCard
11
+ } from "/assets/js/components.js";
12
+ import { entriesOf, valuesOf, totalOf, summaryFields, readExact, exactRow, lazyRow, familyState, loadFamily } from "/assets/js/records.js";
8
13
 
9
- export function renderEvidence(container, data, t, locale, actions) {
14
+ const DELIVERY_TONES = { merged: "ok", partial: "warn", unverified: "warn", uncovered: "warn", pending: "info", none: "idle", unavailable: "idle" };
15
+
16
+ function newest(left, right) {
17
+ return Date.parse(right.createdAt || 0) - Date.parse(left.createdAt || 0);
18
+ }
19
+
20
+ // Only http(s) links without credentials leave the page.
21
+ function safeUrl(value) {
22
+ try {
23
+ const url = new URL(value);
24
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password ? url.href : null;
25
+ } catch { return null; }
26
+ }
27
+
28
+ export function renderDelivery(panel, data, t, locale, ctx) {
29
+ const view = data.viewState;
30
+ const core = data.core;
31
+ const task = data.task;
32
+ view.exact = view.exact || {};
33
+ view.openRows = view.openRows || {};
34
+ view.reports = view.reports || {};
35
+ view.panels = view.panels || {};
36
+ view.panels.delivery = panel;
37
+
38
+ // --- Work items ------------------------------------------------------------
39
+ const work = listCard({ id: "detail-work", title: t("work.title"), icon: "layers", count: totalOf(core, "work-item") || null, hint: t("work.openHint") });
40
+ function drawWork() {
41
+ clear(work.list);
42
+ const family = familyState(view, "work-item");
43
+ const entries = entriesOf(core, "work-item");
44
+ const titles = {};
45
+ family.items.forEach(function (item) { const fields = summaryFields(item.summary); if (fields.title) titles[item.ref.refId] = fields.title; });
46
+ valuesOf(core, "work-item").forEach(function (item) { titles[item.id] = item.title; });
47
+ const shown = new Set(entries.map(function (entry) { return entry.ref.refId; }));
48
+ const extraOpen = family.items.filter(function (item) { return item.status === "open" && !shown.has(item.ref.refId); });
49
+ const finished = family.items.filter(function (item) { return item.status !== "open"; });
50
+ entries.forEach(function (entry) {
51
+ work.list.append(exactRow(entry, task.id, t, ctx, view.exact, function (value) { return workItemCard(value, t, locale, titles); }));
52
+ });
53
+ extraOpen.forEach(function (item) { work.list.append(workRow(item, titles)); });
54
+ const loaded = family.key && family.pages;
55
+ if (loaded) setCardCount(work, family.total || null);
56
+ if (!entries.length && !extraOpen.length) {
57
+ work.list.append(emptyState(loaded && family.total === 0 ? t("work.none") : t("work.noneOpen"), "layers"));
58
+ }
59
+ if (family.error) work.list.append(note(t("records.unavailable") + " " + family.error, "bad"));
60
+ if (!loaded) {
61
+ if (family.pending) work.list.append(h("p.row-label", null, t("work.reading")));
62
+ return;
63
+ }
64
+ const finishedTotal = Math.max(finished.length, (family.total || 0) - totalOf(core, "work-item"));
65
+ if (!finishedTotal) return;
66
+ const group = h("div.row-stack.is-finished", null, h("p.row-label", null, t("work.finished") + " · " + finishedTotal));
67
+ finished.forEach(function (item) { group.append(workRow(item, titles)); });
68
+ work.list.append(group);
69
+ if (family.nextCursor) work.list.append(listFoot(family, function () {
70
+ return loadFamily(view, task.id, "work-item", family.key, ctx, { more: true, limit: 40 });
71
+ }));
72
+ }
73
+ function workRow(item, titles) {
74
+ const fields = summaryFields(item.summary);
75
+ return lazyRow(item, task.id, t, ctx, {
76
+ cache: view.exact, openRows: view.openRows,
77
+ head: [dot(tone("work", item.status)), h("span.lazy-title", null, fields.title || item.ref.refId),
78
+ item.assignee ? h("span.faint.small.lazy-meta", null, item.assignee) : null,
79
+ statusBadge(t, "work", "work", item.status)],
80
+ render: function (value) { return workItemBody(value, t, locale, titles); }
81
+ });
82
+ }
83
+ function listFoot(family, more) {
84
+ const next = button(t("history.more"), { icon: "chevron", variant: "ghost" });
85
+ next.addEventListener("click", function () {
86
+ next.disabled = true;
87
+ more().then(refresh);
88
+ });
89
+ return h("div.list-foot", null,
90
+ h("span.faint.small", null, t("history.loaded").replace("{shown}", String(family.items.length)).replace("{total}", String(family.total))),
91
+ next);
92
+ }
93
+
94
+ // --- Reviews ---------------------------------------------------------------
95
+ const reviews = listCard({ id: "detail-reviews", title: t("reviews.title"), icon: "eye", hint: t("reviews.hint") });
96
+ function drawReviews() {
97
+ clear(reviews.list);
98
+ const evidence = view.evidence && view.evidence.key && view.evidence.value;
99
+ if (evidence) {
100
+ const rounds = evidence.reviews.slice().sort(newest);
101
+ setCardCount(reviews, rounds.length || null);
102
+ if (!rounds.length) reviews.list.append(emptyState(t("reviews.none"), "eye"));
103
+ rounds.forEach(function (round) { reviews.list.append(reviewRow(round)); });
104
+ return;
105
+ }
106
+ const entries = entriesOf(core, "review-round");
107
+ setCardCount(reviews, entries.length || null);
108
+ entries.forEach(function (entry) {
109
+ reviews.list.append(exactRow(entry, task.id, t, ctx, view.exact, function (value) { return reviewRow(value); }));
110
+ });
111
+ if (view.evidence && view.evidence.error) reviews.list.append(note(t("records.unavailable") + " " + view.evidence.error, "bad"));
112
+ else reviews.list.append(h("p.row-label", null, t("evidence.loading")));
113
+ }
114
+ function reviewRow(round) {
115
+ const element = reviewCard(round, t, locale);
116
+ if (!round.reviewerRunId) return element;
117
+ const holder = h("div.stack");
118
+ const cached = view.reports[round.id];
119
+ if (cached) { holder.append(richText(t("evidence.report"), cached, t, { threshold: 480 })); element.append(holder); return element; }
120
+ const report = button(t("evidence.readReport"), { icon: "eye", variant: "link" });
121
+ report.addEventListener("click", async function () {
122
+ report.disabled = true;
123
+ try {
124
+ const source = await ctx.inspect(task.id, { store: "run", refId: round.reviewerRunId });
125
+ const output = (source.result && source.result.output) || (source.value && source.value.result && source.value.result.output) || t("evidence.noReport");
126
+ view.reports[round.id] = output;
127
+ holder.append(richText(t("evidence.report"), output, t, { threshold: 480 }));
128
+ report.remove();
129
+ } catch (error) { holder.append(note(error.message, "bad")); report.disabled = false; }
130
+ });
131
+ element.append(h("div.record-actions", null, report), holder);
132
+ return element;
133
+ }
134
+
135
+ // --- Integration and remote delivery ------------------------------------------
136
+ const delivery = card({ id: "detail-results", title: t("delivery.title"), icon: "merge", hint: t("delivery.hint") });
137
+ const deliveryBody = h("div.stack.delivery-stack");
138
+ delivery.body.append(deliveryBody);
139
+ function remoteDelivery() {
140
+ const entry = entriesOf(core, "remote-delivery")[0];
141
+ if (entry && !entry.omitted) return entry.value;
142
+ const cached = entry && view.exact["remote-delivery/" + entry.ref.refId];
143
+ if (cached && cached.digest === entry.ref.digest && cached.result) return cached.result.value;
144
+ return (data.runtime && data.runtime.remoteDelivery) || null;
145
+ }
146
+ function drawDelivery() {
147
+ clear(deliveryBody);
148
+ const remote = remoteDelivery();
149
+ if (remote) {
150
+ deliveryBody.append(h("h4.sub-head", null, icon("package", "icon-sm"), h("span", null, t("evidence.remote")),
151
+ badge(t("remoteDelivery." + remote.status, remote.status), DELIVERY_TONES[remote.status] || "idle", { dot: true })));
152
+ if (remote.codeProjectCount) {
153
+ deliveryBody.append(h("p.faint.small", null, t("delivery.counts")
154
+ .replace("{merged}", String(remote.mergedProjectCount)).replace("{verified}", String(remote.verifiedProjectCount))
155
+ .split("{total}").join(String(remote.codeProjectCount))));
156
+ }
157
+ (remote.projects || []).forEach(function (project) { deliveryBody.append(projectRow(project)); });
158
+ if (!(remote.projects || []).length) deliveryBody.append(note(t("delivery.noProjects")));
159
+ } else deliveryBody.append(note(t("evidence.observationUnavailable")));
160
+
161
+ const state = view.evidence || {};
162
+ const evidence = state.key && state.value;
163
+ deliveryBody.append(h("h4.sub-head", null, icon("merge", "icon-sm"), h("span", null, t("evidence.integrations")),
164
+ evidence ? h("span.count", null, String(evidence.integrations.length)) : null));
165
+ if (!evidence) {
166
+ deliveryBody.append(state.error ? note(t("evidence.originalUnavailable") + " " + state.error, "bad") : h("p.faint.small", null, t("evidence.loading")));
167
+ return;
168
+ }
169
+ if (!evidence.integrations.length) deliveryBody.append(note(t("evidence.noIntegrations")));
170
+ const rows = h("div.row-stack.boxed");
171
+ evidence.integrations.slice().sort(newest).forEach(function (attempt) { rows.append(integrationRow(attempt)); });
172
+ if (rows.childNodes.length) deliveryBody.append(rows);
173
+ deliveryBody.append(h("p.faint.small", null, t("delivery.readAt") + " " + formatClock(state.readAt, locale)
174
+ + (state.pending ? " · " + t("evidence.loading") : "")));
175
+ }
176
+ function projectRow(project) {
177
+ const publication = project.publication;
178
+ const href = publication && publication.externalUrl && safeUrl(publication.externalUrl);
179
+ const element = h("article.record.delivery-project");
180
+ element.append(h("header.record-head", null, icon("folder", "icon-sm"), h("strong", null, project.directory || project.projectId),
181
+ project.coverage ? chip(label(t, "coverage", project.coverage)) : null,
182
+ h("span.spacer"),
183
+ publication ? (href
184
+ ? h("a.link", { href: href, rel: "noopener noreferrer", target: "_blank" }, label(t, "publication", publication.externalKind) + " #" + publication.externalId)
185
+ : mono(publication.externalKind + " " + publication.externalId)) : null));
186
+ if (publication && publication.title) element.append(h("p.small", null, publication.title));
187
+ if (project.reason) element.append(h("p.faint.small", null, project.reason));
188
+ element.append(h("div.meta-line", null,
189
+ h("span", null, t("evidence.expectedCommit") + " "), mono(project.expectedLocalCommit ? project.expectedLocalCommit.slice(0, 12) : t("common.unknown")),
190
+ project.state ? chip(label(t, "publicationState", project.state)) : null,
191
+ project.verification ? chip(t("evidence.verification") + " · " + label(t, "verification", project.verification)) : null,
192
+ project.adoption ? h("span", null, t("evidence.adoption") + " ", mono(project.adoption.id)) : null));
193
+ return element;
194
+ }
195
+ function integrationRow(attempt) {
196
+ const source = attempt.source || {};
197
+ const element = h("article.record.integration-row");
198
+ element.dataset.status = attempt.status;
199
+ element.append(h("header.record-head", null, dot(tone("integration", attempt.status)), mono(attempt.id),
200
+ chip(label(t, "integration.source", source.kind) + (source.workItemId ? " · " + source.workItemId : "")),
201
+ source.strategy ? chip(source.strategy) : null,
202
+ h("span.spacer"),
203
+ statusBadge(t, "integration", "integration", attempt.status)));
204
+ if (attempt.summary) element.append(h("p.small", null, attempt.summary));
205
+ element.append(h("div.meta-line", null,
206
+ attempt.projectId ? h("span", null, attempt.projectId) : null,
207
+ attempt.targetRef ? mono(attempt.targetRef) : null,
208
+ h("span", null, t("evidence.candidate") + " "), mono(attempt.candidateCommit ? attempt.candidateCommit.slice(0, 12) : t("evidence.noCandidate")),
209
+ timeTag(attempt.createdAt, locale, t)));
210
+ if (attempt.checks && attempt.checks.length) {
211
+ element.append(h("ul.check-list", null, attempt.checks.map(function (check) {
212
+ return h("li", null, dot(check.outcome === "passed" ? "ok" : check.outcome === "failed" ? "bad" : "idle"),
213
+ h("span.mono-line", null, check.name), h("span.faint", null, label(t, "check", check.outcome)),
214
+ check.details ? h("span.faint.small.check-details", null, check.details) : null);
215
+ })));
216
+ } else element.append(h("p.faint.small", null, t("evidence.noChecks")));
217
+ return element;
218
+ }
219
+
220
+ // --- Workspaces and cleanup --------------------------------------------------
221
+ const workspaces = disclosure(t("workspaces.title"), "workspaces", { className: "card-disclosure", meta: String(totalOf(core, "managed-workspace")) });
222
+ workspaces.id = "detail-workspaces";
223
+ function drawWorkspaces() {
224
+ clear(workspaces.body);
225
+ const evidence = view.evidence && view.evidence.key && view.evidence.value;
226
+ const list = evidence ? evidence.workspaces : valuesOf(core, "managed-workspace");
227
+ if (!list.length) workspaces.body.append(note(t("evidence.noWorkspaces")));
228
+ const rows = h("div.row-stack.boxed");
229
+ list.forEach(function (workspace) {
230
+ const owner = workspace.owner || {};
231
+ const ownerId = owner.workItemId || owner.reviewRoundId || owner.integrationAttemptId
232
+ || (owner.executionGroupId ? owner.executionGroupId + (owner.executionLaneId ? "/" + owner.executionLaneId : "") : "") || owner.taskId || "";
233
+ rows.append(h("article.record.is-compact", null,
234
+ h("header.record-head", null, chip(label(t, "workspace.owner", owner.type)), mono(ownerId), h("span.spacer"), timeTag(workspace.updatedAt, locale, t)),
235
+ h("p.small.mono-line", null, workspace.root),
236
+ (workspace.entries || []).length ? h("div.meta-line", null, workspace.entries.map(function (entry) {
237
+ return h("span", null, entry.directory + " · ", mono(entry.branch || entry.baseRef || ""));
238
+ })) : null));
239
+ });
240
+ if (rows.childNodes.length) workspaces.body.append(rows);
241
+ workspaces.body.append(note(t("evidence.cleanupNote")), h("pre.code-block", null, "yui task archive-preflight " + task.id + " --integrated"));
242
+ }
243
+
244
+ panel.append(work, reviews, delivery, filesCard(data, t, locale, ctx), workspaces);
245
+ panel.redraw = function () { drawWork(); drawReviews(); drawDelivery(); drawWorkspaces(); };
246
+ panel.redraw();
247
+
248
+ // Reads outlive one render: they redraw whichever Delivery panel is current
249
+ // and re-read when the Context cursor moved while they were in flight.
250
+ function refresh() {
251
+ const current = view.panels.delivery;
252
+ if (!current || !current.isConnected) return;
253
+ current.redraw();
254
+ if (!current.hidden) current.onShow();
255
+ }
256
+ panel.onShow = function () {
257
+ const key = core.coreCursor;
258
+ const previous = view.evidence || {};
259
+ if (previous.key !== key && previous.errorKey !== key && !previous.pending) {
260
+ view.evidence = { key: previous.key, value: previous.value, readAt: previous.readAt, error: null, pending: true };
261
+ ctx.evidence(task.id).then(function (value) {
262
+ view.evidence = { key: key, value: value, readAt: new Date().toISOString(), error: null, pending: false };
263
+ }, function (error) {
264
+ view.evidence = { key: previous.key, value: previous.value, readAt: previous.readAt, error: error.message, errorKey: key, pending: false };
265
+ }).then(refresh);
266
+ }
267
+ const entry = entriesOf(core, "remote-delivery")[0];
268
+ if (entry && entry.omitted && !(data.runtime && data.runtime.remoteDelivery)) {
269
+ const hit = view.exact["remote-delivery/" + entry.ref.refId];
270
+ if (!(hit && hit.digest === entry.ref.digest && hit.result)) {
271
+ readExact(entry, task.id, ctx, view.exact).then(refresh, function () {});
272
+ }
273
+ }
274
+ const family = familyState(view, "work-item");
275
+ if (family.key !== key && family.errorKey !== key && !family.pending) loadFamily(view, task.id, "work-item", key, ctx, { limit: 40 }).then(refresh);
276
+ };
277
+ }
278
+
279
+ // --- Saved files ------------------------------------------------------------------
280
+ // Files are read on request at a fixed revision; the selected revision
281
+ // survives refreshes.
282
+ function filesCard(data, t, locale, actions) {
10
283
  const state = data.viewState;
11
- const files = card({ title: t("evidence.files"), icon: "file", hint: t("evidence.filesHint") });
284
+ const load = button(state.artifactList ? t("evidence.reload") : t("evidence.load"), { icon: "refresh", variant: "ghost" });
285
+ const files = card({ id: "detail-files", title: t("evidence.files"), icon: "file", hint: t("evidence.filesHint"), actions: load });
12
286
  const list = h("div.file-list");
13
287
  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
288
  let readSequence = 0;
17
289
  async function read(path, commit) {
18
290
  const sequence = ++readSequence;
@@ -41,8 +313,7 @@ export function renderEvidence(container, data, t, locale, actions) {
41
313
  viewer.append(h("header.viewer-head", null, icon("file"), h("strong", null, value.relativePath), h("span.spacer"), copy));
42
314
  viewer.append(h("p.viewer-ref", null, mono(ref)));
43
315
  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);
316
+ viewer.append(h("pre.code-block.artifact-text", { tabIndex: 0, "aria-label": t("evidence.fixedText") }, value.content));
46
317
  }
47
318
  function drawList() {
48
319
  clear(list);
@@ -63,6 +334,7 @@ export function renderEvidence(container, data, t, locale, actions) {
63
334
  list.dataset.reading = "true";
64
335
  try {
65
336
  state.artifactList = await actions.artifacts(data.task.id);
337
+ if (placeholder) { placeholder.remove(); placeholder = null; }
66
338
  drawList();
67
339
  } catch (error) {
68
340
  clear(list);
@@ -82,91 +354,11 @@ export function renderEvidence(container, data, t, locale, actions) {
82
354
  referenced.append(h("button.file-row", { type: "button", onclick: function () { read(match[2], match[1]); } },
83
355
  icon("flag", "icon-sm"), h("span.file-name", null, match[2]), badge(t("evidence.referenced"), "accent")));
84
356
  });
85
- if (!state.artifactList && !seen.size) files.body.append(emptyState(t("evidence.notLoaded"), "file"));
357
+ let placeholder = !state.artifactList && !seen.size ? emptyState(t("evidence.notLoaded"), "file") : null;
358
+ if (placeholder) files.body.append(placeholder);
86
359
  files.body.append(referenced, list, viewer);
87
360
  drawList();
88
361
  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);
362
+ return files;
171
363
  }
172
364
  `;