@zq-silk/yui 2.16.0 → 2.18.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.
Files changed (118) hide show
  1. package/dist/cli/commandCatalog.js +19 -12
  2. package/dist/cli/interactionCandidates.js +20 -5
  3. package/dist/cli/interactionPolicy.js +5 -4
  4. package/dist/cli/roleWizard.js +7 -0
  5. package/dist/commands/controllerCommands.js +13 -4
  6. package/dist/commands/durableJobCommands.js +50 -3
  7. package/dist/commands/globalRoleCommands.js +31 -14
  8. package/dist/commands/inboxCommands.js +1 -0
  9. package/dist/commands/operatorCommands.js +12 -2
  10. package/dist/commands/profileCommands.js +19 -2
  11. package/dist/commands/roleConfiguration.js +3 -0
  12. package/dist/commands/taskCommands.js +16 -0
  13. package/dist/commands/taskRoleRuntimeStatus.js +3 -0
  14. package/dist/controlPlaneCli.js +21 -4
  15. package/dist/controller/agentJobControl.js +9 -1
  16. package/dist/controller/clientRuntime.js +34 -13
  17. package/dist/controller/controller.js +35 -22
  18. package/dist/controller/diagnosticJournal.js +214 -0
  19. package/dist/controller/fileSchedulerStoreAdapter.js +174 -105
  20. package/dist/controller/globalInputDelivery.js +19 -9
  21. package/dist/controller/globalRuntimeAttention.js +11 -3
  22. package/dist/controller/jobControl.js +1 -1
  23. package/dist/controller/jobDefinitionControl.js +24 -13
  24. package/dist/controller/jobSupervisor.js +4 -0
  25. package/dist/controller/providerRetryAdmission.js +9 -0
  26. package/dist/controller/providerRetryDelivery.js +16 -0
  27. package/dist/controller/runtime.js +80 -4
  28. package/dist/controller/runtimeLaunchCoordinator.js +3 -1
  29. package/dist/controller/workerSchedulerStore.js +24 -0
  30. package/dist/core/boundedRpc.js +4 -0
  31. package/dist/core/controllerServer.js +1 -1
  32. package/dist/core/controllerTelemetry.js +92 -19
  33. package/dist/executor/fileRoleLaunchPlanner.js +3 -0
  34. package/dist/job/globalShell.js +51 -0
  35. package/dist/job/jobBusinessResult.js +4 -3
  36. package/dist/job/jobDefinition.js +6 -1
  37. package/dist/job/jobRunner.js +9 -0
  38. package/dist/job/scheduleService.js +2 -0
  39. package/dist/message/globalInboxRecovery.js +30 -19
  40. package/dist/message/globalProviderRetry.js +7 -1
  41. package/dist/message/messageContinuation.js +4 -0
  42. package/dist/output/rolePresentation.js +3 -0
  43. package/dist/profile/agentProfile.js +8 -0
  44. package/dist/profile/agentProfileRuntime.js +16 -1
  45. package/dist/review/reviewDecision.js +6 -0
  46. package/dist/role/availability.js +28 -0
  47. package/dist/role/role.js +6 -0
  48. package/dist/runtime/builtinAgentErrorMappers.js +4 -0
  49. package/dist/runtime/codexNativeAccess.js +21 -4
  50. package/dist/runtime/globalSessionHealth.js +39 -0
  51. package/dist/runtime/tmuxAdapters.js +5 -0
  52. package/dist/scheduler/activeRoleRunDelivery.js +9 -0
  53. package/dist/scheduler/leaderWakeupProcessor.js +8 -1
  54. package/dist/scheduler/roleRunLiveness.js +3 -0
  55. package/dist/scheduler/roleRunStall.js +10 -10
  56. package/dist/storage/persistenceWorker.js +41 -19
  57. package/dist/storage/sqliteSchema.js +43 -9
  58. package/dist/storage/sqliteStore.js +86 -8
  59. package/dist/storage/storageVersions.js +1 -1
  60. package/dist/storage/storeRpc.js +25 -11
  61. package/dist/storage/taskReadRevisionSchema.js +39 -0
  62. package/dist/web/assets/client/app/catalog.js +0 -29
  63. package/dist/web/assets/client/app/jobs.js +115 -0
  64. package/dist/web/assets/client/app/main.js +32 -28
  65. package/dist/web/assets/client/app/manage.js +1 -0
  66. package/dist/web/assets/client/app/settings.js +1 -5
  67. package/dist/web/assets/client/app/taskView.js +7 -11
  68. package/dist/web/assets/client/i18n/en.js +265 -36
  69. package/dist/web/assets/client/i18n/zh-CN.js +264 -35
  70. package/dist/web/assets/client/index.js +4 -0
  71. package/dist/web/assets/client/layout/manageShell.js +61 -15
  72. package/dist/web/assets/client/layout/router.js +2 -1
  73. package/dist/web/assets/client/layout/shortcuts.js +2 -2
  74. package/dist/web/assets/client/layout/workspace.js +99 -112
  75. package/dist/web/assets/client/lib/prefs.js +1 -1
  76. package/dist/web/assets/client/ui/primitives.js +15 -2
  77. package/dist/web/assets/client/views/dock/conversation.js +421 -123
  78. package/dist/web/assets/client/views/dock/session.js +229 -0
  79. package/dist/web/assets/client/views/dock/terminal.js +48 -39
  80. package/dist/web/assets/client/views/jobs.js +1042 -160
  81. package/dist/web/assets/client/views/overview.js +2 -2
  82. package/dist/web/assets/client/views/sidebar.js +3 -0
  83. package/dist/web/assets/client/views/task/page.js +2 -12
  84. package/dist/web/assets/shared/manageNav.js +3 -1
  85. package/dist/web/assets/shell/appRail.js +2 -1
  86. package/dist/web/assets/shell/dock.js +9 -27
  87. package/dist/web/assets/shell/manage.js +7 -2
  88. package/dist/web/assets/shell/models.js +4 -3
  89. package/dist/web/assets/shell/settingsContent.js +0 -3
  90. package/dist/web/assets/shell/sidebar.js +0 -1
  91. package/dist/web/assets/styles/views/conversation.js +96 -17
  92. package/dist/web/assets/styles/views/dock.js +17 -5
  93. package/dist/web/assets/styles/views/jobs.js +301 -12
  94. package/dist/web/assets/styles/views/sidebar.js +1 -2
  95. package/dist/web/assets/styles/views/task.js +1 -2
  96. package/dist/web/assets/styles/views/terminal.js +12 -8
  97. package/dist/web/webConversation.js +1 -0
  98. package/dist/web/webServer.js +28 -1
  99. package/dist/web/webSettings.js +10 -2
  100. package/docs/global-shell-jobs.md +78 -0
  101. package/docs/job-runs.md +6 -3
  102. package/docs/managed-turn-and-session-runtime.md +84 -0
  103. package/docs/managed-turn-and-session-runtime.zh-CN.md +57 -0
  104. package/docs/release-workflow.md +67 -0
  105. package/docs/release-workflow.zh-CN.md +48 -0
  106. package/docs/roles-and-configuration.md +45 -1
  107. package/docs/roles-and-configuration.zh-CN.md +35 -0
  108. package/docs/storage-baseline.md +19 -1
  109. package/docs/storage-baseline.zh-CN.md +10 -1
  110. package/docs/testing/review-evaluation.md +35 -1
  111. package/docs/testing/review-evaluation.zh-CN.md +25 -1
  112. package/docs/testing/verification-levels.md +2 -2
  113. package/docs/testing/verification-levels.zh-CN.md +2 -2
  114. package/package.json +1 -1
  115. package/skills/yui-leader/VERSION +1 -1
  116. package/skills/yui-leader/references/execution.md +19 -8
  117. package/skills/yui-reviewer/SKILL.md +13 -2
  118. package/skills/yui-reviewer/VERSION +1 -1
@@ -1,9 +1,10 @@
1
1
  import { BREAKPOINTS, DOCK } from "../../shared/geometry.js";
2
2
  export const WORKSPACE_SCRIPT = String.raw `
3
- // The sidebar | center | dock workspace. The dock holds the Task discussion
4
- // or a live native Session; on desktop it is a resizable column (at the end or
5
- // in the center), below the narrow breakpoint a full-screen sheet opened
6
- // explicitly per Task. Everything here is view state and browser preference.
3
+ // The sidebar | center | dock workspace. The dock holds the session page
4
+ // (one Role and Session, as chat or terminal), the Task discussion or the
5
+ // preview; on desktop it is a resizable column (at the end or in the center),
6
+ // below the narrow breakpoint a full-screen sheet opened explicitly per Task.
7
+ // Everything here is view state and browser preference.
7
8
  import { readPreference, writePreference, readSessionAccessMode } from "/assets/js/lib/prefs.js";
8
9
  import { markSelected, h, icon, clear } from "/assets/js/lib/dom.js";
9
10
  import { createSizing } from "/assets/js/layout/sizing.js";
@@ -12,19 +13,21 @@ import { layoutTransition, leave } from "/assets/js/lib/motion.js";
12
13
  const NARROW_QUERY = "(max-width: ${BREAKPOINTS.narrow}px)";
13
14
  const DOCK_INITIAL = ${DOCK.initial};
14
15
 
15
- // deps: { el, state, t, terminal, conversation, preview, sessionOpened }
16
+ // deps: { el, state, t, session, preview }
16
17
  // el the shell elements (dock, dividers, sidebar, the dock's target
17
- // menu, and the page's [data-dock-toggle] / [data-operator-toggle]
18
+ // menu, and the page's [data-panel-toggle] / legacy [data-operator-toggle]
18
19
  // buttons inside el.detail)
19
20
  // state read-only here: state.selected
20
- // terminal the dock's native Session (views/dock/terminal)
21
- // sessionOpened() a Session was just opened in the dock; the caller
22
- // refreshes the Session target chips (app/taskView).
23
- // The dock state (open, mode, side, width, sheet) is private to this module.
21
+ // session the dock's session page (views/dock/session): owner, selected
22
+ // Session and its chat / terminal mode
23
+ // The dock state (open, view, side, width, sheet) and the Role list offered
24
+ // by the target menu are private to this module.
24
25
  export function createWorkspace(deps) {
25
26
  const ws = {
26
27
  deps: deps,
27
28
  dock: readDockState(),
29
+ // Roles the target menu offers besides Operator (setTargets, app/taskView).
30
+ targets: [],
28
31
  // Task list width: null keeps the CSS default (--sidebar-w); otherwise
29
32
  // the width the user dragged to.
30
33
  sidebar: { width: Number(readPreference("yui.sidebar.width", "")) || null },
@@ -38,30 +41,24 @@ export function createWorkspace(deps) {
38
41
  dockVisible: function () { return dockVisible(ws); },
39
42
  updateLayout: function () { updateLayout(ws); },
40
43
  syncDockButtons: function () { syncDockButtons(ws); },
44
+ setTargets: function (targets) { ws.targets = targets; drawTarget(ws); },
41
45
  setDockOpen: function (open) { glide(function () { setDockOpen(ws, open); }); },
42
46
  toggleDock: function (mode) { glide(function () { toggleDock(ws, mode); }); },
43
- openSession: function (target) { openSession(ws, target); },
47
+ // Shortcuts into the session page: a Role's live Session opens in the
48
+ // terminal mode, a continuation of the Leader in the chat mode.
49
+ openSession: function (target) { openSessionPage(ws, target, { mode: "terminal", current: true }); },
44
50
  openConversation: function (target, materials) {
45
- ws.deps.terminal.close();
46
- showDock(ws, true);
47
- ws.dock.mode = "conversation";
48
- updateLayout(ws);
49
- ws.deps.conversation.open(target, { current: true, materials: materials });
50
- syncDockButtons(ws);
51
+ openSessionPage(ws, target, { mode: "chat", current: true, ...(materials ? { materials: materials } : {}) });
51
52
  },
52
53
  openOperator: function () { glide(function () { openOperator(ws); }); },
53
54
  toggleOperator: function () { glide(function () { toggleOperator(ws); }); },
54
55
  // A newly selected Task starts with the narrow-screen sheet closed.
55
56
  enterTask: function () {
56
- ws.deps.terminal.close();
57
- ws.terminalTarget = { scope: "task", taskId: ws.deps.state.selected, roleName: "leader" };
58
57
  ws.dock.sheet = false;
59
- ws.dock.mode = preferredMode();
60
- ws.deps.conversation.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
61
- pauseHiddenConversation(ws);
62
- if (ws.dock.mode === "session" && !ws.narrow.matches && ws.dock.open) {
63
- ws.deps.terminal.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
64
- }
58
+ ws.dock.mode = "conversation";
59
+ ws.deps.session.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" },
60
+ { current: true, mode: preferredMode() });
61
+ pauseHiddenSession(ws);
65
62
  syncDockButtons(ws);
66
63
  },
67
64
  leaveTask: function () { leaveTask(ws); }
@@ -79,6 +76,8 @@ function readDockState() {
79
76
  return {
80
77
  // The panel starts closed on every load and opens only on request.
81
78
  open: false,
79
+ // Which dock view: the session page ("conversation"), the Task
80
+ // discussion or the preview.
82
81
  mode: "conversation",
83
82
  center: readPreference("yui.dock.side", "end") === "center",
84
83
  width: Number(readPreference("yui.dock.width", String(DOCK_INITIAL))) || DOCK_INITIAL,
@@ -88,22 +87,22 @@ function readDockState() {
88
87
  };
89
88
  }
90
89
 
90
+ // The Settings preference picks the session page's starting mode.
91
91
  function preferredMode() {
92
- return readSessionAccessMode() === "native" ? "session" : "conversation";
92
+ return readSessionAccessMode() === "native" ? "terminal" : "chat";
93
93
  }
94
94
 
95
95
  function dockVisible(ws) {
96
96
  const dock = ws.dock;
97
97
  if (ws.narrow.matches ? !dock.sheet : !dock.open) return false;
98
- if (ws.deps.state.selected) return true;
99
- if (dock.mode === "conversation") return true;
100
- return dock.mode === "session" && !!ws.deps.terminal.current();
98
+ return !!ws.deps.state.selected || dock.mode === "conversation";
101
99
  }
102
100
 
103
101
  function updateLayout(ws) {
104
102
  const el = ws.deps.el;
105
103
  const dock = ws.dock;
106
104
  const selected = !!ws.deps.state.selected;
105
+ if (!selected && dock.mode !== "conversation") dock.mode = "conversation";
107
106
  const visible = dockVisible(ws);
108
107
  document.body.classList.toggle("task-open", selected);
109
108
  document.body.classList.toggle("dock-open", visible);
@@ -111,7 +110,6 @@ function updateLayout(ws) {
111
110
  revealDock(ws, visible);
112
111
  el.divider.hidden = !visible;
113
112
  el.discussion.hidden = dock.mode !== "discussion";
114
- el.session.hidden = dock.mode !== "session";
115
113
  el.conversation.hidden = dock.mode !== "conversation";
116
114
  el.preview.hidden = dock.mode !== "preview";
117
115
  el.dockTabPreview.setAttribute("aria-selected", String(dock.mode === "preview"));
@@ -120,7 +118,6 @@ function updateLayout(ws) {
120
118
  else ws.deps.preview.close();
121
119
  el.dockTabConversation.setAttribute("aria-selected", String(dock.mode === "conversation"));
122
120
  el.dockTabDiscussion.setAttribute("aria-selected", String(dock.mode === "discussion"));
123
- el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
124
121
  el.dockTabDiscussion.disabled = !selected;
125
122
  el.dockSwap.setAttribute("aria-pressed", String(dock.center));
126
123
  ws.sizing.applySidebarWidth();
@@ -149,21 +146,21 @@ function revealDock(ws, visible) {
149
146
  function syncDockButtons(ws) {
150
147
  const el = ws.deps.el;
151
148
  const visible = dockVisible(ws);
152
- markSelected(el.detail.querySelectorAll("[data-dock-toggle]"), "aria-pressed", function (button) {
153
- return visible && ws.dock.mode === button.dataset.dockToggle;
149
+ el.detail.querySelectorAll("[data-panel-toggle]").forEach(function (button) {
150
+ button.setAttribute("aria-expanded", String(visible));
151
+ button.setAttribute("aria-label", ws.deps.t(visible ? "dock.hide" : "dock.show"));
154
152
  });
155
153
  markSelected(el.detail.querySelectorAll("[data-operator-toggle]"), "aria-pressed", function () { return operatorShown(ws); });
156
154
  drawTarget(ws);
157
155
  }
158
156
 
159
157
  // --- Conversation target -----------------------------------------------------
160
- // The dock header names who the panel talks to. Operator is always offered;
161
- // a selected Task adds its Leader. Other Roles use the global message dialog.
158
+ // The dock header names who the session page talks to; it is the only Role
159
+ // selector for both chat and terminal. Operator is always offered; a selected
160
+ // Task adds its Roles. Other global Roles use the global message dialog.
162
161
  function currentTarget(ws) {
163
- const mode = ws.dock.mode;
164
162
  const selected = ws.deps.state.selected;
165
- if (mode === "session") return ws.deps.terminal.current() || ws.terminalTarget || null;
166
- if (mode === "conversation") return ws.deps.conversation.current();
163
+ if (ws.dock.mode === "conversation") return ws.deps.session.current();
167
164
  return selected ? { scope: "task", taskId: selected, roleName: "leader" } : null;
168
165
  }
169
166
 
@@ -177,45 +174,57 @@ function sameTarget(a, b) {
177
174
  return !!a && !!b && a.scope === b.scope && a.roleName === b.roleName && (a.scope !== "task" || a.taskId === b.taskId);
178
175
  }
179
176
 
177
+ function targetOptions(ws, current) {
178
+ const selected = ws.deps.state.selected;
179
+ const options = [{ scope: "global", roleName: "operator" }];
180
+ if (selected) {
181
+ const roles = ws.targets.filter(function (target) { return target.scope === "task" && target.taskId === selected; });
182
+ if (!roles.some(function (target) { return target.roleName === "leader"; })) options.push({ scope: "task", taskId: selected, roleName: "leader" });
183
+ roles.forEach(function (target) { options.push(target); });
184
+ }
185
+ if (current && !options.some(function (option) { return sameTarget(option, current); })) options.push(current);
186
+ return options;
187
+ }
188
+
180
189
  function drawTarget(ws) {
181
190
  const el = ws.deps.el;
182
191
  const current = currentTarget(ws);
183
192
  el.dockTargetLabel.textContent = targetLabel(ws, current);
184
193
  el.dockTargetIcon.replaceChildren(icon(current && current.scope === "task" ? "target" : "broadcast"));
185
- const options = [{ scope: "global", roleName: "operator" }];
186
- if (ws.deps.state.selected) options.push({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
187
- if (current && !options.some(function (option) { return sameTarget(option, current); })) options.push(current);
194
+ const options = targetOptions(ws, current);
188
195
  const signature = JSON.stringify([options, current, ws.deps.t("actions.operator")]);
189
196
  if (el.dockTargetOptions.dataset.signature === signature) return;
190
197
  el.dockTargetOptions.dataset.signature = signature;
191
198
  clear(el.dockTargetOptions);
192
199
  options.forEach(function (target) {
200
+ const chosen = sameTarget(target, current);
193
201
  el.dockTargetOptions.append(h("button.dock-target-item", {
194
- type: "button", "aria-pressed": String(sameTarget(target, current)),
202
+ type: "button", "aria-pressed": String(chosen),
195
203
  onclick: function () { chooseTarget(ws, target); }
196
- }, icon(target.scope === "task" ? "target" : "broadcast"), h("span", null, targetLabel(ws, target)),
197
- sameTarget(target, current) ? icon("check", "icon-sm dock-target-check") : null));
204
+ }, target.scope === "task" ? h("i.dock-target-dot" + (target.live ? ".is-live" : ""), { "aria-hidden": "true" }) : icon("broadcast"),
205
+ h("span", null, targetLabel(ws, target)),
206
+ chosen ? icon("check", "icon-sm dock-target-check") : null));
198
207
  });
199
208
  }
200
209
 
201
- // Keep the current view (live terminal or structured conversation) and switch
202
- // only its owner; Task-only views fall back to the conversation.
210
+ // Keep the session page's mode (chat or terminal) and switch only its owner;
211
+ // from the discussion or preview, the session page opens again.
203
212
  function chooseTarget(ws, target) {
204
213
  ws.deps.el.dockTarget.open = false;
205
- if (ws.dock.mode === "session") { openSession(ws, target); return; }
206
- ws.deps.terminal.close();
214
+ openSessionPage(ws, { scope: target.scope, roleName: target.roleName, ...(target.scope === "task" ? { taskId: target.taskId } : {}) }, {});
215
+ }
216
+
217
+ function openSessionPage(ws, target, options) {
207
218
  showDock(ws, true);
208
219
  ws.dock.mode = "conversation";
209
220
  updateLayout(ws);
210
- ws.deps.conversation.open(target);
221
+ ws.deps.session.open(target, options);
211
222
  syncDockButtons(ws);
212
223
  }
213
224
 
214
225
  // The dock currently shows the global operator Session.
215
226
  function operatorShown(ws) {
216
- if (ws.dock.mode === "conversation") return dockVisible(ws) && ws.deps.conversation.current()?.scope === "global";
217
- const current = ws.deps.terminal.current();
218
- return dockVisible(ws) && ws.dock.mode === "session" && !!current && current.scope === "global";
227
+ return dockVisible(ws) && ws.dock.mode === "conversation" && ws.deps.session.current()?.scope === "global";
219
228
  }
220
229
 
221
230
  function showDock(ws, open) {
@@ -226,70 +235,54 @@ function showDock(ws, open) {
226
235
  }
227
236
 
228
237
  function setDockOpen(ws, open) {
238
+ // Closing with focus inside the panel returns it to the page's panel switch
239
+ // rather than leaving it on a hidden element.
240
+ const active = document.activeElement;
241
+ const focusInside = !open && !!active && ws.deps.el.dock.contains(active);
229
242
  showDock(ws, open);
230
- if (!open) {
231
- ws.terminalTarget = ws.deps.terminal.current() || ws.terminalTarget;
232
- ws.deps.terminal.close();
233
- }
234
- if (!open) ws.deps.conversation.close();
235
- if (open && ws.dock.mode === "conversation") ws.deps.conversation.reconnect();
236
- if (open && ws.dock.mode === "session") {
237
- // Keep the selected owner, not a Session identity cached before hiding.
238
- const target = ws.terminalTarget || { scope: "global", roleName: "operator" };
239
- ws.deps.terminal.open({ scope: target.scope, roleName: target.roleName,
240
- ...(target.scope === "task" ? { taskId: target.taskId } : {}) });
243
+ // Hiding stops polling and detaches the terminal; showing resumes the same
244
+ // owner and selected Session in the same mode.
245
+ if (!open) ws.deps.session.close();
246
+ else if (ws.dock.mode === "conversation") {
247
+ if (ws.deps.session.current()) ws.deps.session.reconnect();
248
+ else ws.deps.session.open({ scope: "global", roleName: "operator" }, { mode: preferredMode() });
241
249
  }
242
250
  updateLayout(ws);
251
+ if (focusInside) {
252
+ const toggle = ws.deps.el.detail.querySelector("[data-panel-toggle]");
253
+ if (toggle) toggle.focus();
254
+ }
243
255
  }
244
256
 
245
257
  function setDockMode(ws, mode) {
246
- if (mode === "preview" && ws.deps.state.selected) {
247
- ws.deps.terminal.close();
248
- ws.deps.conversation.close();
249
- ws.dock.mode = "preview";
250
- updateLayout(ws);
251
- return;
252
- }
258
+ if (mode === ws.dock.mode) return;
253
259
  if (mode === "conversation") {
254
- const target = ws.deps.terminal.current() || ws.deps.conversation.current();
255
- ws.deps.terminal.close();
256
260
  ws.dock.mode = mode;
257
261
  updateLayout(ws);
258
- ws.deps.conversation.open(target ? { scope: target.scope, ...(target.scope === "task" ? { taskId: target.taskId } : {}), roleName: target.roleName } : ws.deps.state.selected
262
+ if (ws.deps.session.current()) ws.deps.session.reconnect();
263
+ else ws.deps.session.open(ws.deps.state.selected
259
264
  ? { scope: "task", taskId: ws.deps.state.selected, roleName: "leader" }
260
- : { scope: "global", roleName: "operator" });
265
+ : { scope: "global", roleName: "operator" }, { mode: preferredMode() });
261
266
  syncDockButtons(ws);
262
267
  return;
263
268
  }
264
- if (mode === "session") {
265
- const target = ws.deps.conversation.selection();
266
- if (!target) return;
267
- openSession(ws, target);
268
- return;
269
- }
270
- ws.deps.terminal.close();
271
- ws.deps.conversation.close();
272
- ws.dock.mode = mode === "session" || !ws.deps.state.selected ? "session" : "discussion";
269
+ if (!ws.deps.state.selected) return;
270
+ ws.deps.session.close();
271
+ ws.dock.mode = mode === "preview" ? "preview" : "discussion";
273
272
  updateLayout(ws);
274
273
  }
275
274
 
275
+ // mode "session" (the legacy native entry) opens the session page in its
276
+ // terminal mode; "discussion" and "preview" need a selected Task.
276
277
  function toggleDock(ws, mode) {
277
278
  const dock = ws.dock;
278
279
  const selected = ws.deps.state.selected;
279
- if (dockVisible(ws) && (!mode || dock.mode === mode)) { setDockOpen(ws, false); return; }
280
- if (mode) dock.mode = mode === "session" || !selected ? mode : "discussion";
281
- if (!selected && dock.mode === "discussion") dock.mode = "session";
280
+ const view = mode === "session" ? "conversation" : mode;
281
+ if (dockVisible(ws) && (!view || dock.mode === view)) { setDockOpen(ws, false); return; }
282
+ if (view) dock.mode = view === "conversation" || selected ? view : "conversation";
283
+ if (!selected && dock.mode !== "conversation") dock.mode = "conversation";
282
284
  setDockOpen(ws, true);
283
- }
284
-
285
- function openSession(ws, target) {
286
- ws.terminalTarget = target;
287
- ws.deps.conversation.close();
288
- ws.dock.mode = "session";
289
- showDock(ws, true);
290
- ws.deps.terminal.open(target);
291
- updateLayout(ws);
292
- ws.deps.sessionOpened();
285
+ if (mode === "session") ws.deps.session.setMode("terminal");
293
286
  }
294
287
 
295
288
  function toggleOperator(ws) {
@@ -298,28 +291,23 @@ function toggleOperator(ws) {
298
291
  }
299
292
 
300
293
  function openOperator(ws) {
301
- ws.deps.terminal.close();
302
- showDock(ws, true);
303
- setDockMode(ws, "conversation");
304
- ws.deps.conversation.open({ scope: "global", roleName: "operator" });
305
- if (preferredMode() === "session") openSession(ws, { scope: "global", roleName: "operator" });
306
- syncDockButtons(ws);
294
+ const current = ws.deps.session.current();
295
+ openSessionPage(ws, { scope: "global", roleName: "operator" },
296
+ current && current.scope === "global" && current.roleName === "operator" ? {} : { mode: preferredMode() });
307
297
  }
308
298
 
309
- // Leaving a Task: when the dock's last Session belonged to a Task and is no
310
- // longer connected, the dock returns to the discussion mode.
299
+ // Leaving a Task: the session page returns to Operator, hidden unless shown.
311
300
  function leaveTask(ws) {
312
- ws.deps.terminal.close();
313
301
  ws.dock.mode = "conversation";
314
- ws.deps.conversation.open({ scope: "global", roleName: "operator" });
315
- pauseHiddenConversation(ws);
302
+ ws.deps.session.open({ scope: "global", roleName: "operator" });
303
+ pauseHiddenSession(ws);
316
304
  syncDockButtons(ws);
317
305
  }
318
306
 
319
- // The conversation keeps its owner while the panel is hidden but stops
320
- // polling; opening the panel reconnects it.
321
- function pauseHiddenConversation(ws) {
322
- if (!dockVisible(ws) || ws.dock.mode !== "conversation") ws.deps.conversation.close();
307
+ // The session page keeps its owner while the panel is hidden but stops
308
+ // polling and detaches; opening the panel reconnects it.
309
+ function pauseHiddenSession(ws) {
310
+ if (!dockVisible(ws) || ws.dock.mode !== "conversation") ws.deps.session.close();
323
311
  }
324
312
 
325
313
  function bindDockControls(ws) {
@@ -327,7 +315,6 @@ function bindDockControls(ws) {
327
315
  const dock = ws.dock;
328
316
  el.dockTabDiscussion.addEventListener("click", function () { setDockMode(ws, "discussion"); });
329
317
  el.dockTabConversation.addEventListener("click", function () { setDockMode(ws, "conversation"); });
330
- el.dockTabSession.addEventListener("click", function () { setDockMode(ws, "session"); });
331
318
  el.dockTabPreview.addEventListener("click", function () { setDockMode(ws, "preview"); });
332
319
  el.dockClose.addEventListener("click", function () { glide(function () { setDockOpen(ws, false); }); });
333
320
  el.dockSwap.addEventListener("click", function () {
@@ -18,7 +18,7 @@ export function clearPreference(key) {
18
18
  // switching back restores the filters, Task or section the user left. Only
19
19
  // same-origin paths of the expected area are accepted; anything else falls back.
20
20
  const PLACES = { workbench: /^\/(?:\?|$)/, models: /^\/models(?:[/?]|$)/, settings: /^\/settings(?:[/?]|$)/,
21
- manage: /^\/(?:models|settings)(?:[/?]|$)/ };
21
+ jobs: /^\/jobs(?:[/?]|$)/, manage: /^\/(?:jobs|models|settings)(?:[/?]|$)/ };
22
22
 
23
23
  export function rememberPlace(area, href) {
24
24
  if (!PLACES[area] || !PLACES[area].test(href)) return;
@@ -42,8 +42,21 @@ export function button(text, options) {
42
42
  return element;
43
43
  }
44
44
 
45
- // The page header's Operator switch: the same place on the overview and on
46
- // every Task page; layout/workspace marks it pressed while the dock shows it.
45
+ // The page header's session-panel switch: the same place on the overview and
46
+ // on every Task page. It shows or hides the dock with its last target and view
47
+ // (Operator, discussion, terminal and preview are chosen inside the panel);
48
+ // layout/workspace keeps aria-expanded and the accessible name current.
49
+ export function panelToggle(t, onClick) {
50
+ const element = h("button.btn.btn-ghost.dock-toggle.panel-toggle", {
51
+ type: "button", "aria-label": t("dock.show"), title: t("dock.label") + " · D",
52
+ "aria-expanded": "false", "aria-controls": "dock", onclick: onClick
53
+ }, icon("dock"), h("span", null, t("dock.label")));
54
+ element.dataset.panelToggle = "true";
55
+ return element;
56
+ }
57
+
58
+ // The former Operator-only header switch, kept unchanged for pages that still
59
+ // import it; layout/workspace marks it pressed while the dock shows Operator.
47
60
  export function operatorToggle(t, onClick) {
48
61
  const element = h("button.btn.btn-ghost.dock-toggle.operator-toggle", {
49
62
  type: "button", title: t("actions.operator") + " · O", "aria-pressed": "false", onclick: onClick