@zq-silk/yui 2.1.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 (135) hide show
  1. package/README.md +8 -2
  2. package/dist/cli/commandCatalog.js +32 -6
  3. package/dist/cli/commandDiscovery.js +4 -1
  4. package/dist/cli/interactionPolicy.js +4 -0
  5. package/dist/cli.js +23 -3078
  6. package/dist/commands/taskCommands.js +84 -28
  7. package/dist/commands/taskIntegrationCommands.js +8 -17
  8. package/dist/commands/taskUpstreamCommands.js +3 -1
  9. package/dist/context/runContextPack.js +132 -5
  10. package/dist/context/sourceRunContext.js +4 -2
  11. package/dist/context/taskContext.js +6 -1
  12. package/dist/controlPlaneCli.js +3099 -0
  13. package/dist/controller/clientRuntime.js +3 -2
  14. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  15. package/dist/controller/jobControl.js +21 -0
  16. package/dist/executor/agentExecutor.js +1 -1
  17. package/dist/executor/effectiveLaunch.js +18 -5
  18. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  19. package/dist/integration/gitIntegrationService.js +58 -82
  20. package/dist/job/jobCleanupInspection.js +67 -0
  21. package/dist/message/messageContinuation.js +7 -2
  22. package/dist/nativeAgent/agent.js +176 -74
  23. package/dist/nativeAgent/cliDemo.js +37 -0
  24. package/dist/nativeAgent/codingTools.js +11 -0
  25. package/dist/nativeAgent/commandTool.js +215 -0
  26. package/dist/nativeAgent/compactionDemo.js +158 -0
  27. package/dist/nativeAgent/composition.js +71 -0
  28. package/dist/nativeAgent/context/budget.js +44 -0
  29. package/dist/nativeAgent/context/index.js +339 -0
  30. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  31. package/dist/nativeAgent/demo.js +12 -1
  32. package/dist/nativeAgent/evaluation/cases.js +38 -0
  33. package/dist/nativeAgent/evaluation/checks.js +91 -0
  34. package/dist/nativeAgent/evaluation/demo.js +19 -0
  35. package/dist/nativeAgent/evaluation/files.js +54 -0
  36. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  37. package/dist/nativeAgent/evaluation/index.js +239 -0
  38. package/dist/nativeAgent/executionOwner.js +209 -0
  39. package/dist/nativeAgent/filePatterns.js +170 -0
  40. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  41. package/dist/nativeAgent/index.js +13 -0
  42. package/dist/nativeAgent/interaction/cli.js +358 -0
  43. package/dist/nativeAgent/interaction/contracts.js +1 -0
  44. package/dist/nativeAgent/interaction/index.js +3 -0
  45. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  46. package/dist/nativeAgent/interaction/renderer.js +34 -0
  47. package/dist/nativeAgent/localSafety.js +258 -0
  48. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  49. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  50. package/dist/nativeAgent/model/errors.js +47 -0
  51. package/dist/nativeAgent/model/gateway.js +423 -0
  52. package/dist/nativeAgent/model/index.js +7 -0
  53. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  54. package/dist/nativeAgent/model/protocols.js +19 -0
  55. package/dist/nativeAgent/model/responses.js +263 -0
  56. package/dist/nativeAgent/model/types.js +1 -0
  57. package/dist/nativeAgent/model/wire.js +73 -0
  58. package/dist/nativeAgent/observability/index.js +220 -0
  59. package/dist/nativeAgent/product/catalog.js +82 -0
  60. package/dist/nativeAgent/product/config.js +295 -0
  61. package/dist/nativeAgent/product/facts.js +30 -0
  62. package/dist/nativeAgent/product/index.js +62 -0
  63. package/dist/nativeAgent/product/location.js +44 -0
  64. package/dist/nativeAgent/product/runtime.js +276 -0
  65. package/dist/nativeAgent/product/storage.js +49 -0
  66. package/dist/nativeAgent/product/tools.js +47 -0
  67. package/dist/nativeAgent/product/transport.js +54 -0
  68. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  69. package/dist/nativeAgent/searchTools.js +305 -0
  70. package/dist/nativeAgent/session/backends.js +293 -0
  71. package/dist/nativeAgent/session/catalog.js +97 -0
  72. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  73. package/dist/nativeAgent/session/contracts.js +1 -0
  74. package/dist/nativeAgent/session/format.js +269 -0
  75. package/dist/nativeAgent/session/index.js +5 -0
  76. package/dist/nativeAgent/session/location.js +36 -0
  77. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  78. package/dist/nativeAgent/session/store.js +248 -0
  79. package/dist/nativeAgent/textTools.js +270 -133
  80. package/dist/nativeAgent/toolManager/executor.js +290 -0
  81. package/dist/nativeAgent/toolManager/index.js +4 -0
  82. package/dist/nativeAgent/validation.js +2 -2
  83. package/dist/repository/gitWorkspace.js +41 -12
  84. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  85. package/dist/repository/taskWorkspacePreparer.js +39 -43
  86. package/dist/repository/workspaceCleanupInspection.js +88 -7
  87. package/dist/resources/resourceRegistrar.js +16 -10
  88. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  89. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  90. package/dist/task/archiveDiagnostics.js +26 -8
  91. package/dist/task/archivePreflight.js +26 -9
  92. package/dist/task/leaderArchive.js +9 -15
  93. package/dist/task/taskAuthority.js +56 -0
  94. package/dist/web/assets/client/api.js +8 -0
  95. package/dist/web/assets/client/app.js +74 -9
  96. package/dist/web/assets/client/components.js +95 -36
  97. package/dist/web/assets/client/detail.js +110 -64
  98. package/dist/web/assets/client/dock.js +95 -11
  99. package/dist/web/assets/client/evidence.js +286 -94
  100. package/dist/web/assets/client/i18n.js +182 -62
  101. package/dist/web/assets/client/overview.js +9 -10
  102. package/dist/web/assets/client/records.js +142 -7
  103. package/dist/web/assets/client/sections.js +234 -198
  104. package/dist/web/assets/client/sidebar.js +31 -2
  105. package/dist/web/assets/icons.js +4 -0
  106. package/dist/web/assets/shell.js +2 -1
  107. package/dist/web/assets/styles/components.js +64 -15
  108. package/dist/web/assets/styles/layout.js +11 -4
  109. package/dist/web/assets/styles/responsive.js +16 -6
  110. package/dist/web/assets/styles/tokens.js +1 -1
  111. package/dist/web/assets/styles/views.js +62 -42
  112. package/dist/web/webServer.js +12 -1
  113. package/dist/web/webTaskSurface.js +8 -1
  114. package/docs/agent-result-consumption.md +16 -0
  115. package/docs/agent-result-consumption.zh-CN.md +13 -0
  116. package/docs/examples/agent-offline.mjs +194 -0
  117. package/docs/native-agent.md +283 -0
  118. package/docs/observability/README.md +13 -3
  119. package/docs/observability/README.zh-CN.md +10 -3
  120. package/docs/release-workflow.md +72 -9
  121. package/docs/release-workflow.zh-CN.md +55 -6
  122. package/docs/roles-and-configuration.md +32 -0
  123. package/docs/roles-and-configuration.zh-CN.md +26 -0
  124. package/docs/task-delivery.md +36 -8
  125. package/docs/task-delivery.zh-CN.md +24 -7
  126. package/i18n/README.zh-CN.md +3 -2
  127. package/package.json +1 -1
  128. package/skills/yui-leader/SKILL.md +9 -0
  129. package/skills/yui-leader/references/execution.md +2 -1
  130. package/skills/yui-leader/references/integration.md +20 -0
  131. package/skills/yui-operator/SKILL.md +4 -0
  132. package/skills/yui-reviewer/SKILL.md +8 -0
  133. package/skills/yui-runtime/SKILL.md +11 -0
  134. package/skills/yui-runtime/references/publication.md +20 -4
  135. package/skills/yui-worker/SKILL.md +3 -0
@@ -51,6 +51,62 @@ export function assertTaskDeliveryAuthority(store, environment, taskId) {
51
51
  }
52
52
  return actor;
53
53
  }
54
+ /** Project effects consume the current Session's captured scope, never the
55
+ * Role's desired configuration or the mere presence of a writable directory.
56
+ * Task management (including abort/inspection) remains a separate authority. */
57
+ export function assertTaskProjectWriteAuthority(store, environment, taskId, projectId) {
58
+ const actor = assertTaskDeliveryAuthority(store, environment, taskId);
59
+ if (actor !== "leader")
60
+ return actor;
61
+ const caller = currentManagedRuntime(store, environment, taskId, LEADER_ROLE);
62
+ const session = store.getTaskRoleSessionSet(taskId, LEADER_ROLE)?.sessions[caller.agentId];
63
+ const effective = session?.effective;
64
+ const task = store.getTask(taskId);
65
+ const main = store.getTaskWorkspace(taskId);
66
+ const entry = main?.entries.find(entry => entry.projectId === projectId);
67
+ if (task?.status !== "active" || task.executionGate.state !== "enabled"
68
+ || !task.projectBindings.some(binding => binding.projectId === projectId)
69
+ || effective?.profileAccess !== "write" || !effective.writeProjectIds.includes(projectId)
70
+ || main?.owner.type !== "task" || main.owner.taskId !== taskId
71
+ || effective.workspace.root !== main.root || entry?.access !== "write"
72
+ || !effective.workspace.entries.some(captured => captured.projectId === projectId
73
+ && captured.access === "write" && captured.path === entry.path)) {
74
+ throw usageError("Project write requires this Task's current delivery Session with a captured writable Project in its Task-main workspace. Read the Session scope; use formal Session replacement to adopt a new boundary.");
75
+ }
76
+ return actor;
77
+ }
78
+ /** A managed Leader integrates into its captured Task-main, not any branch
79
+ * reachable through the same Git repository. The executor also verifies that
80
+ * this ref is physically checked out here immediately before target effects. */
81
+ export function taskIntegrationTargetCheckout(store, environment, taskId, projectId, targetRef) {
82
+ if (assertTaskProjectWriteAuthority(store, environment, taskId, projectId) !== "leader") {
83
+ return undefined;
84
+ }
85
+ return taskIntegrationSettlementCheckout(store, environment, taskId, projectId, targetRef);
86
+ }
87
+ /** Confirming an already-applied CAS is Task management, not a Project write
88
+ * grant. Keep the current delivery identity and captured target ownership, but
89
+ * do not require or change its declared write scope. Never use this for CAS. */
90
+ export function taskIntegrationSettlementCheckout(store, environment, taskId, projectId, targetRef) {
91
+ if (assertTaskDeliveryAuthority(store, environment, taskId) !== "leader")
92
+ return undefined;
93
+ const task = store.getTask(taskId);
94
+ const main = store.getTaskWorkspace(taskId);
95
+ const entry = main?.entries.find(entry => entry.projectId === projectId);
96
+ const caller = currentManagedRuntime(store, environment, taskId, LEADER_ROLE);
97
+ const effective = store.getTaskRoleSessionSet(taskId, LEADER_ROLE)?.sessions[caller.agentId]?.effective;
98
+ const captured = effective?.workspace.entries.find(entry => entry.projectId === projectId);
99
+ const branchRef = (ref) => ref.startsWith("refs/heads/") ? ref : `refs/heads/${ref}`;
100
+ if (task?.status !== "active" || !task.projectBindings.some(binding => binding.projectId === projectId)
101
+ || main?.owner.type !== "task" || main.owner.taskId !== taskId
102
+ || effective?.workspace.root !== main.root || entry?.access !== "write"
103
+ || captured?.path !== entry.path || entry.branch === undefined || captured.branch === undefined
104
+ || branchRef(entry.branch) !== branchRef(captured.branch)
105
+ || branchRef(targetRef) !== branchRef(captured.branch)) {
106
+ throw usageError("Managed Leader Integration target must be its captured Task-main branch and checkout.");
107
+ }
108
+ return entry.path;
109
+ }
54
110
  /**
55
111
  * Authority for a live steer/interrupt of an exact current native Turn
56
112
  * (decision-3 §1/§9, message-5 gap B). The legal evidence is the native
@@ -84,6 +84,14 @@ export const api = {
84
84
  result = await requestJson(task(taskId) + "/inspect?" + query);
85
85
  }
86
86
  },
87
+ // One page of a Context family: summaries and exact references, no bodies.
88
+ list: function (taskId, store, options) {
89
+ const query = new URLSearchParams({ store: store });
90
+ ["status", "cursor", "limit"].forEach(function (key) {
91
+ if (options && options[key] !== undefined && options[key] !== null) query.set(key, String(options[key]));
92
+ });
93
+ return requestJson(task(taskId) + "/list?" + query);
94
+ },
87
95
  artifacts: function (taskId) { return requestJson(task(taskId) + "/artifacts"); },
88
96
  artifact: function (taskId, path, commit) {
89
97
  return requestJson(task(taskId) + "/artifacts?" + new URLSearchParams({ path, commit }));
@@ -9,13 +9,13 @@ 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
 
16
16
  const $ = function (selector) { return document.querySelector(selector); };
17
17
  const el = {
18
- sidebar: $(".sidebar"), center: $("#center"), detail: $("#detail"),
18
+ sidebar: $(".sidebar"), sidebarDivider: $("#sidebar-divider"), center: $("#center"), detail: $("#detail"),
19
19
  search: $("#search"), filters: $("#status-filters"), attention: $("#attention-bar"), tasks: $("#task-list"),
20
20
  catalogNext: $("#catalog-next"), catalogReset: $("#catalog-reset"), catalogCount: $("#catalog-count"),
21
21
  catalogAttentionReset: $("#catalog-attention-reset"),
@@ -43,6 +43,9 @@ function readPreference(key, fallback) {
43
43
  function writePreference(key, value) {
44
44
  try { localStorage.setItem(key, value); } catch {}
45
45
  }
46
+ function clearPreference(key) {
47
+ try { localStorage.removeItem(key); } catch {}
48
+ }
46
49
  const dock = {
47
50
  open: readPreference("yui.dock.open", "true") !== "false",
48
51
  mode: "discussion",
@@ -52,6 +55,10 @@ const dock = {
52
55
  // Task, never restored from the desktop preference.
53
56
  sheet: false
54
57
  };
58
+ // Task list width: null keeps the CSS default (--sidebar-w); otherwise the width
59
+ // the user dragged to. It is a preference; the width actually applied is clamped
60
+ // to the current window.
61
+ const sidebar = { width: Number(readPreference("yui.sidebar.width", "")) || null, resizing: false };
55
62
  const narrow = window.matchMedia("(max-width: 900px)");
56
63
 
57
64
  const i18n = createI18n(el.locale);
@@ -111,6 +118,7 @@ function detailContext() {
111
118
  openSession: function (roleName) { openSession({ scope: "task", taskId: state.selected, roleName: roleName }); },
112
119
  answerInput: answerInput,
113
120
  inspect: api.inspect,
121
+ list: api.list,
114
122
  artifacts: api.artifacts,
115
123
  readArtifact: api.artifact,
116
124
  evidence: api.evidence,
@@ -125,6 +133,7 @@ function dockActions() {
125
133
  return {
126
134
  sendMessage: api.sendMessage,
127
135
  inspect: api.inspect,
136
+ list: api.list,
128
137
  afterWrite: function () { refreshDashboard({ quiet: true }); }
129
138
  };
130
139
  }
@@ -223,15 +232,17 @@ function switchTab(tab, options) {
223
232
  if (!(options && options.keepScroll)) el.center.scrollTop = 0;
224
233
  setSectionParam(tab);
225
234
  }
226
- // A section may name a tab or any anchored block inside one (for example
227
- // "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).
228
237
  function applySection(section) {
229
- if (!section || TABS.includes(section)) { switchTab(section || "overview"); return; }
230
- 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));
231
241
  const panel = anchor && anchor.closest(".tab-panel");
232
242
  if (!panel) { switchTab("overview"); return; }
233
243
  state.activeTab = panel.id.replace(/^panel-/, "");
234
244
  selectTab(el.detail, state.activeTab);
245
+ if (anchor.tagName === "DETAILS") anchor.open = true;
235
246
  anchor.scrollIntoView({ block: "start" });
236
247
  }
237
248
 
@@ -510,7 +521,7 @@ function updateLayout() {
510
521
  el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
511
522
  el.dockTabDiscussion.disabled = !state.selected;
512
523
  el.dockSwap.setAttribute("aria-pressed", String(dock.center));
513
- setDockWidth(dock.width, false);
524
+ applySidebarWidth();
514
525
  syncDockButtons();
515
526
  }
516
527
  function syncDockButtons() {
@@ -598,9 +609,63 @@ el.divider.addEventListener("keydown", function (event) {
598
609
  event.preventDefault();
599
610
  setDockWidth(event.key === "Home" ? 320 : event.key === "End" ? maxDockWidth() : dock.width + delta, true);
600
611
  });
601
- window.addEventListener("resize", function () { setDockWidth(dock.width, false); });
602
612
  narrow.addEventListener("change", updateLayout);
603
613
 
614
+ // --- Sidebar width ----------------------------------------------------------------
615
+ const SIDEBAR_MIN = 240;
616
+ const SIDEBAR_MAX = 560;
617
+ function maxSidebarWidth() {
618
+ // An open dock can shrink to its minimum, so it reserves only that much.
619
+ const reserve = dockVisible() ? 320 + 9 : 0;
620
+ return Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, window.innerWidth - reserve - 420));
621
+ }
622
+ function applySidebarWidth() {
623
+ const root = document.documentElement.style;
624
+ if (sidebar.width === null) root.removeProperty("--sidebar-w");
625
+ else root.setProperty("--sidebar-w", Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), sidebar.width)) + "px");
626
+ // A wider task list leaves the dock less room; the dock keeps its stored width.
627
+ setDockWidth(dock.width, false);
628
+ el.sidebarDivider.setAttribute("aria-valuemax", String(maxSidebarWidth()));
629
+ el.sidebarDivider.setAttribute("aria-valuenow", String(el.sidebar.offsetWidth));
630
+ }
631
+ function setSidebarWidth(width, persist) {
632
+ sidebar.width = width === null ? null : Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), Math.round(width)));
633
+ if (persist) {
634
+ if (sidebar.width === null) clearPreference("yui.sidebar.width");
635
+ else writePreference("yui.sidebar.width", String(sidebar.width));
636
+ }
637
+ applySidebarWidth();
638
+ }
639
+ el.sidebarDivider.addEventListener("pointerdown", function (event) {
640
+ if (event.button !== 0 || window.innerWidth <= 900) return;
641
+ sidebar.resizing = true;
642
+ el.sidebarDivider.setPointerCapture(event.pointerId);
643
+ document.body.classList.add("sidebar-resizing");
644
+ event.preventDefault();
645
+ });
646
+ el.sidebarDivider.addEventListener("pointermove", function (event) {
647
+ if (!sidebar.resizing) return;
648
+ setSidebarWidth(event.clientX - el.sidebar.getBoundingClientRect().left, false);
649
+ });
650
+ function finishSidebarResize() {
651
+ if (!sidebar.resizing) return;
652
+ sidebar.resizing = false;
653
+ document.body.classList.remove("sidebar-resizing");
654
+ if (sidebar.width !== null) writePreference("yui.sidebar.width", String(sidebar.width));
655
+ }
656
+ el.sidebarDivider.addEventListener("pointerup", finishSidebarResize);
657
+ el.sidebarDivider.addEventListener("pointercancel", finishSidebarResize);
658
+ el.sidebarDivider.addEventListener("dblclick", function () { setSidebarWidth(null, true); });
659
+ el.sidebarDivider.addEventListener("keydown", function (event) {
660
+ const current = el.sidebar.offsetWidth;
661
+ const next = event.key === "ArrowRight" ? current + 16 : event.key === "ArrowLeft" ? current - 16
662
+ : event.key === "Home" ? SIDEBAR_MIN : event.key === "End" ? maxSidebarWidth() : null;
663
+ if (next === null) return;
664
+ event.preventDefault();
665
+ setSidebarWidth(next, true);
666
+ });
667
+ window.addEventListener("resize", applySidebarWidth);
668
+
604
669
  // --- Sidebar controls -------------------------------------------------------------
605
670
  let searchTimer = null;
606
671
  el.search.addEventListener("input", function () {
@@ -696,7 +761,7 @@ document.addEventListener("keydown", function (event) {
696
761
  else if (key === "r") refreshDashboard();
697
762
  else if (key === "o") openOperator();
698
763
  else if (key === "d") toggleDock();
699
- 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]);
700
765
  });
701
766
 
702
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)));