@zq-silk/yui 2.15.0 → 2.17.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 (104) hide show
  1. package/dist/cli/commandCatalog.js +17 -11
  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/globalRoleCommands.js +13 -2
  6. package/dist/commands/operatorCommands.js +2 -0
  7. package/dist/commands/profileCommands.js +19 -2
  8. package/dist/commands/roleConfiguration.js +3 -0
  9. package/dist/commands/taskCommands.js +16 -0
  10. package/dist/commands/taskRoleRuntimeStatus.js +3 -0
  11. package/dist/controller/agentJobControl.js +9 -1
  12. package/dist/controller/fileSchedulerStoreAdapter.js +35 -4
  13. package/dist/controller/globalInputDelivery.js +14 -8
  14. package/dist/controller/providerRetryAdmission.js +9 -0
  15. package/dist/controller/providerRetryDelivery.js +16 -0
  16. package/dist/controller/runtime.js +14 -0
  17. package/dist/controller/runtimeLaunchCoordinator.js +3 -1
  18. package/dist/executor/fileRoleLaunchPlanner.js +3 -0
  19. package/dist/message/messageContinuation.js +4 -0
  20. package/dist/output/rolePresentation.js +3 -0
  21. package/dist/profile/agentProfile.js +8 -0
  22. package/dist/profile/agentProfileRuntime.js +16 -1
  23. package/dist/review/reviewDecision.js +6 -0
  24. package/dist/role/availability.js +28 -0
  25. package/dist/role/role.js +6 -0
  26. package/dist/runtime/codexNativeAccess.js +21 -4
  27. package/dist/runtime/tmuxAdapters.js +5 -0
  28. package/dist/scheduler/activeRoleRunDelivery.js +9 -0
  29. package/dist/scheduler/leaderWakeupProcessor.js +8 -1
  30. package/dist/scheduler/roleRunLiveness.js +3 -0
  31. package/dist/storage/sqliteSchema.js +10 -0
  32. package/dist/storage/storageVersions.js +1 -1
  33. package/dist/web/assets/client/app/catalog.js +40 -14
  34. package/dist/web/assets/client/app/main.js +43 -26
  35. package/dist/web/assets/client/app/manage.js +18 -0
  36. package/dist/web/assets/client/app/selection.js +4 -1
  37. package/dist/web/assets/client/app/taskView.js +29 -16
  38. package/dist/web/assets/client/i18n/en.js +72 -21
  39. package/dist/web/assets/client/i18n/zh-CN.js +72 -21
  40. package/dist/web/assets/client/index.js +6 -0
  41. package/dist/web/assets/client/layout/manageShell.js +30 -8
  42. package/dist/web/assets/client/layout/shortcuts.js +9 -5
  43. package/dist/web/assets/client/layout/sidebarNav.js +147 -0
  44. package/dist/web/assets/client/layout/sizing.js +5 -2
  45. package/dist/web/assets/client/layout/workspace.js +193 -107
  46. package/dist/web/assets/client/lib/motion.js +59 -0
  47. package/dist/web/assets/client/lib/prefs.js +29 -2
  48. package/dist/web/assets/client/lib/theme.js +11 -4
  49. package/dist/web/assets/client/ui/primitives.js +40 -0
  50. package/dist/web/assets/client/views/dock/conversation.js +421 -123
  51. package/dist/web/assets/client/views/dock/session.js +229 -0
  52. package/dist/web/assets/client/views/dock/terminal.js +48 -39
  53. package/dist/web/assets/client/views/overview.js +8 -5
  54. package/dist/web/assets/client/views/task/page.js +7 -17
  55. package/dist/web/assets/shared/brand.js +22 -0
  56. package/dist/web/assets/shared/geometry.js +7 -1
  57. package/dist/web/assets/shared/icons.js +4 -0
  58. package/dist/web/assets/shared/motion.js +74 -0
  59. package/dist/web/assets/shell/appRail.js +14 -0
  60. package/dist/web/assets/shell/dialogs.js +1 -0
  61. package/dist/web/assets/shell/dock.js +19 -24
  62. package/dist/web/assets/shell/index.js +6 -2
  63. package/dist/web/assets/shell/login.js +9 -8
  64. package/dist/web/assets/shell/manage.js +32 -20
  65. package/dist/web/assets/shell/settingsContent.js +1 -1
  66. package/dist/web/assets/shell/sidebar.js +16 -23
  67. package/dist/web/assets/styles/base.js +1 -1
  68. package/dist/web/assets/styles/index.js +2 -0
  69. package/dist/web/assets/styles/layout/responsive.js +63 -19
  70. package/dist/web/assets/styles/layout/workspace.js +53 -16
  71. package/dist/web/assets/styles/motion.js +51 -0
  72. package/dist/web/assets/styles/tokens.js +5 -3
  73. package/dist/web/assets/styles/ui/controls.js +1 -1
  74. package/dist/web/assets/styles/ui/forms.js +1 -1
  75. package/dist/web/assets/styles/ui/surfaces.js +1 -2
  76. package/dist/web/assets/styles/views/conversation.js +97 -18
  77. package/dist/web/assets/styles/views/dock.js +34 -2
  78. package/dist/web/assets/styles/views/login.js +16 -13
  79. package/dist/web/assets/styles/views/manage.js +25 -42
  80. package/dist/web/assets/styles/views/page.js +2 -3
  81. package/dist/web/assets/styles/views/records.js +1 -1
  82. package/dist/web/assets/styles/views/settings.js +1 -1
  83. package/dist/web/assets/styles/views/sidebar.js +25 -20
  84. package/dist/web/assets/styles/views/task.js +8 -15
  85. package/dist/web/assets/styles/views/terminal.js +13 -9
  86. package/dist/web/webConversation.js +1 -0
  87. package/dist/web/webSettings.js +10 -2
  88. package/docs/managed-turn-and-session-runtime.md +10 -0
  89. package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
  90. package/docs/release-workflow.md +46 -0
  91. package/docs/release-workflow.zh-CN.md +34 -0
  92. package/docs/roles-and-configuration.md +45 -1
  93. package/docs/roles-and-configuration.zh-CN.md +35 -0
  94. package/docs/storage-baseline.md +6 -1
  95. package/docs/storage-baseline.zh-CN.md +5 -1
  96. package/docs/testing/review-evaluation.md +35 -1
  97. package/docs/testing/review-evaluation.zh-CN.md +25 -1
  98. package/docs/testing/verification-levels.md +2 -2
  99. package/docs/testing/verification-levels.zh-CN.md +2 -2
  100. package/package.json +1 -1
  101. package/skills/yui-leader/VERSION +1 -1
  102. package/skills/yui-leader/references/execution.md +19 -8
  103. package/skills/yui-reviewer/SKILL.md +13 -2
  104. package/skills/yui-reviewer/VERSION +1 -1
@@ -1,28 +1,33 @@
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
- import { markSelected } from "/assets/js/lib/dom.js";
9
+ import { markSelected, h, icon, clear } from "/assets/js/lib/dom.js";
9
10
  import { createSizing } from "/assets/js/layout/sizing.js";
11
+ import { layoutTransition, leave } from "/assets/js/lib/motion.js";
10
12
 
11
13
  const NARROW_QUERY = "(max-width: ${BREAKPOINTS.narrow}px)";
12
14
  const DOCK_INITIAL = ${DOCK.initial};
13
15
 
14
- // deps: { el, state, terminal, sessionOpened }
15
- // el the shell elements (dock, dividers, sidebar, operator button,
16
- // and the Task page's [data-dock-toggle] buttons inside el.detail)
16
+ // deps: { el, state, t, session, preview }
17
+ // el the shell elements (dock, dividers, sidebar, the dock's target
18
+ // menu, and the page's [data-panel-toggle] / legacy [data-operator-toggle]
19
+ // buttons inside el.detail)
17
20
  // state read-only here: state.selected
18
- // terminal the dock's native Session (views/dock/terminal)
19
- // sessionOpened() a Session was just opened in the dock; the caller
20
- // refreshes the Session target chips (app/taskView).
21
- // 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.
22
25
  export function createWorkspace(deps) {
23
26
  const ws = {
24
27
  deps: deps,
25
28
  dock: readDockState(),
29
+ // Roles the target menu offers besides Operator (setTargets, app/taskView).
30
+ targets: [],
26
31
  // Task list width: null keeps the CSS default (--sidebar-w); otherwise
27
32
  // the width the user dragged to.
28
33
  sidebar: { width: Number(readPreference("yui.sidebar.width", "")) || null },
@@ -36,41 +41,43 @@ export function createWorkspace(deps) {
36
41
  dockVisible: function () { return dockVisible(ws); },
37
42
  updateLayout: function () { updateLayout(ws); },
38
43
  syncDockButtons: function () { syncDockButtons(ws); },
39
- setDockOpen: function (open) { setDockOpen(ws, open); },
40
- toggleDock: function (mode) { toggleDock(ws, mode); },
41
- openSession: function (target) { openSession(ws, target); },
44
+ setTargets: function (targets) { ws.targets = targets; drawTarget(ws); },
45
+ setDockOpen: function (open) { glide(function () { setDockOpen(ws, open); }); },
46
+ toggleDock: function (mode) { glide(function () { toggleDock(ws, mode); }); },
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 }); },
42
50
  openConversation: function (target, materials) {
43
- ws.deps.terminal.close();
44
- showDock(ws, true);
45
- ws.dock.mode = "conversation";
46
- updateLayout(ws);
47
- ws.deps.conversation.open(target, { current: true, materials: materials });
48
- syncDockButtons(ws);
49
- },
50
- openOperator: function () { openOperator(ws); },
51
- restoreDefaultMode: function () {
52
- if (!ws.deps.state.selected && ws.dock.open && !ws.narrow.matches && preferredMode() === "session") {
53
- openSession(ws, { scope: "global", roleName: "operator" });
54
- }
51
+ openSessionPage(ws, target, { mode: "chat", current: true, ...(materials ? { materials: materials } : {}) });
55
52
  },
53
+ openOperator: function () { glide(function () { openOperator(ws); }); },
54
+ toggleOperator: function () { glide(function () { toggleOperator(ws); }); },
56
55
  // A newly selected Task starts with the narrow-screen sheet closed.
57
56
  enterTask: function () {
58
- ws.deps.terminal.close();
59
- ws.terminalTarget = { scope: "task", taskId: ws.deps.state.selected, roleName: "leader" };
60
57
  ws.dock.sheet = false;
61
- ws.dock.mode = preferredMode();
62
- ws.deps.conversation.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
63
- if (ws.dock.mode === "session" && !ws.narrow.matches && ws.dock.open) {
64
- ws.deps.terminal.open({ scope: "task", taskId: ws.deps.state.selected, roleName: "leader" });
65
- }
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);
62
+ syncDockButtons(ws);
66
63
  },
67
64
  leaveTask: function () { leaveTask(ws); }
68
65
  };
69
66
  }
70
67
 
68
+ // User-initiated dock changes (open, close, swap sides) glide the columns to
69
+ // their new widths; internal re-layouts stay immediate.
70
+ function glide(update) {
71
+ if (typeof layoutTransition === "function") layoutTransition(update);
72
+ else update();
73
+ }
74
+
71
75
  function readDockState() {
72
76
  return {
73
- open: readPreference("yui.dock.open", "true") !== "false",
77
+ // The panel starts closed on every load and opens only on request.
78
+ open: false,
79
+ // Which dock view: the session page ("conversation"), the Task
80
+ // discussion or the preview.
74
81
  mode: "conversation",
75
82
  center: readPreference("yui.dock.side", "end") === "center",
76
83
  width: Number(readPreference("yui.dock.width", String(DOCK_INITIAL))) || DOCK_INITIAL,
@@ -80,30 +87,29 @@ function readDockState() {
80
87
  };
81
88
  }
82
89
 
90
+ // The Settings preference picks the session page's starting mode.
83
91
  function preferredMode() {
84
- return readSessionAccessMode() === "native" ? "session" : "conversation";
92
+ return readSessionAccessMode() === "native" ? "terminal" : "chat";
85
93
  }
86
94
 
87
95
  function dockVisible(ws) {
88
96
  const dock = ws.dock;
89
97
  if (ws.narrow.matches ? !dock.sheet : !dock.open) return false;
90
- if (ws.deps.state.selected) return true;
91
- if (dock.mode === "conversation") return true;
92
- return dock.mode === "session" && !!ws.deps.terminal.current();
98
+ return !!ws.deps.state.selected || dock.mode === "conversation";
93
99
  }
94
100
 
95
101
  function updateLayout(ws) {
96
102
  const el = ws.deps.el;
97
103
  const dock = ws.dock;
98
104
  const selected = !!ws.deps.state.selected;
105
+ if (!selected && dock.mode !== "conversation") dock.mode = "conversation";
99
106
  const visible = dockVisible(ws);
100
107
  document.body.classList.toggle("task-open", selected);
101
108
  document.body.classList.toggle("dock-open", visible);
102
109
  document.body.classList.toggle("dock-center", dock.center);
103
- el.dock.hidden = !visible;
110
+ revealDock(ws, visible);
104
111
  el.divider.hidden = !visible;
105
112
  el.discussion.hidden = dock.mode !== "discussion";
106
- el.session.hidden = dock.mode !== "session";
107
113
  el.conversation.hidden = dock.mode !== "conversation";
108
114
  el.preview.hidden = dock.mode !== "preview";
109
115
  el.dockTabPreview.setAttribute("aria-selected", String(dock.mode === "preview"));
@@ -112,118 +118,196 @@ function updateLayout(ws) {
112
118
  else ws.deps.preview.close();
113
119
  el.dockTabConversation.setAttribute("aria-selected", String(dock.mode === "conversation"));
114
120
  el.dockTabDiscussion.setAttribute("aria-selected", String(dock.mode === "discussion"));
115
- el.dockTabSession.setAttribute("aria-selected", String(dock.mode === "session"));
116
121
  el.dockTabDiscussion.disabled = !selected;
117
122
  el.dockSwap.setAttribute("aria-pressed", String(dock.center));
118
123
  ws.sizing.applySidebarWidth();
119
124
  syncDockButtons(ws);
120
125
  }
121
126
 
127
+ // On narrow screens the dock is a full-screen sheet: CSS slides it up on
128
+ // open, and closing slides it back down before it hides (an immediate close
129
+ // would cut the motion off). Wider screens change the grid; user-initiated
130
+ // changes there glide through glide().
131
+ function revealDock(ws, visible) {
132
+ const dockEl = ws.deps.el.dock;
133
+ if (ws.closingDock) { ws.closingDock.cancel(); ws.closingDock = null; }
134
+ if (visible || dockEl.hidden || !ws.narrow.matches || typeof leave !== "function") { dockEl.hidden = !visible; return; }
135
+ const closing = leave(dockEl, "sheetDown");
136
+ if (!closing.animation) { dockEl.hidden = true; return; }
137
+ ws.closingDock = closing;
138
+ closing.finished.then(function () {
139
+ if (ws.closingDock !== closing) return;
140
+ ws.closingDock = null;
141
+ dockEl.hidden = true;
142
+ closing.cancel();
143
+ });
144
+ }
145
+
122
146
  function syncDockButtons(ws) {
123
147
  const el = ws.deps.el;
124
148
  const visible = dockVisible(ws);
125
- markSelected(el.detail.querySelectorAll("[data-dock-toggle]"), "aria-pressed", function (button) {
126
- 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"));
152
+ });
153
+ markSelected(el.detail.querySelectorAll("[data-operator-toggle]"), "aria-pressed", function () { return operatorShown(ws); });
154
+ drawTarget(ws);
155
+ }
156
+
157
+ // --- Conversation target -----------------------------------------------------
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.
161
+ function currentTarget(ws) {
162
+ const selected = ws.deps.state.selected;
163
+ if (ws.dock.mode === "conversation") return ws.deps.session.current();
164
+ return selected ? { scope: "task", taskId: selected, roleName: "leader" } : null;
165
+ }
166
+
167
+ function targetLabel(ws, target) {
168
+ if (!target) return ws.deps.t("actions.operator");
169
+ if (target.scope === "global") return target.roleName === "operator" ? ws.deps.t("actions.operator") : target.roleName;
170
+ return target.roleName + " · " + target.taskId;
171
+ }
172
+
173
+ function sameTarget(a, b) {
174
+ return !!a && !!b && a.scope === b.scope && a.roleName === b.roleName && (a.scope !== "task" || a.taskId === b.taskId);
175
+ }
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
+
189
+ function drawTarget(ws) {
190
+ const el = ws.deps.el;
191
+ const current = currentTarget(ws);
192
+ el.dockTargetLabel.textContent = targetLabel(ws, current);
193
+ el.dockTargetIcon.replaceChildren(icon(current && current.scope === "task" ? "target" : "broadcast"));
194
+ const options = targetOptions(ws, current);
195
+ const signature = JSON.stringify([options, current, ws.deps.t("actions.operator")]);
196
+ if (el.dockTargetOptions.dataset.signature === signature) return;
197
+ el.dockTargetOptions.dataset.signature = signature;
198
+ clear(el.dockTargetOptions);
199
+ options.forEach(function (target) {
200
+ const chosen = sameTarget(target, current);
201
+ el.dockTargetOptions.append(h("button.dock-target-item", {
202
+ type: "button", "aria-pressed": String(chosen),
203
+ onclick: function () { chooseTarget(ws, target); }
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));
127
207
  });
128
- el.operator.setAttribute("aria-pressed", String(operatorShown(ws)));
208
+ }
209
+
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.
212
+ function chooseTarget(ws, target) {
213
+ ws.deps.el.dockTarget.open = false;
214
+ openSessionPage(ws, { scope: target.scope, roleName: target.roleName, ...(target.scope === "task" ? { taskId: target.taskId } : {}) }, {});
215
+ }
216
+
217
+ function openSessionPage(ws, target, options) {
218
+ showDock(ws, true);
219
+ ws.dock.mode = "conversation";
220
+ updateLayout(ws);
221
+ ws.deps.session.open(target, options);
222
+ syncDockButtons(ws);
129
223
  }
130
224
 
131
225
  // The dock currently shows the global operator Session.
132
226
  function operatorShown(ws) {
133
- if (ws.dock.mode === "conversation") return dockVisible(ws) && ws.deps.conversation.current()?.scope === "global";
134
- const current = ws.deps.terminal.current();
135
- 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";
136
228
  }
137
229
 
138
230
  function showDock(ws, open) {
139
231
  if (ws.narrow.matches) ws.dock.sheet = open;
140
232
  else {
141
233
  ws.dock.open = open;
142
- writePreference("yui.dock.open", String(open));
143
234
  }
144
235
  }
145
236
 
146
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);
147
242
  showDock(ws, open);
148
- if (!open) {
149
- ws.terminalTarget = ws.deps.terminal.current() || ws.terminalTarget;
150
- ws.deps.terminal.close();
151
- }
152
- if (!open) ws.deps.conversation.close();
153
- if (open && ws.dock.mode === "conversation") ws.deps.conversation.reconnect();
154
- if (open && ws.dock.mode === "session") {
155
- // Keep the selected owner, not a Session identity cached before hiding.
156
- const target = ws.terminalTarget || { scope: "global", roleName: "operator" };
157
- ws.deps.terminal.open({ scope: target.scope, roleName: target.roleName,
158
- ...(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() });
159
249
  }
160
250
  updateLayout(ws);
251
+ if (focusInside) {
252
+ const toggle = ws.deps.el.detail.querySelector("[data-panel-toggle]");
253
+ if (toggle) toggle.focus();
254
+ }
161
255
  }
162
256
 
163
257
  function setDockMode(ws, mode) {
164
- if (mode === "preview" && ws.deps.state.selected) {
165
- ws.deps.terminal.close();
166
- ws.deps.conversation.close();
167
- ws.dock.mode = "preview";
168
- updateLayout(ws);
169
- return;
170
- }
258
+ if (mode === ws.dock.mode) return;
171
259
  if (mode === "conversation") {
172
- const target = ws.deps.terminal.current() || ws.deps.conversation.current();
173
- ws.deps.terminal.close();
174
260
  ws.dock.mode = mode;
175
261
  updateLayout(ws);
176
- 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
177
264
  ? { scope: "task", taskId: ws.deps.state.selected, roleName: "leader" }
178
- : { scope: "global", roleName: "operator" });
179
- return;
180
- }
181
- if (mode === "session") {
182
- const target = ws.deps.conversation.selection();
183
- if (!target) return;
184
- openSession(ws, target);
265
+ : { scope: "global", roleName: "operator" }, { mode: preferredMode() });
266
+ syncDockButtons(ws);
185
267
  return;
186
268
  }
187
- ws.deps.terminal.close();
188
- ws.deps.conversation.close();
189
- 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";
190
272
  updateLayout(ws);
191
273
  }
192
274
 
275
+ // mode "session" (the legacy native entry) opens the session page in its
276
+ // terminal mode; "discussion" and "preview" need a selected Task.
193
277
  function toggleDock(ws, mode) {
194
278
  const dock = ws.dock;
195
279
  const selected = ws.deps.state.selected;
196
- if (dockVisible(ws) && (!mode || dock.mode === mode)) { setDockOpen(ws, false); return; }
197
- if (mode) dock.mode = mode === "session" || !selected ? mode : "discussion";
198
- 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";
199
284
  setDockOpen(ws, true);
285
+ if (mode === "session") ws.deps.session.setMode("terminal");
200
286
  }
201
287
 
202
- function openSession(ws, target) {
203
- ws.terminalTarget = target;
204
- ws.deps.conversation.close();
205
- ws.dock.mode = "session";
206
- showDock(ws, true);
207
- ws.deps.terminal.open(target);
208
- updateLayout(ws);
209
- ws.deps.sessionOpened();
288
+ function toggleOperator(ws) {
289
+ if (operatorShown(ws)) setDockOpen(ws, false);
290
+ else openOperator(ws);
210
291
  }
211
292
 
212
293
  function openOperator(ws) {
213
- ws.deps.terminal.close();
214
- showDock(ws, true);
215
- setDockMode(ws, "conversation");
216
- ws.deps.conversation.open({ scope: "global", roleName: "operator" });
217
- if (preferredMode() === "session") openSession(ws, { scope: "global", roleName: "operator" });
218
- 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() });
219
297
  }
220
298
 
221
- // Leaving a Task: when the dock's last Session belonged to a Task and is no
222
- // longer connected, the dock returns to the discussion mode.
299
+ // Leaving a Task: the session page returns to Operator, hidden unless shown.
223
300
  function leaveTask(ws) {
224
- ws.deps.terminal.close();
225
301
  ws.dock.mode = "conversation";
226
- ws.deps.conversation.open({ scope: "global", roleName: "operator" });
302
+ ws.deps.session.open({ scope: "global", roleName: "operator" });
303
+ pauseHiddenSession(ws);
304
+ syncDockButtons(ws);
305
+ }
306
+
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();
227
311
  }
228
312
 
229
313
  function bindDockControls(ws) {
@@ -231,17 +315,19 @@ function bindDockControls(ws) {
231
315
  const dock = ws.dock;
232
316
  el.dockTabDiscussion.addEventListener("click", function () { setDockMode(ws, "discussion"); });
233
317
  el.dockTabConversation.addEventListener("click", function () { setDockMode(ws, "conversation"); });
234
- el.dockTabSession.addEventListener("click", function () { setDockMode(ws, "session"); });
235
318
  el.dockTabPreview.addEventListener("click", function () { setDockMode(ws, "preview"); });
236
- el.dockClose.addEventListener("click", function () { setDockOpen(ws, false); });
319
+ el.dockClose.addEventListener("click", function () { glide(function () { setDockOpen(ws, false); }); });
237
320
  el.dockSwap.addEventListener("click", function () {
238
321
  dock.center = !dock.center;
239
322
  writePreference("yui.dock.side", dock.center ? "center" : "end");
240
- updateLayout(ws);
323
+ glide(function () { updateLayout(ws); });
324
+ });
325
+ el.globalInputOpen.addEventListener("click", function () { el.dockTarget.open = false; });
326
+ document.addEventListener("click", function (event) {
327
+ if (el.dockTarget.open && !el.dockTarget.contains(event.target)) el.dockTarget.open = false;
241
328
  });
242
- el.operator.addEventListener("click", function () {
243
- if (operatorShown(ws)) setDockOpen(ws, false);
244
- else openOperator(ws);
329
+ el.dockTarget.addEventListener("keydown", function (event) {
330
+ if (event.key === "Escape" && el.dockTarget.open) { event.stopPropagation(); el.dockTarget.open = false; el.dockTarget.querySelector("summary").focus(); }
245
331
  });
246
332
  }
247
333
  `;
@@ -0,0 +1,59 @@
1
+ import { BREAKPOINTS } from "../../shared/geometry.js";
2
+ import { MOTION } from "../../shared/motion.js";
3
+ export const MOTION_SCRIPT = String.raw `
4
+ // Script-side motion, on the same durations, curves and presets as the
5
+ // stylesheets (shared/motion.ts). Every helper degrades to an immediate,
6
+ // synchronous change when the browser lacks the API, the user prefers
7
+ // reduced motion, or (for layout transitions) the screen is narrow, where
8
+ // drawers and sheets carry their own CSS motion.
9
+ const M = ${JSON.stringify(MOTION)};
10
+ const NARROW = ${BREAKPOINTS.narrow};
11
+
12
+ export const motion = M;
13
+
14
+ export function reducedMotion() {
15
+ return typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
16
+ }
17
+
18
+ function options(preset, extra) {
19
+ return Object.assign({ duration: M.durations[preset.duration], easing: M.easings[preset.easing] }, extra || {});
20
+ }
21
+
22
+ function canAnimate(el) { return Boolean(el) && typeof el.animate === "function" && !reducedMotion(); }
23
+
24
+ // Plays a preset on an element that has just appeared. Returns the
25
+ // Animation, or null when nothing was played.
26
+ export function enter(el, name) {
27
+ const preset = M.presets[name || "rise"];
28
+ if (!preset || !canAnimate(el)) return null;
29
+ return el.animate(preset.frames, options(preset));
30
+ }
31
+
32
+ // Plays an exit preset and resolves once it has finished (or at once when
33
+ // motion is unavailable). The final frame is held until the caller hides
34
+ // the element; call cancel() on the returned animation to abort.
35
+ export function leave(el, name) {
36
+ const preset = M.presets[name || "fade"];
37
+ if (!preset || !canAnimate(el)) return { finished: Promise.resolve(), cancel() {} , animation: null };
38
+ const animation = el.animate(preset.frames, options(preset, { fill: "forwards" }));
39
+ return { finished: animation.finished.then(function () {}, function () {}), cancel() { animation.cancel(); }, animation };
40
+ }
41
+
42
+ // Runs a DOM update that moves the workspace columns (sidebar collapse,
43
+ // dock open or close) inside a View Transition, so the columns glide to
44
+ // their new widths. Without support the update runs synchronously.
45
+ let layoutRunning = 0;
46
+ export function layoutTransition(update) {
47
+ const supported = typeof document !== "undefined" && typeof document.startViewTransition === "function" &&
48
+ typeof window !== "undefined" && window.innerWidth > NARROW && !reducedMotion() && !document.hidden;
49
+ if (!supported) { update(); return; }
50
+ // The column names exist only during the transition (styles/motion).
51
+ const root = document.documentElement;
52
+ const done = function () { if (--layoutRunning === 0) root.classList.remove("vt-layout"); };
53
+ layoutRunning += 1;
54
+ root.classList.add("vt-layout");
55
+ let transition;
56
+ try { transition = document.startViewTransition(update); } catch { done(); update(); return; }
57
+ transition.finished.finally(done);
58
+ }
59
+ `;
@@ -14,11 +14,38 @@ export function clearPreference(key) {
14
14
  try { localStorage.removeItem(key); return true; } catch { return false; }
15
15
  }
16
16
 
17
+ // Per-tab return points between the workbench and the management areas, so
18
+ // switching back restores the filters, Task or section the user left. Only
19
+ // same-origin paths of the expected area are accepted; anything else falls back.
20
+ const PLACES = { workbench: /^\/(?:\?|$)/, models: /^\/models(?:[/?]|$)/, settings: /^\/settings(?:[/?]|$)/,
21
+ manage: /^\/(?:models|settings)(?:[/?]|$)/ };
22
+
23
+ export function rememberPlace(area, href) {
24
+ if (!PLACES[area] || !PLACES[area].test(href)) return;
25
+ try { sessionStorage.setItem("yui.nav." + area, href); } catch { /* storage unavailable: links keep defaults */ }
26
+ }
27
+
28
+ export function recallPlace(area, fallback) {
29
+ try {
30
+ const value = sessionStorage.getItem("yui.nav." + area);
31
+ return value && PLACES[area] && PLACES[area].test(value) ? value : fallback;
32
+ } catch { return fallback; }
33
+ }
34
+
35
+ // Point every a[data-nav-area] inside root at its area's return point.
36
+ export function linkPlaces(root) {
37
+ root.querySelectorAll("a[data-nav-area]").forEach(function (link) {
38
+ if (!link.dataset.navDefault) link.dataset.navDefault = link.getAttribute("href");
39
+ link.setAttribute("href", recallPlace(link.dataset.navArea, link.dataset.navDefault));
40
+ });
41
+ }
42
+
17
43
  // Shared with session access consumers: no server configuration or live
18
44
  // Session is changed by this browser-local preference.
19
45
  export function readSessionAccessMode() {
20
- const value = readPreference("yui.session.accessMode", "native");
21
- return value === "structured" ? "structured" : "native";
46
+ // The structured conversation is the default; the native terminal is opt-in.
47
+ const value = readPreference("yui.session.accessMode", "structured");
48
+ return value === "native" ? "native" : "structured";
22
49
  }
23
50
 
24
51
  export function writeSessionAccessMode(value) {
@@ -1,3 +1,4 @@
1
+ import { BRAND_ICONS } from "../../shared/brand.js";
1
2
  import { DEFAULT_DARK_THEME, DEFAULT_LIGHT_THEME, THEME_OPTIONS } from "../../shared/themes.js";
2
3
  // Shared by the parser-blocking head script and the later settings controller.
3
4
  // Both resolve the same browser preference without publishing global state.
@@ -16,15 +17,21 @@ function storedPreference() {
16
17
  function resolveTheme(preference, media) {
17
18
  return preference === "system" ? (media.matches ? DEFAULT_LIGHT : DEFAULT_DARK) : preference;
18
19
  }
20
+
21
+ // Stamp the resolved theme on <html> and swap in its pre-coloured favicon.
22
+ const BRAND_ICONS = ${JSON.stringify(BRAND_ICONS)};
23
+ function stampTheme(id) {
24
+ document.documentElement.dataset.theme = id;
25
+ const icon = document.querySelector('link[rel="icon"]');
26
+ if (icon && BRAND_ICONS[id] && icon.getAttribute("href") !== BRAND_ICONS[id]) icon.setAttribute("href", BRAND_ICONS[id]);
27
+ }
19
28
  `;
20
29
  // A same-origin classic script runs before styles/body, unlike the deferred
21
30
  // application module graph. No inline script or weaker CSP is needed.
22
31
  export const THEME_BOOTSTRAP_SCRIPT = String.raw `
23
32
  (function () {
24
33
  ${THEME_PREFERENCE_SCRIPT}
25
- document.documentElement.dataset.theme = resolveTheme(
26
- storedPreference(), window.matchMedia("(prefers-color-scheme: light)")
27
- );
34
+ stampTheme(resolveTheme(storedPreference(), window.matchMedia("(prefers-color-scheme: light)")));
28
35
  })();
29
36
  `;
30
37
  export const THEME_SCRIPT = String.raw `
@@ -60,7 +67,7 @@ export function createThemeController(container, t) {
60
67
  return resolveTheme(preference, media);
61
68
  }
62
69
  function apply() {
63
- document.documentElement.dataset.theme = resolved();
70
+ stampTheme(resolved());
64
71
  if (container) markSelected(container.querySelectorAll("[data-theme-option]"), "aria-pressed", function (button) {
65
72
  return button.dataset.themeOption === preference;
66
73
  });
@@ -42,6 +42,46 @@ export function button(text, options) {
42
42
  return element;
43
43
  }
44
44
 
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.
60
+ export function operatorToggle(t, onClick) {
61
+ const element = h("button.btn.btn-ghost.dock-toggle.operator-toggle", {
62
+ type: "button", title: t("actions.operator") + " · O", "aria-pressed": "false", onclick: onClick
63
+ }, icon("broadcast"), h("span", null, t("actions.operator")));
64
+ element.dataset.operatorToggle = "true";
65
+ return element;
66
+ }
67
+
68
+ // The task-list switch at the start of the page header. It shows only while
69
+ // the list is out of view (collapsed, or a drawer on smaller screens);
70
+ // layout/sidebarNav keeps aria-expanded current.
71
+ export function sidebarToggle(t, onClick) {
72
+ const element = h("button.icon-btn.side-expand", {
73
+ type: "button", "aria-label": t("sidebar.show"), title: t("sidebar.show") + " · [", "aria-expanded": "false", onclick: onClick
74
+ }, h("span.i-sidebar", null, icon("sidebar")), h("span.i-menu", null, icon("menu")));
75
+ element.dataset.sidebarToggle = "true";
76
+ return element;
77
+ }
78
+
79
+ // The top bar every page starts with (the management shell renders the same
80
+ // markup statically): the list switch, the location, then page actions.
81
+ export function topBar(t, revealSidebar, location, actions) {
82
+ return h("header.top-bar", null, sidebarToggle(t, revealSidebar), location, h("span.spacer"), actions || []);
83
+ }
84
+
45
85
  export function note(text, toneName) {
46
86
  return h("p.note" + (toneName ? ".tone-" + toneName : ""), null, text);
47
87
  }