@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
@@ -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": "概览",
@@ -851,10 +857,7 @@ export const ZH_CN = {
851
857
  "store.job": "操作",
852
858
  "dock.label": "会话面板",
853
859
  "dock.discussion": "讨论",
854
- "dock.session": "终端",
855
860
  "conversation.title": "会话",
856
- "conversation.defaultMode": "默认展示",
857
- "conversation.defaultSaved": "已保存为此浏览器今后打开时的默认展示;当前视图和 Session 未切换。",
858
861
  "conversation.nativeRequest": "Codex 原生请求",
859
862
  "conversation.nativeOmitted": "更多或较大的原生请求请在终端处理。",
860
863
  "conversation.leaderQuestions": "Leader 问题 · Task 输入通道",
@@ -870,15 +873,22 @@ export const ZH_CN = {
870
873
  "conversation.input": "发送到选中的会话",
871
874
  "conversation.send": "发送",
872
875
  "conversation.model": "当前会话模型",
873
- "conversation.modelsLoad": "读取可用模型",
874
- "conversation.modelApply": "空闲时采用",
875
- "conversation.launchModel": "固定启动请求",
876
- "conversation.confirmedModel": "最近原生确认",
876
+ "conversation.modelsLoad": "重新加载",
877
+ "conversation.modelApply": "应用选择",
878
+ "conversation.selectedModel": "选中模型",
879
+ "conversation.modelNotSelected": "请选择",
880
+ "conversation.modelsLoading": "正在加载模型…",
881
+ "conversation.modelsFailed": "模型列表读取失败,请重新加载。",
882
+ "conversation.modelsEmpty": "暂无可用模型",
883
+ "conversation.modelBusy": "当前正在执行,结束后可应用选择。",
884
+ "conversation.modelTerminalBusy": "请先在原生终端退出 Codex,再应用选择。仅关闭终端面板不会退出。",
885
+ "conversation.launchModel": "启动配置",
886
+ "conversation.confirmedModel": "当前执行模型",
877
887
  "conversation.modelBoundary": "只切换同一 Codex Thread,且必须空闲。不更改角色默认配置或当前轮次。Host 重连会恢复固定启动配置,请重新核对原生确认。不会自动重试。",
878
- "conversation.modelApplying": "正在采用模型,等待原生确认…",
879
- "conversation.modelConfirmed": "原生已确认当前连接采用该模型。",
880
- "conversation.modelFailed": "未采用。请查看原因后再决定是否重试。",
881
- "conversation.modelUnknown": "采用结果未确认。请勿重试;查看最近确认,或通过现有生命周期控制明确选择新会话。",
888
+ "conversation.modelApplying": "正在切换模型…",
889
+ "conversation.modelConfirmed": "模型已切换",
890
+ "conversation.modelFailed": "未能切换模型,请确认会话空闲后再试。",
891
+ "conversation.modelUnknown": "切换结果待确认,暂不可重复提交。",
882
892
  "conversation.commands": "支持的命令",
883
893
  "conversation.literal": "作为普通文本发送",
884
894
  "conversation.command.help": "查看支持的操作",
@@ -889,7 +899,6 @@ export const ZH_CN = {
889
899
  "conversation.command.latest": "返回最新消息",
890
900
  "conversation.commandHelp": "/help · /status · /model · /stop · /latest。Tab 补全唯一匹配;Ctrl/⌘+Enter 发送。这些是 Yui 操作,不是任意原生 CLI 命令。",
891
901
  "conversation.commandUnknown": "不支持此命令。请使用 /help,或明确选择作为普通文本发送。",
892
- "conversation.draftSaved": "草稿已保存在当前浏览器标签页 · Ctrl/⌘+Enter 发送",
893
902
  "conversation.storageFailed": "浏览器存储不可用。离开前请保留草稿副本。",
894
903
  "conversation.receiptDetails": "精确回执",
895
904
  "conversation.receipt.sending": "正在发送,不会重复提交…",
@@ -917,13 +926,48 @@ export const ZH_CN = {
917
926
  "conversation.empty": "此页没有公开消息,可继续读取更早的历史。",
918
927
  "conversation.liveUnavailable": "实时回复观测不可用,仅显示原生历史",
919
928
  "conversation.truncated": "长消息仅显示摘要;完整内容请打开原生入口。",
920
- "conversation.placeholder": "给当前会话发消息…",
921
- "conversation.more": "更多",
929
+ "conversation.placeholder": "给当前会话发消息…(/ 查看命令)",
922
930
  "conversation.copy": "复制",
923
931
  "conversation.copied": "已复制",
924
932
  "conversation.jump": "回到最新",
925
933
  "conversation.selectModel": "选择模型",
926
934
  "conversation.worked": "已处理",
935
+ "conversation.modelConfirmedNote": "{time} 于启动、恢复或切换时确认;非实时读取,外部 Desktop 改动可能未反映",
936
+ "conversation.modelStateConfirmed": "最近确认",
937
+ "conversation.modelStatePending": "待应用",
938
+ "conversation.modelConfirmedShort": "最近原生确认 {time};非实时",
939
+ "conversation.details": "会话详情",
940
+ "conversation.copyIdentity": "复制标识",
941
+ "conversation.copyFailed": "此浏览器无法复制",
942
+ "conversation.modelUnconfirmed": "模型未确认",
943
+ "conversation.modelUnconfirmedHint": "此会话尚无原生确认;不会把启动配置当作当前模型。",
944
+ "conversation.modelPendingLabel": "应用中",
945
+ "conversation.modelHistorical": "历史会话不做实时观测。",
946
+ "conversation.modelNoRead": "尚未报告。",
947
+ "conversation.effortUnobserved": "未报告推理强度",
948
+ "conversation.launchNote": "启动时请求;非当前",
949
+ "conversation.agentDefault": "Agent 默认",
950
+ "conversation.sendHint": "发送(Ctrl/⌘+Enter)",
951
+ "conversation.backCurrent": "回到当前会话",
952
+ "conversation.backCurrentShort": "当前",
953
+ "conversation.readOnlyHistory": "历史会话 · 只读",
954
+ "conversation.placeholderHistory": "历史会话只读",
955
+ "conversation.pendingInput": "等待上一条输入的回执",
956
+ "conversation.turnUnsettled": "上一条输入的投递尚未确定",
957
+ "conversation.inputUnavailable": "此会话暂不可输入",
958
+ "conversation.sessionActive": "运行中",
959
+ "conversation.sessionEnded": "已结束",
960
+ "conversation.sessionFailed": "失败",
961
+ "conversation.previousSessions": "历史",
962
+ "conversation.noSessionShort": "无会话",
963
+ "conversation.sessionLabel": "会话",
964
+ "conversation.sessionId": "会话 ID",
965
+ "conversation.authority": "执行权",
966
+ "conversation.agent": "Agent",
967
+ "conversation.turn": "Turn",
968
+ "conversation.writer": "输入方",
969
+ "conversation.stateUnknown": "未知",
970
+ "conversation.disconnectedShort": "已断开",
927
971
  "conversation.activity.commandExecution": "运行",
928
972
  "conversation.activity.fileChange": "修改文件",
929
973
  "conversation.activity.mcpToolCall": "调用工具",
@@ -932,6 +976,7 @@ export const ZH_CN = {
932
976
  "conversation.activity.collabAgentToolCall": "询问其他 Agent",
933
977
  "dock.swap": "与任务详情交换位置",
934
978
  "dock.hide": "隐藏会话面板",
979
+ "dock.show": "显示会话面板",
935
980
  "dock.resize": "调整会话面板宽度",
936
981
  "dock.target": "会话目标",
937
982
  "discussion.title": "任务讨论",
@@ -989,13 +1034,19 @@ export const ZH_CN = {
989
1034
  "terminal.closed": "已分离",
990
1035
  "terminal.error": "不可用",
991
1036
  "terminal.errorDetail": "无法打开该原生会话。请确认 Controller 正在运行且该 Role 已有会话。",
992
- "terminal.global": "全局",
993
- "terminal.noTargets": "暂无 Role",
994
- "terminal.emptyTitle": "连接到原生会话",
995
- "terminal.emptyText": "在上方选择一个 Role。面板显示该会话的当前画面;连接不会启动或重启 Agent。",
1037
+ "session.mode": "会话视图",
1038
+ "session.modeChat": "聊天",
1039
+ "session.modeTerminal": "终端",
1040
+ "terminal.unavailable": "不可连接",
1041
+ "terminal.noSessionTitle": "没有可连接的会话",
1042
+ "terminal.noSessionText": "该角色还没有可连接的会话;连接不会启动 Agent。",
1043
+ "terminal.notCurrentTitle": "不是当前会话",
1044
+ "terminal.notCurrentText": "终端只能连接当前会话;所选会话已不是当前会话。",
1045
+ "conversation.roleReadOnly": "此角色在这里只读,输入通过其分派控制。",
996
1046
  "terminal.cliHint": "完整终端",
997
1047
  "terminal.historyLimited": "这个 Role 的 tmux 历史只有 {current} 行;退出并重新进入后可使用 {target} 行。",
998
1048
  "global.open": "全局消息",
1049
+ "global.openRole": "给其他 Role 发消息…",
999
1050
  "global.title": "全局 Role 消息",
1000
1051
  "global.help": "默认排队;取消请求不代表执行已停止。",
1001
1052
  "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";
@@ -35,6 +36,7 @@ import { WORK_SCRIPT } from "./domain/work.js";
35
36
  import { DISCUSSION_SCRIPT } from "./views/dock/discussion.js";
36
37
  import { CONVERSATION_SCRIPT } from "./views/dock/conversation.js";
37
38
  import { TERMINAL_SCRIPT } from "./views/dock/terminal.js";
39
+ import { SESSION_PAGE_SCRIPT } from "./views/dock/session.js";
38
40
  import { PREVIEW_SCRIPT } from "./views/dock/preview.js";
39
41
  import { GLOBAL_INPUT_SCRIPT } from "./views/globalInput.js";
40
42
  import { CREATE_TASK_SCRIPT } from "./views/createTask.js";
@@ -55,6 +57,7 @@ import { RESIZER_SCRIPT } from "./layout/resizer.js";
55
57
  import { ROUTER_SCRIPT } from "./layout/router.js";
56
58
  import { SHORTCUTS_SCRIPT } from "./layout/shortcuts.js";
57
59
  import { SIZING_SCRIPT } from "./layout/sizing.js";
60
+ import { SIDEBAR_NAV_SCRIPT } from "./layout/sidebarNav.js";
58
61
  import { WORKSPACE_SCRIPT } from "./layout/workspace.js";
59
62
  import { CATALOG_SCRIPT } from "./app/catalog.js";
60
63
  import { SELECTION_SCRIPT } from "./app/selection.js";
@@ -77,6 +80,7 @@ export const CLIENT_MODULES = Object.freeze({
77
80
  "lib/dom": DOM_SCRIPT,
78
81
  "lib/format": FORMAT_SCRIPT,
79
82
  "lib/prefs": PREFS_SCRIPT,
83
+ "lib/motion": MOTION_SCRIPT,
80
84
  "lib/i18n": I18N_SCRIPT,
81
85
  "lib/theme": THEME_SCRIPT,
82
86
  "lib/api": API_SCRIPT,
@@ -103,6 +107,7 @@ export const CLIENT_MODULES = Object.freeze({
103
107
  "views/dock/discussion": DISCUSSION_SCRIPT,
104
108
  "views/dock/conversation": CONVERSATION_SCRIPT,
105
109
  "views/dock/terminal": TERMINAL_SCRIPT,
110
+ "views/dock/session": SESSION_PAGE_SCRIPT,
106
111
  "views/dock/preview": PREVIEW_SCRIPT,
107
112
  "views/task/page": TASK_PAGE_SCRIPT,
108
113
  "views/task/actions": TASK_ACTIONS_SCRIPT,
@@ -116,6 +121,7 @@ export const CLIENT_MODULES = Object.freeze({
116
121
  "views/task/files": TASK_FILES_SCRIPT,
117
122
  "layout/resizer": RESIZER_SCRIPT,
118
123
  "layout/sizing": SIZING_SCRIPT,
124
+ "layout/sidebarNav": SIDEBAR_NAV_SCRIPT,
119
125
  "layout/workspace": WORKSPACE_SCRIPT,
120
126
  "layout/router": ROUTER_SCRIPT,
121
127
  "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 }
10
- // el #search, #settings-dialog and #terminal-host
9
+ // deps: { el, state, globalInput, workspace, sidebarNav, selection, catalog, taskView }
10
+ // el #search, #settings-dialog and the dock (its 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
@@ -19,17 +20,19 @@ export function bindShortcuts(deps) {
19
20
  if (document.querySelector("dialog[open]")) return;
20
21
  const active = document.activeElement;
21
22
  const typing = isEditing(active);
22
- const inTerminal = deps.el.terminalHost.contains(active);
23
+ const inTerminal = !!active && !!active.closest && !!active.closest(".terminal-host");
23
24
  if (event.key === "Escape") { escape(deps, active, typing, inTerminal); return; }
24
25
  if (typing || inTerminal || event.metaKey || event.ctrlKey || event.altKey) return;
25
26
  shortcut(deps, event);
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
  }
@@ -0,0 +1,147 @@
1
+ import { LAYOUT_GEOMETRY } from "../../shared/geometry.js";
2
+ export const SIDEBAR_NAV_SCRIPT = String.raw `
3
+ // Task list visibility. On wide screens the list is a column the user can
4
+ // collapse (remembered in this browser). Where it cannot sit beside the
5
+ // workspace — narrow screens, or a medium screen with the dock open — it
6
+ // opens as a drawer over the workspace instead, and closes on a choice,
7
+ // on the scrim, on Escape or by dragging it away. Every [data-sidebar-toggle]
8
+ // reports the state. Drawer motion lives in CSS (layout/responsive); a drag
9
+ // only moves the panels with the finger and hands the release back to CSS.
10
+ // Collapsing the inline column glides through lib/motion.
11
+ import { readPreference, writePreference } from "/assets/js/lib/prefs.js";
12
+ import { layoutTransition } from "/assets/js/lib/motion.js";
13
+
14
+ const G = ${JSON.stringify(LAYOUT_GEOMETRY)};
15
+
16
+ // deps: { el: { sidebar, rail?, scrim, collapse }, relayout }
17
+ // rail the global rail, which travels with the list on narrow screens
18
+ // relayout() re-applies column widths after the list appears or hides
19
+ export function createSidebarNav(deps) {
20
+ const body = document.body;
21
+ const state = { collapsed: readPreference("yui.sidebar.collapsed", "0") === "1" };
22
+
23
+ // Whether the list can sit in the grid at the current width and dock state.
24
+ function inlineAvailable() {
25
+ const width = window.innerWidth;
26
+ if (width <= G.narrow) return false;
27
+ return !(width <= G.focusMax && body.classList.contains("dock-open"));
28
+ }
29
+ function drawerOpen() { return body.classList.contains("sidebar-drawer"); }
30
+ function visible() { return drawerOpen() || (inlineAvailable() && !state.collapsed); }
31
+ function sync() {
32
+ const expanded = String(visible());
33
+ document.querySelectorAll("[data-sidebar-toggle]").forEach(function (button) {
34
+ if (button.getAttribute("aria-expanded") !== expanded) button.setAttribute("aria-expanded", expanded);
35
+ });
36
+ }
37
+ function setCollapsed(collapsed) {
38
+ state.collapsed = collapsed;
39
+ writePreference("yui.sidebar.collapsed", collapsed ? "1" : "0");
40
+ // The columns glide to their new widths (View Transition, desktop only).
41
+ const update = function () {
42
+ body.classList.toggle("sidebar-collapsed", collapsed);
43
+ deps.relayout();
44
+ sync();
45
+ };
46
+ if (typeof layoutTransition === "function") layoutTransition(update);
47
+ else update();
48
+ }
49
+ function setDrawer(open) {
50
+ if (open === drawerOpen()) return;
51
+ body.classList.toggle("sidebar-drawer", open);
52
+ if (open) deps.el.collapse.focus({ preventScroll: true });
53
+ sync();
54
+ }
55
+ function reveal() {
56
+ if (!inlineAvailable()) setDrawer(true);
57
+ else if (state.collapsed) setCollapsed(false);
58
+ }
59
+ function hide() {
60
+ if (drawerOpen()) setDrawer(false);
61
+ else if (inlineAvailable()) setCollapsed(true);
62
+ }
63
+
64
+ body.classList.toggle("sidebar-collapsed", state.collapsed);
65
+ deps.el.collapse.addEventListener("click", hide);
66
+ deps.el.scrim.addEventListener("click", function () { setDrawer(false); });
67
+ // A choice made in the drawer hands the screen back to the workspace.
68
+ deps.el.sidebar.addEventListener("click", function (event) {
69
+ if (drawerOpen() && event.target.closest(".task-row, .manage-link, #create-task, #body-search-open")) setDrawer(false);
70
+ });
71
+ bindDrag(deps.el, drawerOpen, function () { setDrawer(false); });
72
+ window.addEventListener("resize", function () { if (drawerOpen() && inlineAvailable()) setDrawer(false); sync(); });
73
+ // The dock opening or closing changes whether the list fits.
74
+ new MutationObserver(sync).observe(body, { attributes: true, attributeFilter: ["class"] });
75
+ sync();
76
+
77
+ return {
78
+ toggle: function () { if (visible()) hide(); else reveal(); },
79
+ reveal: reveal,
80
+ hide: hide,
81
+ closeDrawer: function () { setDrawer(false); },
82
+ drawerOpen: drawerOpen,
83
+ sync: sync
84
+ };
85
+ }
86
+
87
+ // Drag to dismiss, as native drawers do: once a touch moves mostly sideways,
88
+ // the panels follow the finger (leftward only) and the scrim thins with them.
89
+ // Releasing past a third of the width, or with a quick flick, closes; any
90
+ // other release springs back. Either way CSS animates from the current spot.
91
+ function bindDrag(el, isOpen, close) {
92
+ const panels = [el.sidebar, el.rail].filter(Boolean);
93
+ let drag = null;
94
+ function place(dx) {
95
+ const offset = "translate3d(" + dx + "px,0,0)";
96
+ panels.forEach(function (panel) { panel.style.transform = offset; });
97
+ el.scrim.style.opacity = String(Math.max(0, 1 + dx / drag.width));
98
+ }
99
+ function release() {
100
+ panels.forEach(function (panel) { panel.style.transform = ""; });
101
+ el.scrim.style.opacity = "";
102
+ document.body.classList.remove("drawer-dragging");
103
+ }
104
+ function start(event) {
105
+ if (!isOpen() || event.touches.length !== 1) return;
106
+ const touch = event.touches[0];
107
+ const width = panels.reduce(function (sum, panel) { return sum + panel.getBoundingClientRect().width; }, 0);
108
+ drag = { x: touch.clientX, y: touch.clientY, width: width || 1, active: false, dx: 0, at: event.timeStamp, v: 0 };
109
+ }
110
+ function move(event) {
111
+ if (!drag) return;
112
+ const touch = event.touches[0];
113
+ const dx = touch.clientX - drag.x;
114
+ const dy = touch.clientY - drag.y;
115
+ if (!drag.active) {
116
+ if (Math.abs(dx) < 8 && Math.abs(dy) < 8) return;
117
+ // A vertical start is a scroll; leave it alone.
118
+ if (Math.abs(dy) >= Math.abs(dx) || dx > 0) { drag = null; return; }
119
+ drag.active = true;
120
+ document.body.classList.add("drawer-dragging");
121
+ }
122
+ const next = Math.min(0, dx);
123
+ const elapsed = event.timeStamp - drag.at;
124
+ if (elapsed > 0) drag.v = (next - drag.dx) / elapsed;
125
+ drag.dx = next;
126
+ drag.at = event.timeStamp;
127
+ place(next);
128
+ }
129
+ function end() {
130
+ if (!drag) return;
131
+ const finished = drag;
132
+ drag = null;
133
+ if (!finished.active) return;
134
+ // Removing the inline offset in the same frame as the class change lets
135
+ // the CSS transition continue from where the finger let go.
136
+ const dismiss = finished.dx < -finished.width / 3 || finished.v < -0.45;
137
+ release();
138
+ if (dismiss) close();
139
+ }
140
+ [el.sidebar, el.rail, el.scrim].filter(Boolean).forEach(function (node) {
141
+ node.addEventListener("touchstart", start, { passive: true });
142
+ node.addEventListener("touchmove", move, { passive: true });
143
+ node.addEventListener("touchend", end, { passive: true });
144
+ node.addEventListener("touchcancel", end, { passive: true });
145
+ });
146
+ }
147
+ `;
@@ -18,7 +18,10 @@ export function createSizing(el, dock, sidebar, dockVisible) {
18
18
  const root = document.documentElement.style;
19
19
 
20
20
  function maxDockWidth() {
21
- const aside = window.innerWidth > G.narrow ? el.sidebar.offsetWidth : 0;
21
+ // The global rail and an inline task list both take room from the
22
+ // workspace; a drawer floats over it and takes none.
23
+ const inline = !document.body.classList.contains("sidebar-drawer");
24
+ const aside = window.innerWidth > G.narrow ? G.rail + (inline ? el.sidebar.offsetWidth : 0) : 0;
22
25
  return Math.max(G.dock.min, Math.min(G.dock.max, window.innerWidth - aside - G.centerMin));
23
26
  }
24
27
  function setDockWidth(width, persist) {
@@ -30,7 +33,7 @@ export function createSizing(el, dock, sidebar, dockVisible) {
30
33
  }
31
34
  function maxSidebarWidth() {
32
35
  // An open dock can shrink to its minimum, so it reserves only that much.
33
- const reserve = dockVisible() ? G.dock.min + G.divider : 0;
36
+ const reserve = G.rail + (dockVisible() ? G.dock.min + G.divider : 0);
34
37
  return Math.max(G.sidebar.min, Math.min(G.sidebar.max, window.innerWidth - reserve - G.centerMin));
35
38
  }
36
39
  function applySidebarWidth() {