@zq-silk/yui 0.15.9 → 0.15.11
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.
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/cli/commandCatalog.js +26 -6
- package/dist/cli/interactionPolicy.js +3 -3
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +336 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +511 -83
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/taskContext.js +38 -2
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +3 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +136 -4
- package/dist/storage/sqliteStore.js +40 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/web/assets/client/app.js +55 -0
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +106 -0
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +117 -1
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/testing/verification-levels.md +24 -0
- package/docs/testing/verification-levels.zh-CN.md +11 -0
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +151 -49
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +6 -1
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -131,6 +131,8 @@ function detailActions() {
|
|
|
131
131
|
sendMessage: (taskId, body, requestId, intent) => submitMutation(taskId + "/messages",
|
|
132
132
|
"/api/tasks/" + encodeURIComponent(taskId) + "/messages",
|
|
133
133
|
{ body, requestId, ...(intent === undefined ? {} : { intent }) }),
|
|
134
|
+
controlInput: (taskId, payload, requestId) => submitMutation(taskId + "/control/" + requestId,
|
|
135
|
+
"/api/tasks/" + encodeURIComponent(taskId) + "/control", payload),
|
|
134
136
|
updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
|
|
135
137
|
"/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
|
|
136
138
|
panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
|
|
@@ -659,9 +661,62 @@ elements.refresh.addEventListener("click", function () { refreshDashboard(); });
|
|
|
659
661
|
elements.operatorTerminal.addEventListener("click", function () {
|
|
660
662
|
openTerminal({ scope: "global", roleName: "operator" });
|
|
661
663
|
});
|
|
664
|
+
const globalDialog = document.querySelector("#global-input-dialog");
|
|
665
|
+
const globalForm = document.querySelector("#global-input-form");
|
|
666
|
+
const globalRole = document.querySelector("#global-input-role");
|
|
667
|
+
const globalAction = document.querySelector("#global-input-action");
|
|
668
|
+
const globalBody = document.querySelector("#global-input-body");
|
|
669
|
+
const globalTarget = document.querySelector("#global-input-target");
|
|
670
|
+
const globalThen = document.querySelector("#global-input-then");
|
|
671
|
+
const globalSubmit = document.querySelector("#global-input-submit");
|
|
672
|
+
const globalReceipt = document.querySelector("#global-input-receipt");
|
|
673
|
+
const globalState = document.querySelector("#global-input-state");
|
|
674
|
+
document.querySelector("#global-input-open").addEventListener("click", () => globalDialog.showModal());
|
|
675
|
+
document.querySelector("#global-input-close").addEventListener("click", () => globalDialog.close());
|
|
676
|
+
globalAction.addEventListener("change", () => {
|
|
677
|
+
document.querySelector("#global-input-body-label").hidden = globalAction.value === "interrupt";
|
|
678
|
+
document.querySelector("#global-input-target-label").hidden = globalAction.value === "queue";
|
|
679
|
+
document.querySelector("#global-input-then-label").hidden = globalAction.value !== "interrupt";
|
|
680
|
+
globalBody.required = globalAction.value !== "interrupt";
|
|
681
|
+
globalTarget.required = globalAction.value !== "queue";
|
|
682
|
+
});
|
|
683
|
+
document.querySelector("#global-input-inspect").addEventListener("click", async () => {
|
|
684
|
+
try {
|
|
685
|
+
const facts = await requestJson("/api/roles/" + encodeURIComponent(globalRole.value.trim()) + "/control");
|
|
686
|
+
globalState.textContent = JSON.stringify(facts, null, 2);
|
|
687
|
+
globalTarget.value = facts.turn?.nativeTurnId || facts.turn?.attemptId || "";
|
|
688
|
+
} catch (error) { globalState.textContent = error.message; }
|
|
689
|
+
});
|
|
690
|
+
globalForm.addEventListener("submit", async event => {
|
|
691
|
+
event.preventDefault();
|
|
692
|
+
if (globalSubmit.disabled) return;
|
|
693
|
+
globalSubmit.disabled = true;
|
|
694
|
+
const role = globalRole.value.trim();
|
|
695
|
+
const action = globalAction.value;
|
|
696
|
+
const requestId = crypto.randomUUID();
|
|
697
|
+
const input = { action, requestId };
|
|
698
|
+
if (action !== "interrupt") input.body = globalBody.value;
|
|
699
|
+
if (action !== "queue") input.expectedTarget = globalTarget.value.trim();
|
|
700
|
+
if (action === "interrupt" && globalThen.value.trim()) input.thenMessage = globalThen.value.trim();
|
|
701
|
+
globalReceipt.textContent = "Waiting for receipt · 等待回执 · " + requestId;
|
|
702
|
+
try {
|
|
703
|
+
const receipt = await submitMutation("global/" + role, "/api/roles/" + encodeURIComponent(role) + "/control", input);
|
|
704
|
+
const status = receipt.steer || receipt.interrupt || receipt.delivery || {};
|
|
705
|
+
globalReceipt.textContent = JSON.stringify(receipt);
|
|
706
|
+
const unknown = ["pending", "delivery-unknown", "steer-unknown", "interrupt-unknown"].includes(status.state)
|
|
707
|
+
|| status.code === "DELIVERY_UNKNOWN";
|
|
708
|
+
globalSubmit.disabled = unknown;
|
|
709
|
+
if (!unknown && !status.code) globalBody.value = "";
|
|
710
|
+
} catch (error) {
|
|
711
|
+
globalReceipt.textContent = error.disposition === "not-submitted" ? error.message
|
|
712
|
+
: "Outcome unknown; read state before acting. 结果未知,请先读取状态,不要重发。 · " + requestId;
|
|
713
|
+
globalSubmit.disabled = error.disposition !== "not-submitted";
|
|
714
|
+
}
|
|
715
|
+
});
|
|
662
716
|
elements.detailBack.addEventListener("click", clearSelection);
|
|
663
717
|
elements.terminalClose.addEventListener("click", closeTerminalPanel);
|
|
664
718
|
document.addEventListener("keydown", function (event) {
|
|
719
|
+
if (globalDialog.open) return;
|
|
665
720
|
const active = document.activeElement;
|
|
666
721
|
const typing = active && ["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName);
|
|
667
722
|
if (event.key === "Escape" && terminalPanelOpen()) {
|
|
@@ -238,6 +238,7 @@ const messages = {
|
|
|
238
238
|
"stats.running": "running",
|
|
239
239
|
"status.active": "Active",
|
|
240
240
|
"status.all": "All",
|
|
241
|
+
"filter.unarchived": "Unarchived",
|
|
241
242
|
"status.archived": "Archived",
|
|
242
243
|
"status.completed": "Completed",
|
|
243
244
|
"status.draft": "Draft",
|
|
@@ -305,6 +306,7 @@ const messages = {
|
|
|
305
306
|
"workExec.candidateStatus.none": "None",
|
|
306
307
|
"workExec.candidateStatus.observed": "Observed",
|
|
307
308
|
"workExec.candidateStatus.unknown": "Unknown",
|
|
309
|
+
"workExec.action.execute-directly": "Leader executes directly",
|
|
308
310
|
"workExec.action.dispatch-work": "Dispatch work",
|
|
309
311
|
"workExec.action.wait-for-lanes": "Wait for Lanes",
|
|
310
312
|
"workExec.action.retry-or-settle-lanes": "Retry or settle exact Lanes",
|
|
@@ -344,6 +346,7 @@ const messages = {
|
|
|
344
346
|
"exec.action.inspect-attention": "Inspect attention",
|
|
345
347
|
"exec.action.recover-leader": "Recover leader",
|
|
346
348
|
"exec.action.resolve-blocker": "Resolve blocker",
|
|
349
|
+
"exec.action.resolve-integration-conflicts": "Resolve Integration conflicts",
|
|
347
350
|
"exec.action.complete-task": "Complete task",
|
|
348
351
|
"exec.action.none": "—",
|
|
349
352
|
"exec.attention.leader-recovery": "Leader recovery",
|
|
@@ -624,6 +627,7 @@ const messages = {
|
|
|
624
627
|
"stats.running": "运行中",
|
|
625
628
|
"status.active": "进行中",
|
|
626
629
|
"status.all": "全部",
|
|
630
|
+
"filter.unarchived": "未归档",
|
|
627
631
|
"status.archived": "已归档",
|
|
628
632
|
"status.completed": "已完成",
|
|
629
633
|
"status.draft": "草稿",
|
|
@@ -691,6 +695,7 @@ const messages = {
|
|
|
691
695
|
"workExec.candidateStatus.none": "无",
|
|
692
696
|
"workExec.candidateStatus.observed": "已观测",
|
|
693
697
|
"workExec.candidateStatus.unknown": "未知",
|
|
698
|
+
"workExec.action.execute-directly": "Leader 直接执行",
|
|
694
699
|
"workExec.action.dispatch-work": "派发工作",
|
|
695
700
|
"workExec.action.wait-for-lanes": "等待 Lane",
|
|
696
701
|
"workExec.action.retry-or-settle-lanes": "重试或结算指定 Lane",
|
|
@@ -730,6 +735,7 @@ const messages = {
|
|
|
730
735
|
"exec.action.inspect-attention": "检查注意项",
|
|
731
736
|
"exec.action.recover-leader": "恢复负责人",
|
|
732
737
|
"exec.action.resolve-blocker": "解决阻塞",
|
|
738
|
+
"exec.action.resolve-integration-conflicts": "解决集成冲突",
|
|
733
739
|
"exec.action.complete-task": "完成任务",
|
|
734
740
|
"exec.action.none": "—",
|
|
735
741
|
"exec.attention.leader-recovery": "负责人恢复",
|
|
@@ -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
|
|
@@ -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,8 +66,10 @@ 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"
|
|
69
|
+
if (label) label.textContent = status === "all" ? t("filter.unarchived") : translatedStatus(t, "status", status);
|
|
70
|
+
const count = status === "all"
|
|
71
|
+
? (counts.total === undefined ? undefined : counts.total - (counts.archived || 0))
|
|
72
|
+
: counts[status];
|
|
71
73
|
const badge = btn.querySelector(".filter-count");
|
|
72
74
|
if (badge) {
|
|
73
75
|
if (count !== undefined && count !== null) {
|
|
@@ -105,6 +107,7 @@ export function renderTasks(container, state, t, locale, onSelect) {
|
|
|
105
107
|
const attentionIds = new Set((state.attention || []).map(function (item) { return item.taskId; }));
|
|
106
108
|
const groups = { attention: [], active: [], draft: [], finished: [], cancelled: [], archived: [] };
|
|
107
109
|
(state.tasks || []).forEach(function (task) {
|
|
110
|
+
if (state.filter === "all" && task.status === "archived") return;
|
|
108
111
|
if (state.filter !== "all" && task.status !== state.filter) return;
|
|
109
112
|
if (!taskMatchesQuery(task, query, locale)) return;
|
|
110
113
|
groups[taskGroupOf(task, attentionIds)].push(task);
|
|
@@ -215,7 +218,7 @@ export function renderOverview(detail, state, t, locale, onSelect) {
|
|
|
215
218
|
if (activeTasks.length) {
|
|
216
219
|
duo.append(overviewBlock(t("overview.activeNow"), activeTasks, t, locale, onSelect));
|
|
217
220
|
}
|
|
218
|
-
const recentTasks = (state.tasks || []).
|
|
221
|
+
const recentTasks = (state.tasks || []).filter(function (task) { return task.status !== "archived"; }).sort(byNewest).slice(0, 8);
|
|
219
222
|
if (recentTasks.length) {
|
|
220
223
|
duo.append(overviewBlock(t("overview.recent"), recentTasks, t, locale, onSelect));
|
|
221
224
|
}
|
package/dist/web/assets/shell.js
CHANGED
|
@@ -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">
|
|
@@ -91,6 +92,28 @@ export const DASHBOARD_HTML = `<!doctype html>
|
|
|
91
92
|
</nav>
|
|
92
93
|
<main id="detail" class="detail" aria-labelledby="page-title" tabindex="-1"></main>
|
|
93
94
|
</div>
|
|
95
|
+
<dialog id="global-input-dialog" class="global-input-dialog" aria-labelledby="global-input-title">
|
|
96
|
+
<h2 id="global-input-title">Global Role input · 全局 Role 消息</h2>
|
|
97
|
+
<p class="muted">Queue is the default. A cancel request does not prove the Turn stopped.<br>默认排队;取消请求不代表执行已停止。</p>
|
|
98
|
+
<form id="global-input-form" class="record-block">
|
|
99
|
+
<label>Role<input id="global-input-role" value="operator" required pattern="[A-Za-z0-9_-]+"></label>
|
|
100
|
+
<label>Action · 动作<select id="global-input-action">
|
|
101
|
+
<option value="queue">Queue · 排队</option>
|
|
102
|
+
<option value="steer">Steer · 当前轮插话</option>
|
|
103
|
+
<option value="interrupt">Interrupt · 请求取消</option>
|
|
104
|
+
</select></label>
|
|
105
|
+
<label id="global-input-body-label">Message · 消息<textarea id="global-input-body" maxlength="8000" required></textarea></label>
|
|
106
|
+
<label id="global-input-target-label" hidden>Expected Turn · 精确目标<input id="global-input-target"></label>
|
|
107
|
+
<label id="global-input-then-label" hidden>Then-message ID · 后续消息引用<input id="global-input-then"></label>
|
|
108
|
+
<div class="record-actions">
|
|
109
|
+
<button id="global-input-inspect" type="button" class="record-open">Read state · 读取状态</button>
|
|
110
|
+
<button id="global-input-submit" type="submit" class="record-open">Submit · 提交</button>
|
|
111
|
+
<button id="global-input-close" type="button" class="record-open">Close · 关闭</button>
|
|
112
|
+
</div>
|
|
113
|
+
<p id="global-input-receipt" role="status" class="muted">Not submitted · 未提交</p>
|
|
114
|
+
<pre id="global-input-state" class="global-input-state"></pre>
|
|
115
|
+
</form>
|
|
116
|
+
</dialog>
|
|
94
117
|
<aside id="terminal-panel" class="terminal-panel" aria-labelledby="terminal-title" aria-hidden="true" hidden>
|
|
95
118
|
<header class="terminal-head">
|
|
96
119
|
<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}
|
|
@@ -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)}
|
package/dist/web/webServer.js
CHANGED
|
@@ -99,6 +99,38 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
99
99
|
sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
+
const globalControl = /^\/api\/roles\/([^/]+)\/control$/.exec(pathname);
|
|
103
|
+
if (globalControl && dependencies.surface) {
|
|
104
|
+
try {
|
|
105
|
+
const roleName = decodeURIComponent(globalControl[1]);
|
|
106
|
+
if (!/^[A-Za-z0-9_-]+$/.test(roleName))
|
|
107
|
+
throw new WebRequestRejected("Invalid Global Role.");
|
|
108
|
+
if (method === "GET") {
|
|
109
|
+
sendJson(response, 200, dependencies.surface.globalState(roleName), false);
|
|
110
|
+
}
|
|
111
|
+
else if (method === "POST") {
|
|
112
|
+
const body = await readMutationBody(request);
|
|
113
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)
|
|
114
|
+
|| Object.keys(body).some(key => !["action", "requestId", "body", "expectedTarget", "thenMessage"].includes(key))) {
|
|
115
|
+
throw new WebRequestRejected("Global input accepts only action, input and exact target fields.");
|
|
116
|
+
}
|
|
117
|
+
const input = body;
|
|
118
|
+
const parsed = parseWebControlInput({ ...input,
|
|
119
|
+
...(input.action === "interrupt" ? { role: roleName }
|
|
120
|
+
: input.action === "steer" ? { to: roleName } : {}) });
|
|
121
|
+
sendJson(response, 200, {
|
|
122
|
+
...await dependencies.surface.globalControl(roleName, parsed), requestId: parsed.requestId
|
|
123
|
+
}, false);
|
|
124
|
+
}
|
|
125
|
+
else
|
|
126
|
+
sendJson(response, 405, { error: "Method not allowed.", disposition: "not-submitted" }, false);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
sendJson(response, 409, { error: error instanceof Error ? error.message : "Global input unavailable.",
|
|
130
|
+
disposition: error instanceof WebRequestRejected ? "not-submitted" : "unknown" }, false);
|
|
131
|
+
}
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
102
134
|
const panelTarget = /^\/api\/tasks\/([^/]+)\/panels$/.exec(pathname);
|
|
103
135
|
if (panelTarget && dependencies.panels) {
|
|
104
136
|
try {
|
|
@@ -130,7 +162,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
130
162
|
}
|
|
131
163
|
return;
|
|
132
164
|
}
|
|
133
|
-
const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages)$/.exec(pathname);
|
|
165
|
+
const surfaceTarget = /^\/api\/tasks\/([^/]+)\/(context|delta|inspect|metadata|messages|control)$/.exec(pathname);
|
|
134
166
|
if (surfaceTarget && dependencies.surface) {
|
|
135
167
|
try {
|
|
136
168
|
let taskId;
|
|
@@ -169,6 +201,10 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
169
201
|
// requestId is threaded as the submission key (§2.3) and echoed back on the receipt.
|
|
170
202
|
value = { ...dependencies.surface.message(taskId, body.body, intent, body.requestId), requestId: body.requestId };
|
|
171
203
|
}
|
|
204
|
+
else if (method === "POST" && action === "control") {
|
|
205
|
+
const body = await readMutationBody(request);
|
|
206
|
+
value = { ...await dependencies.surface.control(taskId, parseWebControlInput(body)), requestId: parseWebControlRequestId(body) };
|
|
207
|
+
}
|
|
172
208
|
else if (method === "POST" && action === "metadata") {
|
|
173
209
|
const body = await readMutationBody(request);
|
|
174
210
|
if (typeof body !== "object" || body === null || Array.isArray(body)
|
|
@@ -429,6 +465,86 @@ function parseAnswerPath(pathname) {
|
|
|
429
465
|
return null;
|
|
430
466
|
}
|
|
431
467
|
}
|
|
468
|
+
/** The three-action control body is validated to the exact CLI-equivalent
|
|
469
|
+
* shape before it reaches the surface, so an unknown or malformed field is a
|
|
470
|
+
* visible not-submitted rejection rather than a silent default. `requestId` is
|
|
471
|
+
* the caller-supplied idempotency key common to all three actions. */
|
|
472
|
+
function parseWebControlRequestId(value) {
|
|
473
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
474
|
+
throw new WebRequestRejected("Control input must be an object.");
|
|
475
|
+
}
|
|
476
|
+
const requestId = value.requestId;
|
|
477
|
+
if (typeof requestId !== "string" || !requestId.trim()) {
|
|
478
|
+
throw new WebRequestRejected("A non-empty requestId is required.");
|
|
479
|
+
}
|
|
480
|
+
return requestId;
|
|
481
|
+
}
|
|
482
|
+
function requiredControlString(input, key) {
|
|
483
|
+
const value = input[key];
|
|
484
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
485
|
+
throw new WebRequestRejected(`${key} is required.`);
|
|
486
|
+
}
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
function optionalControlString(input, key) {
|
|
490
|
+
if (!(key in input) || input[key] === undefined)
|
|
491
|
+
return undefined;
|
|
492
|
+
const value = input[key];
|
|
493
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
494
|
+
throw new WebRequestRejected(`${key} must be a non-empty string when provided.`);
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
function parseWebControlInput(value) {
|
|
499
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
500
|
+
throw new WebRequestRejected("Control input must be an object.");
|
|
501
|
+
}
|
|
502
|
+
const input = value;
|
|
503
|
+
const action = input.action;
|
|
504
|
+
if (action === "queue") {
|
|
505
|
+
for (const key of Object.keys(input)) {
|
|
506
|
+
if (!["action", "body", "requestId", "to", "workItem", "reviewRound"].includes(key)) {
|
|
507
|
+
throw new WebRequestRejected(`Unexpected field for queue: ${key}.`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (input.workItem !== undefined && input.reviewRound !== undefined) {
|
|
511
|
+
throw new WebRequestRejected("A queue takes at most one of workItem or reviewRound.");
|
|
512
|
+
}
|
|
513
|
+
return { action: "queue", body: requiredControlString(input, "body"),
|
|
514
|
+
requestId: requiredControlString(input, "requestId"),
|
|
515
|
+
...(optionalControlString(input, "to") === undefined ? {} : { to: optionalControlString(input, "to") }),
|
|
516
|
+
...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
|
|
517
|
+
...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
|
|
518
|
+
}
|
|
519
|
+
if (action === "steer") {
|
|
520
|
+
for (const key of Object.keys(input)) {
|
|
521
|
+
if (!["action", "body", "requestId", "expectedTarget", "to", "workItem", "reviewRound"].includes(key)) {
|
|
522
|
+
throw new WebRequestRejected(`Unexpected field for steer: ${key}.`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (input.workItem !== undefined && input.reviewRound !== undefined) {
|
|
526
|
+
throw new WebRequestRejected("A steer takes at most one of workItem or reviewRound.");
|
|
527
|
+
}
|
|
528
|
+
return { action: "steer", body: requiredControlString(input, "body"),
|
|
529
|
+
requestId: requiredControlString(input, "requestId"),
|
|
530
|
+
expectedTarget: requiredControlString(input, "expectedTarget"),
|
|
531
|
+
to: requiredControlString(input, "to"),
|
|
532
|
+
...(optionalControlString(input, "workItem") === undefined ? {} : { workItem: optionalControlString(input, "workItem") }),
|
|
533
|
+
...(optionalControlString(input, "reviewRound") === undefined ? {} : { reviewRound: optionalControlString(input, "reviewRound") }) };
|
|
534
|
+
}
|
|
535
|
+
if (action === "interrupt") {
|
|
536
|
+
for (const key of Object.keys(input)) {
|
|
537
|
+
if (!["action", "requestId", "expectedTarget", "role", "thenMessage"].includes(key)) {
|
|
538
|
+
throw new WebRequestRejected(`Unexpected field for interrupt: ${key}.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { action: "interrupt", requestId: requiredControlString(input, "requestId"),
|
|
542
|
+
expectedTarget: requiredControlString(input, "expectedTarget"),
|
|
543
|
+
role: requiredControlString(input, "role"),
|
|
544
|
+
...(optionalControlString(input, "thenMessage") === undefined ? {} : { thenMessage: optionalControlString(input, "thenMessage") }) };
|
|
545
|
+
}
|
|
546
|
+
throw new WebRequestRejected("action must be one of queue, steer, or interrupt.");
|
|
547
|
+
}
|
|
432
548
|
function parseWebInputAnswer(value) {
|
|
433
549
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
434
550
|
throw new Error("Input answer must be an object.");
|
package/dist/web/webSnapshot.js
CHANGED
|
@@ -25,14 +25,15 @@ export function buildWebDashboardSnapshot(store, now = new Date()) {
|
|
|
25
25
|
statusCounts[task.status] += 1;
|
|
26
26
|
const taskOpenInputs = reader.listInputRequests(task.id)
|
|
27
27
|
.filter((request) => request.status === "open").length;
|
|
28
|
-
|
|
28
|
+
if (task.status !== "archived")
|
|
29
|
+
openInputs += taskOpenInputs;
|
|
29
30
|
const taskOpen = reader.listInputRequests(task.id)
|
|
30
31
|
.filter((request) => request.status === "open");
|
|
31
|
-
for (const request of taskOpen) {
|
|
32
|
+
for (const request of task.status === "archived" ? [] : taskOpen) {
|
|
32
33
|
attention.push({ taskId: task.id, taskTitle: task.title, request });
|
|
33
34
|
}
|
|
34
35
|
const events = reader.listEvents?.(task.id) ?? [];
|
|
35
|
-
const needsAttentionCount = reader.listRuns(task.id)
|
|
36
|
+
const needsAttentionCount = task.status === "archived" ? 0 : reader.listRuns(task.id)
|
|
36
37
|
.filter((run) => run.status === "active" && isRoleRunStalled(events, run.id))
|
|
37
38
|
.length;
|
|
38
39
|
const execution = buildTaskExecutionProjection(reader, task.id, task, now);
|