@zq-silk/yui 2.0.0 → 2.2.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/README.md +4 -0
- package/dist/cli/commandCatalog.js +318 -289
- package/dist/cli/commandDiscovery.js +72 -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 +23 -3030
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/globalRoleCommands.js +2 -1
- package/dist/commands/taskCommands.js +90 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +12 -3
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +28 -1
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +275 -0
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +40 -0
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +16 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +293 -0
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -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 +61 -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 +615 -862
- 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 +1024 -791
- package/dist/web/assets/client/overview.js +118 -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 +146 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +125 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +169 -0
- package/dist/web/assets/styles/layout.js +56 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +41 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +292 -0
- package/dist/web/webServer.js +2 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +61 -0
- package/docs/release-workflow.zh-CN.md +46 -0
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +33 -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,294 @@
|
|
|
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"), sidebarDivider: $("#sidebar-divider"), 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
|
-
|
|
85
|
-
// --- URL / history ---------------------------------------------------------
|
|
86
|
-
// Reflect the current view (selected task, status filter, search query) in the
|
|
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);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function buildSearch(params) {
|
|
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("&");
|
|
101
|
-
}
|
|
102
39
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
const search = buildSearch(params);
|
|
106
|
-
const url = window.location.pathname + search + window.location.hash;
|
|
107
|
-
if (replace) {
|
|
108
|
-
history.replaceState({ task: params.get("task") || null }, "", url);
|
|
109
|
-
} else {
|
|
110
|
-
history.pushState({ task: params.get("task") || null }, "", url);
|
|
111
|
-
}
|
|
40
|
+
function readPreference(key, fallback) {
|
|
41
|
+
try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
|
|
112
42
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return readQuery().get("task") || null;
|
|
43
|
+
function writePreference(key, value) {
|
|
44
|
+
try { localStorage.setItem(key, value); } catch {}
|
|
116
45
|
}
|
|
117
|
-
|
|
118
|
-
|
|
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 });
|
|
46
|
+
function clearPreference(key) {
|
|
47
|
+
try { localStorage.removeItem(key); } catch {}
|
|
128
48
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
49
|
+
const dock = {
|
|
50
|
+
open: readPreference("yui.dock.open", "true") !== "false",
|
|
51
|
+
mode: "discussion",
|
|
52
|
+
center: readPreference("yui.dock.side", "end") === "center",
|
|
53
|
+
width: Number(readPreference("yui.dock.width", "440")) || 440,
|
|
54
|
+
// On narrow screens the dock is a full-screen sheet opened explicitly per
|
|
55
|
+
// Task, never restored from the desktop preference.
|
|
56
|
+
sheet: false
|
|
57
|
+
};
|
|
58
|
+
// Task list width: null keeps the CSS default (--sidebar-w); otherwise the width
|
|
59
|
+
// the user dragged to. It is a preference; the width actually applied is clamped
|
|
60
|
+
// to the current window.
|
|
61
|
+
const sidebar = { width: Number(readPreference("yui.sidebar.width", "")) || null, resizing: false };
|
|
62
|
+
const narrow = window.matchMedia("(max-width: 900px)");
|
|
63
|
+
|
|
64
|
+
const i18n = createI18n(el.locale);
|
|
65
|
+
const t = i18n.t;
|
|
66
|
+
const locale = i18n.getLocale;
|
|
67
|
+
const theme = createThemeController(el.themeOptions, t);
|
|
68
|
+
const terminal = createTerminalController({
|
|
69
|
+
host: el.terminalHost, empty: el.terminalEmpty, state: el.terminalState, targets: el.sessionTargets, cli: el.terminalCli
|
|
70
|
+
}, t, locale, showToast);
|
|
71
|
+
theme.subscribe(function () { terminal.retheme(); });
|
|
72
|
+
|
|
73
|
+
let toastTimer = null;
|
|
74
|
+
function showToast(message) {
|
|
75
|
+
el.toast.textContent = message;
|
|
76
|
+
el.toast.classList.add("show");
|
|
77
|
+
window.clearTimeout(toastTimer);
|
|
78
|
+
toastTimer = window.setTimeout(function () { el.toast.classList.remove("show"); }, 3200);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --- URL / history ---------------------------------------------------------------
|
|
82
|
+
// The selected Task, section, status filter, search and attention filter live
|
|
83
|
+
// in the query string so back/forward, refresh and deep links keep context.
|
|
84
|
+
function readQuery() { return new URLSearchParams(window.location.search); }
|
|
85
|
+
function applyUrl(params, replace) {
|
|
86
|
+
const entries = Array.from(params.entries()).filter(function (entry) { return entry[1] !== ""; });
|
|
87
|
+
const search = entries.length ? "?" + entries.map(function (entry) {
|
|
88
|
+
return encodeURIComponent(entry[0]) + "=" + encodeURIComponent(entry[1]);
|
|
89
|
+
}).join("&") : "";
|
|
90
|
+
const url = window.location.pathname + search + window.location.hash;
|
|
91
|
+
history[replace ? "replaceState" : "pushState"]({ task: params.get("task") || null }, "", url);
|
|
137
92
|
}
|
|
138
|
-
|
|
139
|
-
function
|
|
93
|
+
function urlTaskId() { return readQuery().get("task") || null; }
|
|
94
|
+
function syncUrl(replace) {
|
|
140
95
|
const params = readQuery();
|
|
141
96
|
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);
|
|
97
|
+
else { params.delete("task"); params.delete("section"); }
|
|
98
|
+
if (state.filter !== "all") params.set("filter", state.filter); else params.delete("filter");
|
|
99
|
+
if (state.query) params.set("q", state.query); else params.delete("q");
|
|
100
|
+
if (state.attentionFilter) params.set("attention", state.attentionFilter); else params.delete("attention");
|
|
101
|
+
if (state.catalogAll) params.set("all", "true"); else params.delete("all");
|
|
102
|
+
applyUrl(params, replace);
|
|
103
|
+
}
|
|
104
|
+
function setSectionParam(section) {
|
|
105
|
+
const params = readQuery();
|
|
106
|
+
if (!state.selected || params.get("section") === section) return;
|
|
107
|
+
if (section === "overview") params.delete("section"); else params.set("section", section);
|
|
108
|
+
applyUrl(params, true);
|
|
155
109
|
}
|
|
156
110
|
|
|
157
|
-
|
|
111
|
+
// --- Detail rendering -------------------------------------------------------------
|
|
112
|
+
function detailContext() {
|
|
158
113
|
return {
|
|
159
|
-
|
|
114
|
+
activeTab: state.activeTab,
|
|
115
|
+
onTab: switchTab,
|
|
116
|
+
onBack: clearSelection,
|
|
117
|
+
showDock: toggleDock,
|
|
118
|
+
openSession: function (roleName) { openSession({ scope: "task", taskId: state.selected, roleName: roleName }); },
|
|
160
119
|
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
|
-
})
|
|
120
|
+
inspect: api.inspect,
|
|
121
|
+
artifacts: api.artifacts,
|
|
122
|
+
readArtifact: api.artifact,
|
|
123
|
+
evidence: api.evidence,
|
|
124
|
+
panels: api.panels,
|
|
125
|
+
readPanel: api.readPanel,
|
|
126
|
+
control: api.control,
|
|
127
|
+
updateTask: api.updateTask,
|
|
128
|
+
afterWrite: function () { refreshDashboard({ quiet: true }); }
|
|
180
129
|
};
|
|
181
130
|
}
|
|
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;
|
|
131
|
+
function dockActions() {
|
|
132
|
+
return {
|
|
133
|
+
sendMessage: api.sendMessage,
|
|
134
|
+
inspect: api.inspect,
|
|
135
|
+
afterWrite: function () { refreshDashboard({ quiet: true }); }
|
|
136
|
+
};
|
|
236
137
|
}
|
|
237
138
|
|
|
238
|
-
// Fingerprint of the rendered
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
function detailKeyOf(detail) {
|
|
244
|
-
const text = JSON.stringify(detail);
|
|
139
|
+
// Fingerprint of the rendered payload. Quiet polling skips identical reads so
|
|
140
|
+
// local view state (open blocks, list pages, drafts) survives.
|
|
141
|
+
function fingerprint(value) {
|
|
142
|
+
const text = JSON.stringify(value);
|
|
245
143
|
let hash = 0;
|
|
246
|
-
for (let i = 0; i < text.length; i += 1)
|
|
247
|
-
hash = (hash * 31 + text.charCodeAt(i)) | 0;
|
|
248
|
-
}
|
|
144
|
+
for (let i = 0; i < text.length; i += 1) hash = (hash * 31 + text.charCodeAt(i)) | 0;
|
|
249
145
|
return text.length + ":" + hash;
|
|
250
146
|
}
|
|
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
|
-
: "-");
|
|
147
|
+
// Only the observed identity participates: observation timestamps advance on
|
|
148
|
+
// every poll and would otherwise re-render continuously.
|
|
149
|
+
function runtimeSignature(detail) {
|
|
150
|
+
const runtime = detail.runtime;
|
|
151
|
+
return detail.runtimeStatus + "|" + JSON.stringify(runtime ? [
|
|
152
|
+
runtime.sessions && runtime.sessions.sessions, runtime.remoteDelivery,
|
|
153
|
+
runtime.execution && runtime.execution.attention, runtime.execution && runtime.execution.blockers,
|
|
154
|
+
runtime.execution && runtime.execution.next, runtime.execution && runtime.execution.status
|
|
155
|
+
] : null) + "|" + ((runtime && runtime.roles) || []).map(function (role) {
|
|
156
|
+
return role.name + ":" + (role.runtimeSession ? role.runtimeSession.nativeSessionId + "/" + role.runtimeSession.status : "-") + ":" + role.status;
|
|
268
157
|
}).join(",");
|
|
269
158
|
}
|
|
270
159
|
|
|
160
|
+
let renderedDetailKey = null;
|
|
161
|
+
let renderedDiscussionKey = null;
|
|
162
|
+
function centerIsBusy() {
|
|
163
|
+
const active = document.activeElement;
|
|
164
|
+
const editing = active && (["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName) || active.isContentEditable);
|
|
165
|
+
return el.detail.dataset.taskId === (state.detail && state.detail.task.id)
|
|
166
|
+
&& (el.detail.querySelector('[data-unsent="true"]') || el.detail.querySelector('[data-reading="true"]')
|
|
167
|
+
|| editing && el.center.contains(active));
|
|
168
|
+
}
|
|
271
169
|
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();
|
|
170
|
+
if (!state.detail) {
|
|
171
|
+
if (!state.selected) { renderedDetailKey = null; showOverview(); }
|
|
172
|
+
return;
|
|
305
173
|
}
|
|
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");
|
|
174
|
+
renderDock();
|
|
175
|
+
// Polling must not replace an unsent draft or an in-flight form, including
|
|
176
|
+
// after focus moved. This is view state, never Task state.
|
|
177
|
+
if (centerIsBusy()) return;
|
|
178
|
+
const key = locale() + "|" + state.detailKey + "|" + runtimeSignature(state.detail);
|
|
179
|
+
if (!force && key === renderedDetailKey) return;
|
|
180
|
+
const openSections = Array.from(el.detail.querySelectorAll("details[data-view-key][open]")).map(function (d) { return d.dataset.viewKey; });
|
|
181
|
+
const focusedTab = el.detail.querySelector(".tab:focus");
|
|
182
|
+
const focusedTabId = focusedTab && focusedTab.id;
|
|
183
|
+
const scroll = el.detail.dataset.taskId === state.detail.task.id ? el.center.scrollTop : 0;
|
|
184
|
+
renderTaskDetail(el.detail, state.detail, t, locale(), detailContext());
|
|
185
|
+
el.detail.querySelectorAll("details[data-view-key]").forEach(function (d) { d.open = openSections.includes(d.dataset.viewKey); });
|
|
186
|
+
if (focusedTabId) el.detail.querySelector("#" + CSS.escape(focusedTabId)).focus({ preventScroll: true });
|
|
187
|
+
el.center.scrollTop = scroll;
|
|
188
|
+
renderedDetailKey = key;
|
|
189
|
+
syncDockButtons();
|
|
190
|
+
}
|
|
191
|
+
function renderDock() {
|
|
192
|
+
if (!state.detail) return;
|
|
193
|
+
const key = locale() + "|" + state.detail.task.id + "|" + state.detailKey + "|" + state.detail.task.status;
|
|
194
|
+
if (key !== renderedDiscussionKey || el.discussion.dataset.taskId !== state.detail.task.id) {
|
|
195
|
+
renderDiscussion(el.discussion, state.detail, t, locale(), dockActions());
|
|
196
|
+
renderedDiscussionKey = key;
|
|
337
197
|
}
|
|
338
|
-
|
|
339
|
-
|
|
198
|
+
updateSessionTargets();
|
|
199
|
+
}
|
|
200
|
+
function updateSessionTargets() {
|
|
201
|
+
const targets = [];
|
|
202
|
+
if (state.detail) {
|
|
203
|
+
const runtimeRoles = (state.detail.runtime && state.detail.runtime.roles) || [];
|
|
204
|
+
entriesOf(state.detail.core, "role").forEach(function (entry) {
|
|
205
|
+
const runtimeRole = runtimeRoles.find(function (role) { return role.name === entry.ref.refId; });
|
|
206
|
+
targets.push({ scope: "task", taskId: state.detail.task.id, roleName: entry.ref.refId,
|
|
207
|
+
live: !!(runtimeRole && runtimeRole.runtimeSession && runtimeRole.runtimeSession.nativeSessionId) });
|
|
208
|
+
});
|
|
340
209
|
}
|
|
210
|
+
targets.push({ scope: "global", roleName: "operator" });
|
|
211
|
+
const current = terminal.current();
|
|
212
|
+
if (current && !targets.some(function (target) { return sameTarget(target, current); })) targets.unshift(current);
|
|
213
|
+
terminal.setTargets(targets);
|
|
341
214
|
}
|
|
342
215
|
|
|
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);
|
|
216
|
+
function showOverview() {
|
|
217
|
+
delete el.detail.dataset.taskId;
|
|
218
|
+
renderOverview(el.detail, state, t, locale(), {
|
|
219
|
+
select: selectTask,
|
|
220
|
+
filterAttention: pickAttention,
|
|
221
|
+
readSessions: readPageSessions,
|
|
222
|
+
openOperator: openOperator
|
|
369
223
|
});
|
|
370
|
-
if (state.detail) queueSectionParamSync(activeId);
|
|
371
224
|
}
|
|
372
225
|
|
|
373
|
-
function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
226
|
+
function switchTab(tab, options) {
|
|
227
|
+
if (!TABS.includes(tab)) return;
|
|
228
|
+
state.activeTab = tab;
|
|
229
|
+
selectTab(el.detail, tab);
|
|
230
|
+
if (!(options && options.keepScroll)) el.center.scrollTop = 0;
|
|
231
|
+
setSectionParam(tab);
|
|
377
232
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
}
|
|
397
|
-
|
|
398
|
-
async function requestJson(path, options) {
|
|
399
|
-
const response = await fetch(path, {
|
|
400
|
-
...options,
|
|
401
|
-
headers: {
|
|
402
|
-
accept: "application/json",
|
|
403
|
-
"x-yui-web-token": token,
|
|
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();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
async function submitMutation(key, path, body) {
|
|
421
|
-
if (submittedRequests.has(key)) throw new Error("An earlier submission is unresolved; read current facts.");
|
|
422
|
-
submittedRequests.add(key);
|
|
423
|
-
try {
|
|
424
|
-
const receipt = await requestJson(path, {
|
|
425
|
-
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body)
|
|
426
|
-
});
|
|
427
|
-
submittedRequests.delete(key);
|
|
428
|
-
return receipt;
|
|
429
|
-
} catch (error) {
|
|
430
|
-
if (error.disposition === "not-submitted") submittedRequests.delete(key);
|
|
431
|
-
throw error;
|
|
432
|
-
}
|
|
233
|
+
// A section may name a tab or any anchored block inside one (for example
|
|
234
|
+
// "reviews" → the Work tab, scrolled to #detail-reviews).
|
|
235
|
+
function applySection(section) {
|
|
236
|
+
if (!section || TABS.includes(section)) { switchTab(section || "overview"); return; }
|
|
237
|
+
const anchor = el.detail.querySelector("#detail-" + CSS.escape(section));
|
|
238
|
+
const panel = anchor && anchor.closest(".tab-panel");
|
|
239
|
+
if (!panel) { switchTab("overview"); return; }
|
|
240
|
+
state.activeTab = panel.id.replace(/^panel-/, "");
|
|
241
|
+
selectTab(el.detail, state.activeTab);
|
|
242
|
+
anchor.scrollIntoView({ block: "start" });
|
|
433
243
|
}
|
|
434
244
|
|
|
245
|
+
// --- Loading ----------------------------------------------------------------------
|
|
435
246
|
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
247
|
if (navigate) {
|
|
441
|
-
|
|
442
|
-
|
|
248
|
+
el.detail.replaceChildren(h("div.page", null, h("div.loading-block", null, h("span.spinner"), h("span", null, t("loading.detail")))));
|
|
249
|
+
delete el.detail.dataset.taskId;
|
|
250
|
+
el.center.scrollTop = 0;
|
|
443
251
|
}
|
|
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");
|
|
252
|
+
// Rendering consumes the current Context snapshot, not event pages.
|
|
253
|
+
const core = await api.context(taskId);
|
|
449
254
|
const taskEntry = core.records.find(function (entry) { return entry.ref.store === "task"; });
|
|
450
255
|
if (!taskEntry) throw new Error("Task reference unavailable.");
|
|
451
|
-
const task = taskEntry.omitted ? (await
|
|
256
|
+
const task = taskEntry.omitted ? (await api.inspect(taskId, taskEntry.ref)).value : taskEntry.value;
|
|
257
|
+
if (state.selected !== taskId) return;
|
|
452
258
|
const previous = state.detail && state.detail.task.id === taskId ? state.detail : null;
|
|
453
259
|
const detail = {
|
|
454
|
-
task, core,
|
|
260
|
+
task: task, core: core,
|
|
455
261
|
viewState: previous ? previous.viewState : {},
|
|
456
|
-
runtime: previous
|
|
262
|
+
runtime: previous ? previous.runtime : null,
|
|
457
263
|
runtimeStatus: previous ? previous.runtimeStatus : "waiting",
|
|
458
264
|
runtimeObservedAt: previous ? previous.runtimeObservedAt : new Date().toISOString()
|
|
459
265
|
};
|
|
460
|
-
if (state.selected !== taskId) return;
|
|
461
266
|
state.detail = detail;
|
|
462
|
-
state.detailKey =
|
|
463
|
-
renderCurrentDetail();
|
|
464
|
-
|
|
465
|
-
|
|
267
|
+
state.detailKey = fingerprint(core);
|
|
268
|
+
renderCurrentDetail(navigate);
|
|
269
|
+
if (navigate) applySection(readQuery().get("section"));
|
|
270
|
+
// The optional observation never participates in core readiness.
|
|
271
|
+
api.observation(taskId).then(function (runtime) {
|
|
466
272
|
if (state.detail !== detail) return;
|
|
467
273
|
detail.runtime = runtime;
|
|
468
274
|
detail.runtimeStatus = "available";
|
|
469
275
|
detail.runtimeObservedAt = new Date().toISOString();
|
|
470
|
-
|
|
276
|
+
observationArrived(detail);
|
|
471
277
|
}).catch(function () {
|
|
472
278
|
if (state.detail !== detail) return;
|
|
473
279
|
detail.runtime = null;
|
|
474
280
|
detail.runtimeStatus = "unavailable";
|
|
475
281
|
detail.runtimeObservedAt = new Date().toISOString();
|
|
476
|
-
|
|
282
|
+
observationArrived(detail);
|
|
477
283
|
});
|
|
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
284
|
}
|
|
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.
|
|
285
|
+
function observationArrived(detail) {
|
|
286
|
+
if (el.detail.dataset.taskId === detail.task.id) updateObservation(el.detail, detail, t, locale(), detailContext());
|
|
287
|
+
// Patching slots is not enough when the observed identity changed facts the
|
|
288
|
+
// detail drew earlier; the render key decides whether a re-render is due.
|
|
510
289
|
if (state.detail === detail) renderCurrentDetail();
|
|
511
290
|
}
|
|
512
291
|
|
|
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
292
|
let refreshing = false;
|
|
590
293
|
let catalogRequest = 0;
|
|
591
294
|
function resetCatalog() {
|
|
@@ -594,14 +297,15 @@ function resetCatalog() {
|
|
|
594
297
|
refreshDashboard();
|
|
595
298
|
}
|
|
596
299
|
async function refreshDashboard(options) {
|
|
597
|
-
|
|
300
|
+
const quiet = options && options.quiet;
|
|
301
|
+
if (refreshing && quiet) return;
|
|
598
302
|
const request = ++catalogRequest;
|
|
599
303
|
refreshing = true;
|
|
600
|
-
|
|
601
|
-
|
|
304
|
+
el.catalogNext.disabled = true;
|
|
305
|
+
el.sync.dataset.state = "syncing";
|
|
602
306
|
if (!quiet) {
|
|
603
|
-
|
|
604
|
-
if (!state.tasks.length)
|
|
307
|
+
el.refresh.disabled = true;
|
|
308
|
+
if (!state.tasks.length) el.tasks.replaceChildren(h("div.loading-block", null, h("span.spinner"), h("span", null, t("loading.dashboard"))));
|
|
605
309
|
}
|
|
606
310
|
const previousInputs = state.counts ? state.counts.openInputs : null;
|
|
607
311
|
try {
|
|
@@ -611,11 +315,11 @@ async function refreshDashboard(options) {
|
|
|
611
315
|
if (state.query.trim()) query.set("search", state.query.trim());
|
|
612
316
|
if (state.attentionFilter) query.set("attention", state.attentionFilter);
|
|
613
317
|
if (state.catalogCursor) query.set("cursor", state.catalogCursor);
|
|
614
|
-
const dashboard = await
|
|
318
|
+
const dashboard = await api.dashboard(query.toString());
|
|
615
319
|
if (request !== catalogRequest) return;
|
|
616
320
|
state.tasks = dashboard.tasks;
|
|
617
321
|
if (state.catalogQuery !== query.toString() || (state.sessionOverview && JSON.stringify(
|
|
618
|
-
state.sessionOverview.tasks.map(task
|
|
322
|
+
state.sessionOverview.tasks.map(function (task) { return task.taskId; })) !== JSON.stringify(dashboard.tasks.map(function (task) { return task.id; })))) {
|
|
619
323
|
state.sessionOverview = null;
|
|
620
324
|
}
|
|
621
325
|
state.catalogQuery = query.toString();
|
|
@@ -629,422 +333,471 @@ async function refreshDashboard(options) {
|
|
|
629
333
|
return { taskId: ref.taskId, taskTitle: task ? task.title : ref.taskId };
|
|
630
334
|
});
|
|
631
335
|
state.generatedAt = new Date().toISOString();
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (previousInputs !== null && state.counts.openInputs > previousInputs)
|
|
636
|
-
|
|
637
|
-
|
|
336
|
+
el.sync.dataset.state = "ok";
|
|
337
|
+
renderSidebar();
|
|
338
|
+
if (!state.selected) showOverview();
|
|
339
|
+
if (previousInputs !== null && state.counts.openInputs > previousInputs) showToast(t("input.new"));
|
|
340
|
+
// A selected Task may live on another page or outside current filters;
|
|
341
|
+
// only its own read can establish that it no longer exists.
|
|
638
342
|
if (state.selected) {
|
|
639
|
-
try { await loadTaskDetail(state.selected, false); }
|
|
640
|
-
|
|
641
|
-
}
|
|
343
|
+
try { await loadTaskDetail(state.selected, false); }
|
|
344
|
+
catch { showToast(t("errors.disconnected")); }
|
|
642
345
|
}
|
|
643
346
|
} catch {
|
|
644
347
|
if (request !== catalogRequest) return;
|
|
348
|
+
el.sync.dataset.state = "error";
|
|
645
349
|
if (!quiet) {
|
|
646
|
-
|
|
647
|
-
showToast(
|
|
350
|
+
el.tasks.replaceChildren(h("div.list-empty.is-error", null, h("p", null, t("errors.dashboard"))));
|
|
351
|
+
showToast(t("errors.dashboard"));
|
|
648
352
|
}
|
|
649
353
|
} finally {
|
|
650
354
|
if (request === catalogRequest) {
|
|
651
355
|
refreshing = false;
|
|
652
|
-
|
|
653
|
-
|
|
356
|
+
el.refresh.disabled = false;
|
|
357
|
+
el.catalogNext.disabled = !state.nextCursor;
|
|
654
358
|
}
|
|
655
359
|
}
|
|
656
360
|
}
|
|
657
361
|
|
|
658
|
-
function
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
362
|
+
function renderSidebar() {
|
|
363
|
+
renderAttentionBar(el.attention, state, t, pickAttention);
|
|
364
|
+
renderFilters(el.filters, state, t, function (filter) {
|
|
365
|
+
state.filter = filter;
|
|
366
|
+
state.attentionFilter = null;
|
|
367
|
+
state.catalogAll = false;
|
|
368
|
+
syncUrl(false);
|
|
369
|
+
resetCatalog();
|
|
666
370
|
});
|
|
667
|
-
|
|
668
|
-
|
|
371
|
+
el.catalogCount.textContent = t("catalog.count").replace("{shown}", String(state.tasks.length)).replace("{total}", String(state.catalogTotal));
|
|
372
|
+
el.catalogReset.hidden = !state.catalogCursor;
|
|
373
|
+
el.catalogNext.hidden = !state.nextCursor && !state.catalogCursor;
|
|
374
|
+
el.catalogAttentionReset.hidden = !state.attentionFilter;
|
|
375
|
+
const scroll = el.tasks.scrollTop;
|
|
376
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
377
|
+
el.tasks.scrollTop = scroll;
|
|
378
|
+
el.lastSync.textContent = state.generatedAt ? formatClock(state.generatedAt, locale()) : "—";
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function readPageSessions() {
|
|
382
|
+
if (state.sessionLoading) return;
|
|
383
|
+
const query = state.catalogQuery;
|
|
384
|
+
state.sessionLoading = true;
|
|
385
|
+
showOverview();
|
|
386
|
+
try {
|
|
387
|
+
const result = await api.pageSessions(query);
|
|
388
|
+
if (query !== state.catalogQuery) return;
|
|
389
|
+
if (JSON.stringify(result.tasks.map(function (task) { return task.taskId; })) !== JSON.stringify(state.tasks.map(function (task) { return task.id; }))) {
|
|
390
|
+
throw new Error(t("overview.sessionsPageChanged"));
|
|
391
|
+
}
|
|
392
|
+
state.sessionOverview = result;
|
|
393
|
+
state.sessionError = null;
|
|
394
|
+
} catch (error) { state.sessionError = error.message; }
|
|
395
|
+
finally {
|
|
396
|
+
state.sessionLoading = false;
|
|
397
|
+
if (!state.selected) showOverview();
|
|
398
|
+
}
|
|
669
399
|
}
|
|
670
400
|
|
|
671
|
-
function
|
|
672
|
-
|
|
673
|
-
|
|
401
|
+
function pickAttention(kind) {
|
|
402
|
+
state.attentionFilter = kind;
|
|
403
|
+
state.catalogAll = !!kind && !!state.catalogScope && state.catalogScope.archived === "included";
|
|
404
|
+
state.filter = "all";
|
|
405
|
+
state.query = "";
|
|
406
|
+
el.search.value = "";
|
|
407
|
+
syncUrl(false);
|
|
408
|
+
resetCatalog();
|
|
674
409
|
}
|
|
675
410
|
|
|
676
|
-
|
|
677
|
-
|
|
411
|
+
// --- Selection --------------------------------------------------------------------
|
|
412
|
+
function hasUnsent() {
|
|
413
|
+
return !!(el.detail.querySelector('[data-unsent="true"]') || discussionHasUnsent(el.discussion));
|
|
678
414
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
elements.terminalPanel.hidden = false;
|
|
682
|
-
elements.terminalPanel.setAttribute("aria-hidden", "false");
|
|
683
|
-
document.body.classList.add("terminal-active");
|
|
684
|
-
updateConversationLayout();
|
|
415
|
+
function canLeaveDetail() {
|
|
416
|
+
return !hasUnsent() || window.confirm(t("confirm.leave"));
|
|
685
417
|
}
|
|
418
|
+
window.addEventListener("beforeunload", function (event) {
|
|
419
|
+
if (!hasUnsent()) return;
|
|
420
|
+
event.preventDefault();
|
|
421
|
+
event.returnValue = "";
|
|
422
|
+
});
|
|
686
423
|
|
|
687
|
-
function
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
424
|
+
async function selectTask(taskId) {
|
|
425
|
+
// Re-selecting the visible Task is not navigation.
|
|
426
|
+
if (state.selected === taskId && state.detail) return;
|
|
427
|
+
if (state.selected !== taskId && !canLeaveDetail()) { syncUrl(true); return; }
|
|
428
|
+
if (urlTaskId() !== taskId) {
|
|
429
|
+
const params = readQuery();
|
|
430
|
+
params.delete("section");
|
|
431
|
+
applyUrl(params, true);
|
|
432
|
+
}
|
|
433
|
+
state.selected = taskId;
|
|
434
|
+
state.detail = null;
|
|
435
|
+
state.activeTab = "overview";
|
|
436
|
+
dock.sheet = false;
|
|
437
|
+
renderedDetailKey = null;
|
|
438
|
+
renderedDiscussionKey = null;
|
|
439
|
+
el.discussion.replaceChildren();
|
|
440
|
+
delete el.discussion.dataset.taskId;
|
|
441
|
+
syncUrl(state.selected === urlTaskId());
|
|
442
|
+
updateLayout();
|
|
443
|
+
const scroll = el.tasks.scrollTop;
|
|
444
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
445
|
+
el.tasks.scrollTop = scroll;
|
|
446
|
+
try {
|
|
447
|
+
await loadTaskDetail(taskId, true);
|
|
448
|
+
} catch {
|
|
449
|
+
if (state.selected !== taskId) return;
|
|
450
|
+
el.detail.replaceChildren(h("div.page", null, h("div.list-empty.is-error", null, h("p", null, t("errors.detail")))));
|
|
451
|
+
showToast(t("errors.detail"));
|
|
452
|
+
}
|
|
693
453
|
}
|
|
694
454
|
|
|
695
|
-
function
|
|
696
|
-
if (!
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
455
|
+
function clearSelection() {
|
|
456
|
+
if (!canLeaveDetail()) return;
|
|
457
|
+
state.selected = null;
|
|
458
|
+
state.detail = null;
|
|
459
|
+
el.discussion.replaceChildren();
|
|
460
|
+
delete el.discussion.dataset.taskId;
|
|
461
|
+
if (dock.mode === "discussion" || (terminal.current() && terminal.current().scope === "task")) {
|
|
462
|
+
if (!terminal.connected()) dock.mode = "discussion";
|
|
463
|
+
}
|
|
464
|
+
syncUrl(!urlTaskId());
|
|
465
|
+
updateLayout();
|
|
466
|
+
showOverview();
|
|
467
|
+
const scroll = el.tasks.scrollTop;
|
|
468
|
+
renderTasks(el.tasks, state, t, locale(), selectTask);
|
|
469
|
+
el.tasks.scrollTop = scroll;
|
|
470
|
+
updateSessionTargets();
|
|
704
471
|
}
|
|
705
472
|
|
|
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 };
|
|
473
|
+
async function answerInput(input, answer, control) {
|
|
474
|
+
if (!state.detail) return;
|
|
475
|
+
const taskId = state.detail.task.id;
|
|
476
|
+
if (control) control.disabled = true;
|
|
477
|
+
try {
|
|
478
|
+
await api.answerInput(taskId, input.id, answer);
|
|
479
|
+
releaseMutation(taskId + "/input/" + input.id);
|
|
480
|
+
showToast(t("input.answered"));
|
|
481
|
+
const form = control && control.closest("form");
|
|
482
|
+
if (form) form.dataset.unsent = "false";
|
|
483
|
+
await refreshDashboard({ quiet: true });
|
|
484
|
+
} catch (error) {
|
|
485
|
+
if (control) control.disabled = error.disposition !== "not-submitted";
|
|
486
|
+
showToast(error.disposition === "not-submitted" ? error.message : t("input.unknown"));
|
|
487
|
+
}
|
|
790
488
|
}
|
|
791
489
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
490
|
+
// --- Dock layout ------------------------------------------------------------------
|
|
491
|
+
function dockVisible() {
|
|
492
|
+
if (narrow.matches ? !dock.sheet : !dock.open) return false;
|
|
493
|
+
if (state.selected) return true;
|
|
494
|
+
return dock.mode === "session" && terminal.connected();
|
|
495
|
+
}
|
|
496
|
+
function maxDockWidth() {
|
|
497
|
+
const sidebar = window.innerWidth > 900 ? el.sidebar.offsetWidth : 0;
|
|
498
|
+
return Math.max(320, Math.min(760, window.innerWidth - sidebar - 420));
|
|
499
|
+
}
|
|
500
|
+
function setDockWidth(width, persist) {
|
|
501
|
+
const effective = Math.max(320, Math.min(maxDockWidth(), width));
|
|
502
|
+
document.documentElement.style.setProperty("--dock-w", effective + "px");
|
|
503
|
+
el.divider.setAttribute("aria-valuemax", String(maxDockWidth()));
|
|
504
|
+
el.divider.setAttribute("aria-valuenow", String(effective));
|
|
505
|
+
if (persist) { dock.width = effective; writePreference("yui.dock.width", String(effective)); }
|
|
506
|
+
}
|
|
507
|
+
function updateLayout() {
|
|
508
|
+
const visible = dockVisible();
|
|
509
|
+
document.body.classList.toggle("task-open", !!state.selected);
|
|
510
|
+
document.body.classList.toggle("dock-open", visible);
|
|
511
|
+
document.body.classList.toggle("dock-center", dock.center);
|
|
512
|
+
el.dock.hidden = !visible;
|
|
513
|
+
el.divider.hidden = !visible;
|
|
514
|
+
el.discussion.hidden = dock.mode !== "discussion";
|
|
515
|
+
el.session.hidden = dock.mode !== "session";
|
|
516
|
+
el.dockTabDiscussion.setAttribute("aria-selected", String(dock.mode === "discussion"));
|
|
517
|
+
el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
|
|
518
|
+
el.dockTabDiscussion.disabled = !state.selected;
|
|
519
|
+
el.dockSwap.setAttribute("aria-pressed", String(dock.center));
|
|
520
|
+
applySidebarWidth();
|
|
521
|
+
syncDockButtons();
|
|
522
|
+
}
|
|
523
|
+
function syncDockButtons() {
|
|
524
|
+
const visible = dockVisible();
|
|
525
|
+
el.detail.querySelectorAll("[data-dock-toggle]").forEach(function (button) {
|
|
526
|
+
button.setAttribute("aria-pressed", String(visible && dock.mode === button.dataset.dockToggle));
|
|
807
527
|
});
|
|
528
|
+
el.operator.setAttribute("aria-pressed", String(visible && dock.mode === "session"
|
|
529
|
+
&& !!terminal.current() && terminal.current().scope === "global"));
|
|
808
530
|
}
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
531
|
+
function setDockOpen(open) {
|
|
532
|
+
if (narrow.matches) dock.sheet = open;
|
|
533
|
+
else {
|
|
534
|
+
dock.open = open;
|
|
535
|
+
writePreference("yui.dock.open", String(open));
|
|
536
|
+
}
|
|
537
|
+
if (!open && terminal.connected()) terminal.close();
|
|
538
|
+
updateLayout();
|
|
539
|
+
}
|
|
540
|
+
function setDockMode(mode) {
|
|
541
|
+
dock.mode = mode === "session" || !state.selected ? "session" : "discussion";
|
|
542
|
+
updateLayout();
|
|
543
|
+
}
|
|
544
|
+
function toggleDock(mode) {
|
|
545
|
+
if (dockVisible() && (!mode || dock.mode === mode)) { setDockOpen(false); return; }
|
|
546
|
+
if (mode) dock.mode = mode === "session" || !state.selected ? mode : "discussion";
|
|
547
|
+
if (!state.selected && dock.mode === "discussion") dock.mode = "session";
|
|
548
|
+
setDockOpen(true);
|
|
549
|
+
}
|
|
550
|
+
function openSession(target) {
|
|
551
|
+
dock.mode = "session";
|
|
552
|
+
if (narrow.matches) dock.sheet = true;
|
|
553
|
+
else {
|
|
554
|
+
dock.open = true;
|
|
555
|
+
writePreference("yui.dock.open", "true");
|
|
556
|
+
}
|
|
557
|
+
terminal.open(target);
|
|
558
|
+
updateLayout();
|
|
559
|
+
updateSessionTargets();
|
|
560
|
+
}
|
|
561
|
+
function openOperator() { openSession({ scope: "global", roleName: "operator" }); }
|
|
562
|
+
|
|
563
|
+
el.dockTabDiscussion.addEventListener("click", function () { setDockMode("discussion"); });
|
|
564
|
+
el.dockTabSession.addEventListener("click", function () { setDockMode("session"); });
|
|
565
|
+
el.dockClose.addEventListener("click", function () { setDockOpen(false); });
|
|
566
|
+
el.dockSwap.addEventListener("click", function () {
|
|
567
|
+
dock.center = !dock.center;
|
|
568
|
+
writePreference("yui.dock.side", dock.center ? "center" : "end");
|
|
569
|
+
updateLayout();
|
|
816
570
|
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
571
|
+
el.operator.addEventListener("click", function () {
|
|
572
|
+
const current = terminal.current();
|
|
573
|
+
if (dockVisible() && dock.mode === "session" && current && current.scope === "global") { setDockOpen(false); return; }
|
|
574
|
+
openOperator();
|
|
821
575
|
});
|
|
822
|
-
|
|
823
|
-
|
|
576
|
+
|
|
577
|
+
let resizing = false;
|
|
578
|
+
el.divider.addEventListener("pointerdown", function (event) {
|
|
579
|
+
if (event.button !== 0 || window.innerWidth <= 900) return;
|
|
580
|
+
resizing = true;
|
|
581
|
+
el.divider.setPointerCapture(event.pointerId);
|
|
582
|
+
document.body.classList.add("dock-resizing");
|
|
583
|
+
event.preventDefault();
|
|
824
584
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
585
|
+
el.divider.addEventListener("pointermove", function (event) {
|
|
586
|
+
if (!resizing) return;
|
|
587
|
+
const rect = el.dock.getBoundingClientRect();
|
|
588
|
+
const width = dock.center ? event.clientX - rect.left : rect.right - event.clientX;
|
|
589
|
+
setDockWidth(width, false);
|
|
590
|
+
dock.width = Math.max(320, Math.min(maxDockWidth(), width));
|
|
830
591
|
});
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
592
|
+
function finishResize() {
|
|
593
|
+
if (!resizing) return;
|
|
594
|
+
resizing = false;
|
|
595
|
+
document.body.classList.remove("dock-resizing");
|
|
596
|
+
writePreference("yui.dock.width", String(dock.width));
|
|
597
|
+
}
|
|
598
|
+
el.divider.addEventListener("pointerup", finishResize);
|
|
599
|
+
el.divider.addEventListener("pointercancel", finishResize);
|
|
600
|
+
el.divider.addEventListener("keydown", function (event) {
|
|
601
|
+
const grow = dock.center ? "ArrowRight" : "ArrowLeft";
|
|
602
|
+
const shrink = dock.center ? "ArrowLeft" : "ArrowRight";
|
|
603
|
+
const delta = event.key === grow ? 24 : event.key === shrink ? -24 : 0;
|
|
604
|
+
if (!delta && event.key !== "Home" && event.key !== "End") return;
|
|
605
|
+
event.preventDefault();
|
|
606
|
+
setDockWidth(event.key === "Home" ? 320 : event.key === "End" ? maxDockWidth() : dock.width + delta, true);
|
|
607
|
+
});
|
|
608
|
+
narrow.addEventListener("change", updateLayout);
|
|
609
|
+
|
|
610
|
+
// --- Sidebar width ----------------------------------------------------------------
|
|
611
|
+
const SIDEBAR_MIN = 240;
|
|
612
|
+
const SIDEBAR_MAX = 560;
|
|
613
|
+
function maxSidebarWidth() {
|
|
614
|
+
// An open dock can shrink to its minimum, so it reserves only that much.
|
|
615
|
+
const reserve = dockVisible() ? 320 + 9 : 0;
|
|
616
|
+
return Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, window.innerWidth - reserve - 420));
|
|
617
|
+
}
|
|
618
|
+
function applySidebarWidth() {
|
|
619
|
+
const root = document.documentElement.style;
|
|
620
|
+
if (sidebar.width === null) root.removeProperty("--sidebar-w");
|
|
621
|
+
else root.setProperty("--sidebar-w", Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), sidebar.width)) + "px");
|
|
622
|
+
// A wider task list leaves the dock less room; the dock keeps its stored width.
|
|
623
|
+
setDockWidth(dock.width, false);
|
|
624
|
+
el.sidebarDivider.setAttribute("aria-valuemax", String(maxSidebarWidth()));
|
|
625
|
+
el.sidebarDivider.setAttribute("aria-valuenow", String(el.sidebar.offsetWidth));
|
|
626
|
+
}
|
|
627
|
+
function setSidebarWidth(width, persist) {
|
|
628
|
+
sidebar.width = width === null ? null : Math.max(SIDEBAR_MIN, Math.min(maxSidebarWidth(), Math.round(width)));
|
|
629
|
+
if (persist) {
|
|
630
|
+
if (sidebar.width === null) clearPreference("yui.sidebar.width");
|
|
631
|
+
else writePreference("yui.sidebar.width", String(sidebar.width));
|
|
852
632
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
resetCatalog();
|
|
633
|
+
applySidebarWidth();
|
|
634
|
+
}
|
|
635
|
+
el.sidebarDivider.addEventListener("pointerdown", function (event) {
|
|
636
|
+
if (event.button !== 0 || window.innerWidth <= 900) return;
|
|
637
|
+
sidebar.resizing = true;
|
|
638
|
+
el.sidebarDivider.setPointerCapture(event.pointerId);
|
|
639
|
+
document.body.classList.add("sidebar-resizing");
|
|
640
|
+
event.preventDefault();
|
|
862
641
|
});
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
642
|
+
el.sidebarDivider.addEventListener("pointermove", function (event) {
|
|
643
|
+
if (!sidebar.resizing) return;
|
|
644
|
+
setSidebarWidth(event.clientX - el.sidebar.getBoundingClientRect().left, false);
|
|
866
645
|
});
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
646
|
+
function finishSidebarResize() {
|
|
647
|
+
if (!sidebar.resizing) return;
|
|
648
|
+
sidebar.resizing = false;
|
|
649
|
+
document.body.classList.remove("sidebar-resizing");
|
|
650
|
+
if (sidebar.width !== null) writePreference("yui.sidebar.width", String(sidebar.width));
|
|
651
|
+
}
|
|
652
|
+
el.sidebarDivider.addEventListener("pointerup", finishSidebarResize);
|
|
653
|
+
el.sidebarDivider.addEventListener("pointercancel", finishSidebarResize);
|
|
654
|
+
el.sidebarDivider.addEventListener("dblclick", function () { setSidebarWidth(null, true); });
|
|
655
|
+
el.sidebarDivider.addEventListener("keydown", function (event) {
|
|
656
|
+
const current = el.sidebar.offsetWidth;
|
|
657
|
+
const next = event.key === "ArrowRight" ? current + 16 : event.key === "ArrowLeft" ? current - 16
|
|
658
|
+
: event.key === "Home" ? SIDEBAR_MIN : event.key === "End" ? maxSidebarWidth() : null;
|
|
659
|
+
if (next === null) return;
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
setSidebarWidth(next, true);
|
|
662
|
+
});
|
|
663
|
+
window.addEventListener("resize", applySidebarWidth);
|
|
664
|
+
|
|
665
|
+
// --- Sidebar controls -------------------------------------------------------------
|
|
666
|
+
let searchTimer = null;
|
|
667
|
+
el.search.addEventListener("input", function () {
|
|
668
|
+
state.query = el.search.value;
|
|
669
|
+
syncUrl(true);
|
|
670
|
+
window.clearTimeout(searchTimer);
|
|
671
|
+
searchTimer = window.setTimeout(resetCatalog, 200);
|
|
672
|
+
});
|
|
673
|
+
el.catalogNext.addEventListener("click", function () {
|
|
674
|
+
if (!state.nextCursor) return;
|
|
675
|
+
state.catalogCursor = state.nextCursor;
|
|
676
|
+
refreshDashboard();
|
|
677
|
+
});
|
|
678
|
+
el.catalogReset.addEventListener("click", resetCatalog);
|
|
679
|
+
el.catalogAttentionReset.addEventListener("click", function () { pickAttention(null); });
|
|
680
|
+
el.refresh.addEventListener("click", function () { refreshDashboard(); });
|
|
681
|
+
el.settingsOpen.addEventListener("click", function () { el.settings.showModal(); });
|
|
682
|
+
|
|
683
|
+
// --- Global Role input ------------------------------------------------------------
|
|
684
|
+
const globalDialog = $("#global-input-dialog");
|
|
685
|
+
const globalForm = $("#global-input-form");
|
|
686
|
+
const globalRole = $("#global-input-role");
|
|
687
|
+
const globalAction = $("#global-input-action");
|
|
688
|
+
const globalBody = $("#global-input-body");
|
|
689
|
+
const globalTarget = $("#global-input-target");
|
|
690
|
+
const globalThen = $("#global-input-then");
|
|
691
|
+
const globalSubmit = $("#global-input-submit");
|
|
692
|
+
const globalReceipt = $("#global-input-receipt");
|
|
693
|
+
const globalState = $("#global-input-state");
|
|
694
|
+
$("#global-input-open").addEventListener("click", function () { globalDialog.showModal(); });
|
|
695
|
+
$("#global-input-close").addEventListener("click", function () { globalDialog.close(); });
|
|
696
|
+
globalAction.addEventListener("change", function () {
|
|
697
|
+
$("#global-input-body-label").hidden = globalAction.value === "interrupt";
|
|
698
|
+
$("#global-input-target-label").hidden = globalAction.value === "queue";
|
|
699
|
+
$("#global-input-then-label").hidden = globalAction.value !== "interrupt";
|
|
883
700
|
globalBody.required = globalAction.value !== "interrupt";
|
|
884
701
|
globalTarget.required = globalAction.value !== "queue";
|
|
885
702
|
});
|
|
886
|
-
|
|
703
|
+
$("#global-input-inspect").addEventListener("click", async function () {
|
|
704
|
+
globalState.hidden = false;
|
|
887
705
|
try {
|
|
888
|
-
const facts = await
|
|
706
|
+
const facts = await api.globalState(globalRole.value.trim());
|
|
889
707
|
globalState.textContent = JSON.stringify(facts, null, 2);
|
|
890
|
-
globalTarget.value = facts.turn
|
|
708
|
+
globalTarget.value = (facts.turn && (facts.turn.nativeTurnId || facts.turn.attemptId)) || "";
|
|
891
709
|
} catch (error) { globalState.textContent = error.message; }
|
|
892
710
|
});
|
|
893
|
-
globalForm.addEventListener("submit", async event
|
|
711
|
+
globalForm.addEventListener("submit", async function (event) {
|
|
894
712
|
event.preventDefault();
|
|
895
713
|
if (globalSubmit.disabled) return;
|
|
896
714
|
globalSubmit.disabled = true;
|
|
897
715
|
const role = globalRole.value.trim();
|
|
898
716
|
const action = globalAction.value;
|
|
899
717
|
const requestId = crypto.randomUUID();
|
|
900
|
-
const input = { action, requestId };
|
|
718
|
+
const input = { action: action, requestId: requestId };
|
|
901
719
|
if (action !== "interrupt") input.body = globalBody.value;
|
|
902
720
|
if (action !== "queue") input.expectedTarget = globalTarget.value.trim();
|
|
903
721
|
if (action === "interrupt" && globalThen.value.trim()) input.thenMessage = globalThen.value.trim();
|
|
904
|
-
globalReceipt.
|
|
722
|
+
globalReceipt.dataset.state = "pending";
|
|
723
|
+
globalReceipt.textContent = t("receipt.waiting") + " · " + requestId;
|
|
905
724
|
try {
|
|
906
|
-
const receipt = await
|
|
725
|
+
const receipt = await api.globalControl(role, input);
|
|
907
726
|
const status = receipt.steer || receipt.interrupt || receipt.delivery || {};
|
|
727
|
+
globalReceipt.dataset.state = "ok";
|
|
908
728
|
globalReceipt.textContent = JSON.stringify(receipt);
|
|
909
729
|
const unknown = ["pending", "delivery-unknown", "steer-unknown", "interrupt-unknown"].includes(status.state)
|
|
910
730
|
|| status.code === "DELIVERY_UNKNOWN";
|
|
911
731
|
globalSubmit.disabled = unknown;
|
|
912
732
|
if (!unknown && !status.code) globalBody.value = "";
|
|
913
733
|
} catch (error) {
|
|
914
|
-
globalReceipt.
|
|
915
|
-
|
|
734
|
+
globalReceipt.dataset.state = error.disposition === "not-submitted" ? "bad" : "warn";
|
|
735
|
+
globalReceipt.textContent = error.disposition === "not-submitted" ? error.message : t("receipt.unknownControl") + " · " + requestId;
|
|
916
736
|
globalSubmit.disabled = error.disposition !== "not-submitted";
|
|
917
737
|
}
|
|
918
738
|
});
|
|
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); });
|
|
739
|
+
|
|
740
|
+
// --- Keyboard ---------------------------------------------------------------------
|
|
967
741
|
document.addEventListener("keydown", function (event) {
|
|
968
|
-
if (globalDialog.open) return;
|
|
742
|
+
if (globalDialog.open || el.settings.open) return;
|
|
969
743
|
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();
|
|
744
|
+
const typing = active && (["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName) || active.isContentEditable);
|
|
745
|
+
const inTerminal = el.terminalHost.contains(active);
|
|
746
|
+
if (event.key === "Escape") {
|
|
747
|
+
// A native Session needs its own Escape key.
|
|
748
|
+
if (inTerminal) return;
|
|
749
|
+
if (typing) { active.blur(); return; }
|
|
750
|
+
if (dockVisible()) { setDockOpen(false); return; }
|
|
751
|
+
if (state.selected) clearSelection();
|
|
989
752
|
return;
|
|
990
753
|
}
|
|
991
|
-
if (
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
|
|
997
|
-
|
|
754
|
+
if (typing || inTerminal || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
755
|
+
const key = event.key.toLowerCase();
|
|
756
|
+
if (key === "/") { event.preventDefault(); el.search.focus(); el.search.select(); }
|
|
757
|
+
else if (key === "r") refreshDashboard();
|
|
758
|
+
else if (key === "o") openOperator();
|
|
759
|
+
else if (key === "d") toggleDock();
|
|
760
|
+
else if (/^[1-5]$/.test(key) && state.detail) switchTab(TABS[Number(key) - 1]);
|
|
998
761
|
});
|
|
999
762
|
|
|
1000
|
-
//
|
|
763
|
+
// --- Boot -------------------------------------------------------------------------
|
|
1001
764
|
function applyStateFromUrl() {
|
|
1002
765
|
const params = readQuery();
|
|
1003
766
|
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;
|
|
767
|
+
state.filter = STATUS_FILTERS.includes(filter) ? filter : "all";
|
|
768
|
+
state.query = params.get("q") || "";
|
|
769
|
+
state.attentionFilter = ATTENTION_KINDS.includes(params.get("attention")) ? params.get("attention") : null;
|
|
1012
770
|
state.catalogAll = params.get("all") === "true";
|
|
1013
|
-
|
|
771
|
+
el.search.value = state.query;
|
|
1014
772
|
const taskId = params.get("task");
|
|
1015
773
|
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();
|
|
774
|
+
if (taskId === state.selected && state.detail) applySection(params.get("section"));
|
|
775
|
+
else selectTask(taskId);
|
|
776
|
+
return;
|
|
1027
777
|
}
|
|
778
|
+
if (state.selected && !canLeaveDetail()) { syncUrl(true); return; }
|
|
779
|
+
state.selected = null;
|
|
780
|
+
state.detail = null;
|
|
781
|
+
el.discussion.replaceChildren();
|
|
782
|
+
delete el.discussion.dataset.taskId;
|
|
783
|
+
updateLayout();
|
|
784
|
+
showOverview();
|
|
1028
785
|
}
|
|
1029
|
-
|
|
1030
786
|
window.addEventListener("popstate", function () {
|
|
1031
787
|
applyStateFromUrl();
|
|
1032
788
|
resetCatalog();
|
|
1033
789
|
});
|
|
1034
|
-
|
|
1035
790
|
i18n.subscribe(function () {
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
791
|
+
theme.render();
|
|
792
|
+
terminal.relabel();
|
|
793
|
+
renderSidebar();
|
|
794
|
+
renderedDiscussionKey = null;
|
|
795
|
+
if (state.detail) renderCurrentDetail(true); else showOverview();
|
|
796
|
+
updateLayout();
|
|
1040
797
|
});
|
|
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
798
|
|
|
799
|
+
updateLayout();
|
|
800
|
+
showOverview();
|
|
1048
801
|
applyStateFromUrl();
|
|
1049
802
|
refreshDashboard();
|
|
1050
803
|
window.setInterval(function () { refreshDashboard({ quiet: true }); }, 5000);
|