@zq-silk/yui 2.15.0 → 2.16.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 (53) hide show
  1. package/dist/web/assets/client/app/catalog.js +40 -14
  2. package/dist/web/assets/client/app/main.js +33 -9
  3. package/dist/web/assets/client/app/manage.js +18 -0
  4. package/dist/web/assets/client/app/selection.js +4 -1
  5. package/dist/web/assets/client/app/taskView.js +22 -5
  6. package/dist/web/assets/client/i18n/en.js +10 -3
  7. package/dist/web/assets/client/i18n/zh-CN.js +10 -3
  8. package/dist/web/assets/client/index.js +4 -0
  9. package/dist/web/assets/client/layout/manageShell.js +30 -8
  10. package/dist/web/assets/client/layout/shortcuts.js +7 -3
  11. package/dist/web/assets/client/layout/sidebarNav.js +147 -0
  12. package/dist/web/assets/client/layout/sizing.js +5 -2
  13. package/dist/web/assets/client/layout/workspace.js +120 -21
  14. package/dist/web/assets/client/lib/motion.js +59 -0
  15. package/dist/web/assets/client/lib/prefs.js +29 -2
  16. package/dist/web/assets/client/lib/theme.js +11 -4
  17. package/dist/web/assets/client/ui/primitives.js +27 -0
  18. package/dist/web/assets/client/views/overview.js +8 -5
  19. package/dist/web/assets/client/views/task/page.js +10 -10
  20. package/dist/web/assets/shared/brand.js +22 -0
  21. package/dist/web/assets/shared/geometry.js +7 -1
  22. package/dist/web/assets/shared/icons.js +4 -0
  23. package/dist/web/assets/shared/motion.js +74 -0
  24. package/dist/web/assets/shell/appRail.js +14 -0
  25. package/dist/web/assets/shell/dialogs.js +1 -0
  26. package/dist/web/assets/shell/dock.js +19 -6
  27. package/dist/web/assets/shell/index.js +6 -2
  28. package/dist/web/assets/shell/login.js +9 -8
  29. package/dist/web/assets/shell/manage.js +32 -20
  30. package/dist/web/assets/shell/settingsContent.js +1 -1
  31. package/dist/web/assets/shell/sidebar.js +16 -23
  32. package/dist/web/assets/styles/base.js +1 -1
  33. package/dist/web/assets/styles/index.js +2 -0
  34. package/dist/web/assets/styles/layout/responsive.js +63 -19
  35. package/dist/web/assets/styles/layout/workspace.js +53 -16
  36. package/dist/web/assets/styles/motion.js +51 -0
  37. package/dist/web/assets/styles/tokens.js +5 -3
  38. package/dist/web/assets/styles/ui/controls.js +1 -1
  39. package/dist/web/assets/styles/ui/forms.js +1 -1
  40. package/dist/web/assets/styles/ui/surfaces.js +1 -2
  41. package/dist/web/assets/styles/views/conversation.js +1 -1
  42. package/dist/web/assets/styles/views/dock.js +22 -2
  43. package/dist/web/assets/styles/views/login.js +16 -13
  44. package/dist/web/assets/styles/views/manage.js +25 -42
  45. package/dist/web/assets/styles/views/page.js +2 -3
  46. package/dist/web/assets/styles/views/records.js +1 -1
  47. package/dist/web/assets/styles/views/settings.js +1 -1
  48. package/dist/web/assets/styles/views/sidebar.js +25 -20
  49. package/dist/web/assets/styles/views/task.js +9 -15
  50. package/dist/web/assets/styles/views/terminal.js +1 -1
  51. package/docs/release-workflow.md +22 -0
  52. package/docs/release-workflow.zh-CN.md +15 -0
  53. package/package.json +1 -1
@@ -51,8 +51,21 @@ async function refresh(deps, progress, options) {
51
51
  const query = catalogQuery(state);
52
52
  const dashboard = await deps.api.dashboard(query);
53
53
  if (request !== progress.request) return;
54
- applyDashboard(state, dashboard, query);
55
- showDashboard(deps, progress, previousInputs);
54
+ // An unchanged quiet poll leaves the list and overview alone: rebuilding
55
+ // them every few seconds would interrupt scrolling and animations. They
56
+ // still redraw once a minute so relative times stay true.
57
+ const signature = query + "|" + JSON.stringify(dashboard);
58
+ const now = Date.now();
59
+ if (quiet && signature === progress.signature && now - progress.drawnAt < 60000) {
60
+ state.generatedAt = new Date().toISOString();
61
+ el.sync.dataset.state = "ok";
62
+ el.lastSync.textContent = formatClock(state.generatedAt, deps.locale());
63
+ } else {
64
+ progress.signature = signature;
65
+ progress.drawnAt = now;
66
+ applyDashboard(state, dashboard, query);
67
+ showDashboard(deps, progress, previousInputs);
68
+ }
56
69
  // A selected Task may live on another page or outside current filters;
57
70
  // only its own read can establish that it no longer exists.
58
71
  if (state.selected) {
@@ -98,18 +111,10 @@ function renderSidebar(deps, progress) {
98
111
  reset(deps, progress);
99
112
  });
100
113
  el.catalogCount.textContent = fill(t("catalog.count"), { shown: state.tasks.length, total: state.catalogTotal });
101
- let source = el.filters.querySelector("select");
102
- if (!source) {
103
- source = h("select", { "aria-label": t("jobs.taskCategory") });
104
- source.addEventListener("change", function () {
105
- state.source = source.value; state.attentionFilter = null; state.query = ""; el.search.value = "";
106
- syncUrl(state, false); reset(deps, progress);
107
- });
108
- el.filters.append(source);
109
- }
110
- source.replaceChildren(...["ordinary", "job", "all"].map(value =>
111
- h("option", { value }, t("jobs.category." + value))));
112
- source.value = state.query.trim() || state.attentionFilter ? "all" : state.source || "ordinary";
114
+ renderSource(el.source, state, t, function (value) {
115
+ state.source = value; state.attentionFilter = null; state.query = ""; el.search.value = "";
116
+ syncUrl(state, false); reset(deps, progress);
117
+ });
113
118
  el.catalogReset.hidden = !state.catalogCursor;
114
119
  el.catalogNext.hidden = !state.nextCursor && !state.catalogCursor;
115
120
  el.catalogAttentionReset.hidden = !state.attentionFilter;
@@ -117,6 +122,27 @@ function renderSidebar(deps, progress) {
117
122
  el.lastSync.textContent = state.generatedAt ? formatClock(state.generatedAt, deps.locale()) : "—";
118
123
  }
119
124
 
125
+ // Task category: search and attention are cross-category, so they show "all";
126
+ // picking a category clears them, as the server filter then applies again.
127
+ function renderSource(container, state, t, onPick) {
128
+ const current = state.query.trim() || state.attentionFilter ? "all" : state.source || "ordinary";
129
+ // Bound once; the handler reads the live state, not this render's snapshot.
130
+ if (!container.childElementCount) {
131
+ ["ordinary", "job", "all"].forEach(function (value) {
132
+ container.append(h("button.seg-btn", { type: "button", role: "radio", dataset: { source: value },
133
+ onclick: function () {
134
+ if (value !== (state.source || "ordinary") || state.query.trim() || state.attentionFilter) onPick(value);
135
+ } }));
136
+ });
137
+ }
138
+ container.querySelectorAll("[data-source]").forEach(function (button) {
139
+ const value = button.dataset.source;
140
+ button.textContent = t("jobs.categoryShort." + value);
141
+ button.title = t("jobs.category." + value);
142
+ button.setAttribute("aria-checked", String(value === current));
143
+ });
144
+ }
145
+
120
146
  // Redraw the list in place, keeping its scroll position.
121
147
  function renderTaskList(deps) {
122
148
  const scroll = deps.el.tasks.scrollTop;
@@ -17,7 +17,7 @@ export const APP_SCRIPT = String.raw `
17
17
  // refresh catalog.refresh → sidebar, then the overview (nothing selected)
18
18
  // or selection.reload → taskView.render
19
19
  // select sidebar row / overview row / URL → selection.selectTask
20
- // leave back button or Escape → selection.clearSelection
20
+ // leave rail Workbench or Escape → selection.clearSelection
21
21
  // write Task page or discussion afterWrite → quiet catalog refresh
22
22
  // URL load and back/forward → catalog.applyFilters + selection.followUrl
23
23
  import { createI18n } from "/assets/js/lib/i18n.js";
@@ -31,7 +31,9 @@ import { bindGlobalInput } from "/assets/js/views/globalInput.js";
31
31
  import { bindCreateTask } from "/assets/js/views/createTask.js";
32
32
  import { bindBodySearch } from "/assets/js/views/bodySearch.js";
33
33
  import { readQuery } from "/assets/js/layout/router.js";
34
+ import { rememberPlace, linkPlaces } from "/assets/js/lib/prefs.js";
34
35
  import { createWorkspace } from "/assets/js/layout/workspace.js";
36
+ import { createSidebarNav } from "/assets/js/layout/sidebarNav.js";
35
37
  import { bindShortcuts } from "/assets/js/layout/shortcuts.js";
36
38
  import { createTaskView } from "/assets/js/app/taskView.js";
37
39
  import { createSelection } from "/assets/js/app/selection.js";
@@ -39,12 +41,14 @@ import { createCatalog } from "/assets/js/app/catalog.js";
39
41
 
40
42
  const $ = function (selector) { return document.querySelector(selector); };
41
43
  const el = {
42
- sidebar: $(".sidebar"), sidebarDivider: $("#sidebar-divider"), center: $("#center"), detail: $("#detail"),
43
- search: $("#search"), filters: $("#status-filters"), attention: $("#attention-bar"), tasks: $("#task-list"),
44
+ sidebar: $(".sidebar"), rail: $(".app-rail"), railWorkbench: $("#rail-workbench"),
45
+ sidebarScrim: $("#sidebar-scrim"), sidebarCollapse: $("#sidebar-collapse"), sidebarDivider: $("#sidebar-divider"), center: $("#center"), detail: $("#detail"),
46
+ search: $("#search"), source: $("#catalog-source"), filters: $("#status-filters"), attention: $("#attention-bar"), tasks: $("#task-list"),
44
47
  catalogNext: $("#catalog-next"), catalogReset: $("#catalog-reset"), catalogCount: $("#catalog-count"),
45
48
  catalogAttentionReset: $("#catalog-attention-reset"),
46
49
  refresh: $("#refresh"), sync: $("#sync-state"), lastSync: $("#last-sync"), toast: $("#toast"),
47
- operator: $("#operator-terminal"), settingsOpen: $("#settings-open"), settings: $("#settings-dialog"),
50
+ dockTarget: $("#dock-target"), dockTargetLabel: $("#dock-target-label"), dockTargetIcon: $("#dock-target-icon"),
51
+ dockTargetOptions: $("#dock-target-options"), globalInputOpen: $("#global-input-open"), settings: $("#settings-dialog"),
48
52
  locale: $("#locale-select"), themeOptions: $("#theme-options"),
49
53
  dock: $("#dock"), divider: $("#dock-divider"), dockSwap: $("#dock-swap"), dockClose: $("#dock-close"),
50
54
  dockTabDiscussion: $("#dock-tab-discussion"), dockTabSession: $("#dock-tab-session"),
@@ -52,7 +56,7 @@ const el = {
52
56
  dockTabPreview: $("#dock-tab-preview"), preview: $("#dock-preview"),
53
57
  discussion: $("#dock-discussion"), session: $("#dock-session"),
54
58
  terminalHost: $("#terminal-host"), terminalEmpty: $("#terminal-empty"), terminalState: $("#terminal-state"),
55
- sessionTargets: $("#session-targets"), terminalCli: $("#terminal-cli")
59
+ sessionTargets: $("#session-targets"), settingsOpen: $("#settings-open"), terminalCli: $("#terminal-cli")
56
60
  };
57
61
 
58
62
  // View state. Each group has exactly one writer.
@@ -86,13 +90,18 @@ let taskView = null;
86
90
  let selection = null;
87
91
  let catalog = null;
88
92
  const workspace = createWorkspace({
89
- el: el, state: state, terminal: terminal, conversation: conversation, preview: preview,
93
+ el: el, state: state, t: t, terminal: terminal, conversation: conversation, preview: preview,
90
94
  sessionOpened: function () { taskView.updateSessionTargets(); }
91
95
  });
96
+ const sidebarNav = createSidebarNav({
97
+ el: { sidebar: el.sidebar, rail: el.rail, scrim: el.sidebarScrim, collapse: el.sidebarCollapse },
98
+ relayout: function () { workspace.updateLayout(); }
99
+ });
92
100
  taskView = createTaskView({
93
101
  el: el, state: state, t: t, locale: locale, api: api, toast: toast, terminal: terminal, workspace: workspace,
94
102
  selectTask: function (taskId) { return selection.selectTask(taskId); },
95
103
  clearSelection: function () { selection.clearSelection(); },
104
+ revealSidebar: function () { sidebarNav.reveal(); },
96
105
  pickAttention: function (kind) { catalog.pickAttention(kind); },
97
106
  readPageSessions: function () { return catalog.readPageSessions(); },
98
107
  refreshCatalog: function () { return catalog.refresh({ quiet: true }); }
@@ -114,10 +123,24 @@ bindBodySearch({ t: t, api: api, selectTask: selection.selectTask,
114
123
  currentTask: function () { return state.selected; },
115
124
  captureDraftTarget: conversation.captureDraftTarget, insertReference: conversation.insertReference });
116
125
  bindShortcuts({
117
- el: el, state: state, globalInput: globalInput, workspace: workspace,
126
+ el: el, state: state, globalInput: globalInput, workspace: workspace, sidebarNav: sidebarNav,
118
127
  selection: selection, catalog: catalog, taskView: taskView
119
128
  });
120
- el.settingsOpen.addEventListener("click", function () { window.location.assign("/settings"); });
129
+ // Workbench <-> Models/Settings: each side remembers where the user was, so a
130
+ // round trip returns to the same filters, Task and management section.
131
+ function rememberWorkbench() { rememberPlace("workbench", window.location.pathname + window.location.search); }
132
+ el.rail.querySelectorAll("a[data-nav-area]").forEach(function (link) { link.addEventListener("click", rememberWorkbench); });
133
+ // The rail's Workbench entry on the workbench itself returns to the overview.
134
+ el.railWorkbench.addEventListener("click", function (event) {
135
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
136
+ event.preventDefault();
137
+ sidebarNav.closeDrawer();
138
+ selection.clearSelection();
139
+ });
140
+ window.addEventListener("pagehide", rememberWorkbench);
141
+ function linkAll() { linkPlaces(el.rail); linkPlaces(el.sidebar); }
142
+ window.addEventListener("pageshow", linkAll);
143
+ linkAll();
121
144
 
122
145
  // The URL names the catalog filters and the selected Task (with its section).
123
146
  function applyStateFromUrl() {
@@ -140,9 +163,10 @@ i18n.subscribe(function () {
140
163
 
141
164
  workspace.updateLayout();
142
165
  taskView.showOverview();
166
+ // The panel starts closed: name its owner without polling until it opens.
143
167
  conversation.open({ scope: "global", roleName: "operator" });
168
+ conversation.close();
144
169
  applyStateFromUrl();
145
- workspace.restoreDefaultMode();
146
170
  catalog.refresh();
147
171
  window.setInterval(function () { catalog.refresh({ quiet: true }); }, 5000);
148
172
  `;
@@ -2,5 +2,23 @@ export const MANAGE_SCRIPT = String.raw `
2
2
  import "/assets/js/app/settings.js";
3
3
  import "/assets/js/app/models.js";
4
4
  import { startManagement } from "/assets/js/layout/manageShell.js";
5
+ import { createSidebarNav } from "/assets/js/layout/sidebarNav.js";
6
+ import { isEditing } from "/assets/js/lib/dom.js";
7
+
8
+ // The section list behaves like the workbench task list: collapsible beside
9
+ // the content, a drawer (with the global rail) on narrow screens.
10
+ const sidebarNav = createSidebarNav({
11
+ el: { sidebar: document.querySelector(".manage-rail"), rail: document.querySelector(".app-rail"), scrim: document.querySelector("#sidebar-scrim"),
12
+ collapse: document.querySelector("#sidebar-collapse") },
13
+ relayout: function () {}
14
+ });
15
+ document.querySelectorAll("[data-sidebar-toggle]").forEach(function (button) {
16
+ button.addEventListener("click", sidebarNav.reveal);
17
+ });
18
+ document.addEventListener("keydown", function (event) {
19
+ if (document.querySelector("dialog[open]") || event.metaKey || event.ctrlKey || event.altKey) return;
20
+ if (event.key === "Escape" && sidebarNav.drawerOpen()) { sidebarNav.closeDrawer(); return; }
21
+ if (event.key === "[" && !isEditing(document.activeElement)) sidebarNav.toggle();
22
+ });
5
23
  startManagement();
6
24
  `;
@@ -74,6 +74,7 @@ function clearSelection(deps) {
74
74
  syncUrl(deps.state, !urlTaskId());
75
75
  deps.workspace.updateLayout();
76
76
  deps.taskView.showOverview();
77
+ deps.taskView.revealPage();
77
78
  deps.renderTaskList();
78
79
  deps.taskView.updateSessionTargets();
79
80
  }
@@ -96,9 +97,11 @@ function followUrl(deps, params) {
96
97
  return;
97
98
  }
98
99
  if (state.selected && !canLeave(deps)) { syncUrl(state, true); return; }
100
+ const leaving = !!state.selected;
99
101
  dropSelection(deps);
100
102
  deps.workspace.updateLayout();
101
103
  deps.taskView.showOverview();
104
+ if (leaving) deps.taskView.revealPage();
102
105
  }
103
106
 
104
107
  async function load(deps, taskId, navigate) {
@@ -120,7 +123,7 @@ async function load(deps, taskId, navigate) {
120
123
  state.detail = detail;
121
124
  state.detailKey = fingerprint(core);
122
125
  deps.taskView.render(navigate);
123
- if (navigate) deps.taskView.applySection(readQuery().get("section"));
126
+ if (navigate) { deps.taskView.applySection(readQuery().get("section")); deps.taskView.revealPage(); }
124
127
  observe(deps, taskId, detail);
125
128
  }
126
129
 
@@ -14,6 +14,7 @@ import { updateObservation } from "/assets/js/views/task/observation.js";
14
14
  import { renderDiscussion, discussionHasUnsent } from "/assets/js/views/dock/discussion.js";
15
15
  import { sameTarget } from "/assets/js/views/dock/terminal.js";
16
16
  import { setSectionParam } from "/assets/js/layout/router.js";
17
+ import { enter } from "/assets/js/lib/motion.js";
17
18
 
18
19
  // Only the observed identity participates: observation timestamps advance on
19
20
  // every poll and would otherwise re-render continuously.
@@ -29,7 +30,7 @@ function runtimeSignature(detail) {
29
30
  }
30
31
 
31
32
  // deps: { el, state, t, locale, api, toast, terminal, workspace,
32
- // selectTask, clearSelection, pickAttention, readPageSessions, refreshCatalog }
33
+ // selectTask, clearSelection, revealSidebar, pickAttention, readPageSessions, refreshCatalog }
33
34
  // el #detail, #center and the dock discussion
34
35
  // state reads selected, detail, detailKey and the catalog fields the
35
36
  // overview shows; writes activeTab
@@ -37,7 +38,8 @@ function runtimeSignature(detail) {
37
38
  // terminal the dock's native Session (views/dock/terminal)
38
39
  // workspace layout/workspace: dock toggles and opening Sessions
39
40
  // selectTask(taskId) an overview row was chosen (app/selection)
40
- // clearSelection() the Task page's back button (app/selection)
41
+ // clearSelection() the Task page's "Tasks" crumb (app/selection)
42
+ // revealSidebar() the page header's task-list switch (layout/sidebarNav)
41
43
  // pickAttention(kind), readPageSessions() overview actions (app/catalog)
42
44
  // refreshCatalog() quiet catalog refresh after a write; resolves when done
43
45
  export function createTaskView(deps) {
@@ -59,7 +61,10 @@ export function createTaskView(deps) {
59
61
  showOverview: function () { showOverview(deps); },
60
62
  updateSessionTargets: function () { updateSessionTargets(deps); },
61
63
  switchTab: function (tab, options) { switchTab(deps, tab, options); },
62
- applySection: function (section) { applySection(deps, section); }
64
+ applySection: function (section) { applySection(deps, section); },
65
+ // The page content after an explicit navigation (a Task opened, back to
66
+ // the overview) rises in; polling redraws never call this.
67
+ revealPage: function () { reveal(deps.el.detail.querySelector(":scope > .page")); }
63
68
  };
64
69
  return view.controller;
65
70
  }
@@ -143,9 +148,15 @@ function detailContext(view) {
143
148
  const api = deps.api;
144
149
  return {
145
150
  activeTab: deps.state.activeTab,
146
- onTab: view.controller.switchTab,
151
+ onTab: function (tab, options) {
152
+ const changed = tab !== deps.state.activeTab;
153
+ switchTab(deps, tab, options);
154
+ if (changed) reveal(deps.el.detail.querySelector(".tab-panel:not([hidden])"));
155
+ },
147
156
  onBack: deps.clearSelection,
157
+ revealSidebar: deps.revealSidebar,
148
158
  showDock: deps.workspace.toggleDock,
159
+ toggleOperator: deps.workspace.toggleOperator,
149
160
  openConversation: function (materials) {
150
161
  deps.workspace.openConversation({ scope: "task", taskId: deps.state.selected, roleName: "leader" }, materials);
151
162
  },
@@ -253,8 +264,14 @@ function showOverview(deps) {
253
264
  select: deps.selectTask,
254
265
  filterAttention: deps.pickAttention,
255
266
  readSessions: deps.readPageSessions,
256
- openOperator: deps.workspace.openOperator
267
+ toggleOperator: deps.workspace.toggleOperator,
268
+ revealSidebar: deps.revealSidebar
257
269
  });
270
+ deps.workspace.syncDockButtons();
271
+ }
272
+
273
+ function reveal(element) {
274
+ if (element && typeof enter === "function") enter(element, "rise");
258
275
  }
259
276
 
260
277
  function switchTab(deps, tab, options) {
@@ -66,6 +66,9 @@ export const EN = {
66
66
  "jobs.category.ordinary": "Ordinary Tasks",
67
67
  "jobs.category.job": "Job execution Tasks",
68
68
  "jobs.category.all": "All Tasks",
69
+ "jobs.categoryShort.ordinary": "Ordinary",
70
+ "jobs.categoryShort.job": "Jobs",
71
+ "jobs.categoryShort.all": "All",
69
72
  "jobs.recentSuccess": "Most recent successful Run",
70
73
  "jobs.diagnostics": "Execution diagnostics",
71
74
  "jobs.artifacts": "Registered fixed artifacts",
@@ -146,6 +149,9 @@ export const EN = {
146
149
  "models.window.monthly": "Monthly",
147
150
  "manage.nav": "Main navigation",
148
151
  "manage.workbench": "Workbench",
152
+ "manage.sections": "Sections",
153
+ "manage.collapseSections": "Collapse sections",
154
+ "manage.showSections": "Show sections",
149
155
  "settings.section.preferences": "Preferences",
150
156
  "settings.section.skills": "Skills",
151
157
  "settings.section.server": "Agents & server",
@@ -444,6 +450,8 @@ export const EN = {
444
450
  "sidebar.workItems": "Work items",
445
451
  "sidebar.resize": "Resize task list",
446
452
  "sidebar.resizeHint": "Drag to resize · double-click to reset",
453
+ "sidebar.collapse": "Collapse task list",
454
+ "sidebar.show": "Show task list",
447
455
  "search.label": "Search tasks",
448
456
  "bodySearch.title": "Search task contents",
449
457
  "bodySearch.query": "Text to find",
@@ -505,7 +513,6 @@ export const EN = {
505
513
  "catalog.help.executionSignals": "Live runs, open work and recovery signals to inspect; not failures",
506
514
  "actions.refresh": "Refresh",
507
515
  "actions.operator": "Operator",
508
- "actions.back": "Back to tasks",
509
516
  "actions.close": "Close",
510
517
  "actions.answer": "Answer",
511
518
  "actions.save": "Save",
@@ -558,6 +565,7 @@ export const EN = {
558
565
  "shortcut.refresh": "Refresh",
559
566
  "shortcut.dock": "Show or hide the session panel",
560
567
  "shortcut.operator": "Operator session",
568
+ "shortcut.sidebar": "Show or hide the task list",
561
569
  "shortcut.tabs": "Switch task sections",
562
570
  "shortcut.escape": "Close panel / leave task",
563
571
  "loading.dashboard": "Reading local state…",
@@ -572,7 +580,6 @@ export const EN = {
572
580
  "common.notReported": "not reported",
573
581
  "detail.unobserved": "unknown",
574
582
  "detail.partial": "partial",
575
- "overview.kicker": "Workspace",
576
583
  "overview.title": "Overview",
577
584
  "overview.sub": "{total} tasks · {active} active",
578
585
  "overview.inbox": "Waiting on you",
@@ -604,7 +611,6 @@ export const EN = {
604
611
  "metrics.completed": "Completed",
605
612
  "metrics.total": "Total",
606
613
  "crumbs.label": "Breadcrumb",
607
- "crumbs.tasks": "Tasks",
608
614
  "task.updated": "Updated",
609
615
  "tabs.label": "Task sections",
610
616
  "tabs.overview": "Overview",
@@ -1000,6 +1006,7 @@ export const EN = {
1000
1006
  "terminal.cliHint": "Full terminal",
1001
1007
  "terminal.historyLimited": "This Role has a {current}-line tmux history. Exit and re-enter it to use {target} lines.",
1002
1008
  "global.open": "Global input",
1009
+ "global.openRole": "Message another Role…",
1003
1010
  "global.title": "Global Role input",
1004
1011
  "global.help": "Queue is the default. A cancel request does not prove the Turn stopped.",
1005
1012
  "global.role": "Role",
@@ -64,6 +64,9 @@ export const ZH_CN = {
64
64
  "jobs.category.ordinary": "普通任务",
65
65
  "jobs.category.job": "Job 执行任务",
66
66
  "jobs.category.all": "全部任务",
67
+ "jobs.categoryShort.ordinary": "普通",
68
+ "jobs.categoryShort.job": "Job",
69
+ "jobs.categoryShort.all": "全部",
67
70
  "jobs.recentSuccess": "最近成功的 Run",
68
71
  "jobs.diagnostics": "执行诊断",
69
72
  "jobs.artifacts": "已登记固定产物",
@@ -144,6 +147,9 @@ export const ZH_CN = {
144
147
  "models.window.monthly": "每月",
145
148
  "manage.nav": "主导航",
146
149
  "manage.workbench": "工作台",
150
+ "manage.sections": "分区",
151
+ "manage.collapseSections": "收起分区列表",
152
+ "manage.showSections": "展开分区列表",
147
153
  "settings.section.preferences": "偏好设置",
148
154
  "settings.section.skills": "Skill",
149
155
  "settings.section.server": "Agent 与服务",
@@ -440,6 +446,8 @@ export const ZH_CN = {
440
446
  "sidebar.workItems": "工作项",
441
447
  "sidebar.resize": "调整任务列表宽度",
442
448
  "sidebar.resizeHint": "拖拽调整宽度 · 双击恢复默认",
449
+ "sidebar.collapse": "收起任务列表",
450
+ "sidebar.show": "展开任务列表",
443
451
  "search.label": "搜索任务",
444
452
  "bodySearch.title": "搜索任务正文",
445
453
  "bodySearch.query": "查找正文",
@@ -501,7 +509,6 @@ export const ZH_CN = {
501
509
  "catalog.help.executionSignals": "需要检查的运行、未完成工作和恢复信号;不代表失败",
502
510
  "actions.refresh": "刷新",
503
511
  "actions.operator": "Operator",
504
- "actions.back": "返回任务列表",
505
512
  "actions.close": "关闭",
506
513
  "actions.answer": "回答",
507
514
  "actions.save": "保存",
@@ -554,6 +561,7 @@ export const ZH_CN = {
554
561
  "shortcut.refresh": "刷新",
555
562
  "shortcut.dock": "显示或隐藏会话面板",
556
563
  "shortcut.operator": "Operator 会话",
564
+ "shortcut.sidebar": "显示或隐藏任务列表",
557
565
  "shortcut.tabs": "切换任务分区",
558
566
  "shortcut.escape": "关闭面板/离开任务",
559
567
  "loading.dashboard": "正在读取本地状态…",
@@ -568,7 +576,6 @@ export const ZH_CN = {
568
576
  "common.notReported": "未上报",
569
577
  "detail.unobserved": "未知",
570
578
  "detail.partial": "部分",
571
- "overview.kicker": "工作区",
572
579
  "overview.title": "总览",
573
580
  "overview.sub": "{total} 个任务 · {active} 个进行中",
574
581
  "overview.inbox": "等待你回答",
@@ -600,7 +607,6 @@ export const ZH_CN = {
600
607
  "metrics.completed": "已完成",
601
608
  "metrics.total": "总数",
602
609
  "crumbs.label": "路径",
603
- "crumbs.tasks": "任务",
604
610
  "task.updated": "更新于",
605
611
  "tabs.label": "任务分区",
606
612
  "tabs.overview": "概览",
@@ -996,6 +1002,7 @@ export const ZH_CN = {
996
1002
  "terminal.cliHint": "完整终端",
997
1003
  "terminal.historyLimited": "这个 Role 的 tmux 历史只有 {current} 行;退出并重新进入后可使用 {target} 行。",
998
1004
  "global.open": "全局消息",
1005
+ "global.openRole": "给其他 Role 发消息…",
999
1006
  "global.title": "全局 Role 消息",
1000
1007
  "global.help": "默认排队;取消请求不代表执行已停止。",
1001
1008
  "global.role": "Role",
@@ -16,6 +16,7 @@ import { DOM_SCRIPT } from "./lib/dom.js";
16
16
  import { FORMAT_SCRIPT } from "./lib/format.js";
17
17
  import { I18N_SCRIPT } from "./lib/i18n.js";
18
18
  import { MARKDOWN_SCRIPT } from "./lib/markdown.js";
19
+ import { MOTION_SCRIPT } from "./lib/motion.js";
19
20
  import { PREFS_SCRIPT } from "./lib/prefs.js";
20
21
  import { SESSION_SCRIPT } from "./lib/session.js";
21
22
  import { THEME_SCRIPT } from "./lib/theme.js";
@@ -55,6 +56,7 @@ import { RESIZER_SCRIPT } from "./layout/resizer.js";
55
56
  import { ROUTER_SCRIPT } from "./layout/router.js";
56
57
  import { SHORTCUTS_SCRIPT } from "./layout/shortcuts.js";
57
58
  import { SIZING_SCRIPT } from "./layout/sizing.js";
59
+ import { SIDEBAR_NAV_SCRIPT } from "./layout/sidebarNav.js";
58
60
  import { WORKSPACE_SCRIPT } from "./layout/workspace.js";
59
61
  import { CATALOG_SCRIPT } from "./app/catalog.js";
60
62
  import { SELECTION_SCRIPT } from "./app/selection.js";
@@ -77,6 +79,7 @@ export const CLIENT_MODULES = Object.freeze({
77
79
  "lib/dom": DOM_SCRIPT,
78
80
  "lib/format": FORMAT_SCRIPT,
79
81
  "lib/prefs": PREFS_SCRIPT,
82
+ "lib/motion": MOTION_SCRIPT,
80
83
  "lib/i18n": I18N_SCRIPT,
81
84
  "lib/theme": THEME_SCRIPT,
82
85
  "lib/api": API_SCRIPT,
@@ -116,6 +119,7 @@ export const CLIENT_MODULES = Object.freeze({
116
119
  "views/task/files": TASK_FILES_SCRIPT,
117
120
  "layout/resizer": RESIZER_SCRIPT,
118
121
  "layout/sizing": SIZING_SCRIPT,
122
+ "layout/sidebarNav": SIDEBAR_NAV_SCRIPT,
119
123
  "layout/workspace": WORKSPACE_SCRIPT,
120
124
  "layout/router": ROUTER_SCRIPT,
121
125
  "layout/shortcuts": SHORTCUTS_SCRIPT,
@@ -1,6 +1,8 @@
1
1
  import { MANAGE_SECTIONS } from "../../shared/manageNav.js";
2
2
  export const MANAGE_SHELL_SCRIPT = String.raw `
3
3
  import { createI18n } from "/assets/js/lib/i18n.js";
4
+ import { rememberPlace, linkPlaces } from "/assets/js/lib/prefs.js";
5
+ import { enter } from "/assets/js/lib/motion.js";
4
6
  const SECTIONS = ${JSON.stringify(MANAGE_SECTIONS)};
5
7
  export const i18n = createI18n([document.querySelector("#locale-select"), document.querySelector("#settings-locale")]);
6
8
  const pages = new Map();
@@ -34,6 +36,8 @@ export function manageHref(next) {
34
36
  return "/" + manageArea(next.page, next.section) + "/" + next.section +
35
37
  (next.section === "quota" && next.source === "command-code" ? "?source=command-code" : "");
36
38
  }
39
+ // The center column scrolls, not the document (the frame is the workbench's).
40
+ function scroller() { return document.querySelector("#manage-scroll"); }
37
41
  function render() {
38
42
  if (!started) return;
39
43
  const rail = document.querySelector(".manage-rail");
@@ -43,29 +47,45 @@ function render() {
43
47
  if (active) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
44
48
  if (dirty[link.dataset.page]?.[link.dataset.section]) link.dataset.dirty = "true"; else delete link.dataset.dirty;
45
49
  });
46
- rail.querySelectorAll("a[data-area]").forEach(link => {
47
- if (link.dataset.area === area) link.setAttribute("aria-current", "true"); else link.removeAttribute("aria-current");
50
+ // The global rail marks the current area.
51
+ const app = document.querySelector(".manage-app");
52
+ app.querySelectorAll("a[data-area]").forEach(link => {
53
+ if (link.dataset.area === area) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
48
54
  });
55
+ // The list head and the top bar name where the reader is.
56
+ const areaTitle = i18n.t(area + ".title");
57
+ const sectionTitle = i18n.t((route.page === "models" ? "models" : "settings") + ".section." + route.section);
58
+ document.querySelector("#manage-area-title").textContent = areaTitle;
59
+ document.querySelector("#manage-crumb-area").textContent = areaTitle;
60
+ document.querySelector("#manage-crumb-section").textContent = sectionTitle;
49
61
  rail.querySelectorAll("[data-area-group]").forEach(group => {
50
62
  if (group.dataset.areaGroup === area) group.dataset.active = "true"; else delete group.dataset.active;
51
63
  });
52
- document.title = i18n.t((route.page === "models" ? "models" : "settings") + ".section." + route.section) +
53
- " · " + i18n.t(area + ".title") + " · Yui";
64
+ linkPlaces(app);
65
+ document.title = sectionTitle + " · " + areaTitle + " · Yui";
54
66
  }
55
67
  function apply(next) {
56
68
  route = next;
69
+ rememberPlace(manageArea(next.page, next.section), manageHref(next));
70
+ rememberPlace("manage", manageHref(next));
57
71
  document.querySelector("#models-root").hidden = next.page !== "models";
58
72
  document.querySelector("#settings-root").hidden = next.page !== "settings";
59
73
  pages.get(next.page).show(next);
60
74
  render();
61
75
  }
76
+ // A section the reader switched to rises in (never on the first paint).
77
+ function revealSection() {
78
+ if (typeof enter !== "function") return;
79
+ enter(document.querySelector("#models-root:not([hidden]), #settings-root:not([hidden])"), "rise");
80
+ }
62
81
  export function navigateManage(next) {
63
82
  const normalized = readManageRoute(new URL(manageHref(next), location.origin));
64
83
  if (manageHref(normalized) === location.pathname + location.search) return;
65
- history.replaceState({ ...history.state, scroll: window.scrollY }, "");
84
+ history.replaceState({ ...history.state, scroll: scroller().scrollTop }, "");
66
85
  history.pushState({ scroll: 0 }, "", manageHref(normalized));
67
86
  apply(normalized);
68
- window.scrollTo(0, 0);
87
+ scroller().scrollTop = 0;
88
+ revealSection();
69
89
  }
70
90
  export function createManageShell(options) {
71
91
  return {
@@ -79,7 +99,7 @@ export function registerManagePage(name, controller) { pages.set(name, controlle
79
99
  export function startManagement() {
80
100
  started = true;
81
101
  route = readManageRoute(new URL(location.href));
82
- history.replaceState({ ...history.state, scroll: window.scrollY }, "", manageHref(route));
102
+ history.replaceState({ ...history.state, scroll: scroller().scrollTop }, "", manageHref(route));
83
103
  history.scrollRestoration = "manual";
84
104
  apply(route);
85
105
  document.querySelector(".manage-app").hidden = false;
@@ -96,8 +116,10 @@ export function startManagement() {
96
116
  });
97
117
  window.addEventListener("popstate", () => {
98
118
  apply(readManageRoute(new URL(location.href)));
99
- window.scrollTo(0, history.state?.scroll || 0);
119
+ scroller().scrollTop = history.state?.scroll || 0;
120
+ revealSection();
100
121
  });
122
+ window.addEventListener("pageshow", render);
101
123
  i18n.subscribe(render);
102
124
  }
103
125
  `;
@@ -6,11 +6,12 @@ import { isEditing } from "/assets/js/lib/dom.js";
6
6
 
7
7
  const TABS = ${JSON.stringify(TASK_TABS)};
8
8
 
9
- // deps: { el, state, globalInput, workspace, selection, catalog, taskView }
9
+ // deps: { el, state, globalInput, workspace, sidebarNav, selection, catalog, taskView }
10
10
  // el #search, #settings-dialog and #terminal-host
11
11
  // state read-only here: state.selected, state.detail
12
12
  // globalInput views/globalInput (its dialog blocks shortcuts while open)
13
13
  // workspace layout/workspace: the dock and the operator Session
14
+ // sidebarNav layout/sidebarNav: "[" shows or hides the task list
14
15
  // selection app/selection: Escape leaves the Task
15
16
  // catalog app/catalog: "r" refreshes
16
17
  // taskView app/taskView: 1–4 switch tabs
@@ -26,10 +27,12 @@ export function bindShortcuts(deps) {
26
27
  });
27
28
  }
28
29
 
29
- // Escape unwinds one level: leave a field, close the dock, leave the Task.
30
+ // Escape unwinds one level: leave a field, close the task-list drawer, close
31
+ // the dock, leave the Task.
30
32
  function escape(deps, active, typing, inTerminal) {
31
33
  if (inTerminal) return;
32
34
  if (typing) { active.blur(); return; }
35
+ if (deps.sidebarNav.drawerOpen()) { deps.sidebarNav.closeDrawer(); return; }
33
36
  if (deps.workspace.dockVisible()) { deps.workspace.setDockOpen(false); return; }
34
37
  if (deps.state.selected) deps.selection.clearSelection();
35
38
  }
@@ -37,8 +40,9 @@ function escape(deps, active, typing, inTerminal) {
37
40
  function shortcut(deps, event) {
38
41
  const key = event.key.toLowerCase();
39
42
  if (key === "/") { event.preventDefault(); deps.el.search.focus(); deps.el.search.select(); }
43
+ else if (key === "[") deps.sidebarNav.toggle();
40
44
  else if (key === "r") deps.catalog.refresh();
41
- else if (key === "o") deps.workspace.openOperator();
45
+ else if (key === "o") deps.workspace.toggleOperator();
42
46
  else if (key === "d") deps.workspace.toggleDock();
43
47
  else if (/^[1-9]$/.test(key) && Number(key) <= TABS.length && deps.state.detail) deps.taskView.switchTab(TABS[Number(key) - 1]);
44
48
  }