@zq-silk/yui 2.0.0 → 2.1.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.
- package/dist/cli/commandCatalog.js +294 -286
- package/dist/cli/commandDiscovery.js +69 -0
- package/dist/cli/completion.js +22 -227
- package/dist/cli/dynamicCompletion.js +12 -6
- package/dist/cli/invocationAuthority.js +2 -2
- package/dist/cli/invocationRouter.js +18 -7
- package/dist/cli.js +71 -23
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/globalRoleCommands.js +2 -1
- package/dist/commands/taskCommands.js +11 -0
- package/dist/context/taskContext.js +6 -2
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +28 -1
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/nativeAgent/agent.js +173 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +29 -0
- package/dist/nativeAgent/index.js +3 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/textTools.js +156 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/taskAuthority.js +5 -10
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +555 -863
- package/dist/web/assets/client/components.js +323 -897
- package/dist/web/assets/client/detail.js +333 -0
- package/dist/web/assets/client/dock.js +225 -0
- package/dist/web/assets/client/dom.js +55 -3
- package/dist/web/assets/client/evidence.js +172 -0
- package/dist/web/assets/client/format.js +25 -14
- package/dist/web/assets/client/forms.js +230 -0
- package/dist/web/assets/client/i18n.js +1020 -791
- package/dist/web/assets/client/overview.js +115 -0
- package/dist/web/assets/client/records.js +63 -0
- package/dist/web/assets/client/sections.js +313 -0
- package/dist/web/assets/client/sidebar.js +117 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +124 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +167 -0
- package/dist/web/assets/styles/layout.js +49 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +33 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +289 -0
- package/dist/web/webServer.js +2 -1
- package/docs/release-workflow.md +23 -0
- package/docs/release-workflow.zh-CN.md +16 -0
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +26 -0
- package/dist/web/assets/client/taskSummary.js +0 -350
- package/dist/web/assets/client/taskSurface.js +0 -616
- package/dist/web/assets/client/view.js +0 -608
- package/dist/web/assets/styles/cards.js +0 -247
- package/dist/web/assets/styles/widgets.js +0 -168
|
@@ -1,591 +1,287 @@
|
|
|
1
|
-
export const APP_SCRIPT = `
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export const APP_SCRIPT = String.raw `
|
|
2
|
+
// Application controller: view state, URL routing, the refresh loop, the
|
|
3
|
+
// sidebar | center | dock layout, keyboard shortcuts and dialogs. Everything
|
|
4
|
+
// kept here is view state; Task facts always come from the loopback API.
|
|
4
5
|
import { createI18n } from "/assets/js/i18n.js";
|
|
5
6
|
import { createThemeController } from "/assets/js/theme.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
mainCol: document.querySelector(".main-col"),
|
|
30
|
-
topbar: document.querySelector(".topbar"),
|
|
31
|
-
detailBack: document.querySelector("#detail-back"),
|
|
32
|
-
detailTabs: document.querySelector("#detail-tabs"),
|
|
33
|
-
conversationToggle: document.querySelector("#conversation-toggle"),
|
|
34
|
-
conversationPanel: document.querySelector("#conversation-panel"),
|
|
35
|
-
conversationContent: document.querySelector("#conversation-content"),
|
|
36
|
-
conversationClose: document.querySelector("#conversation-close"),
|
|
37
|
-
conversationSwap: document.querySelector("#conversation-swap"),
|
|
38
|
-
paneDivider: document.querySelector("#pane-divider"),
|
|
39
|
-
pageTitle: document.querySelector("#page-title"),
|
|
40
|
-
toast: document.querySelector("#toast"),
|
|
41
|
-
lastSync: document.querySelector("#last-sync"),
|
|
42
|
-
terminalPanel: document.querySelector("#terminal-panel"),
|
|
43
|
-
terminalHost: document.querySelector("#terminal-host"),
|
|
44
|
-
terminalTitle: document.querySelector("#terminal-title"),
|
|
45
|
-
terminalState: document.querySelector("#terminal-state span"),
|
|
46
|
-
terminalClose: document.querySelector("#terminal-close")
|
|
7
|
+
import { api, releaseMutation } from "/assets/js/api.js";
|
|
8
|
+
import { formatClock } from "/assets/js/format.js";
|
|
9
|
+
import { h } from "/assets/js/dom.js";
|
|
10
|
+
import { renderAttentionBar, renderFilters, renderTasks, STATUS_FILTERS, ATTENTION_KINDS } from "/assets/js/sidebar.js";
|
|
11
|
+
import { renderOverview } from "/assets/js/overview.js";
|
|
12
|
+
import { renderTaskDetail, selectTab, updateObservation, TABS } from "/assets/js/detail.js";
|
|
13
|
+
import { renderDiscussion, discussionHasUnsent, createTerminalController, sameTarget } from "/assets/js/dock.js";
|
|
14
|
+
import { entriesOf } from "/assets/js/records.js";
|
|
15
|
+
|
|
16
|
+
const $ = function (selector) { return document.querySelector(selector); };
|
|
17
|
+
const el = {
|
|
18
|
+
sidebar: $(".sidebar"), center: $("#center"), detail: $("#detail"),
|
|
19
|
+
search: $("#search"), filters: $("#status-filters"), attention: $("#attention-bar"), tasks: $("#task-list"),
|
|
20
|
+
catalogNext: $("#catalog-next"), catalogReset: $("#catalog-reset"), catalogCount: $("#catalog-count"),
|
|
21
|
+
catalogAttentionReset: $("#catalog-attention-reset"),
|
|
22
|
+
refresh: $("#refresh"), sync: $("#sync-state"), lastSync: $("#last-sync"), toast: $("#toast"),
|
|
23
|
+
operator: $("#operator-terminal"), settingsOpen: $("#settings-open"), settings: $("#settings-dialog"),
|
|
24
|
+
locale: $("#locale-select"), themeOptions: $("#theme-options"),
|
|
25
|
+
dock: $("#dock"), divider: $("#dock-divider"), dockSwap: $("#dock-swap"), dockClose: $("#dock-close"),
|
|
26
|
+
dockTabDiscussion: $("#dock-tab-discussion"), dockTabSession: $("#dock-tab-session"),
|
|
27
|
+
discussion: $("#dock-discussion"), session: $("#dock-session"),
|
|
28
|
+
terminalHost: $("#terminal-host"), terminalEmpty: $("#terminal-empty"), terminalState: $("#terminal-state"),
|
|
29
|
+
sessionTargets: $("#session-targets"), terminalCli: $("#terminal-cli")
|
|
47
30
|
};
|
|
48
31
|
|
|
49
|
-
const token = document.querySelector('meta[name="yui-web-token"]').content;
|
|
50
32
|
const state = {
|
|
51
|
-
tasks: [],
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
catalogQuery: "",
|
|
57
|
-
sessionOverview: null,
|
|
58
|
-
sessionLoading: false,
|
|
59
|
-
sessionError: null,
|
|
60
|
-
catalogAll: false,
|
|
61
|
-
catalogTotal: 0,
|
|
62
|
-
catalogCursor: null,
|
|
63
|
-
nextCursor: null,
|
|
64
|
-
attentionFilter: null,
|
|
65
|
-
generatedAt: null,
|
|
66
|
-
filter: "all",
|
|
67
|
-
query: "",
|
|
68
|
-
selected: null,
|
|
69
|
-
detail: null,
|
|
70
|
-
detailKey: null
|
|
33
|
+
tasks: [], counts: null, attention: [], catalogAttention: null, catalogScope: null, catalogQuery: "",
|
|
34
|
+
catalogAll: false, catalogTotal: 0, catalogCursor: null, nextCursor: null,
|
|
35
|
+
sessionOverview: null, sessionLoading: false, sessionError: null,
|
|
36
|
+
attentionFilter: null, generatedAt: null, filter: "all", query: "",
|
|
37
|
+
selected: null, detail: null, detailKey: null, activeTab: "overview"
|
|
71
38
|
};
|
|
72
|
-
const VALID_FILTERS = ["all", "active", "draft", "completed", "cancelled", "archived"];
|
|
73
|
-
const VALID_ATTENTION = ["openInputs", "pendingOperations", "unknownOperations", "executionSignals"];
|
|
74
|
-
let terminalSession = null;
|
|
75
|
-
let terminalStateKey = "terminal.closed";
|
|
76
|
-
const submittedRequests = new Set();
|
|
77
|
-
let conversationVisible = true;
|
|
78
|
-
let paneWidth = Number(localStorage.getItem("yui.conversation.width")) || 400;
|
|
79
|
-
const paneSide = localStorage.getItem("yui.conversation.side") === "left" ? "left" : "right";
|
|
80
|
-
document.body.classList.toggle("pane-left", paneSide === "left");
|
|
81
|
-
|
|
82
|
-
const i18n = createI18n(elements.locale);
|
|
83
|
-
createThemeController(elements.theme);
|
|
84
39
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
// query string so the browser back/forward buttons work and the page can be
|
|
88
|
-
// deep-linked / refreshed without losing context.
|
|
89
|
-
function readQuery() {
|
|
90
|
-
return new URLSearchParams(window.location.search);
|
|
40
|
+
function readPreference(key, fallback) {
|
|
41
|
+
try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
|
|
91
42
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const entries = Array.from(params.entries()).filter(function (entry) {
|
|
95
|
-
return entry[1] !== "" && entry[1] !== null && entry[1] !== undefined;
|
|
96
|
-
});
|
|
97
|
-
if (!entries.length) return "";
|
|
98
|
-
return "?" + entries.map(function (entry) {
|
|
99
|
-
return encodeURIComponent(entry[0]) + "=" + encodeURIComponent(entry[1]);
|
|
100
|
-
}).join("&");
|
|
43
|
+
function writePreference(key, value) {
|
|
44
|
+
try { localStorage.setItem(key, value); } catch {}
|
|
101
45
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
46
|
+
const dock = {
|
|
47
|
+
open: readPreference("yui.dock.open", "true") !== "false",
|
|
48
|
+
mode: "discussion",
|
|
49
|
+
center: readPreference("yui.dock.side", "end") === "center",
|
|
50
|
+
width: Number(readPreference("yui.dock.width", "440")) || 440,
|
|
51
|
+
// On narrow screens the dock is a full-screen sheet opened explicitly per
|
|
52
|
+
// Task, never restored from the desktop preference.
|
|
53
|
+
sheet: false
|
|
54
|
+
};
|
|
55
|
+
const narrow = window.matchMedia("(max-width: 900px)");
|
|
56
|
+
|
|
57
|
+
const i18n = createI18n(el.locale);
|
|
58
|
+
const t = i18n.t;
|
|
59
|
+
const locale = i18n.getLocale;
|
|
60
|
+
const theme = createThemeController(el.themeOptions, t);
|
|
61
|
+
const terminal = createTerminalController({
|
|
62
|
+
host: el.terminalHost, empty: el.terminalEmpty, state: el.terminalState, targets: el.sessionTargets, cli: el.terminalCli
|
|
63
|
+
}, t, locale, showToast);
|
|
64
|
+
theme.subscribe(function () { terminal.retheme(); });
|
|
65
|
+
|
|
66
|
+
let toastTimer = null;
|
|
67
|
+
function showToast(message) {
|
|
68
|
+
el.toast.textContent = message;
|
|
69
|
+
el.toast.classList.add("show");
|
|
70
|
+
window.clearTimeout(toastTimer);
|
|
71
|
+
toastTimer = window.setTimeout(function () { el.toast.classList.remove("show"); }, 3200);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- URL / history ---------------------------------------------------------------
|
|
75
|
+
// The selected Task, section, status filter, search and attention filter live
|
|
76
|
+
// in the query string so back/forward, refresh and deep links keep context.
|
|
77
|
+
function readQuery() { return new URLSearchParams(window.location.search); }
|
|
78
|
+
function applyUrl(params, replace) {
|
|
79
|
+
const entries = Array.from(params.entries()).filter(function (entry) { return entry[1] !== ""; });
|
|
80
|
+
const search = entries.length ? "?" + entries.map(function (entry) {
|
|
81
|
+
return encodeURIComponent(entry[0]) + "=" + encodeURIComponent(entry[1]);
|
|
82
|
+
}).join("&") : "";
|
|
106
83
|
const url = window.location.pathname + search + window.location.hash;
|
|
107
|
-
|
|
108
|
-
history.replaceState({ task: params.get("task") || null }, "", url);
|
|
109
|
-
} else {
|
|
110
|
-
history.pushState({ task: params.get("task") || null }, "", url);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function urlTaskId() {
|
|
115
|
-
return readQuery().get("task") || null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// The visible detail section is part of the URL (?task=X§ion=exec) so a
|
|
119
|
-
// refresh or a shared link lands on the same section. Writes always replace —
|
|
120
|
-
// section moves are not history entries.
|
|
121
|
-
function setSectionParam(targetId) {
|
|
122
|
-
if (!state.detail) return;
|
|
123
|
-
const params = readQuery();
|
|
124
|
-
const section = String(targetId).replace(/^detail-/, "");
|
|
125
|
-
if (params.get("section") === section) return;
|
|
126
|
-
params.set("section", section);
|
|
127
|
-
applyUrl(params, { replace: true });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
let sectionParamTimer = null;
|
|
131
|
-
function queueSectionParamSync(targetId) {
|
|
132
|
-
if (sectionParamTimer !== null) window.clearTimeout(sectionParamTimer);
|
|
133
|
-
sectionParamTimer = window.setTimeout(function () {
|
|
134
|
-
sectionParamTimer = null;
|
|
135
|
-
setSectionParam(targetId);
|
|
136
|
-
}, 250);
|
|
84
|
+
history[replace ? "replaceState" : "pushState"]({ task: params.get("task") || null }, "", url);
|
|
137
85
|
}
|
|
138
|
-
|
|
139
|
-
function
|
|
86
|
+
function urlTaskId() { return readQuery().get("task") || null; }
|
|
87
|
+
function syncUrl(replace) {
|
|
140
88
|
const params = readQuery();
|
|
141
89
|
if (state.selected) params.set("task", state.selected);
|
|
142
|
-
else {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (state.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
applyUrl(params, options);
|
|
90
|
+
else { params.delete("task"); params.delete("section"); }
|
|
91
|
+
if (state.filter !== "all") params.set("filter", state.filter); else params.delete("filter");
|
|
92
|
+
if (state.query) params.set("q", state.query); else params.delete("q");
|
|
93
|
+
if (state.attentionFilter) params.set("attention", state.attentionFilter); else params.delete("attention");
|
|
94
|
+
if (state.catalogAll) params.set("all", "true"); else params.delete("all");
|
|
95
|
+
applyUrl(params, replace);
|
|
96
|
+
}
|
|
97
|
+
function setSectionParam(section) {
|
|
98
|
+
const params = readQuery();
|
|
99
|
+
if (!state.selected || params.get("section") === section) return;
|
|
100
|
+
if (section === "overview") params.delete("section"); else params.set("section", section);
|
|
101
|
+
applyUrl(params, true);
|
|
155
102
|
}
|
|
156
103
|
|
|
157
|
-
|
|
104
|
+
// --- Detail rendering -------------------------------------------------------------
|
|
105
|
+
function detailContext() {
|
|
158
106
|
return {
|
|
159
|
-
|
|
107
|
+
activeTab: state.activeTab,
|
|
108
|
+
onTab: switchTab,
|
|
109
|
+
onBack: clearSelection,
|
|
110
|
+
showDock: toggleDock,
|
|
111
|
+
openSession: function (roleName) { openSession({ scope: "task", taskId: state.selected, roleName: roleName }); },
|
|
160
112
|
answerInput: answerInput,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
evidence:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
controlInput: (taskId, payload, requestId) => submitMutation(taskId + "/control/" + requestId,
|
|
171
|
-
"/api/tasks/" + encodeURIComponent(taskId) + "/control", payload),
|
|
172
|
-
updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
|
|
173
|
-
"/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
|
|
174
|
-
panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
|
|
175
|
-
{ signal: AbortSignal.timeout(3000) }),
|
|
176
|
-
readPanel: (taskId, ref, input) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels", {
|
|
177
|
-
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ref, input }),
|
|
178
|
-
signal: AbortSignal.timeout(3000)
|
|
179
|
-
})
|
|
113
|
+
inspect: api.inspect,
|
|
114
|
+
artifacts: api.artifacts,
|
|
115
|
+
readArtifact: api.artifact,
|
|
116
|
+
evidence: api.evidence,
|
|
117
|
+
panels: api.panels,
|
|
118
|
+
readPanel: api.readPanel,
|
|
119
|
+
control: api.control,
|
|
120
|
+
updateTask: api.updateTask,
|
|
121
|
+
afterWrite: function () { refreshDashboard({ quiet: true }); }
|
|
180
122
|
};
|
|
181
123
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
:
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function setDetailActive(active) {
|
|
190
|
-
document.body.classList.toggle("detail-active", active);
|
|
191
|
-
elements.detailBack.hidden = !active;
|
|
192
|
-
if (elements.detailTabs) elements.detailTabs.hidden = !active;
|
|
193
|
-
if (elements.pageTitle) {
|
|
194
|
-
if (active) {
|
|
195
|
-
elements.pageTitle.textContent = state.detail
|
|
196
|
-
? state.detail.task.title
|
|
197
|
-
: (state.tasks.find(function (task) { return task.id === state.selected; }) || { title: "…" }).title;
|
|
198
|
-
elements.pageTitle.dataset.i18n = "";
|
|
199
|
-
} else {
|
|
200
|
-
elements.pageTitle.textContent = i18n.t("page.title");
|
|
201
|
-
elements.pageTitle.dataset.i18n = "page.title";
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
updateConversationLayout();
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function maxPaneWidth() {
|
|
208
|
-
return Math.max(280, Math.min(640,
|
|
209
|
-
window.innerWidth - elements.tasks.closest(".sidebar").offsetWidth - 460 - 8));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function setPaneWidth(width, persist, keepPreference) {
|
|
213
|
-
const effectiveWidth = Math.max(280, Math.min(maxPaneWidth(), width));
|
|
214
|
-
if (!keepPreference) paneWidth = effectiveWidth;
|
|
215
|
-
document.documentElement.style.setProperty("--pane-w", effectiveWidth + "px");
|
|
216
|
-
elements.paneDivider.setAttribute("aria-valuemax", String(maxPaneWidth()));
|
|
217
|
-
elements.paneDivider.setAttribute("aria-valuenow", String(effectiveWidth));
|
|
218
|
-
if (persist) localStorage.setItem("yui.conversation.width", String(effectiveWidth));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function updateConversationLayout() {
|
|
222
|
-
const visible = !!state.selected && conversationVisible && !terminalPanelOpen();
|
|
223
|
-
document.body.classList.toggle("conversation-active", visible);
|
|
224
|
-
elements.conversationPanel.hidden = !visible;
|
|
225
|
-
elements.paneDivider.hidden = !visible;
|
|
226
|
-
elements.conversationToggle.hidden = !state.selected || terminalPanelOpen();
|
|
227
|
-
elements.conversationToggle.setAttribute("aria-expanded", String(visible));
|
|
228
|
-
elements.conversationToggle.textContent = i18n.t(visible ? "conversation.hide" : "conversation.show");
|
|
229
|
-
elements.conversationToggle.setAttribute("aria-label", elements.conversationToggle.textContent);
|
|
230
|
-
setPaneWidth(paneWidth, false, true);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function showOverview() {
|
|
234
|
-
renderOverview(elements.detail, state, i18n.t, i18n.getLocale(), selectTask);
|
|
235
|
-
elements.mainCol.scrollTop = 0;
|
|
124
|
+
function dockActions() {
|
|
125
|
+
return {
|
|
126
|
+
sendMessage: api.sendMessage,
|
|
127
|
+
inspect: api.inspect,
|
|
128
|
+
afterWrite: function () { refreshDashboard({ quiet: true }); }
|
|
129
|
+
};
|
|
236
130
|
}
|
|
237
131
|
|
|
238
|
-
// Fingerprint of the rendered
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
function detailKeyOf(detail) {
|
|
244
|
-
const text = JSON.stringify(detail);
|
|
132
|
+
// Fingerprint of the rendered payload. Quiet polling skips identical reads so
|
|
133
|
+
// local view state (open blocks, list pages, drafts) survives.
|
|
134
|
+
function fingerprint(value) {
|
|
135
|
+
const text = JSON.stringify(value);
|
|
245
136
|
let hash = 0;
|
|
246
|
-
for (let i = 0; i < text.length; i += 1)
|
|
247
|
-
hash = (hash * 31 + text.charCodeAt(i)) | 0;
|
|
248
|
-
}
|
|
137
|
+
for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) | 0;
|
|
249
138
|
return text.length + ":" + hash;
|
|
250
139
|
}
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
detail.runtime?.execution?.attention, detail.runtime?.execution?.blockers,
|
|
262
|
-
detail.runtime?.execution?.next
|
|
263
|
-
]) + "|" + roles.map(function (role) {
|
|
264
|
-
const session = role.runtimeSession;
|
|
265
|
-
return role.name + ":" + (session
|
|
266
|
-
? session.nativeSessionId + "/" + session.status
|
|
267
|
-
: "-");
|
|
140
|
+
// Only the observed identity participates: observation timestamps advance on
|
|
141
|
+
// every poll and would otherwise re-render continuously.
|
|
142
|
+
function runtimeSignature(detail) {
|
|
143
|
+
const runtime = detail.runtime;
|
|
144
|
+
return detail.runtimeStatus + "|" + JSON.stringify(runtime ? [
|
|
145
|
+
runtime.sessions && runtime.sessions.sessions, runtime.remoteDelivery,
|
|
146
|
+
runtime.execution && runtime.execution.attention, runtime.execution && runtime.execution.blockers,
|
|
147
|
+
runtime.execution && runtime.execution.next, runtime.execution && runtime.execution.status
|
|
148
|
+
] : null) + "|" + ((runtime && runtime.roles) || []).map(function (role) {
|
|
149
|
+
return role.name + ":" + (role.runtimeSession ? role.runtimeSession.nativeSessionId + "/" + role.runtimeSession.status : "-") + ":" + role.status;
|
|
268
150
|
}).join(",");
|
|
269
151
|
}
|
|
270
152
|
|
|
153
|
+
let renderedDetailKey = null;
|
|
154
|
+
let renderedDiscussionKey = null;
|
|
155
|
+
function centerIsBusy() {
|
|
156
|
+
const active = document.activeElement;
|
|
157
|
+
const editing = active && (["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName) || active.isContentEditable);
|
|
158
|
+
return el.detail.dataset.taskId === (state.detail && state.detail.task.id)
|
|
159
|
+
&& (el.detail.querySelector('[data-unsent="true"]') || el.detail.querySelector('[data-reading="true"]')
|
|
160
|
+
|| editing && el.center.contains(active));
|
|
161
|
+
}
|
|
271
162
|
function renderCurrentDetail(force) {
|
|
272
|
-
if (state.detail) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (elements.detail.dataset.taskId === state.detail.task.id
|
|
276
|
-
&& (elements.detail.querySelector('[data-unsent="true"]')
|
|
277
|
-
|| elements.detail.querySelector('[data-reading="true"]')
|
|
278
|
-
|| elements.detail.contains(document.activeElement)
|
|
279
|
-
|| elements.conversationContent.querySelector('[data-unsent="true"]')
|
|
280
|
-
|| elements.conversationContent.contains(document.activeElement))) return;
|
|
281
|
-
const key = i18n.getLocale() + "|" + state.detailKey
|
|
282
|
-
+ "|" + runtimeSignatureOf(state.detail);
|
|
283
|
-
if (!force && key === renderedDetailKey) return;
|
|
284
|
-
const openSections = Array.from(elements.detail.querySelectorAll("details[data-view-key][open]"))
|
|
285
|
-
.map(element => element.dataset.viewKey);
|
|
286
|
-
const oldFeed = elements.conversationContent.querySelector(".conversation-feed");
|
|
287
|
-
const feedScroll = oldFeed ? oldFeed.scrollTop : 0;
|
|
288
|
-
const atFeedEnd = oldFeed && oldFeed.scrollHeight - oldFeed.clientHeight - feedScroll < 32;
|
|
289
|
-
renderTaskDetail(
|
|
290
|
-
elements.detail,
|
|
291
|
-
state.detail,
|
|
292
|
-
i18n.t,
|
|
293
|
-
i18n.getLocale(),
|
|
294
|
-
detailActions()
|
|
295
|
-
);
|
|
296
|
-
elements.detail.querySelectorAll("details[data-view-key]").forEach(element => {
|
|
297
|
-
element.open = openSections.includes(element.dataset.viewKey);
|
|
298
|
-
});
|
|
299
|
-
const newFeed = elements.conversationContent.querySelector(".conversation-feed");
|
|
300
|
-
if (newFeed) newFeed.scrollTop = atFeedEnd ? newFeed.scrollHeight : feedScroll;
|
|
301
|
-
renderedDetailKey = key;
|
|
302
|
-
} else if (!state.selected) {
|
|
303
|
-
renderedDetailKey = null;
|
|
304
|
-
showOverview();
|
|
163
|
+
if (!state.detail) {
|
|
164
|
+
if (!state.selected) { renderedDetailKey = null; showOverview(); }
|
|
165
|
+
return;
|
|
305
166
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
updateMetrics();
|
|
330
|
-
updateStickyOffsets();
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function updateStickyOffsets() {
|
|
334
|
-
const root = document.documentElement;
|
|
335
|
-
if (elements.topbar) {
|
|
336
|
-
root.style.setProperty("--topbar-h", elements.topbar.offsetHeight + "px");
|
|
167
|
+
renderDock();
|
|
168
|
+
// Polling must not replace an unsent draft or an in-flight form, including
|
|
169
|
+
// after focus moved. This is view state, never Task state.
|
|
170
|
+
if (centerIsBusy()) return;
|
|
171
|
+
const key = locale() + "|" + state.detailKey + "|" + runtimeSignature(state.detail);
|
|
172
|
+
if (!force && key === renderedDetailKey) return;
|
|
173
|
+
const openSections = Array.from(el.detail.querySelectorAll("details[data-view-key][open]")).map(function (d) { return d.dataset.viewKey; });
|
|
174
|
+
const focusedTab = el.detail.querySelector(".tab:focus");
|
|
175
|
+
const focusedTabId = focusedTab && focusedTab.id;
|
|
176
|
+
const scroll = el.detail.dataset.taskId === state.detail.task.id ? el.center.scrollTop : 0;
|
|
177
|
+
renderTaskDetail(el.detail, state.detail, t, locale(), detailContext());
|
|
178
|
+
el.detail.querySelectorAll("details[data-view-key]").forEach(function (d) { d.open = openSections.includes(d.dataset.viewKey); });
|
|
179
|
+
if (focusedTabId) el.detail.querySelector("#" + CSS.escape(focusedTabId)).focus({ preventScroll: true });
|
|
180
|
+
el.center.scrollTop = scroll;
|
|
181
|
+
renderedDetailKey = key;
|
|
182
|
+
syncDockButtons();
|
|
183
|
+
}
|
|
184
|
+
function renderDock() {
|
|
185
|
+
if (!state.detail) return;
|
|
186
|
+
const key = locale() + "|" + state.detail.task.id + "|" + state.detailKey + "|" + state.detail.task.status;
|
|
187
|
+
if (key !== renderedDiscussionKey || el.discussion.dataset.taskId !== state.detail.task.id) {
|
|
188
|
+
renderDiscussion(el.discussion, state.detail, t, locale(), dockActions());
|
|
189
|
+
renderedDiscussionKey = key;
|
|
337
190
|
}
|
|
338
|
-
|
|
339
|
-
|
|
191
|
+
updateSessionTargets();
|
|
192
|
+
}
|
|
193
|
+
function updateSessionTargets() {
|
|
194
|
+
const targets = [];
|
|
195
|
+
if (state.detail) {
|
|
196
|
+
const runtimeRoles = (state.detail.runtime && state.detail.runtime.roles) || [];
|
|
197
|
+
entriesOf(state.detail.core, "role").forEach(function (entry) {
|
|
198
|
+
const runtimeRole = runtimeRoles.find(function (role) { return role.name === entry.ref.refId; });
|
|
199
|
+
targets.push({ scope: "task", taskId: state.detail.task.id, roleName: entry.ref.refId,
|
|
200
|
+
live: !!(runtimeRole && runtimeRole.runtimeSession && runtimeRole.runtimeSession.nativeSessionId) });
|
|
201
|
+
});
|
|
340
202
|
}
|
|
203
|
+
targets.push({ scope: "global", roleName: "operator" });
|
|
204
|
+
const current = terminal.current();
|
|
205
|
+
if (current && !targets.some(function (target) { return sameTarget(target, current); })) targets.unshift(current);
|
|
206
|
+
terminal.setTargets(targets);
|
|
341
207
|
}
|
|
342
208
|
|
|
343
|
-
function
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// so "current" means the last section whose top crossed that stacked offset.
|
|
351
|
-
const stickyH = (elements.topbar ? elements.topbar.offsetHeight : 0)
|
|
352
|
-
+ (elements.detailTabs.hidden ? 0 : elements.detailTabs.offsetHeight);
|
|
353
|
-
const threshold = stickyH + 8;
|
|
354
|
-
let activeId = tabs[0].dataset.target;
|
|
355
|
-
let bestTop = -Infinity;
|
|
356
|
-
tabs.forEach(function (tab) {
|
|
357
|
-
const id = tab.dataset.target;
|
|
358
|
-
if (!id) return;
|
|
359
|
-
const el = root.querySelector("#" + id);
|
|
360
|
-
if (!el) return;
|
|
361
|
-
const top = el.getBoundingClientRect().top - rootTop;
|
|
362
|
-
if (top <= threshold && top > bestTop) {
|
|
363
|
-
bestTop = top;
|
|
364
|
-
activeId = id;
|
|
365
|
-
}
|
|
366
|
-
});
|
|
367
|
-
tabs.forEach(function (tab) {
|
|
368
|
-
tab.classList.toggle("is-active", tab.dataset.target === activeId);
|
|
209
|
+
function showOverview() {
|
|
210
|
+
delete el.detail.dataset.taskId;
|
|
211
|
+
renderOverview(el.detail, state, t, locale(), {
|
|
212
|
+
select: selectTask,
|
|
213
|
+
filterAttention: pickAttention,
|
|
214
|
+
readSessions: readPageSessions,
|
|
215
|
+
openOperator: openOperator
|
|
369
216
|
});
|
|
370
|
-
if (state.detail) queueSectionParamSync(activeId);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function syncTabHighlight() {
|
|
374
|
-
// Compute the active tab from the current scroll position. The module-level
|
|
375
|
-
// scroll listener keeps it in sync afterwards.
|
|
376
|
-
updateActiveTabFromScroll();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function showToast(message) {
|
|
380
|
-
elements.toast.textContent = message;
|
|
381
|
-
elements.toast.classList.add("show");
|
|
382
|
-
window.setTimeout(function () { elements.toast.classList.remove("show"); }, 3200);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function clearSelection() {
|
|
386
|
-
if (!canLeaveDetail()) return;
|
|
387
|
-
state.selected = null;
|
|
388
|
-
state.detail = null;
|
|
389
|
-
setDetailActive(false);
|
|
390
|
-
elements.conversationContent.replaceChildren();
|
|
391
|
-
syncUrlFromState({ replace: !urlTaskId() });
|
|
392
|
-
showOverview();
|
|
393
|
-
const savedTaskScroll = elements.tasks.scrollTop;
|
|
394
|
-
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
395
|
-
elements.tasks.scrollTop = savedTaskScroll;
|
|
396
217
|
}
|
|
397
218
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
...(options && options.headers ? options.headers : {})
|
|
405
|
-
}
|
|
406
|
-
});
|
|
407
|
-
if (!response.ok) {
|
|
408
|
-
let message = "HTTP " + response.status;
|
|
409
|
-
let disposition = "unknown";
|
|
410
|
-
try {
|
|
411
|
-
const body = await response.json();
|
|
412
|
-
if (body && body.error) message = body.error;
|
|
413
|
-
if (body && body.disposition === "not-submitted") disposition = "not-submitted";
|
|
414
|
-
} catch {}
|
|
415
|
-
throw Object.assign(new Error(message), { disposition });
|
|
416
|
-
}
|
|
417
|
-
return response.json();
|
|
219
|
+
function switchTab(tab, options) {
|
|
220
|
+
if (!TABS.includes(tab)) return;
|
|
221
|
+
state.activeTab = tab;
|
|
222
|
+
selectTab(el.detail, tab);
|
|
223
|
+
if (!(options && options.keepScroll)) el.center.scrollTop = 0;
|
|
224
|
+
setSectionParam(tab);
|
|
418
225
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
} catch (error) {
|
|
430
|
-
if (error.disposition === "not-submitted") submittedRequests.delete(key);
|
|
431
|
-
throw error;
|
|
432
|
-
}
|
|
226
|
+
// A section may name a tab or any anchored block inside one (for example
|
|
227
|
+
// "reviews" → the Work tab, scrolled to #detail-reviews).
|
|
228
|
+
function applySection(section) {
|
|
229
|
+
if (!section || TABS.includes(section)) { switchTab(section || "overview"); return; }
|
|
230
|
+
const anchor = el.detail.querySelector("#detail-" + CSS.escape(section));
|
|
231
|
+
const panel = anchor && anchor.closest(".tab-panel");
|
|
232
|
+
if (!panel) { switchTab("overview"); return; }
|
|
233
|
+
state.activeTab = panel.id.replace(/^panel-/, "");
|
|
234
|
+
selectTab(el.detail, state.activeTab);
|
|
235
|
+
anchor.scrollIntoView({ block: "start" });
|
|
433
236
|
}
|
|
434
237
|
|
|
238
|
+
// --- Loading ----------------------------------------------------------------------
|
|
435
239
|
async function loadTaskDetail(taskId, navigate) {
|
|
436
|
-
// navigate marks an explicit user navigation (task selection, deep link,
|
|
437
|
-
// back/forward). Only a navigation may move the viewport; the 5s background
|
|
438
|
-
// refresh (navigate=false) preserves the current scroll, so a quiet tick
|
|
439
|
-
// never yanks the page back to the URL section.
|
|
440
240
|
if (navigate) {
|
|
441
|
-
|
|
442
|
-
|
|
241
|
+
el.detail.replaceChildren(h("div.page", null, h("div.loading-block", null, h("span.spinner"), h("span", null, t("loading.detail")))));
|
|
242
|
+
delete el.detail.dataset.taskId;
|
|
243
|
+
el.center.scrollTop = 0;
|
|
443
244
|
}
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
// Rendering consumes the current snapshot, not event pages. Reconnect uses
|
|
447
|
-
// this same read; the independent delta API retains its fixed-bound contract.
|
|
448
|
-
const core = await requestJson(base + "/context");
|
|
245
|
+
// Rendering consumes the current Context snapshot, not event pages.
|
|
246
|
+
const core = await api.context(taskId);
|
|
449
247
|
const taskEntry = core.records.find(function (entry) { return entry.ref.store === "task"; });
|
|
450
248
|
if (!taskEntry) throw new Error("Task reference unavailable.");
|
|
451
|
-
const task = taskEntry.omitted ? (await
|
|
249
|
+
const task = taskEntry.omitted ? (await api.inspect(taskId, taskEntry.ref)).value : taskEntry.value;
|
|
250
|
+
if (state.selected !== taskId) return;
|
|
452
251
|
const previous = state.detail && state.detail.task.id === taskId ? state.detail : null;
|
|
453
252
|
const detail = {
|
|
454
|
-
task, core,
|
|
253
|
+
task: task, core: core,
|
|
455
254
|
viewState: previous ? previous.viewState : {},
|
|
456
|
-
runtime: previous
|
|
255
|
+
runtime: previous ? previous.runtime : null,
|
|
457
256
|
runtimeStatus: previous ? previous.runtimeStatus : "waiting",
|
|
458
257
|
runtimeObservedAt: previous ? previous.runtimeObservedAt : new Date().toISOString()
|
|
459
258
|
};
|
|
460
|
-
if (state.selected !== taskId) return;
|
|
461
259
|
state.detail = detail;
|
|
462
|
-
state.detailKey =
|
|
463
|
-
renderCurrentDetail();
|
|
464
|
-
|
|
465
|
-
|
|
260
|
+
state.detailKey = fingerprint(core);
|
|
261
|
+
renderCurrentDetail(navigate);
|
|
262
|
+
if (navigate) applySection(readQuery().get("section"));
|
|
263
|
+
// The optional observation never participates in core readiness.
|
|
264
|
+
api.observation(taskId).then(function (runtime) {
|
|
466
265
|
if (state.detail !== detail) return;
|
|
467
266
|
detail.runtime = runtime;
|
|
468
267
|
detail.runtimeStatus = "available";
|
|
469
268
|
detail.runtimeObservedAt = new Date().toISOString();
|
|
470
|
-
|
|
269
|
+
observationArrived(detail);
|
|
471
270
|
}).catch(function () {
|
|
472
271
|
if (state.detail !== detail) return;
|
|
473
272
|
detail.runtime = null;
|
|
474
273
|
detail.runtimeStatus = "unavailable";
|
|
475
274
|
detail.runtimeObservedAt = new Date().toISOString();
|
|
476
|
-
|
|
275
|
+
observationArrived(detail);
|
|
477
276
|
});
|
|
478
|
-
// Reveal the tab bar before measuring/scroll so anchors land correctly.
|
|
479
|
-
setDetailActive(true);
|
|
480
|
-
updateStickyOffsets();
|
|
481
|
-
// Only an explicit navigation may reposition the viewport. On navigation a
|
|
482
|
-
// section in the URL wins over the top; a background refresh keeps the reader
|
|
483
|
-
// where they are so the page does not jump on every poll.
|
|
484
|
-
if (navigate) {
|
|
485
|
-
const section = readQuery().get("section");
|
|
486
|
-
const anchor = section ? elements.detail.querySelector("#detail-" + section) : null;
|
|
487
|
-
if (anchor) {
|
|
488
|
-
anchor.scrollIntoView({ block: "start" });
|
|
489
|
-
} else {
|
|
490
|
-
elements.mainCol.scrollTop = savedScrollTop;
|
|
491
|
-
}
|
|
492
|
-
} else {
|
|
493
|
-
elements.mainCol.scrollTop = savedScrollTop;
|
|
494
|
-
}
|
|
495
|
-
syncTabHighlight();
|
|
496
277
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const value = elements.detail.querySelector("[data-runtime-value]");
|
|
502
|
-
if (status) status.textContent = detail.runtimeStatus + " · " + detail.runtimeObservedAt;
|
|
503
|
-
if (value) value.textContent = detail.runtime
|
|
504
|
-
? JSON.stringify({ roles: detail.runtime.roles, runtimeHealth: detail.runtime.runtimeHealth }, null, 2)
|
|
505
|
-
: "";
|
|
506
|
-
// Patching the panel in place is not enough: the observation also feeds facts
|
|
507
|
-
// the detail rendered before it arrived. Re-render when the observed identity
|
|
508
|
-
// has actually changed — renderCurrentDetail still compares the render key,
|
|
509
|
-
// so an unchanged observation costs nothing and an unsent form is preserved.
|
|
278
|
+
function observationArrived(detail) {
|
|
279
|
+
if (el.detail.dataset.taskId === detail.task.id) updateObservation(el.detail, detail, t, locale(), detailContext());
|
|
280
|
+
// Patching slots is not enough when the observed identity changed facts the
|
|
281
|
+
// detail drew earlier; the render key decides whether a re-render is due.
|
|
510
282
|
if (state.detail === detail) renderCurrentDetail();
|
|
511
283
|
}
|
|
512
284
|
|
|
513
|
-
async function inspectRecord(taskId, ref) {
|
|
514
|
-
const query = new URLSearchParams({ store: ref.store, ref: ref.refId });
|
|
515
|
-
if (ref.digest) query.set("digest", ref.digest);
|
|
516
|
-
return requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/inspect?" + query);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
async function selectTask(taskId) {
|
|
520
|
-
// Re-selecting the already visible Task is not navigation. Keep the same
|
|
521
|
-
// draft and fixed artifact selection, just as an ordinary refresh does.
|
|
522
|
-
if (state.selected === taskId && state.detail) return;
|
|
523
|
-
if (state.selected !== taskId && !canLeaveDetail()) {
|
|
524
|
-
syncUrlFromState({ replace: true });
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
// Switching tasks drops the previous section; staying on the same task
|
|
528
|
-
// (URL-driven loads, popstate) keeps it.
|
|
529
|
-
if (urlTaskId() !== taskId) {
|
|
530
|
-
const params = readQuery();
|
|
531
|
-
params.delete("section");
|
|
532
|
-
applyUrl(params, { replace: true });
|
|
533
|
-
}
|
|
534
|
-
state.selected = taskId;
|
|
535
|
-
state.detail = null;
|
|
536
|
-
conversationVisible = true;
|
|
537
|
-
elements.conversationContent.replaceChildren();
|
|
538
|
-
renderedDetailKey = null;
|
|
539
|
-
// When the selection is driven by the URL (initial load or popstate), the
|
|
540
|
-
// URL already reflects the task id, so replace instead of pushing a
|
|
541
|
-
// duplicate history entry.
|
|
542
|
-
syncUrlFromState({ replace: state.selected === urlTaskId() });
|
|
543
|
-
setDetailActive(true);
|
|
544
|
-
const savedTaskScroll = elements.tasks.scrollTop;
|
|
545
|
-
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
546
|
-
elements.tasks.scrollTop = savedTaskScroll;
|
|
547
|
-
try {
|
|
548
|
-
await loadTaskDetail(taskId, true);
|
|
549
|
-
} catch {
|
|
550
|
-
if (state.selected !== taskId) return;
|
|
551
|
-
renderError(elements.detail, i18n.t("errors.detail"));
|
|
552
|
-
showToast(i18n.t("errors.detail"));
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function canLeaveDetail() {
|
|
557
|
-
return !(elements.detail.querySelector('[data-unsent="true"]')
|
|
558
|
-
|| elements.conversationContent.querySelector('[data-unsent="true"]')) || window.confirm(
|
|
559
|
-
i18n.getLocale().startsWith("zh") ? "此任务有未提交或结果尚不确定的输入。仍要离开?"
|
|
560
|
-
: "This Task has unsent input or an unresolved submission. Leave anyway?");
|
|
561
|
-
}
|
|
562
|
-
window.addEventListener("beforeunload", event => {
|
|
563
|
-
if (!elements.detail.querySelector('[data-unsent="true"]')
|
|
564
|
-
&& !elements.conversationContent.querySelector('[data-unsent="true"]')) return;
|
|
565
|
-
event.preventDefault();
|
|
566
|
-
event.returnValue = "";
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
async function answerInput(input, answer) {
|
|
570
|
-
if (!state.detail) return;
|
|
571
|
-
const taskId = state.detail.task.id;
|
|
572
|
-
const key = taskId + "/input/" + input.id;
|
|
573
|
-
try {
|
|
574
|
-
await submitMutation(key,
|
|
575
|
-
"/api/tasks/" + encodeURIComponent(taskId)
|
|
576
|
-
+ "/inputs/" + encodeURIComponent(input.id) + "/answer",
|
|
577
|
-
answer
|
|
578
|
-
);
|
|
579
|
-
showToast(i18n.t("input.answered"));
|
|
580
|
-
submittedRequests.delete(key);
|
|
581
|
-
await refreshDashboard({ quiet: true });
|
|
582
|
-
} catch (error) {
|
|
583
|
-
showToast(error.disposition === "not-submitted" ? error.message : i18n.getLocale().startsWith("zh")
|
|
584
|
-
? "回答结果未知;请刷新检查原问题,不要盲目重发。"
|
|
585
|
-
: "Answer outcome unknown; refresh the original question before resubmitting.");
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
285
|
let refreshing = false;
|
|
590
286
|
let catalogRequest = 0;
|
|
591
287
|
function resetCatalog() {
|
|
@@ -594,14 +290,15 @@ function resetCatalog() {
|
|
|
594
290
|
refreshDashboard();
|
|
595
291
|
}
|
|
596
292
|
async function refreshDashboard(options) {
|
|
597
|
-
|
|
293
|
+
const quiet = options && options.quiet;
|
|
294
|
+
if (refreshing && quiet) return;
|
|
598
295
|
const request = ++catalogRequest;
|
|
599
296
|
refreshing = true;
|
|
600
|
-
|
|
601
|
-
|
|
297
|
+
el.catalogNext.disabled = true;
|
|
298
|
+
el.sync.dataset.state = "syncing";
|
|
602
299
|
if (!quiet) {
|
|
603
|
-
|
|
604
|
-
if (!state.tasks.length)
|
|
300
|
+
el.refresh.disabled = true;
|
|
301
|
+
if (!state.tasks.length) el.tasks.replaceChildren(h("div.loading-block", null, h("span.spinner"), h("span", null, t("loading.dashboard"))));
|
|
605
302
|
}
|
|
606
303
|
const previousInputs = state.counts ? state.counts.openInputs : null;
|
|
607
304
|
try {
|
|
@@ -611,11 +308,11 @@ async function refreshDashboard(options) {
|
|
|
611
308
|
if (state.query.trim()) query.set("search", state.query.trim());
|
|
612
309
|
if (state.attentionFilter) query.set("attention", state.attentionFilter);
|
|
613
310
|
if (state.catalogCursor) query.set("cursor", state.catalogCursor);
|
|
614
|
-
const dashboard = await
|
|
311
|
+
const dashboard = await api.dashboard(query.toString());
|
|
615
312
|
if (request !== catalogRequest) return;
|
|
616
313
|
state.tasks = dashboard.tasks;
|
|
617
314
|
if (state.catalogQuery !== query.toString() || (state.sessionOverview && JSON.stringify(
|
|
618
|
-
state.sessionOverview.tasks.map(task
|
|
315
|
+
state.sessionOverview.tasks.map(function (task) { return task.taskId; })) !== JSON.stringify(dashboard.tasks.map(function (task) { return task.id; })))) {
|
|
619
316
|
state.sessionOverview = null;
|
|
620
317
|
}
|
|
621
318
|
state.catalogQuery = query.toString();
|
|
@@ -629,422 +326,417 @@ async function refreshDashboard(options) {
|
|
|
629
326
|
return { taskId: ref.taskId, taskTitle: task ? task.title : ref.taskId };
|
|
630
327
|
});
|
|
631
328
|
state.generatedAt = new Date().toISOString();
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (previousInputs !== null && state.counts.openInputs > previousInputs)
|
|
636
|
-
|
|
637
|
-
|
|
329
|
+
el.sync.dataset.state = "ok";
|
|
330
|
+
renderSidebar();
|
|
331
|
+
if (!state.selected) showOverview();
|
|
332
|
+
if (previousInputs !== null && state.counts.openInputs > previousInputs) showToast(t("input.new"));
|
|
333
|
+
// A selected Task may live on another page or outside current filters;
|
|
334
|
+
// only its own read can establish that it no longer exists.
|
|
638
335
|
if (state.selected) {
|
|
639
|
-
try { await loadTaskDetail(state.selected, false); }
|
|
640
|
-
|
|
641
|
-
}
|
|
336
|
+
try { await loadTaskDetail(state.selected, false); }
|
|
337
|
+
catch { showToast(t("errors.disconnected")); }
|
|
642
338
|
}
|
|
643
339
|
} catch {
|
|
644
340
|
if (request !== catalogRequest) return;
|
|
341
|
+
el.sync.dataset.state = "error";
|
|
645
342
|
if (!quiet) {
|
|
646
|
-
|
|
647
|
-
showToast(
|
|
343
|
+
el.tasks.replaceChildren(h("div.list-empty.is-error", null, h("p", null, t("errors.dashboard"))));
|
|
344
|
+
showToast(t("errors.dashboard"));
|
|
648
345
|
}
|
|
649
346
|
} finally {
|
|
650
347
|
if (request === catalogRequest) {
|
|
651
348
|
refreshing = false;
|
|
652
|
-
|
|
653
|
-
|
|
349
|
+
el.refresh.disabled = false;
|
|
350
|
+
el.catalogNext.disabled = !state.nextCursor;
|
|
654
351
|
}
|
|
655
352
|
}
|
|
656
353
|
}
|
|
657
354
|
|
|
658
|
-
function
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
355
|
+
function renderSidebar() {
|
|
356
|
+
renderAttentionBar(el.attention, state, t, pickAttention);
|
|
357
|
+
renderFilters(el.filters, state, t, function (filter) {
|
|
358
|
+
state.filter = filter;
|
|
359
|
+
state.attentionFilter = null;
|
|
360
|
+
state.catalogAll = false;
|
|
361
|
+
syncUrl(false);
|
|
362
|
+
resetCatalog();
|
|
666
363
|
});
|
|
667
|
-
|
|
668
|
-
|
|
364
|
+
el.catalogCount.textContent = t("catalog.count").replace("{shown}", String(state.tasks.length)).replace("{total}", String(state.catalogTotal));
|
|
365
|
+
el.catalogReset.hidden = !state.catalogCursor;
|
|
366
|
+
el.catalogNext.hidden = !state.nextCursor && !state.catalogCursor;
|
|
367
|
+
el.catalogAttentionReset.hidden = !state.attentionFilter;
|
|
368
|
+
const scroll = el.tasks.scrollTop;
|
|
369
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
370
|
+
el.tasks.scrollTop = scroll;
|
|
371
|
+
el.lastSync.textContent = state.generatedAt ? formatClock(state.generatedAt, locale()) : "—";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function readPageSessions() {
|
|
375
|
+
if (state.sessionLoading) return;
|
|
376
|
+
const query = state.catalogQuery;
|
|
377
|
+
state.sessionLoading = true;
|
|
378
|
+
showOverview();
|
|
379
|
+
try {
|
|
380
|
+
const result = await api.pageSessions(query);
|
|
381
|
+
if (query !== state.catalogQuery) return;
|
|
382
|
+
if (JSON.stringify(result.tasks.map(function (task) { return task.taskId; })) !== JSON.stringify(state.tasks.map(function (task) { return task.id; }))) {
|
|
383
|
+
throw new Error(t("overview.sessionsPageChanged"));
|
|
384
|
+
}
|
|
385
|
+
state.sessionOverview = result;
|
|
386
|
+
state.sessionError = null;
|
|
387
|
+
} catch (error) { state.sessionError = error.message; }
|
|
388
|
+
finally {
|
|
389
|
+
state.sessionLoading = false;
|
|
390
|
+
if (!state.selected) showOverview();
|
|
391
|
+
}
|
|
669
392
|
}
|
|
670
393
|
|
|
671
|
-
function
|
|
672
|
-
|
|
673
|
-
|
|
394
|
+
function pickAttention(kind) {
|
|
395
|
+
state.attentionFilter = kind;
|
|
396
|
+
state.catalogAll = !!kind && !!state.catalogScope && state.catalogScope.archived === "included";
|
|
397
|
+
state.filter = "all";
|
|
398
|
+
state.query = "";
|
|
399
|
+
el.search.value = "";
|
|
400
|
+
syncUrl(false);
|
|
401
|
+
resetCatalog();
|
|
674
402
|
}
|
|
675
403
|
|
|
676
|
-
|
|
677
|
-
|
|
404
|
+
// --- Selection --------------------------------------------------------------------
|
|
405
|
+
function hasUnsent() {
|
|
406
|
+
return !!(el.detail.querySelector('[data-unsent="true"]') || discussionHasUnsent(el.discussion));
|
|
678
407
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
elements.terminalPanel.hidden = false;
|
|
682
|
-
elements.terminalPanel.setAttribute("aria-hidden", "false");
|
|
683
|
-
document.body.classList.add("terminal-active");
|
|
684
|
-
updateConversationLayout();
|
|
408
|
+
function canLeaveDetail() {
|
|
409
|
+
return !hasUnsent() || window.confirm(t("confirm.leave"));
|
|
685
410
|
}
|
|
411
|
+
window.addEventListener("beforeunload", function (event) {
|
|
412
|
+
if (!hasUnsent()) return;
|
|
413
|
+
event.preventDefault();
|
|
414
|
+
event.returnValue = "";
|
|
415
|
+
});
|
|
686
416
|
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
417
|
+
async function selectTask(taskId) {
|
|
418
|
+
// Re-selecting the visible Task is not navigation.
|
|
419
|
+
if (state.selected === taskId && state.detail) return;
|
|
420
|
+
if (state.selected !== taskId && !canLeaveDetail()) { syncUrl(true); return; }
|
|
421
|
+
if (urlTaskId() !== taskId) {
|
|
422
|
+
const params = readQuery();
|
|
423
|
+
params.delete("section");
|
|
424
|
+
applyUrl(params, true);
|
|
425
|
+
}
|
|
426
|
+
state.selected = taskId;
|
|
427
|
+
state.detail = null;
|
|
428
|
+
state.activeTab = "overview";
|
|
429
|
+
dock.sheet = false;
|
|
430
|
+
renderedDetailKey = null;
|
|
431
|
+
renderedDiscussionKey = null;
|
|
432
|
+
el.discussion.replaceChildren();
|
|
433
|
+
delete el.discussion.dataset.taskId;
|
|
434
|
+
syncUrl(state.selected === urlTaskId());
|
|
435
|
+
updateLayout();
|
|
436
|
+
const scroll = el.tasks.scrollTop;
|
|
437
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
438
|
+
el.tasks.scrollTop = scroll;
|
|
439
|
+
try {
|
|
440
|
+
await loadTaskDetail(taskId, true);
|
|
441
|
+
} catch {
|
|
442
|
+
if (state.selected !== taskId) return;
|
|
443
|
+
el.detail.replaceChildren(h("div.page", null, h("div.list-empty.is-error", null, h("p", null, t("errors.detail")))));
|
|
444
|
+
showToast(t("errors.detail"));
|
|
445
|
+
}
|
|
693
446
|
}
|
|
694
447
|
|
|
695
|
-
function
|
|
696
|
-
if (!
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
448
|
+
function clearSelection() {
|
|
449
|
+
if (!canLeaveDetail()) return;
|
|
450
|
+
state.selected = null;
|
|
451
|
+
state.detail = null;
|
|
452
|
+
el.discussion.replaceChildren();
|
|
453
|
+
delete el.discussion.dataset.taskId;
|
|
454
|
+
if (dock.mode === "discussion" || (terminal.current() && terminal.current().scope === "task")) {
|
|
455
|
+
if (!terminal.connected()) dock.mode = "discussion";
|
|
456
|
+
}
|
|
457
|
+
syncUrl(!urlTaskId());
|
|
458
|
+
updateLayout();
|
|
459
|
+
showOverview();
|
|
460
|
+
const scroll = el.tasks.scrollTop;
|
|
461
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
462
|
+
el.tasks.scrollTop = scroll;
|
|
463
|
+
updateSessionTargets();
|
|
704
464
|
}
|
|
705
465
|
|
|
706
|
-
function
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
background: "#080b11",
|
|
722
|
-
foreground: "#e8eef6",
|
|
723
|
-
cursor: "#49d6ff",
|
|
724
|
-
selectionBackground: "#264b5d"
|
|
725
|
-
}
|
|
726
|
-
});
|
|
727
|
-
const fit = new FitAddon();
|
|
728
|
-
terminal.loadAddon(fit);
|
|
729
|
-
terminal.open(elements.terminalHost);
|
|
730
|
-
fit.fit();
|
|
731
|
-
|
|
732
|
-
let writable = false;
|
|
733
|
-
const socket = new WebSocket(terminalUrl(target, terminal.cols, terminal.rows));
|
|
734
|
-
const input = terminal.onData(function (data) {
|
|
735
|
-
if (writable && socket.readyState === WebSocket.OPEN) {
|
|
736
|
-
socket.send(JSON.stringify({ type: "input", data: data }));
|
|
737
|
-
}
|
|
738
|
-
});
|
|
739
|
-
const resizeObserver = new ResizeObserver(function () {
|
|
740
|
-
if (!terminalSession || terminalSession.terminal !== terminal) return;
|
|
741
|
-
fit.fit();
|
|
742
|
-
if (socket.readyState === WebSocket.OPEN) {
|
|
743
|
-
socket.send(JSON.stringify({
|
|
744
|
-
type: "resize",
|
|
745
|
-
columns: terminal.cols,
|
|
746
|
-
rows: terminal.rows
|
|
747
|
-
}));
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
resizeObserver.observe(elements.terminalHost);
|
|
751
|
-
|
|
752
|
-
socket.addEventListener("message", function (event) {
|
|
753
|
-
let message;
|
|
754
|
-
try {
|
|
755
|
-
message = JSON.parse(event.data);
|
|
756
|
-
} catch {
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
if (message.type === "ready") {
|
|
760
|
-
writable = !message.readOnly;
|
|
761
|
-
setTerminalState(writable ? "terminal.writable" : "terminal.readOnly");
|
|
762
|
-
if (message.history && message.history.limit < message.history.target) {
|
|
763
|
-
const number = new Intl.NumberFormat(i18n.getLocale());
|
|
764
|
-
showToast(
|
|
765
|
-
i18n.t("terminal.historyLimited")
|
|
766
|
-
.replace("{current}", number.format(message.history.limit))
|
|
767
|
-
.replace("{target}", number.format(message.history.target))
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
terminal.focus();
|
|
771
|
-
} else if (message.type === "data") {
|
|
772
|
-
terminal.write(message.data);
|
|
773
|
-
} else if (message.type === "exit") {
|
|
774
|
-
setTerminalState("terminal.closed");
|
|
775
|
-
} else if (message.type === "error") {
|
|
776
|
-
setTerminalState("terminal.error");
|
|
777
|
-
terminal.writeln("\\r\\n" + message.message);
|
|
778
|
-
}
|
|
779
|
-
});
|
|
780
|
-
socket.addEventListener("close", function () {
|
|
781
|
-
writable = false;
|
|
782
|
-
setTerminalState("terminal.closed");
|
|
783
|
-
});
|
|
784
|
-
socket.addEventListener("error", function () {
|
|
785
|
-
writable = false;
|
|
786
|
-
setTerminalState("terminal.error");
|
|
787
|
-
});
|
|
788
|
-
|
|
789
|
-
terminalSession = { terminal, socket, input, resizeObserver };
|
|
466
|
+
async function answerInput(input, answer, control) {
|
|
467
|
+
if (!state.detail) return;
|
|
468
|
+
const taskId = state.detail.task.id;
|
|
469
|
+
if (control) control.disabled = true;
|
|
470
|
+
try {
|
|
471
|
+
await api.answerInput(taskId, input.id, answer);
|
|
472
|
+
releaseMutation(taskId + "/input/" + input.id);
|
|
473
|
+
showToast(t("input.answered"));
|
|
474
|
+
const form = control && control.closest("form");
|
|
475
|
+
if (form) form.dataset.unsent = "false";
|
|
476
|
+
await refreshDashboard({ quiet: true });
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (control) control.disabled = error.disposition !== "not-submitted";
|
|
479
|
+
showToast(error.disposition === "not-submitted" ? error.message : t("input.unknown"));
|
|
480
|
+
}
|
|
790
481
|
}
|
|
791
482
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
483
|
+
// --- Dock layout ------------------------------------------------------------------
|
|
484
|
+
function dockVisible() {
|
|
485
|
+
if (narrow.matches ? !dock.sheet : !dock.open) return false;
|
|
486
|
+
if (state.selected) return true;
|
|
487
|
+
return dock.mode === "session" && terminal.connected();
|
|
488
|
+
}
|
|
489
|
+
function maxDockWidth() {
|
|
490
|
+
const sidebar = window.innerWidth > 900 ? el.sidebar.offsetWidth : 0;
|
|
491
|
+
return Math.max(320, Math.min(760, window.innerWidth - sidebar - 420));
|
|
492
|
+
}
|
|
493
|
+
function setDockWidth(width, persist) {
|
|
494
|
+
const effective = Math.max(320, Math.min(maxDockWidth(), width));
|
|
495
|
+
document.documentElement.style.setProperty("--dock-w", effective + "px");
|
|
496
|
+
el.divider.setAttribute("aria-valuemax", String(maxDockWidth()));
|
|
497
|
+
el.divider.setAttribute("aria-valuenow", String(effective));
|
|
498
|
+
if (persist) { dock.width = effective; writePreference("yui.dock.width", String(effective)); }
|
|
499
|
+
}
|
|
500
|
+
function updateLayout() {
|
|
501
|
+
const visible = dockVisible();
|
|
502
|
+
document.body.classList.toggle("task-open", !!state.selected);
|
|
503
|
+
document.body.classList.toggle("dock-open", visible);
|
|
504
|
+
document.body.classList.toggle("dock-center", dock.center);
|
|
505
|
+
el.dock.hidden = !visible;
|
|
506
|
+
el.divider.hidden = !visible;
|
|
507
|
+
el.discussion.hidden = dock.mode !== "discussion";
|
|
508
|
+
el.session.hidden = dock.mode !== "session";
|
|
509
|
+
el.dockTabDiscussion.setAttribute("aria-selected", String(dock.mode === "discussion"));
|
|
510
|
+
el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
|
|
511
|
+
el.dockTabDiscussion.disabled = !state.selected;
|
|
512
|
+
el.dockSwap.setAttribute("aria-pressed", String(dock.center));
|
|
513
|
+
setDockWidth(dock.width, false);
|
|
514
|
+
syncDockButtons();
|
|
515
|
+
}
|
|
516
|
+
function syncDockButtons() {
|
|
517
|
+
const visible = dockVisible();
|
|
518
|
+
el.detail.querySelectorAll("[data-dock-toggle]").forEach(function (button) {
|
|
519
|
+
button.setAttribute("aria-pressed", String(visible && dock.mode === button.dataset.dockToggle));
|
|
807
520
|
});
|
|
521
|
+
el.operator.setAttribute("aria-pressed", String(visible && dock.mode === "session"
|
|
522
|
+
&& !!terminal.current() && terminal.current().scope === "global"));
|
|
808
523
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
524
|
+
function setDockOpen(open) {
|
|
525
|
+
if (narrow.matches) dock.sheet = open;
|
|
526
|
+
else {
|
|
527
|
+
dock.open = open;
|
|
528
|
+
writePreference("yui.dock.open", String(open));
|
|
529
|
+
}
|
|
530
|
+
if (!open && terminal.connected()) terminal.close();
|
|
531
|
+
updateLayout();
|
|
532
|
+
}
|
|
533
|
+
function setDockMode(mode) {
|
|
534
|
+
dock.mode = mode === "session" || !state.selected ? "session" : "discussion";
|
|
535
|
+
updateLayout();
|
|
536
|
+
}
|
|
537
|
+
function toggleDock(mode) {
|
|
538
|
+
if (dockVisible() && (!mode || dock.mode === mode)) { setDockOpen(false); return; }
|
|
539
|
+
if (mode) dock.mode = mode === "session" || !state.selected ? mode : "discussion";
|
|
540
|
+
if (!state.selected && dock.mode === "discussion") dock.mode = "session";
|
|
541
|
+
setDockOpen(true);
|
|
542
|
+
}
|
|
543
|
+
function openSession(target) {
|
|
544
|
+
dock.mode = "session";
|
|
545
|
+
if (narrow.matches) dock.sheet = true;
|
|
546
|
+
else {
|
|
547
|
+
dock.open = true;
|
|
548
|
+
writePreference("yui.dock.open", "true");
|
|
549
|
+
}
|
|
550
|
+
terminal.open(target);
|
|
551
|
+
updateLayout();
|
|
552
|
+
updateSessionTargets();
|
|
553
|
+
}
|
|
554
|
+
function openOperator() { openSession({ scope: "global", roleName: "operator" }); }
|
|
555
|
+
|
|
556
|
+
el.dockTabDiscussion.addEventListener("click", function () { setDockMode("discussion"); });
|
|
557
|
+
el.dockTabSession.addEventListener("click", function () { setDockMode("session"); });
|
|
558
|
+
el.dockClose.addEventListener("click", function () { setDockOpen(false); });
|
|
559
|
+
el.dockSwap.addEventListener("click", function () {
|
|
560
|
+
dock.center = !dock.center;
|
|
561
|
+
writePreference("yui.dock.side", dock.center ? "center" : "end");
|
|
562
|
+
updateLayout();
|
|
816
563
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
564
|
+
el.operator.addEventListener("click", function () {
|
|
565
|
+
const current = terminal.current();
|
|
566
|
+
if (dockVisible() && dock.mode === "session" && current && current.scope === "global") { setDockOpen(false); return; }
|
|
567
|
+
openOperator();
|
|
821
568
|
});
|
|
822
|
-
|
|
823
|
-
|
|
569
|
+
|
|
570
|
+
let resizing = false;
|
|
571
|
+
el.divider.addEventListener("pointerdown", function (event) {
|
|
572
|
+
if (event.button !== 0 || window.innerWidth <= 900) return;
|
|
573
|
+
resizing = true;
|
|
574
|
+
el.divider.setPointerCapture(event.pointerId);
|
|
575
|
+
document.body.classList.add("dock-resizing");
|
|
576
|
+
event.preventDefault();
|
|
824
577
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
578
|
+
el.divider.addEventListener("pointermove", function (event) {
|
|
579
|
+
if (!resizing) return;
|
|
580
|
+
const rect = el.dock.getBoundingClientRect();
|
|
581
|
+
const width = dock.center ? event.clientX - rect.left : rect.right - event.clientX;
|
|
582
|
+
setDockWidth(width, false);
|
|
583
|
+
dock.width = Math.max(320, Math.min(maxDockWidth(), width));
|
|
830
584
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
} catch (error) { state.sessionError = error.message; }
|
|
847
|
-
finally {
|
|
848
|
-
state.sessionLoading = false;
|
|
849
|
-
if (!state.selected) showOverview();
|
|
850
|
-
}
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
const button = event.target.closest("[data-catalog-attention]");
|
|
854
|
-
if (!button) return;
|
|
855
|
-
state.attentionFilter = button.dataset.catalogAttention;
|
|
856
|
-
state.catalogAll = state.catalogScope && state.catalogScope.archived === "included";
|
|
857
|
-
state.filter = "all";
|
|
858
|
-
state.query = "";
|
|
859
|
-
elements.search.value = "";
|
|
860
|
-
syncUrlFromState({ replace: false });
|
|
861
|
-
resetCatalog();
|
|
585
|
+
function finishResize() {
|
|
586
|
+
if (!resizing) return;
|
|
587
|
+
resizing = false;
|
|
588
|
+
document.body.classList.remove("dock-resizing");
|
|
589
|
+
writePreference("yui.dock.width", String(dock.width));
|
|
590
|
+
}
|
|
591
|
+
el.divider.addEventListener("pointerup", finishResize);
|
|
592
|
+
el.divider.addEventListener("pointercancel", finishResize);
|
|
593
|
+
el.divider.addEventListener("keydown", function (event) {
|
|
594
|
+
const grow = dock.center ? "ArrowRight" : "ArrowLeft";
|
|
595
|
+
const shrink = dock.center ? "ArrowLeft" : "ArrowRight";
|
|
596
|
+
const delta = event.key === grow ? 24 : event.key === shrink ? -24 : 0;
|
|
597
|
+
if (!delta && event.key !== "Home" && event.key !== "End") return;
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
setDockWidth(event.key === "Home" ? 320 : event.key === "End" ? maxDockWidth() : dock.width + delta, true);
|
|
862
600
|
});
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
601
|
+
window.addEventListener("resize", function () { setDockWidth(dock.width, false); });
|
|
602
|
+
narrow.addEventListener("change", updateLayout);
|
|
603
|
+
|
|
604
|
+
// --- Sidebar controls -------------------------------------------------------------
|
|
605
|
+
let searchTimer = null;
|
|
606
|
+
el.search.addEventListener("input", function () {
|
|
607
|
+
state.query = el.search.value;
|
|
608
|
+
syncUrl(true);
|
|
609
|
+
window.clearTimeout(searchTimer);
|
|
610
|
+
searchTimer = window.setTimeout(resetCatalog, 200);
|
|
866
611
|
});
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
612
|
+
el.catalogNext.addEventListener("click", function () {
|
|
613
|
+
if (!state.nextCursor) return;
|
|
614
|
+
state.catalogCursor = state.nextCursor;
|
|
615
|
+
refreshDashboard();
|
|
616
|
+
});
|
|
617
|
+
el.catalogReset.addEventListener("click", resetCatalog);
|
|
618
|
+
el.catalogAttentionReset.addEventListener("click", function () { pickAttention(null); });
|
|
619
|
+
el.refresh.addEventListener("click", function () { refreshDashboard(); });
|
|
620
|
+
el.settingsOpen.addEventListener("click", function () { el.settings.showModal(); });
|
|
621
|
+
|
|
622
|
+
// --- Global Role input ------------------------------------------------------------
|
|
623
|
+
const globalDialog = $("#global-input-dialog");
|
|
624
|
+
const globalForm = $("#global-input-form");
|
|
625
|
+
const globalRole = $("#global-input-role");
|
|
626
|
+
const globalAction = $("#global-input-action");
|
|
627
|
+
const globalBody = $("#global-input-body");
|
|
628
|
+
const globalTarget = $("#global-input-target");
|
|
629
|
+
const globalThen = $("#global-input-then");
|
|
630
|
+
const globalSubmit = $("#global-input-submit");
|
|
631
|
+
const globalReceipt = $("#global-input-receipt");
|
|
632
|
+
const globalState = $("#global-input-state");
|
|
633
|
+
$("#global-input-open").addEventListener("click", function () { globalDialog.showModal(); });
|
|
634
|
+
$("#global-input-close").addEventListener("click", function () { globalDialog.close(); });
|
|
635
|
+
globalAction.addEventListener("change", function () {
|
|
636
|
+
$("#global-input-body-label").hidden = globalAction.value === "interrupt";
|
|
637
|
+
$("#global-input-target-label").hidden = globalAction.value === "queue";
|
|
638
|
+
$("#global-input-then-label").hidden = globalAction.value !== "interrupt";
|
|
883
639
|
globalBody.required = globalAction.value !== "interrupt";
|
|
884
640
|
globalTarget.required = globalAction.value !== "queue";
|
|
885
641
|
});
|
|
886
|
-
|
|
642
|
+
$("#global-input-inspect").addEventListener("click", async function () {
|
|
643
|
+
globalState.hidden = false;
|
|
887
644
|
try {
|
|
888
|
-
const facts = await
|
|
645
|
+
const facts = await api.globalState(globalRole.value.trim());
|
|
889
646
|
globalState.textContent = JSON.stringify(facts, null, 2);
|
|
890
|
-
globalTarget.value = facts.turn
|
|
647
|
+
globalTarget.value = (facts.turn && (facts.turn.nativeTurnId || facts.turn.attemptId)) || "";
|
|
891
648
|
} catch (error) { globalState.textContent = error.message; }
|
|
892
649
|
});
|
|
893
|
-
globalForm.addEventListener("submit", async event
|
|
650
|
+
globalForm.addEventListener("submit", async function (event) {
|
|
894
651
|
event.preventDefault();
|
|
895
652
|
if (globalSubmit.disabled) return;
|
|
896
653
|
globalSubmit.disabled = true;
|
|
897
654
|
const role = globalRole.value.trim();
|
|
898
655
|
const action = globalAction.value;
|
|
899
656
|
const requestId = crypto.randomUUID();
|
|
900
|
-
const input = { action, requestId };
|
|
657
|
+
const input = { action: action, requestId: requestId };
|
|
901
658
|
if (action !== "interrupt") input.body = globalBody.value;
|
|
902
659
|
if (action !== "queue") input.expectedTarget = globalTarget.value.trim();
|
|
903
660
|
if (action === "interrupt" && globalThen.value.trim()) input.thenMessage = globalThen.value.trim();
|
|
904
|
-
globalReceipt.
|
|
661
|
+
globalReceipt.dataset.state = "pending";
|
|
662
|
+
globalReceipt.textContent = t("receipt.waiting") + " · " + requestId;
|
|
905
663
|
try {
|
|
906
|
-
const receipt = await
|
|
664
|
+
const receipt = await api.globalControl(role, input);
|
|
907
665
|
const status = receipt.steer || receipt.interrupt || receipt.delivery || {};
|
|
666
|
+
globalReceipt.dataset.state = "ok";
|
|
908
667
|
globalReceipt.textContent = JSON.stringify(receipt);
|
|
909
668
|
const unknown = ["pending", "delivery-unknown", "steer-unknown", "interrupt-unknown"].includes(status.state)
|
|
910
669
|
|| status.code === "DELIVERY_UNKNOWN";
|
|
911
670
|
globalSubmit.disabled = unknown;
|
|
912
671
|
if (!unknown && !status.code) globalBody.value = "";
|
|
913
672
|
} catch (error) {
|
|
914
|
-
globalReceipt.
|
|
915
|
-
|
|
673
|
+
globalReceipt.dataset.state = error.disposition === "not-submitted" ? "bad" : "warn";
|
|
674
|
+
globalReceipt.textContent = error.disposition === "not-submitted" ? error.message : t("receipt.unknownControl") + " · " + requestId;
|
|
916
675
|
globalSubmit.disabled = error.disposition !== "not-submitted";
|
|
917
676
|
}
|
|
918
677
|
});
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
elements.conversationToggle.addEventListener("click", function () {
|
|
922
|
-
conversationVisible = !conversationVisible;
|
|
923
|
-
updateConversationLayout();
|
|
924
|
-
});
|
|
925
|
-
elements.conversationClose.addEventListener("click", function () {
|
|
926
|
-
conversationVisible = false;
|
|
927
|
-
updateConversationLayout();
|
|
928
|
-
elements.conversationToggle.focus();
|
|
929
|
-
});
|
|
930
|
-
elements.conversationSwap.addEventListener("click", function () {
|
|
931
|
-
const left = !document.body.classList.contains("pane-left");
|
|
932
|
-
document.body.classList.toggle("pane-left", left);
|
|
933
|
-
localStorage.setItem("yui.conversation.side", left ? "left" : "right");
|
|
934
|
-
});
|
|
935
|
-
let resizingPane = false;
|
|
936
|
-
elements.paneDivider.addEventListener("pointerdown", function (event) {
|
|
937
|
-
if (event.button !== 0 || elements.paneDivider.hidden || window.innerWidth <= 900) return;
|
|
938
|
-
resizingPane = true;
|
|
939
|
-
elements.paneDivider.setPointerCapture(event.pointerId);
|
|
940
|
-
document.body.classList.add("pane-resizing");
|
|
941
|
-
event.preventDefault();
|
|
942
|
-
});
|
|
943
|
-
elements.paneDivider.addEventListener("pointermove", function (event) {
|
|
944
|
-
if (!resizingPane) return;
|
|
945
|
-
const left = document.body.classList.contains("pane-left");
|
|
946
|
-
const width = left ? event.clientX - elements.tasks.closest(".sidebar").offsetWidth
|
|
947
|
-
: window.innerWidth - event.clientX;
|
|
948
|
-
setPaneWidth(width - 4, false);
|
|
949
|
-
});
|
|
950
|
-
function finishPaneResize() {
|
|
951
|
-
if (!resizingPane) return;
|
|
952
|
-
resizingPane = false;
|
|
953
|
-
document.body.classList.remove("pane-resizing");
|
|
954
|
-
localStorage.setItem("yui.conversation.width", String(paneWidth));
|
|
955
|
-
}
|
|
956
|
-
elements.paneDivider.addEventListener("pointerup", finishPaneResize);
|
|
957
|
-
elements.paneDivider.addEventListener("pointercancel", finishPaneResize);
|
|
958
|
-
elements.paneDivider.addEventListener("keydown", function (event) {
|
|
959
|
-
const left = document.body.classList.contains("pane-left");
|
|
960
|
-
const delta = left ? (event.key === "ArrowRight" ? 20 : event.key === "ArrowLeft" ? -20 : 0)
|
|
961
|
-
: (event.key === "ArrowLeft" ? 20 : event.key === "ArrowRight" ? -20 : 0);
|
|
962
|
-
if (!delta && !["Home", "End"].includes(event.key)) return;
|
|
963
|
-
event.preventDefault();
|
|
964
|
-
setPaneWidth(event.key === "Home" ? 280 : event.key === "End" ? maxPaneWidth() : paneWidth + delta, true);
|
|
965
|
-
});
|
|
966
|
-
window.addEventListener("resize", function () { setPaneWidth(paneWidth, false, true); });
|
|
678
|
+
|
|
679
|
+
// --- Keyboard ---------------------------------------------------------------------
|
|
967
680
|
document.addEventListener("keydown", function (event) {
|
|
968
|
-
if (globalDialog.open) return;
|
|
681
|
+
if (globalDialog.open || el.settings.open) return;
|
|
969
682
|
const active = document.activeElement;
|
|
970
|
-
const typing = active && ["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName);
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
elements.conversationToggle.focus();
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
if (event.key === "Escape" && state.selected) {
|
|
982
|
-
clearSelection();
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
|
-
if (typing) return;
|
|
986
|
-
if (event.key === "/") {
|
|
987
|
-
event.preventDefault();
|
|
988
|
-
elements.search.focus();
|
|
683
|
+
const typing = active && (["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName) || active.isContentEditable);
|
|
684
|
+
const inTerminal = el.terminalHost.contains(active);
|
|
685
|
+
if (event.key === "Escape") {
|
|
686
|
+
// A native Session needs its own Escape key.
|
|
687
|
+
if (inTerminal) return;
|
|
688
|
+
if (typing) { active.blur(); return; }
|
|
689
|
+
if (dockVisible()) { setDockOpen(false); return; }
|
|
690
|
+
if (state.selected) clearSelection();
|
|
989
691
|
return;
|
|
990
692
|
}
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
693
|
+
if (typing || inTerminal || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
694
|
+
const key = event.key.toLowerCase();
|
|
695
|
+
if (key === "/") { event.preventDefault(); el.search.focus(); el.search.select(); }
|
|
696
|
+
else if (key === "r") refreshDashboard();
|
|
697
|
+
else if (key === "o") openOperator();
|
|
698
|
+
else if (key === "d") toggleDock();
|
|
699
|
+
else if (/^[1-5]$/.test(key) && state.detail) switchTab(TABS[Number(key) - 1]);
|
|
998
700
|
});
|
|
999
701
|
|
|
1000
|
-
//
|
|
702
|
+
// --- Boot -------------------------------------------------------------------------
|
|
1001
703
|
function applyStateFromUrl() {
|
|
1002
704
|
const params = readQuery();
|
|
1003
705
|
const filter = params.get("filter");
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
state.filter = "all";
|
|
1008
|
-
}
|
|
1009
|
-
const query = params.get("q");
|
|
1010
|
-
state.query = query || "";
|
|
1011
|
-
state.attentionFilter = VALID_ATTENTION.includes(params.get("attention")) ? params.get("attention") : null;
|
|
706
|
+
state.filter = STATUS_FILTERS.includes(filter) ? filter : "all";
|
|
707
|
+
state.query = params.get("q") || "";
|
|
708
|
+
state.attentionFilter = ATTENTION_KINDS.includes(params.get("attention")) ? params.get("attention") : null;
|
|
1012
709
|
state.catalogAll = params.get("all") === "true";
|
|
1013
|
-
|
|
710
|
+
el.search.value = state.query;
|
|
1014
711
|
const taskId = params.get("task");
|
|
1015
712
|
if (taskId) {
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
syncUrlFromState({ replace: true });
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
state.selected = null;
|
|
1023
|
-
state.detail = null;
|
|
1024
|
-
setDetailActive(false);
|
|
1025
|
-
elements.conversationContent.replaceChildren();
|
|
1026
|
-
showOverview();
|
|
713
|
+
if (taskId === state.selected && state.detail) applySection(params.get("section"));
|
|
714
|
+
else selectTask(taskId);
|
|
715
|
+
return;
|
|
1027
716
|
}
|
|
717
|
+
if (state.selected && !canLeaveDetail()) { syncUrl(true); return; }
|
|
718
|
+
state.selected = null;
|
|
719
|
+
state.detail = null;
|
|
720
|
+
el.discussion.replaceChildren();
|
|
721
|
+
delete el.discussion.dataset.taskId;
|
|
722
|
+
updateLayout();
|
|
723
|
+
showOverview();
|
|
1028
724
|
}
|
|
1029
|
-
|
|
1030
725
|
window.addEventListener("popstate", function () {
|
|
1031
726
|
applyStateFromUrl();
|
|
1032
727
|
resetCatalog();
|
|
1033
728
|
});
|
|
1034
|
-
|
|
1035
729
|
i18n.subscribe(function () {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
730
|
+
theme.render();
|
|
731
|
+
terminal.relabel();
|
|
732
|
+
renderSidebar();
|
|
733
|
+
renderedDiscussionKey = null;
|
|
734
|
+
if (state.detail) renderCurrentDetail(true); else showOverview();
|
|
735
|
+
updateLayout();
|
|
1040
736
|
});
|
|
1041
|
-
showOverview();
|
|
1042
|
-
|
|
1043
|
-
// Scroll-spy: keep the detail tab bar in sync with the visible section.
|
|
1044
|
-
// Bound once on the scroll container; it reads tabs/sections from the live DOM
|
|
1045
|
-
// so it survives detail re-renders.
|
|
1046
|
-
elements.mainCol.addEventListener("scroll", updateActiveTabFromScroll, { passive: true });
|
|
1047
737
|
|
|
738
|
+
updateLayout();
|
|
739
|
+
showOverview();
|
|
1048
740
|
applyStateFromUrl();
|
|
1049
741
|
refreshDashboard();
|
|
1050
742
|
window.setInterval(function () { refreshDashboard({ quiet: true }); }, 5000);
|