@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
@@ -9,7 +9,7 @@ import { formatClock } from "/assets/js/format.js";
9
9
  import { h } from "/assets/js/dom.js";
10
10
  import { renderAttentionBar, renderFilters, renderTasks, STATUS_FILTERS, ATTENTION_KINDS } from "/assets/js/sidebar.js";
11
11
  import { renderOverview } from "/assets/js/overview.js";
12
- import { renderTaskDetail, selectTab, updateObservation, TABS } from "/assets/js/detail.js";
12
+ import { renderTaskDetail, selectTab, updateObservation, TABS, TAB_ALIASES } from "/assets/js/detail.js";
13
13
  import { renderDiscussion, discussionHasUnsent, createTerminalController, sameTarget } from "/assets/js/dock.js";
14
14
  import { entriesOf } from "/assets/js/records.js";
15
15
 
@@ -118,6 +118,7 @@ function detailContext() {
118
118
  openSession: function (roleName) { openSession({ scope: "task", taskId: state.selected, roleName: roleName }); },
119
119
  answerInput: answerInput,
120
120
  inspect: api.inspect,
121
+ list: api.list,
121
122
  artifacts: api.artifacts,
122
123
  readArtifact: api.artifact,
123
124
  evidence: api.evidence,
@@ -132,6 +133,7 @@ function dockActions() {
132
133
  return {
133
134
  sendMessage: api.sendMessage,
134
135
  inspect: api.inspect,
136
+ list: api.list,
135
137
  afterWrite: function () { refreshDashboard({ quiet: true }); }
136
138
  };
137
139
  }
@@ -230,15 +232,17 @@ function switchTab(tab, options) {
230
232
  if (!(options && options.keepScroll)) el.center.scrollTop = 0;
231
233
  setSectionParam(tab);
232
234
  }
233
- // A section may name a tab or any anchored block inside one (for example
234
- // "reviews" → the Work tab, scrolled to #detail-reviews).
235
+ // A section may name a tab, a retired tab name, or any anchored block inside
236
+ // one (for example "reviews" → the Delivery tab, scrolled to #detail-reviews).
235
237
  function applySection(section) {
236
- if (!section || TABS.includes(section)) { switchTab(section || "overview"); return; }
237
- const anchor = el.detail.querySelector("#detail-" + CSS.escape(section));
238
+ const tab = (section && TAB_ALIASES[section]) || section;
239
+ if (!tab || TABS.includes(tab)) { switchTab(tab || "overview"); return; }
240
+ const anchor = el.detail.querySelector("#detail-" + CSS.escape(tab));
238
241
  const panel = anchor && anchor.closest(".tab-panel");
239
242
  if (!panel) { switchTab("overview"); return; }
240
243
  state.activeTab = panel.id.replace(/^panel-/, "");
241
244
  selectTab(el.detail, state.activeTab);
245
+ if (anchor.tagName === "DETAILS") anchor.open = true;
242
246
  anchor.scrollIntoView({ block: "start" });
243
247
  }
244
248
 
@@ -757,7 +761,7 @@ document.addEventListener("keydown", function (event) {
757
761
  else if (key === "r") refreshDashboard();
758
762
  else if (key === "o") openOperator();
759
763
  else if (key === "d") toggleDock();
760
- else if (/^[1-5]$/.test(key) && state.detail) switchTab(TABS[Number(key) - 1]);
764
+ else if (/^[1-9]$/.test(key) && Number(key) <= TABS.length && state.detail) switchTab(TABS[Number(key) - 1]);
761
765
  });
762
766
 
763
767
  // --- Boot -------------------------------------------------------------------------
@@ -39,7 +39,9 @@ const TONES = {
39
39
  stopped: "bad", idle: "idle", background: "warn" },
40
40
  role: { running: "ok", waiting: "warn", idle: "idle", unknown: "idle", failed: "bad", exited: "idle", detached: "idle" },
41
41
  decision: { active: "ok", superseded: "idle" },
42
- input: { open: "warn", answered: "ok", cancelled: "idle", "auto-resolved": "ok" }
42
+ input: { open: "warn", answered: "ok", cancelled: "idle", "auto-resolved": "ok" },
43
+ integration: { running: "info", validating: "info", committed: "ok", conflicted: "warn", blocked: "bad",
44
+ failed: "bad", superseded: "idle" }
43
45
  };
44
46
  export function tone(kind, value) {
45
47
  return (TONES[kind] && TONES[kind][value]) || "idle";
@@ -81,17 +83,19 @@ export function timeTag(iso, locale, t, options) {
81
83
  }
82
84
 
83
85
  // --- Containers -------------------------------------------------------------------
86
+ // One panel of the single reading column: a heading (icon tile, title, count
87
+ // and hint) with optional actions, then the body. A "list-card" body holds a
88
+ // .row-stack whose rows are divided by hairlines instead of nested boxes.
84
89
  export function card(options) {
85
90
  const opts = options || {};
86
91
  const element = h("section.card" + (opts.className ? "." + opts.className.split(" ").join(".") : ""));
87
92
  if (opts.id) element.id = opts.id;
88
93
  if (opts.title || opts.actions) {
89
- const head = h("header.card-head", null,
90
- h("h3.card-title", null, opts.icon ? icon(opts.icon) : null, h("span", null, opts.title),
94
+ const heading = h("div.card-heading", null,
95
+ h("h3.card-title", null, opts.icon ? h("span.card-icon", null, icon(opts.icon)) : null, h("span", null, opts.title),
91
96
  opts.count !== undefined && opts.count !== null ? h("span.count", null, String(opts.count)) : null),
92
- opts.actions ? h("div.card-actions", null, opts.actions) : null);
93
- if (opts.hint) head.append(h("p.card-hint", null, opts.hint));
94
- element.append(head);
97
+ opts.hint ? h("p.card-hint", null, opts.hint) : null);
98
+ element.append(h("header.card-head", null, heading, opts.actions ? h("div.card-actions", null, opts.actions) : null));
95
99
  }
96
100
  const body = h("div.card-body");
97
101
  element.append(body);
@@ -99,6 +103,39 @@ export function card(options) {
99
103
  return element;
100
104
  }
101
105
 
106
+ // A panel whose body is a divided list of rows.
107
+ export function listCard(options) {
108
+ const element = card({ ...options, className: "list-card" + (options.className ? " " + options.className : "") });
109
+ element.list = h("div.row-stack");
110
+ element.body.append(element.list);
111
+ return element;
112
+ }
113
+
114
+ // Update a card heading count after a later read; null removes it.
115
+ export function setCardCount(element, value) {
116
+ const title = element.querySelector(".card-title");
117
+ let count = title && title.querySelector(".count");
118
+ if (value === undefined || value === null) { if (count) count.remove(); return; }
119
+ if (!title) return;
120
+ if (!count) { count = h("span.count"); title.append(count); }
121
+ count.textContent = String(value);
122
+ }
123
+
124
+ // Local filter chips; selecting one redraws the list in place.
125
+ export function filterChips(ariaLabel, options, selected, onSelect) {
126
+ const row = h("div.chip-tabs", { role: "group", "aria-label": ariaLabel });
127
+ options.forEach(function (option) {
128
+ row.append(h("button.chip-tab", {
129
+ type: "button", "aria-pressed": String(option.value === selected), dataset: { filter: option.value },
130
+ onclick: function () {
131
+ row.querySelectorAll(".chip-tab").forEach(function (b) { b.setAttribute("aria-pressed", String(b.dataset.filter === option.value)); });
132
+ onSelect(option.value);
133
+ }
134
+ }, option.label, option.count === undefined || option.count === null ? null : h("span.count", null, String(option.count))));
135
+ });
136
+ return row;
137
+ }
138
+
102
139
  export function disclosure(title, viewKey, options) {
103
140
  const opts = options || {};
104
141
  const element = h("details.disclosure" + (opts.className ? "." + opts.className : ""));
@@ -112,12 +149,6 @@ export function disclosure(title, viewKey, options) {
112
149
  return element;
113
150
  }
114
151
 
115
- export function sectionTitle(text, count, actions) {
116
- return h("div.section-title", null, h("h2", null, text,
117
- count !== undefined && count !== null ? h("span.count", null, String(count)) : null),
118
- actions ? h("div.section-actions", null, actions) : null);
119
- }
120
-
121
152
  export function kv(rows) {
122
153
  const list = h("dl.kv");
123
154
  rows.forEach(function (row) {
@@ -206,9 +237,24 @@ export function metricTile(labelText, value, options) {
206
237
  return tile;
207
238
  }
208
239
 
240
+ function pageLocale() { return typeof document === "undefined" ? undefined : document.documentElement.lang || undefined; }
241
+
242
+ // Seconds as the largest whole unit plus the next one ("2d 22h", "2小时48分钟").
243
+ const DURATION_UNITS = [["day", 86400], ["hour", 3600], ["minute", 60], ["second", 1]];
244
+ function durationText(seconds) {
245
+ const total = Math.max(0, Math.round(seconds));
246
+ const found = DURATION_UNITS.findIndex(function (unit) { return total >= unit[1]; });
247
+ const first = found < 0 ? DURATION_UNITS.length - 1 : found;
248
+ return DURATION_UNITS.slice(first, first + 2).map(function (unit, index) {
249
+ const count = index ? Math.floor((total % DURATION_UNITS[first][1]) / unit[1]) : Math.floor(total / unit[1]);
250
+ return index && !count ? "" : new Intl.NumberFormat(pageLocale(), { style: "unit", unit: unit[0], unitDisplay: "narrow" }).format(count);
251
+ }).filter(Boolean).join(" ");
252
+ }
253
+
209
254
  export function usageMetricText(metric, t, suffix) {
210
255
  if (!metric || metric.value == null) return t("detail.unobserved");
211
- return metric.value + (suffix || "") + (metric.status === "partial" ? " · " + t("detail.partial") : "");
256
+ const value = suffix === "s" ? durationText(metric.value) : metric.value + (suffix || "");
257
+ return value + (metric.status === "partial" ? " · " + t("detail.partial") : "");
212
258
  }
213
259
 
214
260
  function usageTile(labelText, metric, t, suffix) {
@@ -237,7 +283,7 @@ export function observabilityMetricCard(observability, t) {
237
283
  const meta = node("p", "usage-meta");
238
284
  meta.append(node("span", "", t("usage.scope")));
239
285
  meta.append(node("span", "", t("usage.observedThrough") + " "
240
- + (cost.observedThrough ? formatDateTime(cost.observedThrough) : t("detail.unobserved"))));
286
+ + (cost.observedThrough ? formatDateTime(cost.observedThrough, pageLocale()) : t("detail.unobserved"))));
241
287
  meta.append(node("span", "", t("usage.contextBytes") + " "
242
288
  + (context.totalBytes == null ? t("detail.partial") : context.totalBytes + " B")));
243
289
  wrap.append(meta);
@@ -300,30 +346,35 @@ export function workItemCard(item, t, locale, titles) {
300
346
  const settled = item.status !== "open";
301
347
  const element = h(settled ? "details.work-card.is-settled" : "article.work-card");
302
348
  element.dataset.viewKey = "work:" + item.id;
303
- const head = h(settled ? "summary.work-head" : "header.work-head", null,
349
+ element.append(h(settled ? "summary.work-head" : "header.work-head", null,
304
350
  settled ? icon("chevron", "disclosure-chevron") : null,
305
351
  dot(tone("work", item.status)),
306
352
  h("span.work-title", null, item.title),
307
- statusBadge(t, "work", "work", item.status));
308
- element.append(head);
309
- const body = h("div.work-body");
310
- body.append(h("div.meta-line", null,
353
+ statusBadge(t, "work", "work", item.status)));
354
+ element.append(h("div.work-body", null, workItemBody(item, t, locale, titles)));
355
+ return element;
356
+ }
357
+
358
+ // The body of one work item, shared by the open card and a lazily read row.
359
+ export function workItemBody(item, t, locale, titles) {
360
+ const parts = [];
361
+ parts.push(h("div.meta-line", null,
311
362
  item.assignee ? h("span.meta-strong", null, icon("user", "icon-sm"), item.assignee) : null,
312
363
  mono(item.id), timeTag(item.updatedAt, locale, t)));
313
- if (item.objective && item.objective !== item.title) body.append(richText(t("work.objective"), item.objective, t));
364
+ if (item.objective && item.objective !== item.title) parts.push(richText(t("work.objective"), item.objective, t));
314
365
  if (item.acceptance && item.acceptance.length) {
315
- body.append(h("div.prose-block", null, h("h4.prose-label", null, t("work.acceptance")),
366
+ parts.push(h("div.prose-block", null, h("h4.prose-label", null, t("work.acceptance")),
316
367
  h("ul.checklist", null, item.acceptance.map(function (entry) {
317
368
  return h("li" + (item.status === "accepted" ? ".is-done" : ""), null, icon(item.status === "accepted" ? "check" : "target", "icon-sm"), h("span", null, entry));
318
369
  }))));
319
370
  }
320
371
  const deps = (item.dependsOn || []).map(function (id) { return (titles && titles[id]) || id; });
321
- if (deps.length) body.append(h("div.meta-line", null, h("span.meta-label", null, t("work.dependsOn")), deps.map(function (d) { return chip(d); })));
372
+ if (deps.length) parts.push(h("div.meta-line", null, h("span.meta-label", null, t("work.dependsOn")), deps.map(function (d) { return chip(d); })));
322
373
  if (item.writeProjectIds && item.writeProjectIds.length) {
323
- body.append(h("div.meta-line", null, h("span.meta-label", null, t("work.writeProjects")), item.writeProjectIds.map(function (d) { return chip(d); })));
374
+ parts.push(h("div.meta-line", null, h("span.meta-label", null, t("work.writeProjects")), item.writeProjectIds.map(function (d) { return chip(d); })));
324
375
  }
325
376
  if (item.candidates && item.candidates.length) {
326
- body.append(h("div.prose-block", null, h("h4.prose-label", null, t("work.candidates")),
377
+ parts.push(h("div.prose-block", null, h("h4.prose-label", null, t("work.candidates")),
327
378
  h("ol.candidates", null, item.candidates.slice().sort(function (a, b) { return b.sequence - a.sequence; }).map(function (candidate) {
328
379
  return h("li", null, h("span.candidate-seq", null, "#" + candidate.sequence), h("span", null, candidate.summary),
329
380
  h("span.faint", null, candidate.source && candidate.source.type === "run" ? candidate.source.runId : t("work.direct")),
@@ -331,18 +382,16 @@ export function workItemCard(item, t, locale, titles) {
331
382
  }))));
332
383
  }
333
384
  if (item.disposition) {
334
- body.append(note(t("work.disposition") + ": " + item.disposition.summary
385
+ parts.push(note(t("work.disposition") + ": " + item.disposition.summary
335
386
  + (item.disposition.replacementWorkItemId ? " → " + item.disposition.replacementWorkItemId : "")));
336
387
  }
337
- if (item.outcome) body.append(richText(t("work.outcome"), item.outcome, t, { className: "callout" }));
338
- element.append(body);
339
- return element;
388
+ if (item.outcome) parts.push(richText(t("work.outcome"), item.outcome, t, { className: "callout" }));
389
+ return parts.filter(Boolean);
340
390
  }
341
391
 
342
392
  export function runCard(run, t, locale) {
343
393
  const element = h("article.run-card");
344
394
  element.dataset.status = run.status;
345
- const input = run.inputs && run.inputs.length ? run.inputs[0].input : null;
346
395
  element.append(h("header.run-head", null,
347
396
  dot(tone("run", run.status)),
348
397
  h("span.run-role", null, run.roleName),
@@ -350,20 +399,27 @@ export function runCard(run, t, locale) {
350
399
  run.purpose ? chip(label(t, "run.purpose", run.purpose)) : null,
351
400
  run.workItemId ? chip(run.workItemId) : null,
352
401
  h("span.spacer"),
353
- statusBadge(t, "run", "run", run.status)));
354
- element.append(h("div.meta-line", null,
402
+ statusBadge(t, "run", "run", run.status)), runBody(run, t, locale));
403
+ return element;
404
+ }
405
+
406
+ // What one execution record says: delivery, instruction, report, failure.
407
+ export function runBody(run, t, locale) {
408
+ const parts = [];
409
+ const input = run.inputs && run.inputs.length ? run.inputs[0].input : null;
410
+ parts.push(h("div.meta-line", null,
355
411
  h("span", null, t("run.delivery") + ": " + label(t, "run.delivery", (run.execution && run.execution.delivery) || "unobserved")),
356
412
  run.mode ? h("span", null, label(t, "mode", run.mode)) : null,
357
413
  timeTag((run.result && run.result.completedAt) || run.updatedAt, locale, t),
358
414
  agentChips(run.effective || (run.agentId ? run : null))));
359
415
  const directive = input && (input.directive || input.action);
360
- if (directive) element.append(richText(t("run.instruction"), directive, t, { threshold: 320 }));
361
- if (run.result && run.result.output) element.append(richText(t("run.output"), run.result.output, t, { threshold: 320, className: "callout" }));
362
- if (run.result && run.result.diagnostic) element.append(richText(t("run.failure"), run.result.diagnostic, t, { threshold: 320, className: "callout.tone-bad" }));
416
+ if (directive) parts.push(richText(t("run.instruction"), directive, t, { threshold: 320 }));
417
+ if (run.result && run.result.output) parts.push(richText(t("run.output"), run.result.output, t, { threshold: 320, className: "callout" }));
418
+ if (run.result && run.result.diagnostic) parts.push(richText(t("run.failure"), run.result.diagnostic, t, { threshold: 320, className: "callout.tone-bad" }));
363
419
  if (run.executionGroupId) {
364
- element.append(h("p.faint.mono-line", null, t("run.lineage") + " " + run.executionGroupId + (run.executionLaneId ? "/" + run.executionLaneId : "")));
420
+ parts.push(h("p.faint.mono-line", null, t("run.lineage") + " " + run.executionGroupId + (run.executionLaneId ? "/" + run.executionLaneId : "")));
365
421
  }
366
- return element;
422
+ return parts.filter(Boolean);
367
423
  }
368
424
 
369
425
  export function reviewCard(round, t, locale) {
@@ -377,6 +433,9 @@ export function reviewCard(round, t, locale) {
377
433
  statusBadge(t, "review", "review", round.status)));
378
434
  element.append(h("div.meta-line", null,
379
435
  round.workItemId ? mono(round.workItemId + (round.candidateId ? " · " + round.candidateId : "")) : null,
436
+ round.taskCandidate && round.taskCandidate.projects ? round.taskCandidate.projects.map(function (project) {
437
+ return mono(project.projectId + " @ " + String(project.commit).slice(0, 12));
438
+ }) : null,
380
439
  round.reviewBaseCommit ? h("span", null, t("review.base") + " ", mono(String(round.reviewBaseCommit).slice(0, 12))) : null,
381
440
  round.reviewerRunId ? h("span", null, t("review.run") + " ", mono(round.reviewerRunId)) : null,
382
441
  timeTag(round.createdAt, locale, t)));
@@ -8,11 +8,15 @@ import {
8
8
  badge, statusBadge, label, tone, card, disclosure, richText, bulletList, emptyState,
9
9
  note, button, chip, mono, inputCard, kv, dot, timeTag
10
10
  } from "/assets/js/components.js";
11
- import { entriesOf, valuesOf, recordCard } from "/assets/js/records.js";
12
- import { renderWork, renderExecution, renderHistory, renderDetails } from "/assets/js/sections.js";
11
+ import { entriesOf, valuesOf, totalOf, recordCard, readExact } from "/assets/js/records.js";
12
+ import { renderRuntime, renderRecords } from "/assets/js/sections.js";
13
+ import { renderDelivery } from "/assets/js/evidence.js";
13
14
 
14
- export const TABS = ["overview", "work", "execution", "history", "details"];
15
- const TAB_ICONS = { overview: "target", work: "layers", execution: "pulse", history: "history", details: "dots" };
15
+ // Overview: where it stands. Delivery: what it produced. Runtime: what is
16
+ // live now. Records: what happened. Retired section names stay addressable.
17
+ export const TABS = ["overview", "delivery", "runtime", "records"];
18
+ export const TAB_ALIASES = { work: "delivery", execution: "runtime", history: "records", details: "records" };
19
+ const TAB_ICONS = { overview: "target", delivery: "package", runtime: "pulse", records: "history" };
16
20
  const SESSION_GROUPS = ["active", "waiting", "background", "quiet", "diagnostic", "stopped", "unknown", "idle"];
17
21
 
18
22
  export function renderTaskDetail(container, data, t, locale, ctx) {
@@ -20,6 +24,7 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
20
24
  const task = data.task;
21
25
  const core = data.core;
22
26
  container.dataset.taskId = task.id;
27
+ data.viewState.coreKey = core.coreCursor;
23
28
  const page = h("div.page.task-page");
24
29
 
25
30
  // --- Header ----------------------------------------------------------------
@@ -48,10 +53,12 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
48
53
  (task.projectBindings || []).length ? h("span.meta-item", null, icon("layers", "icon-sm"),
49
54
  task.projectBindings.map(function (binding) { return binding.projectId; }).join(", ")) : null);
50
55
 
56
+ // Tab counts are current totals from the same bounded read, not samples:
57
+ // open questions, open work items and open execution records.
51
58
  const counts = {
52
- work: entriesOf(core, "work-item").length,
53
- execution: entriesOf(core, "run").length,
54
- history: entriesOf(core, "task-decision").length + entriesOf(core, "task-milestone").length + entriesOf(core, "task-message").length
59
+ overview: core.attention.openInputs.count,
60
+ delivery: totalOf(core, "work-item"),
61
+ runtime: totalOf(core, "run")
55
62
  };
56
63
  const tabs = h("nav.tabs", { role: "tablist", "aria-label": t("tabs.label") }, TABS.map(function (tab, index) {
57
64
  return h("button.tab", {
@@ -63,7 +70,7 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
63
70
  dataset: { tab: tab },
64
71
  onclick: function () { ctx.onTab(tab); }
65
72
  }, icon(TAB_ICONS[tab], "icon-sm"), h("span", null, t("tabs." + tab)),
66
- counts[tab] ? h("span.count", null, String(counts[tab])) : null);
73
+ counts[tab] ? h("span.count" + (tab === "overview" ? ".tone-warn" : ""), null, String(counts[tab])) : null);
67
74
  }));
68
75
  tabs.addEventListener("keydown", function (event) {
69
76
  if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
@@ -87,29 +94,38 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
87
94
  page.append(panels[tab]);
88
95
  });
89
96
  renderOverviewPanel(panels.overview, data, t, locale, ctx);
90
- renderWork(panels.work, data, t, locale, ctx);
91
- renderExecution(panels.execution, data, t, locale, ctx);
92
- renderHistory(panels.history, data, t, locale, ctx);
93
- renderDetails(panels.details, data, t, locale, ctx);
97
+ renderDelivery(panels.delivery, data, t, locale, ctx);
98
+ renderRuntime(panels.runtime, data, t, locale, ctx);
99
+ renderRecords(panels.records, data, t, locale, ctx);
94
100
  container.append(page);
95
101
  updateObservation(container, data, t, locale, ctx);
102
+ const active = panels[ctx.activeTab];
103
+ if (active && active.onShow) active.onShow();
96
104
  }
97
105
 
106
+ // A panel may read on first show (evidence, record pages); it keeps those
107
+ // reads in view state, so showing it again does not repeat them.
98
108
  export function selectTab(container, tab) {
99
109
  container.querySelectorAll(".tab").forEach(function (button) {
100
110
  const active = button.dataset.tab === tab;
101
111
  button.setAttribute("aria-selected", String(active));
102
112
  button.tabIndex = active ? 0 : -1;
103
113
  });
104
- container.querySelectorAll(".tab-panel").forEach(function (panel) { panel.hidden = panel.id !== "panel-" + tab; });
114
+ container.querySelectorAll(".tab-panel").forEach(function (panel) {
115
+ panel.hidden = panel.id !== "panel-" + tab;
116
+ if (!panel.hidden && panel.onShow) panel.onShow();
117
+ });
105
118
  }
106
119
 
120
+ // One reading column, top to bottom: where the Task stands now, what needs the
121
+ // user, what it has reported, its goal and its current decisions.
107
122
  function renderOverviewPanel(panel, data, t, locale, ctx) {
108
123
  const task = data.task;
109
124
  const core = data.core;
110
- // Now: the derived execution status (observation) plus the open questions.
125
+ // Now: the derived execution status (observation), the open questions and
126
+ // who acts next. drawNow() tints the banner with the status tone.
111
127
  const now = card({ className: "now-card" });
112
- now.body.append(h("div", { dataset: { slot: "now" } }));
128
+ now.body.append(h("div", { dataset: { slot: "now" } }), h("p.next-line", { dataset: { slot: "next" } }));
113
129
  panel.append(now);
114
130
 
115
131
  // Needs you: open InputRequests with their answer controls.
@@ -129,15 +145,28 @@ function renderOverviewPanel(panel, data, t, locale, ctx) {
129
145
  panel.append(needs);
130
146
  }
131
147
 
132
- // Independent column stacks, not paired rows: a long progress card must not
133
- // push the next row down and leave a gap under the shorter Session card.
134
- // Main column: what the Task is and where it stands. Rail: live and decided facts.
135
- const sessions = card({ title: t("sessions.title"), icon: "terminal", hint: t("sessions.hint") });
136
- sessions.body.append(h("div", { dataset: { slot: "sessions" } }));
137
- panel.append(h("div.card-columns", null,
138
- h("div.card-column", null, progressCard(data, t, locale, ctx), goalCard(data, t, ctx)),
139
- h("div.card-column", null, sessions, decisionsCard(data, t, locale, ctx))));
148
+ const progress = progressCard(data, t, locale, ctx);
149
+ const goal = goalCard(data, t, ctx);
150
+ panel.append(progress);
140
151
  if (task.status === "draft") panel.append(planningCard(data, t, locale));
152
+ panel.append(goal, decisionsCard(data, t, locale, ctx));
153
+ // The Brief is the Overview's main source; when the bounded read withheld
154
+ // its value, read it exactly once per digest and redraw both cards.
155
+ const briefEntry = entriesOf(core, "task-brief")[0];
156
+ if (briefEntry && briefEntry.omitted) {
157
+ const view = data.viewState;
158
+ view.exact = view.exact || {};
159
+ readExact(briefEntry, task.id, ctx, view.exact).then(function (result) {
160
+ if (!progress.isConnected) return;
161
+ const exact = { ...data, briefValue: result.value };
162
+ progress.replaceWith(progressCard(exact, t, locale, ctx));
163
+ goal.replaceWith(goalCard(exact, t, ctx));
164
+ }, function () {});
165
+ }
166
+ }
167
+
168
+ function briefOf(data) {
169
+ return data.briefValue || valuesOf(data.core, "task-brief")[0];
141
170
  }
142
171
 
143
172
  function progressCard(data, t, locale, ctx) {
@@ -148,20 +177,19 @@ function progressCard(data, t, locale, ctx) {
148
177
  if (task.retirementSummary) element.body.append(richText(t("progress.retirement"), task.retirementSummary, t, { className: "callout" }));
149
178
  if (task.archiveSummary || task.archiveReason) element.body.append(richText(t("progress.archive"), [task.archiveSummary, task.archiveReason].filter(Boolean).join("\n\n"), t, { className: "callout" }));
150
179
  const briefEntry = entriesOf(data.core, "task-brief")[0];
151
- const brief = valuesOf(data.core, "task-brief")[0];
180
+ const brief = briefOf(data);
152
181
  if (brief) {
153
182
  element.body.append(richText(t(working ? "progress.focus" : "progress.lastFocus"), brief.currentFocus, t));
154
183
  element.body.append(richText(t(working ? "progress.report" : "progress.lastReport"), brief.leaderSummary, t, { muted: true }));
155
184
  element.body.append(h("p.faint.small", null, t("progress.briefUpdated") + " " + formatDateTime(brief.updatedAt, locale)));
156
185
  } else if (briefEntry) element.body.append(recordCard(briefEntry, task.id, t, ctx, { compact: true }));
157
186
  else element.body.append(emptyState(t("progress.none")));
158
- element.body.append(h("p.next-line", { dataset: { slot: "next" } }));
159
187
  return element;
160
188
  }
161
189
 
162
190
  function goalCard(data, t, ctx) {
163
191
  const task = data.task;
164
- const brief = valuesOf(data.core, "task-brief")[0];
192
+ const brief = briefOf(data);
165
193
  const element = card({ title: t("goal.title"), icon: "target" });
166
194
  if (brief && brief.objective) element.body.append(richText(null, brief.objective, t));
167
195
  if (brief && brief.boundaries && brief.boundaries.length) {
@@ -184,7 +212,8 @@ function goalCard(data, t, ctx) {
184
212
  function decisionsCard(data, t, locale, ctx) {
185
213
  const entries = entriesOf(data.core, "task-decision");
186
214
  const active = entries.filter(function (entry) { return !entry.omitted && entry.value.status === "active"; });
187
- const element = card({ title: t("decisions.title"), icon: "check", count: active.length || null, hint: t("decisions.hint") });
215
+ const total = totalOf(data.core, "task-decision");
216
+ const element = card({ id: "detail-decisions", title: t("decisions.title"), icon: "check", count: total || null, hint: t("decisions.hint") });
188
217
  active.slice(0, 3).forEach(function (entry) {
189
218
  const value = entry.value;
190
219
  element.body.append(h("article.decision", null,
@@ -192,9 +221,10 @@ function decisionsCard(data, t, locale, ctx) {
192
221
  value.rationale ? richText(null, value.rationale, t, { muted: true, threshold: 260 }) : null));
193
222
  });
194
223
  if (!active.length) element.body.append(emptyState(t("decisions.none")));
195
- if (active.length > 3 || data.core.omitted.records || entries.some(function (entry) { return entry.omitted; })) {
196
- element.body.append(button(t("decisions.more"), { variant: "link", icon: "chevron", onClick: function () { ctx.onTab("history"); } }));
197
- }
224
+ element.body.append(button(t("decisions.more"), { variant: "link", icon: "chevron", onClick: function () {
225
+ data.viewState.recordsFilter = "decision";
226
+ ctx.onTab("records");
227
+ } }));
198
228
  return element;
199
229
  }
200
230
 
@@ -240,12 +270,7 @@ export function updateObservation(container, data, t, locale, ctx) {
240
270
  if (execution && !(execution.next.owner === "none" && execution.next.action === "none")) next.append(icon("flag", "icon-sm"), h("span", null, t("now.nextOwner") + " "),
241
271
  h("strong", null, label(t, "exec.owner", execution.next.owner)), h("span", null, " · " + label(t, "exec.action", execution.next.action)));
242
272
  }
243
- const sessions = container.querySelector('[data-slot="sessions"]');
244
- if (sessions && !sessions.contains(document.activeElement)) {
245
- const open = sessions.querySelector("details") && sessions.querySelector("details").open;
246
- drawSessions(sessions, available ? data.runtime.sessions : null, t, locale);
247
- if (open && sessions.querySelector("details")) sessions.querySelector("details").open = true;
248
- }
273
+ drawSessions(container, available ? data.runtime.sessions : null, t, locale);
249
274
  const raw = container.querySelector('[data-slot="runtime-raw"]');
250
275
  if (raw) raw.textContent = available ? JSON.stringify({ roles: data.runtime.roles, runtimeHealth: data.runtime.runtimeHealth }, null, 2) : "";
251
276
  const rawStatus = container.querySelector('[data-slot="runtime-status"]');
@@ -257,9 +282,16 @@ export function updateObservation(container, data, t, locale, ctx) {
257
282
  });
258
283
  }
259
284
 
285
+ const BANNER_TONES = ["tone-info", "tone-ok", "tone-warn", "tone-bad", "tone-idle", "tone-accent"];
286
+
260
287
  function drawNow(slot, data, execution, t, locale) {
261
288
  clear(slot);
262
289
  const inputCount = data.core.attention.openInputs.count;
290
+ const banner = slot.closest(".now-card");
291
+ if (banner) {
292
+ banner.classList.remove.apply(banner.classList, BANNER_TONES);
293
+ banner.classList.add("tone-" + (execution ? tone("exec", execution.status) : data.runtimeStatus === "unavailable" ? "bad" : "idle"));
294
+ }
263
295
  if (!execution) {
264
296
  slot.append(h("div.now-head", null,
265
297
  badge(t("observation." + data.runtimeStatus), data.runtimeStatus === "unavailable" ? "bad" : "idle", { dot: true }),
@@ -300,34 +332,48 @@ function drawNow(slot, data, execution, t, locale) {
300
332
  else slot.append(h("p.now-note", null, inputCount ? t("now.inputs").replace("{count}", String(inputCount)) : t("now.clear")));
301
333
  }
302
334
 
303
- function drawSessions(slot, observation, t, locale) {
304
- clear(slot);
305
- if (!observation) { slot.append(emptyState(t("sessions.unavailable"))); return; }
306
- const chips = SESSION_GROUPS.filter(function (group) { return observation.counts[group]; }).map(function (group) {
307
- return badge(observation.counts[group] + " " + t("session." + group), tone("session", group), { dot: true });
308
- });
309
- if (chips.length) slot.append(h("div.chip-row", null, chips));
310
- if (!observation.sessions.length) { slot.append(note(t("sessions.none"))); return; }
311
- slot.append(h("ul.session-list", null, observation.sessions.map(function (session) {
312
- return h("li.session-row", null,
313
- h("div.session-row-head", null, dot(tone("session", session.group)), h("strong", null, session.roleName),
314
- h("span.faint", null, t("session." + session.group)), h("span.spacer"),
315
- h("span.faint.small", null, session.lastActivityAt ? relativeTime(session.lastActivityAt, locale, t) : t("sessions.unobserved"))),
316
- h("p.small", null, session.reason),
317
- session.waitingReason ? h("p.small", null, t("sessions.waitingFor") + " " + session.waitingReason) : null);
318
- })));
319
- const facts = disclosure(t("sessions.facts"), "sessions");
320
- observation.sessions.forEach(function (session) {
321
- facts.body.append(kv([
322
- [t("sessions.role"), session.roleName],
323
- [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
324
- [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
325
- [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
326
- [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
327
- [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
328
- ]));
335
+ // Native Session observation, drawn into the Runtime tab: one line per Role,
336
+ // the group counts beside the Roles heading, and the timestamps under
337
+ // Diagnostics. Activity is never delivery progress.
338
+ function drawSessions(container, observation, t, locale) {
339
+ const sessions = observation ? observation.sessions : [];
340
+ container.querySelectorAll("[data-role-session]").forEach(function (slot) {
341
+ clear(slot);
342
+ const session = sessions.find(function (item) { return item.roleName === slot.dataset.roleSession; });
343
+ if (!session) {
344
+ slot.append(h("span.faint.small", null, observation ? t("session.noneRecorded") : t("sessions.unavailable")));
345
+ return;
346
+ }
347
+ slot.append(h("div.role-session-head", null, dot(tone("session", session.group)),
348
+ h("strong", null, t("session." + session.group)),
349
+ h("span.faint.small", null, session.lastActivityAt ? relativeTime(session.lastActivityAt, locale, t) : t("sessions.unobserved"))));
350
+ if (session.reason) slot.append(h("p.small", null, session.reason));
351
+ if (session.waitingReason) slot.append(h("p.small", null, t("sessions.waitingFor") + " " + session.waitingReason));
329
352
  });
330
- facts.body.append(h("p.faint.small", null, t("sessions.scope") + " " + formatDateTime(observation.readAt, locale)));
331
- slot.append(facts);
353
+ const counts = container.querySelector('[data-slot="session-counts"]');
354
+ if (counts) {
355
+ clear(counts);
356
+ if (observation) SESSION_GROUPS.forEach(function (group) {
357
+ if (observation.counts[group]) counts.append(badge(observation.counts[group] + " " + t("session." + group), tone("session", group), { dot: true }));
358
+ });
359
+ }
360
+ const facts = container.querySelector('[data-slot="session-facts"]');
361
+ if (facts) {
362
+ clear(facts);
363
+ if (!observation) return;
364
+ facts.append(h("h4.sub-head", null, t("sessions.facts")));
365
+ if (!sessions.length) facts.append(note(t("sessions.none")));
366
+ sessions.forEach(function (session) {
367
+ facts.append(kv([
368
+ [t("sessions.role"), session.roleName],
369
+ [t("sessions.lastActivity"), session.lastActivityAt ? formatDateTime(session.lastActivityAt, locale) : t("sessions.unobserved")],
370
+ [t("sessions.inputUpdated"), formatDateTime(session.sourceUpdatedAt, locale)],
371
+ [t("sessions.operations"), session.operations.length ? session.operations.join(", ") : null],
372
+ [t("sessions.background"), session.background.length ? session.background.map(function (item) { return item.id + " · " + item.execution; }).join(", ") : null],
373
+ [t("sessions.identity"), mono([session.nativeSessionId, session.nativeTurnId, session.attemptId].filter(Boolean).join(" / "))]
374
+ ]));
375
+ });
376
+ facts.append(h("p.faint.small", null, t("sessions.scope") + " " + formatDateTime(observation.readAt, locale)));
377
+ }
332
378
  }
333
379
  `;