@zq-silk/yui 0.2.0 → 0.4.2
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/ARCHITECTURE.md +603 -133
- package/README.md +806 -31
- package/dist/agent/agent.js +2 -1
- package/dist/agent/argumentPolicy.js +3 -1
- package/dist/agent/launchEnvironment.js +106 -0
- package/dist/agent/managedRuntimeEnvironment.js +34 -0
- package/dist/brief/taskBrief.js +11 -1
- package/dist/cli/agentConfigurationPicker.js +287 -0
- package/dist/cli/commandCatalog.js +488 -60
- package/dist/cli/completion.js +146 -22
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli/interactionCandidates.js +53 -15
- package/dist/cli/interactionPolicy.js +267 -30
- package/dist/cli/interactiveSelection.js +6 -2
- package/dist/cli/invocationRouter.js +5 -1
- package/dist/cli/operatorWizard.js +87 -0
- package/dist/cli/roleOptionCatalog.js +1 -0
- package/dist/cli/roleWizard.js +185 -21
- package/dist/cli/updateCommand.js +62 -19
- package/dist/cli/updateOrchestrator.js +539 -0
- package/dist/cli/updatePorts.js +1119 -0
- package/dist/cli/upgradeCommand.js +112 -0
- package/dist/cli.js +1420 -86
- package/dist/commands/agentCommands.js +146 -3
- package/dist/commands/configCommands.js +126 -0
- package/dist/commands/controllerCommands.js +365 -0
- package/dist/commands/globalRoleCommands.js +168 -126
- package/dist/commands/jobCommands.js +18 -8
- package/dist/commands/operatorCommands.js +159 -9
- package/dist/commands/profileCommands.js +203 -0
- package/dist/commands/projectCommands.js +650 -0
- package/dist/commands/roleConfiguration.js +85 -24
- package/dist/commands/roleRuntimeGuard.js +12 -0
- package/dist/commands/roleSkillValidation.js +47 -0
- package/dist/commands/taskActor.js +127 -0
- package/dist/commands/taskCommands.js +4201 -313
- package/dist/commands/taskCompletionGate.js +131 -0
- package/dist/commands/taskContextCommand.js +244 -30
- package/dist/commands/taskInputCommands.js +177 -59
- package/dist/commands/taskIntegrationCommands.js +303 -0
- package/dist/commands/taskOverviewCommand.js +363 -0
- package/dist/commands/taskRoleRuntimeStatus.js +125 -19
- package/dist/commands/textInput.js +15 -0
- package/dist/completion/completionInstaller.js +26 -22
- package/dist/config/yuiConfig.js +4 -3
- package/dist/context/dispatchContext.js +90 -38
- package/dist/context/roleSessionContext.js +119 -0
- package/dist/controller/claudeLifecycleHook.js +203 -0
- package/dist/controller/clientRuntime.js +408 -56
- package/dist/controller/codexLifecycleHook.js +108 -0
- package/dist/controller/controller.js +1089 -32
- package/dist/controller/domainIdentity.js +505 -0
- package/dist/controller/ephemeralResourceReaper.js +131 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
- package/dist/controller/providerHookRunFence.js +127 -0
- package/dist/controller/resourceCleanupLinux.js +286 -0
- package/dist/controller/resourceInventory.js +531 -0
- package/dist/controller/resourceInventoryLinux.js +610 -0
- package/dist/controller/runtime.js +629 -10
- package/dist/controller/runtimeEventInbox.js +564 -0
- package/dist/controller/runtimeEventProcessor.js +248 -0
- package/dist/controller/runtimeLaunchCoordinator.js +477 -0
- package/dist/controller/sessionNotify.js +121 -78
- package/dist/coordination/deadlineScheduler.js +15 -0
- package/dist/coordination/mailboxScheduler.js +108 -0
- package/dist/coordination/workMailbox.js +329 -0
- package/dist/coordination/workMailboxQueue.js +86 -0
- package/dist/core/controllerClient.js +19 -5
- package/dist/core/controllerEndpoint.js +37 -0
- package/dist/core/controllerServer.js +218 -10
- package/dist/core/protocol.js +6 -2
- package/dist/decision/decision.js +2 -1
- package/dist/doctor/doctor.js +681 -32
- package/dist/domain/validation.js +53 -0
- package/dist/errors/cliError.js +5 -3
- package/dist/event/taskEvent.js +7 -3
- package/dist/execution/codexThreadNaming.js +160 -0
- package/dist/execution/executionGroup.js +579 -0
- package/dist/executor/agentAdapter.js +255 -40
- package/dist/executor/agentConfigurationCatalog.js +326 -0
- package/dist/executor/agentConfigurationProbe.js +506 -0
- package/dist/executor/agentExecutor.js +625 -10
- package/dist/executor/codexConfigConflict.js +290 -0
- package/dist/executor/effectiveLaunch.js +340 -0
- package/dist/executor/executorRegistry.js +238 -36
- package/dist/executor/fileRoleLaunchPlanner.js +550 -40
- package/dist/executor/turnCompletion.js +126 -0
- package/dist/input/inputRequest.js +30 -9
- package/dist/integration/changeSet.js +36 -0
- package/dist/integration/checkResult.js +24 -0
- package/dist/integration/gitIntegrationService.js +695 -0
- package/dist/integration/integrationAttempt.js +142 -0
- package/dist/interaction/operatorPresentation.js +96 -0
- package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
- package/dist/lifecycle/exactRunTerminalization.js +572 -0
- package/dist/lifecycle/providerLifecycleMapping.js +190 -0
- package/dist/lifecycle/taskRoleSessionReset.js +124 -0
- package/dist/message/message.js +23 -7
- package/dist/milestone/milestone.js +2 -1
- package/dist/operator/operatorSessionHistory.js +124 -0
- package/dist/output/agentConfigurationPresentation.js +43 -0
- package/dist/output/rolePresentation.js +34 -10
- package/dist/output/terminal.js +8 -0
- package/dist/output/timePresentation.js +55 -0
- package/dist/profile/agentProfile.js +128 -0
- package/dist/repository/gitWorkspace.js +578 -24
- package/dist/repository/project.js +213 -0
- package/dist/repository/taskWorkspaceCoordinator.js +392 -0
- package/dist/repository/taskWorkspacePreparer.js +1688 -191
- package/dist/review/reviewConfig.js +11 -0
- package/dist/review/reviewRound.js +399 -0
- package/dist/review/taskFinalReviewContract.js +90 -0
- package/dist/role/role.js +124 -23
- package/dist/run/agentRun.js +155 -12
- package/dist/run/runIdentity.js +82 -0
- package/dist/runtime/exactControlPlane.js +472 -0
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/lifecycleReservation.js +38 -0
- package/dist/runtime/ports.js +11 -0
- package/dist/runtime/preallocatedNativeSession.js +13 -0
- package/dist/runtime/promptEnvelope.js +30 -0
- package/dist/runtime/runtimeBinding.js +31 -0
- package/dist/runtime/runtimeOwner.js +14 -0
- package/dist/runtime/sessionLaunchRequest.js +62 -0
- package/dist/runtime/sessionTitle.js +54 -0
- package/dist/runtime/taskRuntimeIsolation.js +643 -0
- package/dist/runtime/tmuxAdapters.js +315 -0
- package/dist/runtime/turnCompletion.js +3 -0
- package/dist/runtime/validation.js +23 -0
- package/dist/scheduler/activeRoleRunDelivery.js +342 -32
- package/dist/scheduler/activeTaskProgress.js +63 -0
- package/dist/scheduler/leaderFailure.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +307 -66
- package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
- package/dist/scheduler/operatorNotification.js +44 -2
- package/dist/scheduler/ports.js +28 -1
- package/dist/scheduler/roleRunLiveness.js +131 -25
- package/dist/scheduler/roleRunStall.js +951 -0
- package/dist/scheduler/taskExecutionProjection.js +544 -0
- package/dist/scheduler/wakeupQueue.js +3 -0
- package/dist/setup/setupCommand.js +302 -52
- package/dist/storage/compatibleTaskStore.js +102 -0
- package/dist/storage/migration/baseline.js +78 -0
- package/dist/storage/migration/classifier.js +51 -0
- package/dist/storage/migration/compatibleCodec.js +53 -0
- package/dist/storage/migration/engine.js +147 -0
- package/dist/storage/migration/index.js +33 -0
- package/dist/storage/migration/planner.js +154 -0
- package/dist/storage/migration/productionRegistry.js +486 -0
- package/dist/storage/migration/registry.js +169 -0
- package/dist/storage/migration/report.js +54 -0
- package/dist/storage/migration/types.js +31 -0
- package/dist/storage/storageSchema.js +147 -123
- package/dist/storage/storageVersions.js +11 -0
- package/dist/storage/taskStore.js +1793 -197
- package/dist/storage/upgrade/homeClassification.js +156 -0
- package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
- package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
- package/dist/storage/upgrade/recordVersionScan.js +176 -0
- package/dist/storage/upgrade/recordVersions.js +159 -0
- package/dist/storage/upgrade/switchProgress.js +80 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
- package/dist/storage/upgrade/upgradeReceipt.js +161 -0
- package/dist/storage/upgradeCoordination.js +186 -0
- package/dist/storage/upgradeFence.js +366 -0
- package/dist/task/task.js +132 -26
- package/dist/task/taskRecordReference.js +66 -0
- package/dist/tmux/commandExecutor.js +75 -2
- package/dist/tmux/tmuxManager.js +747 -49
- package/dist/version.js +23 -0
- package/dist/web/assets/assetManifest.js +62 -0
- package/dist/web/assets/client/app.js +631 -0
- package/dist/web/assets/client/components.js +605 -0
- package/dist/web/assets/client/dom.js +14 -0
- package/dist/web/assets/client/format.js +28 -0
- package/dist/web/assets/client/i18n.js +494 -0
- package/dist/web/assets/client/markdown.js +114 -0
- package/dist/web/assets/client/theme.js +32 -0
- package/dist/web/assets/client/view.js +458 -0
- package/dist/web/assets/fontData.js +12 -0
- package/dist/web/assets/fonts.js +12 -0
- package/dist/web/assets/shell.js +114 -0
- package/dist/web/assets/styles/cards.js +135 -0
- package/dist/web/assets/styles/layout.js +47 -0
- package/dist/web/assets/styles/markdown.js +29 -0
- package/dist/web/assets/styles/responsive.js +39 -0
- package/dist/web/assets/styles/tokens.js +101 -0
- package/dist/web/assets/styles/widgets.js +147 -0
- package/dist/web/tmuxWebTerminal.js +158 -0
- package/dist/web/webServer.js +463 -0
- package/dist/web/webSnapshot.js +148 -0
- package/dist/workItem/workItem.js +642 -23
- package/dist/workspace/gitChangeSetCapture.js +86 -0
- package/dist/workspace/workItemChangeSetManager.js +445 -0
- package/dist/worktree/managedWorkspace.js +202 -0
- package/docs/task-local-identity.md +62 -0
- package/i18n/README.zh-CN.md +406 -31
- package/package.json +10 -2
- package/skills/yui-leader/SKILL.md +601 -39
- package/skills/yui-operator/SKILL.md +255 -34
- package/skills/yui-reviewer/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +214 -17
- package/dist/commands/repositoryCommands.js +0 -86
- package/dist/operator/operatorContext.js +0 -66
- package/dist/repository/repository.js +0 -55
- package/dist/scheduler/archivedTaskRuntime.js +0 -12
- package/dist/worktree/roleWorkspace.js +0 -62
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
export const APP_SCRIPT = `
|
|
2
|
+
import { Terminal } from "/assets/vendor/xterm.mjs";
|
|
3
|
+
import { FitAddon } from "/assets/vendor/addon-fit.mjs";
|
|
4
|
+
import { createI18n } from "/assets/js/i18n.js";
|
|
5
|
+
import { createThemeController } from "/assets/js/theme.js";
|
|
6
|
+
import {
|
|
7
|
+
renderError,
|
|
8
|
+
renderFilters,
|
|
9
|
+
renderLoading,
|
|
10
|
+
renderOverview,
|
|
11
|
+
renderTaskDetail,
|
|
12
|
+
renderTasks
|
|
13
|
+
} from "/assets/js/view.js";
|
|
14
|
+
|
|
15
|
+
const elements = {
|
|
16
|
+
locale: document.querySelector("#locale-select"),
|
|
17
|
+
theme: document.querySelector("#theme-select"),
|
|
18
|
+
refresh: document.querySelector("#refresh"),
|
|
19
|
+
operatorTerminal: document.querySelector("#operator-terminal"),
|
|
20
|
+
search: document.querySelector("#search"),
|
|
21
|
+
filters: document.querySelector("#status-filters"),
|
|
22
|
+
tasks: document.querySelector("#task-list"),
|
|
23
|
+
detail: document.querySelector("#detail"),
|
|
24
|
+
mainCol: document.querySelector(".main-col"),
|
|
25
|
+
topbar: document.querySelector(".topbar"),
|
|
26
|
+
detailBack: document.querySelector("#detail-back"),
|
|
27
|
+
detailTabs: document.querySelector("#detail-tabs"),
|
|
28
|
+
pageTitle: document.querySelector("#page-title"),
|
|
29
|
+
toast: document.querySelector("#toast"),
|
|
30
|
+
lastSync: document.querySelector("#last-sync"),
|
|
31
|
+
terminalPanel: document.querySelector("#terminal-panel"),
|
|
32
|
+
terminalHost: document.querySelector("#terminal-host"),
|
|
33
|
+
terminalTitle: document.querySelector("#terminal-title"),
|
|
34
|
+
terminalState: document.querySelector("#terminal-state span"),
|
|
35
|
+
terminalClose: document.querySelector("#terminal-close")
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const token = document.querySelector('meta[name="yui-web-token"]').content;
|
|
39
|
+
const state = {
|
|
40
|
+
tasks: [],
|
|
41
|
+
counts: null,
|
|
42
|
+
attention: [],
|
|
43
|
+
generatedAt: null,
|
|
44
|
+
filter: "all",
|
|
45
|
+
query: "",
|
|
46
|
+
selected: null,
|
|
47
|
+
detail: null,
|
|
48
|
+
detailKey: null
|
|
49
|
+
};
|
|
50
|
+
const VALID_FILTERS = ["all", "active", "draft", "completed", "archived"];
|
|
51
|
+
let terminalSession = null;
|
|
52
|
+
let terminalStateKey = "terminal.closed";
|
|
53
|
+
|
|
54
|
+
const i18n = createI18n(elements.locale);
|
|
55
|
+
createThemeController(elements.theme);
|
|
56
|
+
|
|
57
|
+
// --- URL / history ---------------------------------------------------------
|
|
58
|
+
// Reflect the current view (selected task, status filter, search query) in the
|
|
59
|
+
// query string so the browser back/forward buttons work and the page can be
|
|
60
|
+
// deep-linked / refreshed without losing context.
|
|
61
|
+
function readQuery() {
|
|
62
|
+
return new URLSearchParams(window.location.search);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildSearch(params) {
|
|
66
|
+
const entries = Array.from(params.entries()).filter(function (entry) {
|
|
67
|
+
return entry[1] !== "" && entry[1] !== null && entry[1] !== undefined;
|
|
68
|
+
});
|
|
69
|
+
if (!entries.length) return "";
|
|
70
|
+
return "?" + entries.map(function (entry) {
|
|
71
|
+
return encodeURIComponent(entry[0]) + "=" + encodeURIComponent(entry[1]);
|
|
72
|
+
}).join("&");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function applyUrl(params, options) {
|
|
76
|
+
const replace = options && options.replace;
|
|
77
|
+
const search = buildSearch(params);
|
|
78
|
+
const url = window.location.pathname + search + window.location.hash;
|
|
79
|
+
if (replace) {
|
|
80
|
+
history.replaceState({ task: params.get("task") || null }, "", url);
|
|
81
|
+
} else {
|
|
82
|
+
history.pushState({ task: params.get("task") || null }, "", url);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function urlTaskId() {
|
|
87
|
+
return readQuery().get("task") || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The visible detail section is part of the URL (?task=X§ion=runs) so a
|
|
91
|
+
// refresh or a shared link lands on the same section. Writes always replace —
|
|
92
|
+
// section moves are not history entries.
|
|
93
|
+
function setSectionParam(targetId) {
|
|
94
|
+
if (!state.detail) return;
|
|
95
|
+
const params = readQuery();
|
|
96
|
+
const section = String(targetId).replace(/^detail-/, "");
|
|
97
|
+
if (params.get("section") === section) return;
|
|
98
|
+
params.set("section", section);
|
|
99
|
+
applyUrl(params, { replace: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let sectionParamTimer = null;
|
|
103
|
+
function queueSectionParamSync(targetId) {
|
|
104
|
+
if (sectionParamTimer !== null) window.clearTimeout(sectionParamTimer);
|
|
105
|
+
sectionParamTimer = window.setTimeout(function () {
|
|
106
|
+
sectionParamTimer = null;
|
|
107
|
+
setSectionParam(targetId);
|
|
108
|
+
}, 250);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function syncUrlFromState(options) {
|
|
112
|
+
const params = readQuery();
|
|
113
|
+
if (state.selected) params.set("task", state.selected);
|
|
114
|
+
else {
|
|
115
|
+
params.delete("task");
|
|
116
|
+
params.delete("section");
|
|
117
|
+
}
|
|
118
|
+
if (state.filter && state.filter !== "all") params.set("filter", state.filter);
|
|
119
|
+
else params.delete("filter");
|
|
120
|
+
if (state.query) params.set("q", state.query);
|
|
121
|
+
else params.delete("q");
|
|
122
|
+
applyUrl(params, options);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function detailActions() {
|
|
126
|
+
return {
|
|
127
|
+
answerInput: answerInput,
|
|
128
|
+
openTerminal: openTerminal
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function updateMetrics() {
|
|
133
|
+
elements.lastSync.textContent = state.generatedAt
|
|
134
|
+
? new Intl.DateTimeFormat(i18n.getLocale(), { hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(new Date(state.generatedAt))
|
|
135
|
+
: "—";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function setDetailActive(active) {
|
|
139
|
+
document.body.classList.toggle("detail-active", active);
|
|
140
|
+
elements.detailBack.hidden = !active;
|
|
141
|
+
if (elements.detailTabs) elements.detailTabs.hidden = !active;
|
|
142
|
+
if (elements.pageTitle) {
|
|
143
|
+
if (active) {
|
|
144
|
+
elements.pageTitle.textContent = state.detail
|
|
145
|
+
? state.detail.task.title
|
|
146
|
+
: (state.tasks.find(function (task) { return task.id === state.selected; }) || { title: "…" }).title;
|
|
147
|
+
elements.pageTitle.dataset.i18n = "";
|
|
148
|
+
} else {
|
|
149
|
+
elements.pageTitle.textContent = i18n.t("page.title");
|
|
150
|
+
elements.pageTitle.dataset.i18n = "page.title";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function showOverview() {
|
|
156
|
+
renderOverview(elements.detail, state, i18n.t, i18n.getLocale(), selectTask);
|
|
157
|
+
elements.mainCol.scrollTop = 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fingerprint of the rendered detail payload. Quiet polling re-renders the
|
|
161
|
+
// whole detail on every tick; skipping identical payloads keeps local UI state
|
|
162
|
+
// (expanded blocks, list pages) alive and avoids DOM churn.
|
|
163
|
+
let renderedDetailKey = null;
|
|
164
|
+
|
|
165
|
+
function detailKeyOf(detail) {
|
|
166
|
+
const text = JSON.stringify(detail);
|
|
167
|
+
let hash = 0;
|
|
168
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
169
|
+
hash = (hash * 31 + text.charCodeAt(i)) | 0;
|
|
170
|
+
}
|
|
171
|
+
return text.length + ":" + hash;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderCurrentDetail(force) {
|
|
175
|
+
if (state.detail) {
|
|
176
|
+
const key = i18n.getLocale() + "|" + state.detailKey;
|
|
177
|
+
if (!force && key === renderedDetailKey) return;
|
|
178
|
+
renderTaskDetail(
|
|
179
|
+
elements.detail,
|
|
180
|
+
state.detail,
|
|
181
|
+
i18n.t,
|
|
182
|
+
i18n.getLocale(),
|
|
183
|
+
detailActions()
|
|
184
|
+
);
|
|
185
|
+
renderedDetailKey = key;
|
|
186
|
+
} else if (!state.selected) {
|
|
187
|
+
renderedDetailKey = null;
|
|
188
|
+
showOverview();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderDynamicContent() {
|
|
193
|
+
renderFilters(elements.filters, state, i18n.t, function (filter) {
|
|
194
|
+
state.filter = filter;
|
|
195
|
+
syncUrlFromState({ replace: false });
|
|
196
|
+
renderDynamicContent();
|
|
197
|
+
});
|
|
198
|
+
const savedTaskScroll = elements.tasks.scrollTop;
|
|
199
|
+
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
200
|
+
elements.tasks.scrollTop = savedTaskScroll;
|
|
201
|
+
const preserveScroll = state.detail !== null && elements.mainCol.scrollTop > 0;
|
|
202
|
+
const savedScrollTop = elements.mainCol.scrollTop;
|
|
203
|
+
renderCurrentDetail();
|
|
204
|
+
if (preserveScroll) elements.mainCol.scrollTop = savedScrollTop;
|
|
205
|
+
syncTabHighlight();
|
|
206
|
+
updateMetrics();
|
|
207
|
+
updateStickyOffsets();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function updateStickyOffsets() {
|
|
211
|
+
const root = document.documentElement;
|
|
212
|
+
if (elements.topbar) {
|
|
213
|
+
root.style.setProperty("--topbar-h", elements.topbar.offsetHeight + "px");
|
|
214
|
+
}
|
|
215
|
+
if (elements.detailTabs && !elements.detailTabs.hidden) {
|
|
216
|
+
root.style.setProperty("--tabs-h", elements.detailTabs.offsetHeight + "px");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateActiveTabFromScroll() {
|
|
221
|
+
if (!state.detail || !elements.detailTabs) return;
|
|
222
|
+
const tabs = Array.from(elements.detailTabs.querySelectorAll(".tab"));
|
|
223
|
+
if (!tabs.length) return;
|
|
224
|
+
const root = elements.mainCol;
|
|
225
|
+
const rootTop = root.getBoundingClientRect().top;
|
|
226
|
+
// Sections sit below the sticky topbar + tab bar (see .anchor scroll-margin),
|
|
227
|
+
// so "current" means the last section whose top crossed that stacked offset.
|
|
228
|
+
const stickyH = (elements.topbar ? elements.topbar.offsetHeight : 0)
|
|
229
|
+
+ (elements.detailTabs.hidden ? 0 : elements.detailTabs.offsetHeight);
|
|
230
|
+
const threshold = stickyH + 8;
|
|
231
|
+
let activeId = tabs[0].dataset.target;
|
|
232
|
+
let bestTop = -Infinity;
|
|
233
|
+
tabs.forEach(function (tab) {
|
|
234
|
+
const id = tab.dataset.target;
|
|
235
|
+
if (!id) return;
|
|
236
|
+
const el = root.querySelector("#" + id);
|
|
237
|
+
if (!el) return;
|
|
238
|
+
const top = el.getBoundingClientRect().top - rootTop;
|
|
239
|
+
if (top <= threshold && top > bestTop) {
|
|
240
|
+
bestTop = top;
|
|
241
|
+
activeId = id;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
tabs.forEach(function (tab) {
|
|
245
|
+
tab.classList.toggle("is-active", tab.dataset.target === activeId);
|
|
246
|
+
});
|
|
247
|
+
if (state.detail) queueSectionParamSync(activeId);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function syncTabHighlight() {
|
|
251
|
+
// Compute the active tab from the current scroll position. The module-level
|
|
252
|
+
// scroll listener keeps it in sync afterwards.
|
|
253
|
+
updateActiveTabFromScroll();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function showToast(message) {
|
|
257
|
+
elements.toast.textContent = message;
|
|
258
|
+
elements.toast.classList.add("show");
|
|
259
|
+
window.setTimeout(function () { elements.toast.classList.remove("show"); }, 3200);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function clearSelection() {
|
|
263
|
+
state.selected = null;
|
|
264
|
+
state.detail = null;
|
|
265
|
+
setDetailActive(false);
|
|
266
|
+
syncUrlFromState({ replace: !urlTaskId() });
|
|
267
|
+
showOverview();
|
|
268
|
+
const savedTaskScroll = elements.tasks.scrollTop;
|
|
269
|
+
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
270
|
+
elements.tasks.scrollTop = savedTaskScroll;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function requestJson(path, options) {
|
|
274
|
+
const response = await fetch(path, {
|
|
275
|
+
...options,
|
|
276
|
+
headers: {
|
|
277
|
+
accept: "application/json",
|
|
278
|
+
...(options && options.headers ? options.headers : {})
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
let message = "HTTP " + response.status;
|
|
283
|
+
try {
|
|
284
|
+
const body = await response.json();
|
|
285
|
+
if (body && body.error) message = body.error;
|
|
286
|
+
} catch {}
|
|
287
|
+
throw new Error(message);
|
|
288
|
+
}
|
|
289
|
+
return response.json();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function loadTaskDetail(taskId, showLoading) {
|
|
293
|
+
if (showLoading) {
|
|
294
|
+
renderLoading(elements.detail, i18n.t, "loading.detail");
|
|
295
|
+
elements.mainCol.scrollTop = 0;
|
|
296
|
+
}
|
|
297
|
+
const savedScrollTop = showLoading ? 0 : elements.mainCol.scrollTop;
|
|
298
|
+
const detail = await requestJson("/api/tasks/" + encodeURIComponent(taskId));
|
|
299
|
+
if (state.selected !== taskId) return;
|
|
300
|
+
state.detail = detail;
|
|
301
|
+
state.detailKey = detailKeyOf(detail);
|
|
302
|
+
renderCurrentDetail(true);
|
|
303
|
+
// Reveal the tab bar before measuring/scroll so anchors land correctly.
|
|
304
|
+
setDetailActive(true);
|
|
305
|
+
updateStickyOffsets();
|
|
306
|
+
// A section in the URL wins over the preserved scroll offset.
|
|
307
|
+
const section = readQuery().get("section");
|
|
308
|
+
const anchor = section ? elements.detail.querySelector("#detail-" + section) : null;
|
|
309
|
+
if (anchor) {
|
|
310
|
+
anchor.scrollIntoView({ block: "start" });
|
|
311
|
+
} else {
|
|
312
|
+
elements.mainCol.scrollTop = savedScrollTop;
|
|
313
|
+
}
|
|
314
|
+
syncTabHighlight();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function selectTask(taskId) {
|
|
318
|
+
// Switching tasks drops the previous section; staying on the same task
|
|
319
|
+
// (URL-driven loads, popstate) keeps it.
|
|
320
|
+
if (urlTaskId() !== taskId) {
|
|
321
|
+
const params = readQuery();
|
|
322
|
+
params.delete("section");
|
|
323
|
+
applyUrl(params, { replace: true });
|
|
324
|
+
}
|
|
325
|
+
state.selected = taskId;
|
|
326
|
+
state.detail = null;
|
|
327
|
+
renderedDetailKey = null;
|
|
328
|
+
// When the selection is driven by the URL (initial load or popstate), the
|
|
329
|
+
// URL already reflects the task id, so replace instead of pushing a
|
|
330
|
+
// duplicate history entry.
|
|
331
|
+
syncUrlFromState({ replace: state.selected === urlTaskId() });
|
|
332
|
+
setDetailActive(true);
|
|
333
|
+
const savedTaskScroll = elements.tasks.scrollTop;
|
|
334
|
+
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
335
|
+
elements.tasks.scrollTop = savedTaskScroll;
|
|
336
|
+
try {
|
|
337
|
+
await loadTaskDetail(taskId, true);
|
|
338
|
+
} catch {
|
|
339
|
+
if (state.selected !== taskId) return;
|
|
340
|
+
renderError(elements.detail, i18n.t("errors.detail"));
|
|
341
|
+
showToast(i18n.t("errors.detail"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function answerInput(input, answer) {
|
|
346
|
+
if (!state.detail) return;
|
|
347
|
+
const taskId = state.detail.task.id;
|
|
348
|
+
try {
|
|
349
|
+
await requestJson(
|
|
350
|
+
"/api/tasks/" + encodeURIComponent(taskId)
|
|
351
|
+
+ "/inputs/" + encodeURIComponent(input.id) + "/answer",
|
|
352
|
+
{
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: {
|
|
355
|
+
"content-type": "application/json",
|
|
356
|
+
"x-yui-web-token": token
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify(answer)
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
showToast(i18n.t("input.answered"));
|
|
362
|
+
await refreshDashboard({ quiet: true });
|
|
363
|
+
} catch {
|
|
364
|
+
showToast(i18n.t("errors.answer"));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function refreshDashboard(options) {
|
|
369
|
+
const quiet = options && options.quiet;
|
|
370
|
+
if (!quiet) {
|
|
371
|
+
elements.refresh.disabled = true;
|
|
372
|
+
if (!state.tasks.length) renderLoading(elements.tasks, i18n.t, "loading.dashboard");
|
|
373
|
+
}
|
|
374
|
+
const previousInputs = state.counts ? state.counts.openInputs : null;
|
|
375
|
+
try {
|
|
376
|
+
const dashboard = await requestJson("/api/dashboard");
|
|
377
|
+
state.tasks = dashboard.tasks;
|
|
378
|
+
state.counts = dashboard.counts;
|
|
379
|
+
state.attention = dashboard.attention || [];
|
|
380
|
+
state.generatedAt = dashboard.generatedAt;
|
|
381
|
+
if (state.selected && !state.tasks.some(function (task) { return task.id === state.selected; })) {
|
|
382
|
+
clearSelection();
|
|
383
|
+
} else {
|
|
384
|
+
renderDynamicContent();
|
|
385
|
+
}
|
|
386
|
+
if (previousInputs !== null && dashboard.counts.openInputs > previousInputs) {
|
|
387
|
+
showToast(i18n.t("input.new"));
|
|
388
|
+
}
|
|
389
|
+
if (state.selected && !quiet) {
|
|
390
|
+
try { await loadTaskDetail(state.selected, false); } catch {}
|
|
391
|
+
}
|
|
392
|
+
} catch {
|
|
393
|
+
if (!quiet) {
|
|
394
|
+
renderError(elements.tasks, i18n.t("errors.dashboard"));
|
|
395
|
+
showToast(i18n.t("errors.dashboard"));
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
if (!quiet) elements.refresh.disabled = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function terminalUrl(target, columns, rows) {
|
|
403
|
+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
404
|
+
const parameters = new URLSearchParams({
|
|
405
|
+
scope: target.scope,
|
|
406
|
+
role: target.roleName,
|
|
407
|
+
cols: String(columns),
|
|
408
|
+
rows: String(rows),
|
|
409
|
+
token: token
|
|
410
|
+
});
|
|
411
|
+
if (target.scope === "task") parameters.set("task", target.taskId);
|
|
412
|
+
return protocol + "//" + window.location.host + "/api/terminal?" + parameters.toString();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function setTerminalState(key) {
|
|
416
|
+
terminalStateKey = key;
|
|
417
|
+
elements.terminalState.textContent = i18n.t(key);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function terminalPanelOpen() {
|
|
421
|
+
return !elements.terminalPanel.hidden;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function openTerminalPanel() {
|
|
425
|
+
elements.terminalPanel.hidden = false;
|
|
426
|
+
elements.terminalPanel.setAttribute("aria-hidden", "false");
|
|
427
|
+
document.body.classList.add("terminal-active");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function closeTerminalPanel() {
|
|
431
|
+
disposeTerminal();
|
|
432
|
+
document.body.classList.remove("terminal-active");
|
|
433
|
+
elements.terminalPanel.setAttribute("aria-hidden", "true");
|
|
434
|
+
elements.terminalPanel.hidden = true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function disposeTerminal() {
|
|
438
|
+
if (!terminalSession) return;
|
|
439
|
+
const current = terminalSession;
|
|
440
|
+
terminalSession = null;
|
|
441
|
+
current.resizeObserver.disconnect();
|
|
442
|
+
current.input.dispose();
|
|
443
|
+
current.socket.close();
|
|
444
|
+
current.terminal.dispose();
|
|
445
|
+
elements.terminalHost.replaceChildren();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function openTerminal(target) {
|
|
449
|
+
disposeTerminal();
|
|
450
|
+
elements.terminalTitle.textContent = target.scope === "task"
|
|
451
|
+
? target.taskId + " / " + target.roleName
|
|
452
|
+
: target.roleName;
|
|
453
|
+
setTerminalState("terminal.connecting");
|
|
454
|
+
openTerminalPanel();
|
|
455
|
+
|
|
456
|
+
const terminal = new Terminal({
|
|
457
|
+
cursorBlink: true,
|
|
458
|
+
scrollback: 0,
|
|
459
|
+
convertEol: false,
|
|
460
|
+
fontFamily: '"IBM Plex Mono","JetBrains Mono","SFMono-Regular",Consolas,monospace',
|
|
461
|
+
fontSize: 13,
|
|
462
|
+
theme: {
|
|
463
|
+
background: "#080b11",
|
|
464
|
+
foreground: "#e8eef6",
|
|
465
|
+
cursor: "#49d6ff",
|
|
466
|
+
selectionBackground: "#264b5d"
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
const fit = new FitAddon();
|
|
470
|
+
terminal.loadAddon(fit);
|
|
471
|
+
terminal.open(elements.terminalHost);
|
|
472
|
+
fit.fit();
|
|
473
|
+
|
|
474
|
+
let writable = false;
|
|
475
|
+
const socket = new WebSocket(terminalUrl(target, terminal.cols, terminal.rows));
|
|
476
|
+
const input = terminal.onData(function (data) {
|
|
477
|
+
if (writable && socket.readyState === WebSocket.OPEN) {
|
|
478
|
+
socket.send(JSON.stringify({ type: "input", data: data }));
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const resizeObserver = new ResizeObserver(function () {
|
|
482
|
+
if (!terminalSession || terminalSession.terminal !== terminal) return;
|
|
483
|
+
fit.fit();
|
|
484
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
485
|
+
socket.send(JSON.stringify({
|
|
486
|
+
type: "resize",
|
|
487
|
+
columns: terminal.cols,
|
|
488
|
+
rows: terminal.rows
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
resizeObserver.observe(elements.terminalHost);
|
|
493
|
+
|
|
494
|
+
socket.addEventListener("message", function (event) {
|
|
495
|
+
let message;
|
|
496
|
+
try {
|
|
497
|
+
message = JSON.parse(event.data);
|
|
498
|
+
} catch {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (message.type === "ready") {
|
|
502
|
+
writable = !message.readOnly;
|
|
503
|
+
setTerminalState(writable ? "terminal.writable" : "terminal.readOnly");
|
|
504
|
+
if (message.history && message.history.limit < message.history.target) {
|
|
505
|
+
const number = new Intl.NumberFormat(i18n.getLocale());
|
|
506
|
+
showToast(
|
|
507
|
+
i18n.t("terminal.historyLimited")
|
|
508
|
+
.replace("{current}", number.format(message.history.limit))
|
|
509
|
+
.replace("{target}", number.format(message.history.target))
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
terminal.focus();
|
|
513
|
+
} else if (message.type === "data") {
|
|
514
|
+
terminal.write(message.data);
|
|
515
|
+
} else if (message.type === "exit") {
|
|
516
|
+
setTerminalState("terminal.closed");
|
|
517
|
+
} else if (message.type === "error") {
|
|
518
|
+
setTerminalState("terminal.error");
|
|
519
|
+
terminal.writeln("\\r\\n" + message.message);
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
socket.addEventListener("close", function () {
|
|
523
|
+
writable = false;
|
|
524
|
+
setTerminalState("terminal.closed");
|
|
525
|
+
});
|
|
526
|
+
socket.addEventListener("error", function () {
|
|
527
|
+
writable = false;
|
|
528
|
+
setTerminalState("terminal.error");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
terminalSession = { terminal, socket, input, resizeObserver };
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (elements.detailTabs) {
|
|
535
|
+
elements.detailTabs.addEventListener("click", function (event) {
|
|
536
|
+
const tab = event.target && event.target.closest ? event.target.closest(".tab") : null;
|
|
537
|
+
if (!tab) return;
|
|
538
|
+
const targetId = tab.dataset.target;
|
|
539
|
+
if (!targetId || !elements.detail) return;
|
|
540
|
+
const section = elements.detail.querySelector("#" + targetId);
|
|
541
|
+
if (!section) return;
|
|
542
|
+
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
543
|
+
setSectionParam(targetId);
|
|
544
|
+
elements.detailTabs.querySelectorAll(".tab").forEach(function (other) {
|
|
545
|
+
other.classList.toggle("is-active", other === tab);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
elements.search.addEventListener("input", function () {
|
|
551
|
+
state.query = elements.search.value;
|
|
552
|
+
syncUrlFromState({ replace: true });
|
|
553
|
+
const savedTaskScroll = elements.tasks.scrollTop;
|
|
554
|
+
renderTasks(elements.tasks, state, i18n.t, i18n.getLocale(), selectTask);
|
|
555
|
+
elements.tasks.scrollTop = savedTaskScroll;
|
|
556
|
+
});
|
|
557
|
+
elements.refresh.addEventListener("click", function () { refreshDashboard(); });
|
|
558
|
+
elements.operatorTerminal.addEventListener("click", function () {
|
|
559
|
+
openTerminal({ scope: "global", roleName: "operator" });
|
|
560
|
+
});
|
|
561
|
+
elements.detailBack.addEventListener("click", clearSelection);
|
|
562
|
+
elements.terminalClose.addEventListener("click", closeTerminalPanel);
|
|
563
|
+
document.addEventListener("keydown", function (event) {
|
|
564
|
+
const active = document.activeElement;
|
|
565
|
+
const typing = active && ["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName);
|
|
566
|
+
if (event.key === "Escape" && terminalPanelOpen()) {
|
|
567
|
+
closeTerminalPanel();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (event.key === "Escape" && state.selected) {
|
|
571
|
+
clearSelection();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (typing) return;
|
|
575
|
+
if (event.key === "/") {
|
|
576
|
+
event.preventDefault();
|
|
577
|
+
elements.search.focus();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (event.key.toLowerCase() === "o" && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
581
|
+
openTerminal({ scope: "global", roleName: "operator" });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (event.key.toLowerCase() === "r" && !event.metaKey && !event.ctrlKey && !event.altKey && !terminalPanelOpen()) {
|
|
585
|
+
refreshDashboard();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Restore view state from the URL on load and on browser back/forward.
|
|
590
|
+
function applyStateFromUrl() {
|
|
591
|
+
const params = readQuery();
|
|
592
|
+
const filter = params.get("filter");
|
|
593
|
+
if (filter && VALID_FILTERS.indexOf(filter) !== -1) {
|
|
594
|
+
state.filter = filter;
|
|
595
|
+
} else {
|
|
596
|
+
state.filter = "all";
|
|
597
|
+
}
|
|
598
|
+
const query = params.get("q");
|
|
599
|
+
state.query = query || "";
|
|
600
|
+
if (elements.search) elements.search.value = state.query;
|
|
601
|
+
const taskId = params.get("task");
|
|
602
|
+
if (taskId) {
|
|
603
|
+
selectTask(taskId);
|
|
604
|
+
} else {
|
|
605
|
+
state.selected = null;
|
|
606
|
+
state.detail = null;
|
|
607
|
+
setDetailActive(false);
|
|
608
|
+
showOverview();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
window.addEventListener("popstate", function () {
|
|
613
|
+
applyStateFromUrl();
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
i18n.subscribe(function () {
|
|
617
|
+
renderDynamicContent();
|
|
618
|
+
if (terminalSession) setTerminalState(terminalStateKey);
|
|
619
|
+
if (!state.selected && elements.pageTitle) elements.pageTitle.textContent = i18n.t("page.title");
|
|
620
|
+
});
|
|
621
|
+
showOverview();
|
|
622
|
+
|
|
623
|
+
// Scroll-spy: keep the detail tab bar in sync with the visible section.
|
|
624
|
+
// Bound once on the scroll container; it reads tabs/sections from the live DOM
|
|
625
|
+
// so it survives detail re-renders.
|
|
626
|
+
elements.mainCol.addEventListener("scroll", updateActiveTabFromScroll, { passive: true });
|
|
627
|
+
|
|
628
|
+
applyStateFromUrl();
|
|
629
|
+
refreshDashboard();
|
|
630
|
+
window.setInterval(function () { refreshDashboard({ quiet: true }); }, 5000);
|
|
631
|
+
`;
|