@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -91,6 +91,112 @@ export function renderTaskSurface(container, data, t, locale, actions) {
91
91
  }
92
92
  });
93
93
  summary.append(chat);
94
+ // decision-3 three-action input control (queue / steer / interrupt). This is
95
+ // the same application-layer path the CLI drives; the Web surface adds no
96
+ // fourth action and no auto-fallback. Steer/interrupt name an exact current
97
+ // Turn (expected-target); a mismatch or missing Turn is a visible failure
98
+ // receipt, never a silent downgrade to a queue.
99
+ const control = node("details", "record-card");
100
+ control.append(node("summary", "", say("Redirect a running Role (queue / steer / interrupt)",
101
+ "改向运行中的 Role(排队/即时插话/打断)")));
102
+ const controlForm = node("form", "record-block");
103
+ const actionLabel = node("label", "", say("Action", "动作"));
104
+ const action = node("select", "");
105
+ for (const [val, en, cn] of [["queue", "Queue (deliver at next opportunity)", "排队(下次机会送达)"],
106
+ ["steer", "Steer (inject into the exact current Turn)", "即时插话(注入当前 Turn)"],
107
+ ["interrupt", "Interrupt (native cancel of the current Turn)", "打断(原生取消当前 Turn)"]]) {
108
+ const opt = node("option", "", say(en, cn));
109
+ opt.value = val;
110
+ action.append(opt);
111
+ }
112
+ actionLabel.append(action);
113
+ const roleLabel = node("label", "", say("Target Role", "目标 Role"));
114
+ const role = node("input", "");
115
+ role.placeholder = say("e.g. worker (blank = Leader for queue)", "如 worker(排队留空 = Leader)");
116
+ roleLabel.append(role);
117
+ const bodyLabel = node("label", "", say("Message", "消息内容"));
118
+ const body = node("textarea", "");
119
+ body.maxLength = 8000;
120
+ bodyLabel.append(body);
121
+ const targetLabel = node("label", "", say("Expected current Turn", "预期当前 Turn"));
122
+ const expectedTarget = node("input", "");
123
+ expectedTarget.placeholder = say("exact current Turn id (attempt/native)", "当前 Turn 的精确标识");
124
+ targetLabel.append(expectedTarget);
125
+ const workItemLabel = node("label", "", say("Work Item (optional)", "Work Item(可选)"));
126
+ const workItem = node("input", "");
127
+ workItemLabel.append(workItem);
128
+ const thenLabel = node("label", "", say("Then-message ref (optional)", "打断后续消息引用(可选)"));
129
+ const thenMessage = node("input", "");
130
+ thenMessage.placeholder = say("task/message id already saved", "已保存的 task/message 引用");
131
+ thenLabel.append(thenMessage);
132
+ const controlSend = node("button", "record-open", say("Submit control", "提交控制"));
133
+ controlSend.type = "submit";
134
+ controlSend.disabled = !["active", "draft"].includes(task.status);
135
+ const controlSent = node("p", "muted", say("Not submitted", "未提交"));
136
+ controlSent.setAttribute("role", "status");
137
+ // Field visibility follows the selected action so the form can only submit a
138
+ // legal shape: interrupt carries no body; queue needs no expected-target;
139
+ // then-message is an interrupt-only continuation.
140
+ const applyAction = () => {
141
+ const kind = action.value;
142
+ bodyLabel.hidden = kind === "interrupt";
143
+ targetLabel.hidden = kind === "queue";
144
+ thenLabel.hidden = kind !== "interrupt";
145
+ roleLabel.hidden = false;
146
+ role.required = kind !== "queue";
147
+ body.required = kind !== "interrupt";
148
+ expectedTarget.required = kind !== "queue";
149
+ };
150
+ action.addEventListener("change", applyAction);
151
+ applyAction();
152
+ controlForm.addEventListener("input", () => { controlForm.dataset.unsent = "true"; });
153
+ controlForm.append(actionLabel, roleLabel, bodyLabel, targetLabel, workItemLabel, thenLabel, controlSend, controlSent);
154
+ controlForm.addEventListener("submit", async (event) => {
155
+ event.preventDefault();
156
+ if (controlSend.disabled) return;
157
+ controlSend.disabled = true;
158
+ controlForm.dataset.unsent = "true";
159
+ const requestId = crypto.randomUUID();
160
+ const kind = action.value;
161
+ const payload = { action: kind, requestId };
162
+ if (kind === "interrupt") {
163
+ payload.role = role.value.trim();
164
+ payload.expectedTarget = expectedTarget.value.trim();
165
+ if (thenMessage.value.trim()) payload.thenMessage = thenMessage.value.trim();
166
+ } else {
167
+ payload.body = body.value;
168
+ if (kind === "steer") { payload.to = role.value.trim(); payload.expectedTarget = expectedTarget.value.trim(); }
169
+ else if (role.value.trim()) payload.to = role.value.trim();
170
+ if (workItem.value.trim()) payload.workItem = workItem.value.trim();
171
+ }
172
+ controlSent.textContent = say("Waiting for receipt · ", "等待回执 · ") + requestId;
173
+ try {
174
+ const receipt = await actions.controlInput(task.id, payload, requestId);
175
+ // The receipt states exactly what settled: a live steer/interrupt names its
176
+ // Host outcome; a queue or a not-delivered/idempotent result names its own
177
+ // state. None of these implies the Provider executed the input.
178
+ const settled = receipt.steer || receipt.interrupt || receipt.delivery
179
+ || { state: receipt.disposition };
180
+ const outcome = settled.outcome ? " · outcome " + settled.outcome : "";
181
+ const detail = settled.detail ? " · " + settled.detail : "";
182
+ controlSent.textContent = say("Receipt: ", "回执:") + receipt.action + " · " + settled.state
183
+ + outcome + detail + (receipt.record ? " · " + receipt.record.id : "");
184
+ controlForm.dataset.unsent = "false";
185
+ controlSend.disabled = false;
186
+ } catch (error) {
187
+ if (error.disposition === "not-submitted") {
188
+ controlSent.textContent = say("Not submitted: ", "未提交:") + error.message;
189
+ controlSend.disabled = false;
190
+ return;
191
+ }
192
+ // A committed input whose live edge is delivery-unknown must not be
193
+ // blindly re-sent under a new requestId or a different action.
194
+ controlSent.textContent = say("Unknown outcome. Re-read the Session before acting; do not resend · ",
195
+ "结果未知。请先重新读取 Session,不要盲目重发 · ") + requestId;
196
+ }
197
+ });
198
+ control.append(controlForm);
199
+ summary.append(control);
94
200
  // A Draft's Leader conversation IS its planning Turn, so this entry point does
95
201
  // reach a Leader. Report the real planning facts the snapshot carries — Turn,
96
202
  // Session, environment and the time they were observed — instead of asserting
@@ -277,7 +383,8 @@ export function renderTaskSurface(container, data, t, locale, actions) {
277
383
  const active = values("run").find((run) => run.roleName === role.name && run.status === "active");
278
384
  const incomplete = core.omitted.records > 0 || records("run").some((entry) => entry.omitted);
279
385
  roles.append(roleCard({
280
- ...role, status: active || incomplete ? "unknown" : "idle",
386
+ ...role, providerRetry: entry.providerRetry,
387
+ status: entry.providerRetry?.status === "waiting" ? "waiting" : active || incomplete ? "unknown" : "idle",
281
388
  effectiveLaunch: active ? active.effective : null,
282
389
  launchDrift: active && active.effective.sourceDesiredRevision !== role.launchRevision
283
390
  }, task, t, locale, actions));
@@ -53,7 +53,7 @@ export function renderFilters(container, state, t, onFilter) {
53
53
  dot.classList.add(status);
54
54
  btn.append(dot);
55
55
  }
56
- btn.append(node("span", "filter-label", translatedStatus(t, "status", status)));
56
+ btn.append(node("span", "filter-label", status === "all" ? t("filter.unarchived") : translatedStatus(t, "status", status)));
57
57
  const badge = node("span", "filter-count");
58
58
  btn.append(badge);
59
59
  btn.addEventListener("click", function () { onFilter(status); });
@@ -66,10 +66,17 @@ export function renderFilters(container, state, t, onFilter) {
66
66
  if (!btn) return;
67
67
  btn.classList.toggle("is-active", state.filter === status);
68
68
  const label = btn.querySelector(".filter-label");
69
- if (label) label.textContent = translatedStatus(t, "status", status);
70
- const count = status === "all" ? counts.total : counts[status];
69
+ if (label) label.textContent = status === "all"
70
+ ? t(state.catalogAll ? "catalog.all" : "filter.unarchived") : translatedStatus(t, "status", status);
71
+ const count = status === "all"
72
+ ? (counts.total === undefined ? undefined : counts.total - (state.catalogAll ? 0 : counts.archived || 0))
73
+ : counts[status];
71
74
  const badge = btn.querySelector(".filter-count");
72
75
  if (badge) {
76
+ if (status === "archived" && state.catalogScope && state.catalogScope.archived === "excluded") {
77
+ badge.hidden = true;
78
+ return;
79
+ }
73
80
  if (count !== undefined && count !== null) {
74
81
  badge.textContent = String(count);
75
82
  badge.hidden = false;
@@ -81,7 +88,7 @@ export function renderFilters(container, state, t, onFilter) {
81
88
  }
82
89
 
83
90
  function taskGroupOf(task, attentionIds) {
84
- if (attentionIds.has(task.id)) return "attention";
91
+ if (attentionIds.has(task.id) || (task.attention && (task.attention.openInputs > 0 || task.attention.unknownOperations > 0))) return "attention";
85
92
  if (task.status === "active") return "active";
86
93
  if (task.status === "draft") return "draft";
87
94
  if (task.status === "cancelled") return "cancelled";
@@ -105,8 +112,9 @@ export function renderTasks(container, state, t, locale, onSelect) {
105
112
  const attentionIds = new Set((state.attention || []).map(function (item) { return item.taskId; }));
106
113
  const groups = { attention: [], active: [], draft: [], finished: [], cancelled: [], archived: [] };
107
114
  (state.tasks || []).forEach(function (task) {
115
+ if (state.filter === "all" && !state.catalogAll && task.status === "archived") return;
108
116
  if (state.filter !== "all" && task.status !== state.filter) return;
109
- if (!taskMatchesQuery(task, query, locale)) return;
117
+ if (!state.catalogScope && !taskMatchesQuery(task, query, locale)) return;
110
118
  groups[taskGroupOf(task, attentionIds)].push(task);
111
119
  });
112
120
 
@@ -163,15 +171,33 @@ export function renderOverview(detail, state, t, locale, onSelect) {
163
171
  );
164
172
  wrap.append(rail);
165
173
 
174
+ if (state.catalogAttention) {
175
+ const catalog = node("section", "overview-block");
176
+ catalog.append(sectionHead(t("catalog.attention")));
177
+ catalog.append(node("p", "", t("catalog.signalsHelp")));
178
+ const groups = node("div", "overview-list");
179
+ Object.entries(state.catalogAttention).forEach(function (entry) {
180
+ const kind = entry[0], value = entry[1];
181
+ const button = node("button", "overview-row",
182
+ t("catalog." + kind) + " · " + value.count + " / " + value.taskCount + " " + t("catalog.tasks"));
183
+ button.type = "button";
184
+ button.dataset.catalogAttention = kind;
185
+ button.disabled = value.taskCount === 0;
186
+ groups.append(button);
187
+ });
188
+ catalog.append(groups);
189
+ wrap.append(catalog);
190
+ }
166
191
  const inbox = node("section", "overview-block");
167
192
  const inboxHead = sectionHead(t("overview.inbox"), {
168
- count: (state.attention || []).length
193
+ count: counts ? counts.openInputs : (state.attention || []).length
169
194
  });
170
195
  inbox.append(inboxHead);
171
196
  const items = state.attention || [];
172
197
  if (!items.length) {
173
198
  const empty = node("div", "inbox-empty");
174
- empty.append(node("span", "dot"), node("span", "", t("overview.inboxEmpty")));
199
+ empty.append(node("span", "dot"), node("span", "", counts && counts.openInputs > 0
200
+ ? t("catalog.inspectInputs") : t("overview.inboxEmpty")));
175
201
  inbox.append(empty);
176
202
  } else {
177
203
  const list = node("div", "inbox-list");
@@ -181,6 +207,7 @@ export function renderOverview(detail, state, t, locale, onSelect) {
181
207
  inbox.append(list);
182
208
  }
183
209
  wrap.append(inbox);
210
+ if (state.catalogScope) wrap.append(node("p", "", t("catalog.pageScope")));
184
211
 
185
212
  // Tasks whose Task-first projection says they need attention: blocked,
186
213
  // recovering, or in an attention state. This replaces the raw stalled-AgentRun
@@ -215,7 +242,7 @@ export function renderOverview(detail, state, t, locale, onSelect) {
215
242
  if (activeTasks.length) {
216
243
  duo.append(overviewBlock(t("overview.activeNow"), activeTasks, t, locale, onSelect));
217
244
  }
218
- const recentTasks = (state.tasks || []).slice().sort(byNewest).slice(0, 8);
245
+ const recentTasks = (state.tasks || []).filter(function (task) { return task.status !== "archived"; }).sort(byNewest).slice(0, 8);
219
246
  if (recentTasks.length) {
220
247
  duo.append(overviewBlock(t("overview.recent"), recentTasks, t, locale, onSelect));
221
248
  }
@@ -332,9 +359,13 @@ export function renderTaskDetail(detail, data, t, locale, actions) {
332
359
  "muted mono",
333
360
  project.directory + " · " + project.coverage + " · "
334
361
  + (project.expectedLocalCommit ? project.expectedLocalCommit.slice(0, 12) : "unknown")
362
+ + " → " + (project.deliveryLocalCommit ? project.deliveryLocalCommit.slice(0, 12) : "unknown")
335
363
  + " → "
336
364
  + (project.remoteCommit ? project.remoteCommit.slice(0, 12) : "unknown")
365
+ + " · PR/MR: " + (project.state || "none")
366
+ + (project.adoption ? " · " + project.adoption.id : "")
337
367
  ));
368
+ card.append(node("p", "record-copy", project.reason));
338
369
  });
339
370
  summaryBody.append(card);
340
371
  }
@@ -30,6 +30,7 @@ export const DASHBOARD_HTML = `<!doctype html>
30
30
  </div>
31
31
  <span class="live" title="Local loopback"><i aria-hidden="true"></i><span class="sr-only" data-i18n="brand.connection">Local loopback</span></span>
32
32
  </div>
33
+ <button id="global-input-open" class="operator-open" type="button">Global inputs · 全局消息</button>
33
34
  <label class="search">
34
35
  <span class="sr-only" data-i18n="search.label">Search tasks</span>
35
36
  <input id="search" type="search" placeholder="Filter by title, ID, tag, or project…" data-i18n-placeholder="search.placeholder">
@@ -37,6 +38,12 @@ export const DASHBOARD_HTML = `<!doctype html>
37
38
  </label>
38
39
  <div id="status-filters" class="filters" role="group" aria-label="Filter by status" data-i18n-aria-label="filters.label"></div>
39
40
  <div id="task-list" class="task-list" aria-label="Tasks" data-i18n-aria-label="board.title" aria-live="polite"><div class="loading" data-i18n="loading.dashboard">Reading local state…</div></div>
41
+ <div id="catalog-controls" class="catalog-pager" aria-live="polite">
42
+ <button id="catalog-reset" type="button" data-i18n="catalog.first">First page</button>
43
+ <span id="catalog-count"></span>
44
+ <button id="catalog-next" type="button" data-i18n="catalog.next" disabled>Next page</button>
45
+ <button id="catalog-attention-reset" type="button" data-i18n="catalog.clearAttention" hidden>Clear attention filter</button>
46
+ </div>
40
47
  <div class="sidebar-foot">
41
48
  <div class="sidebar-controls">
42
49
  <label class="select-control">
@@ -91,6 +98,28 @@ export const DASHBOARD_HTML = `<!doctype html>
91
98
  </nav>
92
99
  <main id="detail" class="detail" aria-labelledby="page-title" tabindex="-1"></main>
93
100
  </div>
101
+ <dialog id="global-input-dialog" class="global-input-dialog" aria-labelledby="global-input-title">
102
+ <h2 id="global-input-title">Global Role input · 全局 Role 消息</h2>
103
+ <p class="muted">Queue is the default. A cancel request does not prove the Turn stopped.<br>默认排队;取消请求不代表执行已停止。</p>
104
+ <form id="global-input-form" class="record-block">
105
+ <label>Role<input id="global-input-role" value="operator" required pattern="[A-Za-z0-9_-]+"></label>
106
+ <label>Action · 动作<select id="global-input-action">
107
+ <option value="queue">Queue · 排队</option>
108
+ <option value="steer">Steer · 当前轮插话</option>
109
+ <option value="interrupt">Interrupt · 请求取消</option>
110
+ </select></label>
111
+ <label id="global-input-body-label">Message · 消息<textarea id="global-input-body" maxlength="8000" required></textarea></label>
112
+ <label id="global-input-target-label" hidden>Expected Turn · 精确目标<input id="global-input-target"></label>
113
+ <label id="global-input-then-label" hidden>Then-message ID · 后续消息引用<input id="global-input-then"></label>
114
+ <div class="record-actions">
115
+ <button id="global-input-inspect" type="button" class="record-open">Read state · 读取状态</button>
116
+ <button id="global-input-submit" type="submit" class="record-open">Submit · 提交</button>
117
+ <button id="global-input-close" type="button" class="record-open">Close · 关闭</button>
118
+ </div>
119
+ <p id="global-input-receipt" role="status" class="muted">Not submitted · 未提交</p>
120
+ <pre id="global-input-state" class="global-input-state"></pre>
121
+ </form>
122
+ </dialog>
94
123
  <aside id="terminal-panel" class="terminal-panel" aria-labelledby="terminal-title" aria-hidden="true" hidden>
95
124
  <header class="terminal-head">
96
125
  <div>
@@ -9,7 +9,7 @@ body{margin:0;min-width:320px;height:100vh;overflow:hidden;font-size:14px}
9
9
  .app-shell{display:grid;grid-template-columns:var(--sidebar-w) minmax(0,1fr) 0;grid-template-rows:100vh;height:100vh;transition:grid-template-columns var(--motion-slow) var(--ease)}
10
10
  body.terminal-active .app-shell{grid-template-columns:var(--sidebar-w) minmax(0,1fr) var(--terminal-w)}
11
11
  /* Sidebar = persistent work index */
12
- .sidebar{display:flex;flex-direction:column;min-height:0;height:100vh;padding:16px 14px 14px}
12
+ .sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;height:100vh;padding:16px 14px 14px}
13
13
  .sidebar-brand{display:flex;gap:10px;align-items:center;flex:none;margin-bottom:14px}
14
14
  .brand-text{display:grid;gap:3px;min-width:0;flex:1}
15
15
  .sidebar .search{flex:none;margin-bottom:10px}
@@ -19,6 +19,13 @@ body.terminal-active .app-shell{grid-template-columns:var(--sidebar-w) minmax(0,
19
19
  .task-group-head{display:flex;align-items:baseline;gap:8px;padding:3px 4px 2px}
20
20
  .sidebar-foot{flex:none;padding-top:10px;margin-top:8px;display:grid;gap:8px}
21
21
  .sidebar-controls{display:grid;grid-template-columns:1fr 1fr;gap:8px;align-items:center}
22
+ .catalog-pager{display:grid;grid-template-columns:1fr 1fr;gap:6px;flex:none;padding-top:10px}
23
+ .catalog-pager span{grid-column:1/-1;grid-row:1;text-align:center;font-size:11px;color:var(--muted)}
24
+ .catalog-pager button{min-height:32px;min-width:0;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font:inherit;font-size:11px;cursor:pointer}
25
+ .catalog-pager button:hover:not(:disabled){border-color:var(--accent)}
26
+ .catalog-pager button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
27
+ .catalog-pager button:disabled{opacity:.45;cursor:default}
28
+ #catalog-attention-reset{grid-column:1/-1}
22
29
  /* Main column = top bar + optional section nav + reading surface */
23
30
  .main-col{display:flex;flex-direction:column;min-width:0;min-height:0;height:100vh;overflow-y:auto;scrollbar-gutter:stable}
24
31
  .topbar{position:sticky;top:0;z-index:30;flex:none;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:11px max(var(--page-space),calc((100% - 1100px) / 2));background:color-mix(in srgb,var(--bg) 92%,transparent);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:1px solid var(--border)}
@@ -15,6 +15,18 @@ body{background:var(--ambient),var(--bg);background-attachment:fixed;color:var(-
15
15
  .skip-link:focus{top:14px}
16
16
  button{font:inherit;cursor:pointer}
17
17
  input:focus-visible,select:focus-visible,button:focus-visible,.task:focus-visible,.tab:focus-visible,.overview-row:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
18
+ .global-input-dialog{width:min(34rem,calc(100vw - 2rem));max-height:85vh;overflow:auto;color:var(--text);background:var(--bg-1);border:1px solid var(--border-strong);border-radius:var(--radius-lg);padding:var(--page-space)}
19
+ .global-input-dialog::backdrop{background:color-mix(in srgb,var(--bg) 75%,transparent)}
20
+ .global-input-dialog form,.global-input-dialog label{display:grid;gap:8px}
21
+ .global-input-dialog form{gap:14px}
22
+ .global-input-dialog p{overflow-wrap:anywhere}
23
+ .global-input-dialog form,.global-input-dialog label{min-width:0}
24
+ .global-input-dialog input,.global-input-dialog select,.global-input-dialog textarea{width:100%;box-sizing:border-box;padding:10px;color:var(--text);background:var(--bg);border:1px solid var(--border-strong);border-radius:var(--radius)}
25
+ .global-input-dialog textarea{min-height:100px}
26
+ .global-input-dialog .record-actions{display:flex;flex-wrap:wrap;gap:8px}
27
+ .global-input-dialog button{min-height:44px}
28
+ .global-input-state{white-space:pre-wrap;overflow-wrap:anywhere;font:12px var(--font-mono)}
29
+ .global-input-dialog [hidden]{display:none}
18
30
  /* Sidebar */
19
31
  .sidebar{border-right:1px solid var(--border);background:color-mix(in srgb,var(--bg-1) 84%,transparent);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
20
32
  .brand-mark{width:40px;height:40px;flex:none;display:grid;place-items:center;font-family:var(--font-display);font-size:21px;font-weight:600;color:var(--on-accent);border-radius:11px;background:linear-gradient(140deg,var(--accent),var(--accent-2));box-shadow:var(--glow)}
@@ -1,11 +1,12 @@
1
1
  import { randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import { createServer } from "node:http";
3
3
  import WebSocket, { WebSocketServer } from "ws";
4
- import { usageError } from "../errors/cliError.js";
4
+ import { CliError, usageError } from "../errors/cliError.js";
5
+ import { parseTaskCatalogOptions } from "../context/taskCatalog.js";
5
6
  import { WebRequestRejected } from "./webMutation.js";
6
7
  import { TASK_SUBMISSION_INTENTS } from "../message/message.js";
7
8
  import { DASHBOARD_HTML, findWebAsset } from "./assets/assetManifest.js";
8
- import { buildWebDashboardSnapshot, buildWebTaskDetail } from "./webSnapshot.js";
9
+ import { buildWebDashboardSnapshot, buildWebTaskCatalog, buildWebTaskDetail } from "./webSnapshot.js";
9
10
  const MAX_JSON_BODY_BYTES = 16 * 1024;
10
11
  const MAX_TERMINAL_MESSAGE_BYTES = 64 * 1024;
11
12
  const MAX_TERMINAL_BUFFERED_BYTES = 1024 * 1024;
@@ -99,6 +100,38 @@ async function handleHttpRequest(request, response, store, dependencies, token,
99
100
  sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
100
101
  return;
101
102
  }
103
+ const globalControl = /^\/api\/roles\/([^/]+)\/control$/.exec(pathname);
104
+ if (globalControl && dependencies.surface) {
105
+ try {
106
+ const roleName = decodeURIComponent(globalControl[1]);
107
+ if (!/^[A-Za-z0-9_-]+$/.test(roleName))
108
+ throw new WebRequestRejected("Invalid Global Role.");
109
+ if (method === "GET") {
110
+ sendJson(response, 200, dependencies.surface.globalState(roleName), false);
111
+ }
112
+ else if (method === "POST") {
113
+ const body = await readMutationBody(request);
114
+ if (typeof body !== "object" || body === null || Array.isArray(body)
115
+ || Object.keys(body).some(key => !["action", "requestId", "body", "expectedTarget", "thenMessage"].includes(key))) {
116
+ throw new WebRequestRejected("Global input accepts only action, input and exact target fields.");
117
+ }
118
+ const input = body;
119
+ const parsed = parseWebControlInput({ ...input,
120
+ ...(input.action === "interrupt" ? { role: roleName }
121
+ : input.action === "steer" ? { to: roleName } : {}) });
122
+ sendJson(response, 200, {
123
+ ...await dependencies.surface.globalControl(roleName, parsed), requestId: parsed.requestId
124
+ }, false);
125
+ }
126
+ else
127
+ sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
128
+ }
129
+ catch (error) {
130
+ sendJson(response, 409, { error: error instanceof Error ? error.message : "Global input unavailable.",
131
+ disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
132
+ }
133
+ return;
134
+ }
102
135
  const panelTarget = /^\/api\/tasks\/([^/]+)\/panels$/.exec(pathname);
103
136
  if (panelTarget && dependencies.panels) {
104
137
  try {
@@ -130,7 +163,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
130
163
  }
131
164
  return;
132
165
  }
133
- const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages)$/.exec(pathname);
166
+ const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages|control)$/.exec(pathname);
134
167
  if (surfaceTarget && dependencies.surface) {
135
168
  try {
136
169
  let taskId;
@@ -169,6 +202,10 @@ async function handleHttpRequest(request, response, store, dependencies, token,
169
202
  // requestId is threaded as the submission key (§2.3) and echoed back on the receipt.
170
203
  value = { ...dependencies.surface.message(taskId, body.body, intent, body.requestId), requestId: body.requestId };
171
204
  }
205
+ else if (method === "POST" && action === "control") {
206
+ const body = await readMutationBody(request);
207
+ value = { ...await dependencies.surface.control(taskId, parseWebControlInput(body)), requestId: parseWebControlRequestId(body) };
208
+ }
172
209
  else if (method === "POST" && action === "metadata") {
173
210
  const body = await readMutationBody(request);
174
211
  if (typeof body !== "object" || body === null || Array.isArray(body)
@@ -200,7 +237,13 @@ async function handleHttpRequest(request, response, store, dependencies, token,
200
237
  sendAsset(response, asset, method === "HEAD");
201
238
  }
202
239
  else if (pathname === "/api/dashboard") {
203
- sendJson(response, 200, buildWebDashboardSnapshot(store, now()), method === "HEAD");
240
+ const query = new URL(request.url, "http://localhost").searchParams;
241
+ const snapshot = query.has("view")
242
+ ? buildWebTaskCatalog(store, parseTaskCatalogOptions([...query].flatMap(([key, value]) => key === "all"
243
+ ? (value === "true" ? ["--all"] : ["--invalid-all"])
244
+ : [`--${key}`, value])))
245
+ : buildWebDashboardSnapshot(store, now());
246
+ sendJson(response, 200, snapshot, method === "HEAD");
204
247
  }
205
248
  else if (pathname.startsWith("/api/tasks/")) {
206
249
  const taskId = decodeURIComponent(pathname.slice("/api/tasks/".length));
@@ -214,6 +257,10 @@ async function handleHttpRequest(request, response, store, dependencies, token,
214
257
  }
215
258
  }
216
259
  catch (error) {
260
+ if (error instanceof CliError) {
261
+ sendJson(response, 400, { error: error.message }, method === "HEAD");
262
+ return;
263
+ }
217
264
  if (error instanceof URIError) {
218
265
  sendJson(response, 400, { error: "Invalid URL encoding." }, method === "HEAD");
219
266
  return;
@@ -429,6 +476,86 @@ function parseAnswerPath(pathname) {
429
476
  return null;
430
477
  }
431
478
  }
479
+ /** The three-action control body is validated to the exact CLI-equivalent
480
+ * shape before it reaches the surface, so an unknown or malformed field is a
481
+ * visible not-submitted rejection rather than a silent default. `requestId` is
482
+ * the caller-supplied idempotency key common to all three actions. */
483
+ function parseWebControlRequestId(value) {
484
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
485
+ throw new WebRequestRejected("Control input must be an object.");
486
+ }
487
+ const requestId = value.requestId;
488
+ if (typeof requestId !== "string" || !requestId.trim()) {
489
+ throw new WebRequestRejected("A non-empty requestId is required.");
490
+ }
491
+ return requestId;
492
+ }
493
+ function requiredControlString(input, key) {
494
+ const value = input[key];
495
+ if (typeof value !== "string" || !value.trim()) {
496
+ throw new WebRequestRejected(`${key} is required.`);
497
+ }
498
+ return value;
499
+ }
500
+ function optionalControlString(input, key) {
501
+ if (!(key in input) || input[key] === undefined)
502
+ return undefined;
503
+ const value = input[key];
504
+ if (typeof value !== "string" || !value.trim()) {
505
+ throw new WebRequestRejected(`${key} must be a non-empty string when provided.`);
506
+ }
507
+ return value;
508
+ }
509
+ function parseWebControlInput(value) {
510
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
511
+ throw new WebRequestRejected("Control input must be an object.");
512
+ }
513
+ const input = value;
514
+ const action = input.action;
515
+ if (action === "queue") {
516
+ for (const key of Object.keys(input)) {
517
+ if (!["action", "body", "requestId", "to", "workItem", "reviewRound"].includes(key)) {
518
+ throw new WebRequestRejected(`Unexpected field for queue: ${key}.`);
519
+ }
520
+ }
521
+ if (input.workItem !== undefined && input.reviewRound !== undefined) {
522
+ throw new WebRequestRejected("A queue takes at most one of workItem or reviewRound.");
523
+ }
524
+ return { action: "queue", body: requiredControlString(input, "body"),
525
+ requestId: requiredControlString(input, "requestId"),
526
+ ...(optionalControlString(input, "to") === undefined ? {} : { to: optionalControlString(input, "to") }),
527
+ ...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
528
+ ...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
529
+ }
530
+ if (action === "steer") {
531
+ for (const key of Object.keys(input)) {
532
+ if (!["action", "body", "requestId", "expectedTarget", "to", "workItem", "reviewRound"].includes(key)) {
533
+ throw new WebRequestRejected(`Unexpected field for steer: ${key}.`);
534
+ }
535
+ }
536
+ if (input.workItem !== undefined && input.reviewRound !== undefined) {
537
+ throw new WebRequestRejected("A steer takes at most one of workItem or reviewRound.");
538
+ }
539
+ return { action: "steer", body: requiredControlString(input, "body"),
540
+ requestId: requiredControlString(input, "requestId"),
541
+ expectedTarget: requiredControlString(input, "expectedTarget"),
542
+ to: requiredControlString(input, "to"),
543
+ ...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
544
+ ...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
545
+ }
546
+ if (action === "interrupt") {
547
+ for (const key of Object.keys(input)) {
548
+ if (!["action", "requestId", "expectedTarget", "role", "thenMessage"].includes(key)) {
549
+ throw new WebRequestRejected(`Unexpected field for interrupt: ${key}.`);
550
+ }
551
+ }
552
+ return { action: "interrupt", requestId: requiredControlString(input, "requestId"),
553
+ expectedTarget: requiredControlString(input, "expectedTarget"),
554
+ role: requiredControlString(input, "role"),
555
+ ...(optionalControlString(input, "thenMessage") === undefined ? {} : { thenMessage: optionalControlString(input, "thenMessage") }) };
556
+ }
557
+ throw new WebRequestRejected("action must be one of queue, steer, or interrupt.");
558
+ }
432
559
  function parseWebInputAnswer(value) {
433
560
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
434
561
  throw new Error("Input answer must be an object.");
@@ -1,3 +1,5 @@
1
+ import { providerRetryProjection } from "../runtime/providerRetry.js";
2
+ import { readTaskCatalog } from "../context/taskCatalog.js";
1
3
  import { isRoleRunStalled, latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
2
4
  import { retiredTaskRecordIds } from "../task/taskRecordRetirement.js";
3
5
  import { buildTaskExecutionProjection } from "../scheduler/taskExecutionProjection.js";
@@ -9,6 +11,9 @@ import { runExecutionObservation } from "../agentRun/agentRun.js";
9
11
  import { resolveRuntimeHealth } from "../config/yuiConfig.js";
10
12
  import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
11
13
  import { projectTaskRemoteDelivery } from "../task/remoteDelivery.js";
14
+ export function buildWebTaskCatalog(store, options) {
15
+ return store.transaction(reader => readTaskCatalog(reader, options));
16
+ }
12
17
  export function buildWebDashboardSnapshot(store, now = new Date()) {
13
18
  return store.transaction((reader) => {
14
19
  const statusCounts = {
@@ -25,14 +30,15 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
25
30
  statusCounts[task.status] += 1;
26
31
  const taskOpenInputs = reader.listInputRequests(task.id)
27
32
  .filter((request) => request.status === "open").length;
28
- openInputs += taskOpenInputs;
33
+ if (task.status !== "archived")
34
+ openInputs += taskOpenInputs;
29
35
  const taskOpen = reader.listInputRequests(task.id)
30
36
  .filter((request) => request.status === "open");
31
- for (const request of taskOpen) {
37
+ for (const request of task.status === "archived" ? [] : taskOpen) {
32
38
  attention.push({ taskId: task.id, taskTitle: task.title, request });
33
39
  }
34
40
  const events = reader.listEvents?.(task.id) ?? [];
35
- const needsAttentionCount = reader.listRuns(task.id)
41
+ const needsAttentionCount = task.status === "archived" ? 0 : reader.listRuns(task.id)
36
42
  .filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
37
43
  .length;
38
44
  const execution = buildTaskExecutionProjection(reader, task.id, task, now);
@@ -94,12 +100,15 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
94
100
  const activeSession = sessions?.sessions[activeRun?.effective.agentId ?? sessions.activeAgentId];
95
101
  const effectiveLaunch = activeRun?.effective ?? activeSession?.effective ?? null;
96
102
  const delivery = sessions?.providerBinding?.run?.status;
103
+ const providerRetry = providerRetryProjection(sessions?.providerBinding);
97
104
  return {
98
105
  ...role,
106
+ providerRetry,
99
107
  // An open record is not proof that its Agent is running.
100
- status: delivery === "accepted" ? "running"
101
- : delivery === "submitting" || delivery === "deferred" ? "waiting"
102
- : activeSession === undefined && activeRun === undefined ? "idle" : "unknown",
108
+ status: providerRetry?.status === "waiting" ? "waiting"
109
+ : delivery === "accepted" ? "running"
110
+ : delivery === "submitting" || delivery === "deferred" ? "waiting"
111
+ : activeSession === undefined && activeRun === undefined ? "idle" : "unknown",
103
112
  sessionTokens: projectSessionTokenMetrics(events, resolveSessionTokenIdentity(activeSession === undefined
104
113
  ? null
105
114
  : { taskId, roleName: role.name, ...activeSession })),
@@ -161,6 +170,7 @@ function webRemoteDelivery(reader, task) {
161
170
  publications: reader.listPublicationReferences(task.id),
162
171
  managedWorkspaces: reader.listManagedWorkspaces(task.id),
163
172
  runs: reader.listRuns(task.id),
173
+ integrations: reader.listIntegrationAttempts(task.id),
164
174
  currentCandidate: null
165
175
  });
166
176
  }