@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.
Files changed (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. 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&section=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
+ `;