@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
@@ -0,0 +1,229 @@
1
+ export const SESSION_PAGE_SCRIPT = String.raw `
2
+ // The dock's session page: one owner (a Role) and one selected Session, read
3
+ // either as the structured chat or as the live native terminal. The owner is
4
+ // picked in the dock header and the Session in the chat's picker; both carry
5
+ // across modes. Switching mode destroys the old view (polling, socket,
6
+ // listeners and unsent UI state such as drafts and scroll) and builds the
7
+ // other one afresh. Saved submission receipts and pending model switches are
8
+ // facts, not view state, and survive in the chat's existing storage.
9
+ import { h, icon } from "/assets/js/lib/dom.js";
10
+ import { formatShort } from "/assets/js/lib/format.js";
11
+ import { createConversationController } from "/assets/js/views/dock/conversation.js";
12
+ import { createTerminalController } from "/assets/js/views/dock/terminal.js";
13
+
14
+ export const SESSION_MODES = ["chat", "terminal"];
15
+
16
+ // host: the dock pane the page owns; notify(message) shows a toast;
17
+ // onChange() runs after the mode changed (the workspace redraws its header).
18
+ export function createSessionPage(host, t, locale, notify, onChange) {
19
+ const page = { host: host, t: t, locale: locale, notify: notify || function () {}, onChange: onChange || function () {},
20
+ owner: null, session: null, mode: "chat", view: null, active: false, switcher: null };
21
+ return {
22
+ // options: { mode, current, materials }. current resolves the owner's
23
+ // current Session again instead of keeping the selected one.
24
+ open: function (target, options) { open(page, target, options || {}); },
25
+ close: function () { page.active = false; if (page.view) page.view.stop(); },
26
+ reconnect: function () { page.active = true; if (page.view) page.view.start(); },
27
+ setMode: function (mode) { return setMode(page, mode); },
28
+ mode: function () { return page.mode; },
29
+ current: function () { return page.owner; },
30
+ selection: function () { return page.view ? page.view.selection() : null; },
31
+ connected: function () { return !!page.view && page.view.connected(); },
32
+ captureDraftTarget: function () { return page.view ? page.view.captureDraftTarget() : null; },
33
+ insertReference: function (target, citation) {
34
+ if (!page.view) throw new Error(t("bodySearch.targetChanged"));
35
+ page.view.insertReference(target, citation);
36
+ },
37
+ relabel: function () { if (page.view) page.view.relabel(); },
38
+ retheme: function () { if (page.view) page.view.retheme(); }
39
+ };
40
+ }
41
+
42
+ function ownerOf(target) {
43
+ return { scope: target.scope, ...(target.scope === "task" ? { taskId: target.taskId } : {}), roleName: target.roleName };
44
+ }
45
+
46
+ function sameOwner(a, b) {
47
+ return !!a && !!b && JSON.stringify(ownerOf(a)) === JSON.stringify(ownerOf(b));
48
+ }
49
+
50
+ function open(page, target, options) {
51
+ const ownerChanged = !sameOwner(target, page.owner);
52
+ const mode = SESSION_MODES.includes(options.mode) ? options.mode : page.mode;
53
+ if (ownerChanged || options.current) page.session = null;
54
+ page.owner = ownerOf(target);
55
+ page.active = true;
56
+ if (ownerChanged || options.current || mode !== page.mode || !page.view) {
57
+ page.mode = mode;
58
+ build(page, { materials: options.materials });
59
+ page.onChange();
60
+ } else page.view.start();
61
+ }
62
+
63
+ // Why the terminal cannot attach to the selected identity, or null when it
64
+ // may try (the terminal checks again against the newest facts).
65
+ function terminalBlock(page) {
66
+ const session = page.session;
67
+ if (!session || session.currentId === undefined) return null;
68
+ if (!session.currentId) return "terminal.noSession";
69
+ if (session.id && session.id !== session.currentId) return "terminal.notCurrent";
70
+ return null;
71
+ }
72
+
73
+ function setMode(page, mode) {
74
+ if (!SESSION_MODES.includes(mode) || mode === page.mode || !page.owner) return false;
75
+ const block = mode === "terminal" ? terminalBlock(page) : null;
76
+ if (block) { page.notify(page.t(block + "Text")); return false; }
77
+ const focused = page.host.contains(document.activeElement);
78
+ page.mode = mode;
79
+ build(page, { discard: true });
80
+ if (focused && page.switcher) page.switcher.focus(mode);
81
+ page.onChange();
82
+ return true;
83
+ }
84
+
85
+ function build(page, options) {
86
+ const previous = page.view;
87
+ page.view = null;
88
+ if (previous) previous.destroy(!!options.discard);
89
+ const root = h("div.session-view.is-" + page.mode);
90
+ page.host.replaceChildren(root);
91
+ page.switcher = modeSwitch(page);
92
+ const view = page.mode === "terminal" ? terminalView(page, root, page.switcher.node)
93
+ : chatView(page, root, page.switcher.node, options.materials);
94
+ page.view = view;
95
+ syncSwitch(page);
96
+ if (page.active) view.start();
97
+ }
98
+
99
+ // Chat | Terminal for the same owner and Session: a small radio group in the
100
+ // view's own header row, not another navigation layer.
101
+ function modeSwitch(page) {
102
+ const t = page.t;
103
+ const buttons = {};
104
+ const node = h("div.seg.seg-sm.sp-modes", { role: "radiogroup", "aria-label": t("session.mode"), "data-i18n-aria-label": "session.mode" });
105
+ [["chat", "sparkle", "session.modeChat"], ["terminal", "terminal", "session.modeTerminal"]].forEach(function (entry) {
106
+ const button = h("button.seg-btn", { type: "button", role: "radio", dataset: { mode: entry[0] },
107
+ "aria-label": t(entry[2]), "data-i18n-aria-label": entry[2] },
108
+ icon(entry[1]), h("span", { dataset: { i18n: entry[2] } }, t(entry[2])));
109
+ button.addEventListener("click", function () { setMode(page, entry[0]); });
110
+ buttons[entry[0]] = button;
111
+ node.append(button);
112
+ });
113
+ node.addEventListener("keydown", function (event) {
114
+ if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)) return;
115
+ event.preventDefault();
116
+ const next = page.mode === "chat" ? "terminal" : "chat";
117
+ if (!setMode(page, next)) buttons[next].focus();
118
+ });
119
+ return { node: node, buttons: buttons, focus: function (mode) { buttons[mode].focus(); } };
120
+ }
121
+
122
+ function syncSwitch(page) {
123
+ const switcher = page.switcher;
124
+ if (!switcher) return;
125
+ const block = terminalBlock(page);
126
+ Object.keys(switcher.buttons).forEach(function (mode) {
127
+ const button = switcher.buttons[mode];
128
+ const chosen = mode === page.mode;
129
+ button.setAttribute("aria-checked", String(chosen));
130
+ button.tabIndex = chosen ? 0 : -1;
131
+ });
132
+ const terminal = switcher.buttons.terminal;
133
+ // Blocked stays focusable and explains itself on use instead of going silent.
134
+ terminal.setAttribute("aria-disabled", String(!!block && page.mode !== "terminal"));
135
+ terminal.title = block && page.mode !== "terminal" ? page.t(block + "Text") : page.t("session.modeTerminal");
136
+ }
137
+
138
+ // The chat reports its selected Session so the terminal can follow it.
139
+ function chatView(page, root, tools, materials) {
140
+ let view = null, opened = false;
141
+ const ctl = createConversationController(root, page.t, page.locale, {
142
+ tools: tools,
143
+ onSelection: function (id, currentId) {
144
+ if (page.view !== view) return;
145
+ page.session = { id: id || null, currentId: currentId };
146
+ syncSwitch(page);
147
+ }
148
+ });
149
+ view = {
150
+ kind: "chat",
151
+ start: function () {
152
+ if (opened) { ctl.reconnect(); return; }
153
+ opened = true;
154
+ ctl.open(page.owner, { session: page.session && page.session.id, materials: materials });
155
+ },
156
+ stop: function () { ctl.close(); },
157
+ destroy: function (discard) { ctl.close(); if (discard) ctl.discardDraft(); },
158
+ selection: ctl.selection,
159
+ connected: function () { return false; },
160
+ captureDraftTarget: ctl.captureDraftTarget,
161
+ insertReference: ctl.insertReference,
162
+ relabel: function () {},
163
+ retheme: function () {}
164
+ };
165
+ return view;
166
+ }
167
+
168
+ function sessionName(page, info) {
169
+ const t = page.t;
170
+ const current = !!info.id && info.id === info.currentSessionId;
171
+ const s = info.session;
172
+ const when = s ? formatShort(s.createdAt, page.locale()) : "";
173
+ return (current ? t("conversation.current") : t("conversation.previous")) + ((s && s.title) || when ? " · " + ((s && s.title) || when) : "");
174
+ }
175
+
176
+ function terminalView(page, root, tools) {
177
+ const t = page.t;
178
+ let view = null;
179
+ const name = h("span.sp-term-name", null, t("conversation.sessions"));
180
+ const state = h("span.conn", { dataset: { state: "idle" } }, h("i", { "aria-hidden": "true" }), h("span", null, t("terminal.idle")));
181
+ const host = h("div.terminal-host");
182
+ const emptyTitle = h("p.terminal-empty-title");
183
+ const emptyText = h("p.terminal-empty-text");
184
+ const toCurrent = h("button.btn", { type: "button", hidden: true }, t("conversation.backCurrent"));
185
+ const empty = h("div.terminal-empty", { hidden: true }, icon("terminal", "icon icon-xl"), emptyTitle, emptyText, toCurrent);
186
+ const cli = h("code");
187
+ root.classList.add("dock-session");
188
+ root.append(h("div.session-bar", null, icon("terminal", "icon-sm sp-term-icon"), name, h("span.spacer"), state, tools),
189
+ host, empty,
190
+ h("footer.session-foot", null, h("span", { dataset: { i18n: "terminal.cliHint" } }, t("terminal.cliHint")), cli));
191
+ const ctl = createTerminalController({ host: host, empty: empty, emptyTitle: emptyTitle, emptyText: emptyText, state: state, cli: cli },
192
+ t, page.locale, page.notify, {
193
+ onSession: function (info) {
194
+ if (page.view !== view) return;
195
+ page.session = { id: info.id, currentId: info.currentSessionId };
196
+ name.textContent = info.id ? sessionName(page, info) : t("conversation.noSessionShort");
197
+ name.title = name.textContent;
198
+ toCurrent.hidden = !(info.id && info.currentSessionId && info.id !== info.currentSessionId);
199
+ syncSwitch(page);
200
+ }
201
+ });
202
+ // An explicit choice, like the chat's "Back to current Session".
203
+ toCurrent.addEventListener("click", function () {
204
+ if (!page.session || !page.session.currentId) return;
205
+ page.session = { id: page.session.currentId, currentId: page.session.currentId };
206
+ toCurrent.hidden = true;
207
+ view.start();
208
+ });
209
+ function target() {
210
+ return { ...page.owner, ...(page.session && page.session.id ? { nativeSessionId: page.session.id } : {}) };
211
+ }
212
+ view = {
213
+ kind: "terminal",
214
+ start: function () { ctl.open(target()); },
215
+ stop: function () { ctl.close(); },
216
+ destroy: function () { ctl.close(); },
217
+ selection: function () {
218
+ const session = page.session;
219
+ return session && session.id && session.id === session.currentId ? { ...page.owner, nativeSessionId: session.id } : null;
220
+ },
221
+ connected: ctl.connected,
222
+ captureDraftTarget: function () { return null; },
223
+ insertReference: function () { throw new Error(t("bodySearch.targetChanged")); },
224
+ relabel: function () { ctl.relabel(); },
225
+ retheme: function () { ctl.retheme(); }
226
+ };
227
+ return view;
228
+ }
229
+ `;
@@ -1,13 +1,13 @@
1
1
  export const TERMINAL_SCRIPT = String.raw `
2
- // The live native Session terminal in the dock: target chips, the xterm view
3
- // and its WebSocket. One session is open at a time; opening another target
4
- // disposes the previous one first.
2
+ // The live native terminal of the session page (views/dock/session): the xterm
3
+ // view and its WebSocket for one owner's current Session. The session page
4
+ // builds the surrounding chrome and destroys this controller with its view;
5
+ // opening another target disposes the previous connection first.
5
6
  import { Terminal } from "/assets/vendor/xterm.mjs";
6
7
  import { FitAddon } from "/assets/vendor/addon-fit.mjs";
7
- import { h, icon, clear } from "/assets/js/lib/dom.js";
8
+ import { clear } from "/assets/js/lib/dom.js";
8
9
  import { fill } from "/assets/js/lib/format.js";
9
10
  import { pageToken, requestJson } from "/assets/js/lib/api.js";
10
- import { dot } from "/assets/js/ui/primitives.js";
11
11
  import { sessionExpired } from "/assets/js/lib/session.js";
12
12
 
13
13
  function cssVar(name, fallback) {
@@ -34,18 +34,19 @@ export function sameTarget(left, right) {
34
34
  && (left.scope !== "task" || left.taskId === right.taskId);
35
35
  }
36
36
 
37
- // elements: { host, empty, state, cli, targets }; locale() reads the current
38
- // locale; notify(message) shows a toast.
39
- export function createTerminalController(elements, t, locale, notify) {
40
- const ctl = { elements: elements, t: t, locale: locale, notify: notify, session: null, current: null, generation: 0, stateKey: "terminal.idle", targets: [] };
41
- drawTargets(ctl);
37
+ // elements: { host, empty, emptyTitle, emptyText, state, cli }; locale() reads
38
+ // the current locale; notify(message) shows a toast.
39
+ // hooks.onSession({ id, currentSessionId, session }) reports which Session the
40
+ // open target resolved to (or that it has none), before any connection.
41
+ export function createTerminalController(elements, t, locale, notify, hooks) {
42
+ const ctl = { elements: elements, t: t, locale: locale, notify: notify, hooks: hooks || {}, session: null, current: null,
43
+ generation: 0, stateKey: "terminal.idle", emptyKey: null };
42
44
  return {
43
- open: function (target) { open(ctl, target); },
45
+ open: function (target) { return open(ctl, target); },
44
46
  close: function () { close(ctl); },
45
47
  current: function () { return ctl.current; },
46
48
  connected: function () { return !!ctl.session; },
47
- setTargets: function (next) { ctl.targets = next; drawTargets(ctl); },
48
- relabel: function () { setState(ctl, ctl.stateKey, elements.state.dataset.state); drawTargets(ctl); },
49
+ relabel: function () { setState(ctl, ctl.stateKey, elements.state.dataset.state); showEmpty(ctl, ctl.emptyKey); },
49
50
  retheme: function () { if (ctl.session) ctl.session.terminal.options.theme = terminalTheme(); }
50
51
  };
51
52
  }
@@ -56,19 +57,16 @@ function setState(ctl, key, state) {
56
57
  ctl.elements.state.lastChild.textContent = ctl.t(key);
57
58
  }
58
59
 
59
- function drawTargets(ctl) {
60
- const t = ctl.t;
61
- const list = ctl.elements.targets;
62
- clear(list);
63
- ctl.targets.forEach(function (target) {
64
- list.append(h("button.target-chip", {
65
- type: "button",
66
- "aria-pressed": String(sameTarget(target, ctl.current)),
67
- title: target.scope === "task" ? target.taskId + " / " + target.roleName : t("terminal.global") + " / " + target.roleName,
68
- onclick: function () { open(ctl, target); }
69
- }, target.scope === "task" ? dot(target.live ? "ok" : "idle") : icon("broadcast", "icon-sm"), h("span", null, target.roleName)));
70
- });
71
- if (!ctl.targets.length) list.append(h("span.faint.small", null, t("terminal.noTargets")));
60
+ // Why nothing is attached, in place of the terminal: no Session yet, or the
61
+ // selected Session is no longer the current one. Null hides the message.
62
+ function showEmpty(ctl, key) {
63
+ ctl.emptyKey = key;
64
+ const elements = ctl.elements;
65
+ if (!elements.empty) return;
66
+ elements.empty.hidden = !key;
67
+ if (!key) return;
68
+ if (elements.emptyTitle) elements.emptyTitle.textContent = ctl.t(key + "Title");
69
+ if (elements.emptyText) elements.emptyText.textContent = ctl.t(key + "Text");
72
70
  }
73
71
 
74
72
  function dispose(ctl) {
@@ -86,30 +84,38 @@ function dispose(ctl) {
86
84
  function close(ctl) {
87
85
  dispose(ctl);
88
86
  ctl.current = null;
89
- ctl.elements.empty.hidden = false;
87
+ showEmpty(ctl, null);
90
88
  ctl.elements.cli.textContent = "yui operator enter";
91
89
  setState(ctl, "terminal.idle", "idle");
92
- drawTargets(ctl);
93
90
  }
94
91
 
92
+ // A target without nativeSessionId attaches to the owner's current Session; a
93
+ // target naming one attaches only while it is still current. A replaced
94
+ // Session is reported, never silently exchanged for the new current one.
95
95
  async function open(ctl, target) {
96
96
  dispose(ctl);
97
97
  const generation = ctl.generation;
98
98
  ctl.current = target;
99
- ctl.elements.empty.hidden = true;
99
+ showEmpty(ctl, null);
100
100
  ctl.elements.cli.textContent = cliFor(target);
101
101
  setState(ctl, "terminal.connecting", "connecting");
102
- drawTargets(ctl);
103
102
  try {
104
- if (!target.nativeSessionId) {
105
- const query = new URLSearchParams({ scope: target.scope, role: target.roleName });
106
- if (target.scope === "task") query.set("task", target.taskId);
107
- const facts = await requestJson("/api/conversation?" + query);
108
- if (generation !== ctl.generation) return;
109
- if (!facts.currentSessionId) throw new Error(ctl.t("conversation.noSession"));
110
- target = { ...target, nativeSessionId: facts.currentSessionId };
111
- ctl.current = target;
103
+ const query = new URLSearchParams({ scope: target.scope, role: target.roleName });
104
+ if (target.scope === "task") query.set("task", target.taskId);
105
+ const facts = await requestJson("/api/conversation?" + query);
106
+ if (generation !== ctl.generation) return;
107
+ const currentId = facts.currentSessionId || null;
108
+ const id = target.nativeSessionId || currentId;
109
+ const session = (facts.sessions || []).find(function (s) { return s.nativeSessionId === id; }) || null;
110
+ if (ctl.hooks.onSession) ctl.hooks.onSession({ id: id, currentSessionId: currentId, session: session });
111
+ if (generation !== ctl.generation) return;
112
+ if (!id || id !== currentId) {
113
+ setState(ctl, "terminal.unavailable", "idle");
114
+ showEmpty(ctl, id ? "terminal.notCurrent" : "terminal.noSession");
115
+ return;
112
116
  }
117
+ target = { ...target, nativeSessionId: id };
118
+ ctl.current = target;
113
119
  } catch (error) {
114
120
  if (generation === ctl.generation) { setState(ctl, "terminal.error", "error"); ctl.notify(error.message); }
115
121
  return;
@@ -197,6 +203,9 @@ function ready(ctl, message, terminal, link) {
197
203
  current: number.format(message.history.limit), target: number.format(message.history.target)
198
204
  }));
199
205
  }
200
- terminal.focus();
206
+ // A keyboard user moving through the session page's Chat | Terminal switch
207
+ // keeps focus there (arrow keys must not become terminal input).
208
+ const active = document.activeElement;
209
+ if (!(active && active.closest && active.closest(".sp-modes"))) terminal.focus();
201
210
  }
202
211
  `;
@@ -4,7 +4,7 @@ export const OVERVIEW_SCRIPT = String.raw `
4
4
  // then catalog-wide signals and the explicit Session read.
5
5
  import { h, icon, clear } from "/assets/js/lib/dom.js";
6
6
  import { byNewest, fill, formatDateTime, relativeTime } from "/assets/js/lib/format.js";
7
- import { badge, button, dot, emptyState, note } from "/assets/js/ui/primitives.js";
7
+ import { badge, button, dot, emptyState, note, panelToggle, topBar } from "/assets/js/ui/primitives.js";
8
8
  import { card } from "/assets/js/ui/containers.js";
9
9
  import { metricTile } from "/assets/js/ui/metrics.js";
10
10
  import { label, sessionBadges, tone } from "/assets/js/domain/vocab.js";
@@ -16,6 +16,12 @@ export function renderOverview(container, state, t, locale, actions) {
16
16
  const active = tasks.filter(function (task) { return task.status === "active"; });
17
17
  const recent = tasks.filter(function (task) { return task.status !== "archived"; }).slice().sort(byNewest).slice(0, 8);
18
18
  const page = h("div.page.overview");
19
+ container.append(topBar(t, actions.revealSidebar,
20
+ h("nav.crumbs", { "aria-label": t("crumbs.label") },
21
+ h("span.crumb", null, t("manage.workbench")),
22
+ h("span.crumb-sep", { "aria-hidden": "true" }, "/"),
23
+ h("span.crumb-current", null, t("overview.title"))),
24
+ [panelToggle(t, actions.togglePanel)]));
19
25
  page.append(overviewHead(state.counts, t, actions), metricStrip(state.counts, t));
20
26
  page.append(h("div.card-stack", null,
21
27
  inboxCard(state, t, actions),
@@ -30,13 +36,10 @@ export function renderOverview(container, state, t, locale, actions) {
30
36
  function overviewHead(counts, t, actions) {
31
37
  return h("header.page-head", null,
32
38
  h("div.page-head-main", null,
33
- h("p.kicker", null, t("overview.kicker")),
34
39
  h("h1.page-title", null, t("overview.title")),
35
40
  h("p.page-sub", null, counts
36
41
  ? fill(t("overview.sub"), { total: counts.total, active: counts.active || 0 })
37
- : t("loading.dashboard"))),
38
- h("div.page-head-actions", null,
39
- button(t("actions.operator"), { icon: "terminal", variant: "ghost", onClick: actions.openOperator })));
42
+ : t("loading.dashboard"))));
40
43
  }
41
44
 
42
45
  function metricStrip(counts, t) {
@@ -4,7 +4,7 @@ export const TASK_PAGE_SCRIPT = String.raw `
4
4
  // observation arrives separately from the Context snapshot, so every fact it
5
5
  // feeds lives in a [data-slot] that updateObservation() redraws in place.
6
6
  import { h, icon, clear } from "/assets/js/lib/dom.js";
7
- import { badge, chip, timeTag } from "/assets/js/ui/primitives.js";
7
+ import { badge, chip, panelToggle, timeTag, topBar } from "/assets/js/ui/primitives.js";
8
8
  import { label, statusBadge } from "/assets/js/domain/vocab.js";
9
9
  import { totalOf } from "/assets/js/domain/context.js";
10
10
  import { renderOverviewPanel } from "/assets/js/views/task/overview.js";
@@ -29,8 +29,8 @@ export function renderTaskDetail(container, data, t, locale, ctx) {
29
29
  container.dataset.jobsLocale = locale;
30
30
  data.viewState.coreKey = data.core.coreCursor;
31
31
  const page = h("div.page.task-page");
32
- page.append(h("header.task-header", null, taskBar(task, t, ctx)),
33
- h("div.task-hero", null, h("h1.task-title", null, task.title), taskMeta(task, t, locale)),
32
+ container.append(taskBar(task, t, ctx));
33
+ page.append(h("div.task-hero", null, h("h1.task-title", null, task.title), taskMeta(task, t, locale)),
34
34
  h("div.tabs-wrap", null, taskTabs(data.core, t, ctx)));
35
35
  const panels = {};
36
36
  TABS.forEach(function (tab) {
@@ -74,22 +74,12 @@ export function selectTab(container, tab) {
74
74
  }
75
75
 
76
76
  function taskBar(task, t, ctx) {
77
- return h("div.task-bar", null,
78
- h("button.icon-btn.back-btn", { type: "button", "aria-label": t("actions.back"), title: t("actions.back"), onclick: ctx.onBack }, icon("back")),
77
+ return topBar(t, ctx.revealSidebar,
79
78
  h("nav.crumbs", { "aria-label": t("crumbs.label") },
80
- h("button.crumb", { type: "button", onclick: ctx.onBack }, t("crumbs.tasks")),
79
+ h("button.crumb", { type: "button", title: t("overview.title"), onclick: ctx.onBack }, t("manage.workbench")),
81
80
  h("span.crumb-sep", { "aria-hidden": "true" }, "/"),
82
- h("code.crumb-current", null, task.id)),
83
- h("span.spacer"),
84
- dockToggle("discussion", "chat", t("dock.discussion") + " · D", t, ctx),
85
- dockToggle("session", "terminal", t("dock.session"), t, ctx));
86
- }
87
-
88
- function dockToggle(mode, iconName, title, t, ctx) {
89
- return h("button.btn.btn-ghost.dock-toggle", {
90
- type: "button", dataset: { dockToggle: mode }, title: title,
91
- onclick: function () { ctx.showDock(mode); }
92
- }, icon(iconName), h("span", null, t("dock." + mode)));
81
+ h("code.crumb-current", { title: task.title }, task.id)),
82
+ [panelToggle(t, ctx.togglePanel)]);
93
83
  }
94
84
 
95
85
  function taskMeta(task, t, locale) {
@@ -0,0 +1,22 @@
1
+ import { THEMES } from "./themes.js";
2
+ // The Yui mark: a capital Y with an accent dot on a tile (64×64 grid). One
3
+ // geometry source for the in-page mark and the favicon. The in-page mark is
4
+ // drawn with theme tokens via CSS (tile = ink, Y = canvas, dot = accent), so it
5
+ // follows a theme switch; the favicon cannot read page CSS, so each theme gets
6
+ // a pre-coloured copy that the theme script swaps in.
7
+ const Y_PATH = "M17 17L30 33L43 17M30 33V49";
8
+ const DOT = { cx: 49.5, cy: 46, r: 4.6 };
9
+ // Inline glyph for `.brand-mark`; the tile itself is the element's background.
10
+ export function brandGlyphSvg() {
11
+ return `<svg class="brand-glyph" viewBox="0 0 64 64" aria-hidden="true" focusable="false">`
12
+ + `<path d="${Y_PATH}" fill="none" stroke="currentColor" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>`
13
+ + `<circle class="brand-dot" cx="${DOT.cx}" cy="${DOT.cy}" r="${DOT.r}"/></svg>`;
14
+ }
15
+ function brandIconSvg(tile, glyph, dot) {
16
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">`
17
+ + `<rect width="64" height="64" rx="16" fill="${tile}"/>`
18
+ + `<path d="${Y_PATH}" fill="none" stroke="${glyph}" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>`
19
+ + `<circle cx="${DOT.cx}" cy="${DOT.cy}" r="${DOT.r}" fill="${dot}"/></svg>`;
20
+ }
21
+ // Favicon data URIs keyed by theme id (img-src allows data:).
22
+ export const BRAND_ICONS = Object.freeze(Object.fromEntries(THEMES.map(theme => [theme.id, "data:image/svg+xml," + encodeURIComponent(brandIconSvg(theme.tokens.ink, theme.tokens.canvas, theme.tokens.accent))])));
@@ -9,11 +9,17 @@ export const BREAKPOINTS = Object.freeze({
9
9
  // ≤ small: phone layout for page content.
10
10
  small: 620
11
11
  });
12
+ // The global navigation rail (Workbench / Models / Settings) on every page.
13
+ export const RAIL_WIDTH = 60;
12
14
  export const SIDEBAR = Object.freeze({ min: 240, max: 560, compact: 264, step: 16 });
13
15
  export const DOCK = Object.freeze({ min: 320, max: 760, initial: 440, step: 24 });
14
16
  // The task column never shrinks below this while a divider is dragged.
15
17
  export const CENTER_MIN = 420;
16
18
  export const DIVIDER_WIDTH = 9;
19
+ // Between narrow and this width an open dock and the default task list cannot
20
+ // both fit beside a minimum task column, so the task list steps aside while
21
+ // the dock is open (closing the dock brings it back).
22
+ export const DOCK_FOCUS_MAX = RAIL_WIDTH + SIDEBAR.compact + DIVIDER_WIDTH + DOCK.initial + CENTER_MIN - 1;
17
23
  export const LAYOUT_GEOMETRY = Object.freeze({
18
- narrow: BREAKPOINTS.narrow, sidebar: SIDEBAR, dock: DOCK, centerMin: CENTER_MIN, divider: DIVIDER_WIDTH
24
+ narrow: BREAKPOINTS.narrow, focusMax: DOCK_FOCUS_MAX, rail: RAIL_WIDTH, sidebar: SIDEBAR, dock: DOCK, centerMin: CENTER_MIN, divider: DIVIDER_WIDTH
19
25
  });
@@ -4,10 +4,14 @@
4
4
  export const ICON_PATHS = Object.freeze({
5
5
  refresh: ["M20 11a8 8 0 0 0-14.3-4.9L4 8", "M4 4v4h4", "M4 13a8 8 0 0 0 14.3 4.9L20 16", "M20 20v-4h-4"],
6
6
  search: ["M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14z", "M20 20l-4-4"],
7
+ textSearch: ["M4 6h14", "M4 11h6", "M4 16h4", "M15.5 19a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z", "M21 21l-3-3"],
7
8
  terminal: ["M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z", "M7 10l3 2-3 2", "M12 15h5"],
8
9
  send: ["M4 12l16-8-6 16-2.5-6.5z", "M11.5 13.5L20 4"],
9
10
  swap: ["M7 7h13", "M16 3l4 4-4 4", "M17 17H4", "M8 13l-4 4 4 4"],
10
11
  close: ["M6 6l12 12", "M18 6L6 18"],
12
+ workbench: ["M4 4h7v7H4z", "M13 4h7v4h-7z", "M13 10h7v10h-7z", "M4 13h7v7H4z"],
13
+ sidebar: ["M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z", "M9 5v14", "M5.5 9h1", "M5.5 12h1"],
14
+ menu: ["M4 7h16", "M4 12h16", "M4 17h16"],
11
15
  back: ["M19 12H5", "M11 6l-6 6 6 6"],
12
16
  dock: ["M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z", "M14 5v14", "M17 9h1", "M17 12h1"],
13
17
  settings: ["M4 7h10", "M18 7h2", "M16 5v4", "M4 17h4", "M12 17h8", "M10 15v4"],
@@ -0,0 +1,74 @@
1
+ // The motion system: every duration, easing, keyframe and stacking layer in
2
+ // the web UI comes from here. The stylesheets read the generated CSS custom
3
+ // properties (tokens.ts) and keyframes (styles/motion.ts); client scripts read
4
+ // the same values as JSON (client/lib/motion.ts). Changing a number below
5
+ // retimes the whole interface in one place.
6
+ //
7
+ // The curves follow well-known open-source component libraries: `sheet` is
8
+ // the iOS / Vaul drawer curve, `standard` a Material/Radix-style ease-out for
9
+ // small UI, and `exit` an accelerate curve so things leave quicker than they
10
+ // arrive.
11
+ export const DURATIONS = Object.freeze({
12
+ // Hover, press and colour feedback.
13
+ fast: 120,
14
+ // Popovers, menus, dialogs, content swaps.
15
+ base: 200,
16
+ // Exits are a little quicker than entries.
17
+ exit: 150,
18
+ // Column resizes (sidebar collapse, dock open) via View Transitions.
19
+ layout: 260,
20
+ // Drawers and sheets.
21
+ sheet: 420,
22
+ sheetOut: 300,
23
+ // Cross-document page transitions.
24
+ page: 220
25
+ });
26
+ export const EASINGS = Object.freeze({
27
+ standard: "cubic-bezier(.2,.8,.2,1)",
28
+ sheet: "cubic-bezier(.32,.72,0,1)",
29
+ exit: "cubic-bezier(.4,0,1,1)"
30
+ });
31
+ // Stacking layers, lowest to highest.
32
+ export const LAYERS = Object.freeze({
33
+ raised: 1,
34
+ sticky: 10,
35
+ header: 20,
36
+ popover: 30,
37
+ sheet: 60,
38
+ scrim: 70,
39
+ drawer: 71,
40
+ railOver: 72,
41
+ toast: 90,
42
+ skip: 100
43
+ });
44
+ // Keyframe presets. Each is emitted as a CSS @keyframes rule named
45
+ // `m-<name>` and handed to the Web Animations API by the client.
46
+ export const PRESETS = Object.freeze({
47
+ fade: { frames: [{ opacity: 0 }, { opacity: 1 }], duration: "base", easing: "standard" },
48
+ rise: { frames: [{ opacity: 0, transform: "translate3d(0,6px,0)" }, { opacity: 1, transform: "none" }], duration: "base", easing: "standard" },
49
+ pop: { frames: [{ opacity: 0, transform: "scale(.96)" }, { opacity: 1, transform: "none" }], duration: "base", easing: "standard" },
50
+ sheetUp: { frames: [{ transform: "translate3d(0,100%,0)" }, { transform: "translate3d(0,0,0)" }], duration: "sheet", easing: "sheet" },
51
+ sheetDown: { frames: [{ transform: "translate3d(0,0,0)" }, { transform: "translate3d(0,100%,0)" }], duration: "sheetOut", easing: "sheet" }
52
+ });
53
+ const kebab = (name) => name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
54
+ // CSS custom properties: --t-<duration>, --ease-<easing>, --z-<layer>.
55
+ // `--t-med` and `--ease` stay as aliases for the existing stylesheets.
56
+ export function motionTokenStyles() {
57
+ const t = Object.entries(DURATIONS).map(([k, v]) => `--t-${kebab(k)}:${v}ms`);
58
+ const e = Object.entries(EASINGS).map(([k, v]) => `--ease-${kebab(k)}:${v}`);
59
+ const z = Object.entries(LAYERS).map(([k, v]) => `--z-${kebab(k)}:${v}`);
60
+ return [...t, "--t-med:var(--t-base)", ...e, "--ease:var(--ease-standard)", ...z].join(";") + ";";
61
+ }
62
+ function frameCss(frame) {
63
+ return Object.entries(frame).map(([k, v]) => `${kebab(k)}:${v}`).join(";");
64
+ }
65
+ export function motionKeyframes() {
66
+ const presets = Object.entries(PRESETS).map(([name, p]) => {
67
+ const last = p.frames.length - 1;
68
+ const stops = p.frames.map((f, i) => `${last ? Math.round((i / last) * 100) : 0}%{${frameCss(f)}}`).join("");
69
+ return `@keyframes m-${kebab(name)}{${stops}}`;
70
+ });
71
+ return [...presets, "@keyframes m-spin{to{transform:rotate(360deg)}}", "@keyframes m-blink{50%{opacity:.35}}"].join("\n");
72
+ }
73
+ // The JSON the client motion helper reads.
74
+ export const MOTION = Object.freeze({ durations: DURATIONS, easings: EASINGS, presets: PRESETS });
@@ -0,0 +1,14 @@
1
+ import { brandGlyphSvg } from "../shared/brand.js";
2
+ import { iconSvg } from "../shared/icons.js";
3
+ const item = (area, href, icon, key, label, current, id) => `<a${id ? ` id="${id}"` : ""} class="rail-item" href="${href}" data-area="${area}"${area === current ? ` aria-current="page"` : ""}${area === "workbench" && current === "workbench" ? "" : ` data-nav-area="${area}"`} title="${label}" data-i18n-title="${key}">${iconSvg(icon)}<span data-i18n="${key}">${label}</span></a>`;
4
+ export function appRail(current) {
5
+ const brandNav = current === "workbench" ? "" : ` data-nav-area="workbench"`;
6
+ return `<nav class="app-rail" aria-label="Main navigation" data-i18n-aria-label="manage.nav">
7
+ <a class="rail-brand" href="/"${brandNav} title="Yui"><span class="brand-mark" aria-hidden="true">${brandGlyphSvg()}</span><span class="sr-only">Yui</span></a>
8
+ <div class="rail-items">
9
+ ${item("workbench", "/", "workbench", "manage.workbench", "Workbench", current, current === "workbench" ? "rail-workbench" : undefined)}
10
+ ${item("models", "/models", "layers", "models.title", "Models", current)}
11
+ ${item("settings", "/settings", "settings", "settings.title", "Settings", current, current === "workbench" ? "settings-open" : undefined)}
12
+ </div>
13
+ </nav>`;
14
+ }
@@ -27,6 +27,7 @@ export const DIALOGS_HTML = ` <dialog id="settings-dialog" class="dialog settin
27
27
  <dt><kbd>R</kbd></dt><dd data-i18n="shortcut.refresh">Refresh</dd>
28
28
  <dt><kbd>D</kbd></dt><dd data-i18n="shortcut.dock">Show or hide the session panel</dd>
29
29
  <dt><kbd>O</kbd></dt><dd data-i18n="shortcut.operator">Operator session</dd>
30
+ <dt><kbd>[</kbd></dt><dd data-i18n="shortcut.sidebar">Show or hide the task list</dd>
30
31
  <dt><kbd>1</kbd>–<kbd>${TASK_TABS.length}</kbd></dt><dd data-i18n="shortcut.tabs">Switch task sections</dd>
31
32
  <dt><kbd>Esc</kbd></dt><dd data-i18n="shortcut.escape">Close panel / leave task</dd>
32
33
  </dl>