minecodex 1.2.2 → 2.0.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 (87) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -23
  4. package/features/images/src/library-prompt-page.mjs +239 -0
  5. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  6. package/features/notes/DESIGN.md +66 -0
  7. package/features/notes/README.md +18 -99
  8. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  9. package/features/notes/assets/icons/book-open.svg +16 -0
  10. package/features/notes/assets/native-emojis.json +1 -0
  11. package/features/notes/assets/native-prompt-icons.json +28 -0
  12. package/features/notes/codex-feature.json +12 -70
  13. package/features/notes/src/main.mjs +2 -2
  14. package/features/notes/src/product.mjs +287 -613
  15. package/features/notes/src/prompt-schema.mjs +117 -0
  16. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  17. package/features/notes/src/quick-prompts-server.mjs +167 -0
  18. package/features/session-status/DESIGN.md +10 -2
  19. package/features/session-status/README.md +8 -4
  20. package/features/session-status/src/session-status.mjs +268 -24
  21. package/features/sidebar-focus/DESIGN.md +16 -0
  22. package/features/sidebar-focus/codex-feature.json +20 -0
  23. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  24. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  25. package/features/unread-inbox/DESIGN.md +28 -0
  26. package/features/unread-inbox/codex-feature.json +20 -0
  27. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  28. package/features/usage-meter/DESIGN.md +49 -0
  29. package/features/usage-meter/README.md +18 -0
  30. package/features/usage-meter/codex-feature.json +13 -0
  31. package/features/usage-meter/src/usage-meter.mjs +200 -0
  32. package/package.json +17 -5
  33. package/packages/cli/src/config.mjs +4 -1
  34. package/packages/cli/src/control-server.mjs +2 -2
  35. package/packages/runtime-host/src/codex-injection.mjs +38 -3
  36. package/packages/runtime-host/src/codex-runtime.mjs +41 -20
  37. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  38. package/packages/runtime-host/src/session-status-reader.mjs +40 -16
  39. package/features/notes/assets/icons/circle.svg +0 -15
  40. package/features/notes/assets/icons/ellipsis.svg +0 -17
  41. package/features/notes/assets/icons/external-link.svg +0 -17
  42. package/features/notes/assets/icons/list-todo.svg +0 -19
  43. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  44. package/features/notes/assets/icons/paperclip.svg +0 -15
  45. package/features/notes/assets/icons/star-off.svg +0 -17
  46. package/features/notes/assets/icons/star.svg +0 -15
  47. package/features/notes/assets/icons/sticky-note.svg +0 -16
  48. package/features/notes/assets/icons/trash-2.svg +0 -19
  49. package/features/notes/src/http-server.mjs +0 -580
  50. package/features/notes/web/app.js +0 -1202
  51. package/features/notes/web/codex-artifact-document.svg +0 -1
  52. package/features/notes/web/codex-attachment.svg +0 -1
  53. package/features/notes/web/codex-build.svg +0 -1
  54. package/features/notes/web/codex-code.svg +0 -1
  55. package/features/notes/web/codex-cplusplus.svg +0 -1
  56. package/features/notes/web/codex-css.svg +0 -1
  57. package/features/notes/web/codex-document.svg +0 -1
  58. package/features/notes/web/codex-file.svg +0 -1
  59. package/features/notes/web/codex-folder.svg +0 -1
  60. package/features/notes/web/codex-hashes.svg +0 -1
  61. package/features/notes/web/codex-html.svg +0 -1
  62. package/features/notes/web/codex-image.svg +0 -1
  63. package/features/notes/web/codex-java.svg +0 -1
  64. package/features/notes/web/codex-javascript.svg +0 -1
  65. package/features/notes/web/codex-json.svg +0 -1
  66. package/features/notes/web/codex-notebook.svg +0 -1
  67. package/features/notes/web/codex-pdf.svg +0 -1
  68. package/features/notes/web/codex-pencil.svg +0 -3
  69. package/features/notes/web/codex-php.svg +0 -1
  70. package/features/notes/web/codex-plus.svg +0 -3
  71. package/features/notes/web/codex-presentation.svg +0 -1
  72. package/features/notes/web/codex-python.svg +0 -1
  73. package/features/notes/web/codex-react.svg +0 -1
  74. package/features/notes/web/codex-rust.svg +0 -1
  75. package/features/notes/web/codex-shell.svg +0 -1
  76. package/features/notes/web/codex-skill.svg +0 -1
  77. package/features/notes/web/codex-spreadsheet.svg +0 -1
  78. package/features/notes/web/codex-task.svg +0 -3
  79. package/features/notes/web/codex-terminal.svg +0 -1
  80. package/features/notes/web/codex-toml.svg +0 -1
  81. package/features/notes/web/codex-typescript.svg +0 -1
  82. package/features/notes/web/codex-view-all.svg +0 -3
  83. package/features/notes/web/file-icons.mjs +0 -68
  84. package/features/notes/web/i18n.mjs +0 -237
  85. package/features/notes/web/index.html +0 -15
  86. package/features/notes/web/styles.css +0 -1155
  87. package/features/notes/web/todo-drag.mjs +0 -18
@@ -0,0 +1,16 @@
1
+ # Sidebar Focus
2
+
3
+ 保留当前任务所在分类与 Project,收起其他分支。再次点击恢复进入聚焦前的展开状态;聚焦期间切换任务跟随路径。用户手动调整分支时退出聚焦并保留手动布局,无撤销浮层。
4
+
5
+ - 入口:展开侧栏的 Activity 与 Search 之间,随原生侧栏一起隐藏。
6
+ - 图标:Lucide list-chevrons-down-up(聚焦)/ list-chevrons-up-down(还原),16px,24px viewBox 内 2px 描边。
7
+ - 控件:继承原生 Activity 的 Button 类、28px 热区、secondary 颜色,始终使用 ghost。灰色背景仅在 hover 时出现,聚焦状态由图标切换表达,不保留选中底色。
8
+ - 字体、颜色、圆角、动效全部使用原生控件,不新增主题覆盖。
9
+ - 中英文标题:Focus current task / 聚焦当前任务;Exit focus / 退出聚焦。
10
+ - 折叠通过原生回调执行;React 元数据仅用于读取稳定分类、项目和会话身份。
11
+ - 无可识别当前路径时禁用;Activity 列表展示期间暂停整理。
12
+ - 状态仅作用于当前窗口;卸载不额外改变用户布局。
13
+
14
+ 始终显式使用 secondary 色,避免从激活的 Activity 按钮继承 info 蓝色。Activity 开启时按钮原生 disabled,暂停聚焦操作,关闭后恢复可用性。
15
+
16
+ 聚焦与还原图标采用 24×24 viewBox、16px 显示尺寸,描边 1.575 units(实际 1.05px),对齐相邻原生铃铛和搜索的轮廓粗细。
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "sidebar-focus",
4
+ "version": "0.1.0",
5
+ "label": {
6
+ "en": "Sidebar Focus",
7
+ "zh-CN": "侧栏聚焦"
8
+ },
9
+ "description": {
10
+ "en": "Keep the current task path expanded and collapse other branches.",
11
+ "zh-CN": "保留当前任务路径,收起其他分支。"
12
+ },
13
+ "entry": {
14
+ "kind": "page-script",
15
+ "script": "src/sidebar-focus.mjs"
16
+ },
17
+ "placement": {
18
+ "order": 80
19
+ }
20
+ }
@@ -0,0 +1,134 @@
1
+ const COLLAPSE_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.575\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M3 5h8\" />\n <path d=\"M3 12h8\" />\n <path d=\"M3 19h8\" />\n <path d=\"m15 5 3 3 3-3\" />\n <path d=\"m15 19 3-3 3 3\" />\n</svg>\n";
2
+ const EXPAND_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.575\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M3 5h8\" />\n <path d=\"M3 12h8\" />\n <path d=\"M3 19h8\" />\n <path d=\"m15 8 3-3 3 3\" />\n <path d=\"m15 16 3 3 3-3\" />\n</svg>\n";
3
+ const sectionSelector = "[data-app-action-sidebar-section]";
4
+ const projectSelector = "[data-app-action-sidebar-project-row]";
5
+ const states = new Map();
6
+ const tip = document.createElement("div");
7
+ tip.id = "minecodex-focus-tooltip";
8
+ tip.setAttribute("role", "tooltip");
9
+ tip.hidden = true;
10
+ const style = document.createElement("style");
11
+ style.textContent = `#minecodex-focus-tooltip {position:fixed;z-index:2147483000;pointer-events:none;padding:5px 12px;border:1px solid var(--color-border-tooltip);border-radius:var(--radius-2xl);background:var(--color-background-tooltip);color:var(--color-text-tooltip);box-shadow:var(--shadow-tooltip);font-family:var(--font-sans-default);font-size:var(--text-sm);line-height:18px;font-weight:var(--tooltip-compact-font-weight);letter-spacing:var(--tracking-tooltip);}`;
12
+ document.head.append(style);
13
+ document.body.append(tip);
14
+ function showTip() {
15
+ if (!button) return;
16
+ tip.hidden = false;
17
+ const rect = button.getBoundingClientRect();
18
+ tip.style.left = Math.max(8, rect.x + rect.width / 2 - tip.offsetWidth / 2) + "px";
19
+ tip.style.top = rect.bottom + 8 + "px";
20
+ }
21
+ function hideTip() { tip.hidden = true; }
22
+ let active = false, restoring = false, button, timer, disposed = false;
23
+ const text = (en, cn) => (document.documentElement.lang || navigator.language).startsWith("zh") ? cn : en;
24
+ function props(element, predicate) {
25
+ let fiber = element?.[Object.keys(element).find(key => key.startsWith("__reactFiber"))];
26
+ for (let i = 0; fiber && i < 30; i++, fiber = fiber.return) {
27
+ if (predicate(fiber.memoizedProps || {})) return fiber.memoizedProps;
28
+ }
29
+ return {};
30
+ }
31
+ function branches() {
32
+ return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].map(element => {
33
+ const native = props(element, p => !!p.sectionKey);
34
+ const data = props(element, p => Array.isArray(p.section?.threadKeys)).section;
35
+ return { element, key:"section:" + native.sectionKey, data, expanded:element.dataset.appActionSidebarSectionCollapsed === "false",
36
+ toggle:() => native.onCollapsedChange?.(element.dataset.appActionSidebarSectionCollapsed === "false") };
37
+ }).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].map(element => ({
38
+ element, key:"project:" + element.dataset.appActionSidebarProjectId,
39
+ data:props(element, p => !!p.group?.projectId).group,
40
+ expanded:element.getAttribute("aria-expanded") === "true",
41
+ toggle:() => element.click(),
42
+ })));
43
+ }
44
+ function remember(items) {
45
+ for (const item of items) if (!states.has(item.key)) states.set(item.key, item.expanded);
46
+ }
47
+ function target(items) {
48
+ const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
49
+ const id = document.querySelector("[data-above-composer-conversation-id]")?.dataset.aboveComposerConversationId;
50
+ const key = selected?.dataset.appActionSidebarThreadId || (id ? "local:" + id : null);
51
+ const activeProject = items.map(b => props(b.element, p => p.activeProjectId != null).activeProjectId).find(Boolean);
52
+ const project = items.find(b => b.key.startsWith("project:") && (b.data?.threadKeys?.includes(key) || b.data?.projectId === activeProject));
53
+ const section = items.find(b => b.key.startsWith("section:") && (b.data?.threadKeys?.includes(key) || (activeProject && b.data?.itemKeys?.includes("codex:project:" + activeProject))))
54
+ || items.find(b => b.key.startsWith("section:") && (selected && b.element.contains(selected) || project && b.element.contains(project.element)));
55
+ return section ? { section, project } : null;
56
+ }
57
+ function reconcile() {
58
+ if (disposed) return;
59
+ const sidebar = document.querySelector("#app-shell-sidebar");
60
+ const bell = [...(sidebar?.querySelectorAll("button[aria-pressed]") || [])].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
61
+ if (!bell) { button?.remove(); button = null; hideTip(); return; }
62
+ if (!button?.isConnected) {
63
+ button = bell.cloneNode(false);
64
+ button.removeAttribute("id");
65
+ button.setAttribute("data-minecodex-focus", "");
66
+ // 复用原生内层容器,由其负责图标在热区内居中。
67
+ const inner = bell.firstElementChild.cloneNode(false);
68
+ inner.innerHTML = COLLAPSE_GLYPH;
69
+ button.replaceChildren(inner);
70
+ button.setAttribute("aria-describedby", tip.id);
71
+ button.addEventListener("pointerenter", showTip, { signal });
72
+ button.addEventListener("pointerleave", hideTip, { signal });
73
+ button.addEventListener("focus", showTip, { signal });
74
+ button.addEventListener("blur", hideTip, { signal });
75
+ button.addEventListener("click", () => {
76
+ hideTip();
77
+ if (active) { active = false; restoring = true; }
78
+ else { states.clear(); remember(branches()); active = true; }
79
+ schedule();
80
+ }, { signal });
81
+ // 作为原生按钮组的独立子项,继承组内 gap,不放进铃铛的 Tooltip 包装层。
82
+ let anchor = bell;
83
+ while (anchor.parentElement && getComputedStyle(anchor.parentElement).display !== "flex") {
84
+ anchor = anchor.parentElement;
85
+ }
86
+ anchor.after(button);
87
+ }
88
+ const label = active ? text("Exit focus", "退出聚焦") : text("Focus current task", "聚焦当前任务");
89
+ if (button.getAttribute("aria-label") !== label) button.setAttribute("aria-label", label);
90
+ if (tip.textContent !== label) tip.textContent = label;
91
+ const pressed = String(active);
92
+ if (button.getAttribute("aria-pressed") !== pressed) {
93
+ button.setAttribute("aria-pressed", pressed);
94
+ button.firstElementChild.innerHTML = active ? EXPAND_GLYPH : COLLAPSE_GLYPH;
95
+ }
96
+ button.setAttribute("data-color", "secondary");
97
+ button.setAttribute("data-variant", "ghost");
98
+ const items = branches(), path = target(items);
99
+ const activityActive = bell.getAttribute("aria-pressed") === "true";
100
+ button.disabled = activityActive || (!active && !restoring && !path);
101
+ if (activityActive) { hideTip(); return; }
102
+ if (restoring) {
103
+ // 先恢复已挂载的子分支,再恢复分类,避免父节点收起后子节点消失。
104
+ const change = items.filter(b => b.key.startsWith("project:")).concat(items.filter(b => b.key.startsWith("section:")))
105
+ .find(b => states.has(b.key) && states.get(b.key) !== b.expanded);
106
+ if (change) { change.toggle(); schedule(); }
107
+ else { restoring = false; states.clear(); }
108
+ return;
109
+ }
110
+ if (!active || !path) return;
111
+ remember(items);
112
+ // 一次只触发一个原生更新;等待 React 挂载新分支后重新计算路径。
113
+ const change = items.find(b => b === path.section && !b.expanded)
114
+ || items.find(b => b.key.startsWith("project:") && b.expanded !== (path.section.element.contains(b.element) ? b === path.project : states.get(b.key)))
115
+ || items.find(b => b.key.startsWith("section:") && b.expanded !== (b === path.section));
116
+ if (change) { change.toggle(); schedule(); }
117
+ }
118
+ function schedule() {
119
+ clearTimeout(timer);
120
+ timer = setTimeout(reconcile, 40);
121
+ }
122
+ document.addEventListener("click", event => {
123
+ if (!active || !event.isTrusted) return;
124
+ if (event.target.closest("[data-app-action-sidebar-section-toggle], " + projectSelector)) {
125
+ active = false; states.clear(); schedule();
126
+ }
127
+ }, { capture:true, signal });
128
+ const observer = new MutationObserver(schedule);
129
+ observer.observe(document.documentElement, { subtree:true, childList:true, attributes:true, attributeFilter:[
130
+ "aria-expanded", "aria-pressed", "data-app-action-sidebar-thread-selected",
131
+ "data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
132
+ ] });
133
+ reconcile();
134
+ return () => { disposed = true; clearTimeout(timer); observer.disconnect(); button?.remove(); tip.remove(); style.remove(); };
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -0,0 +1,28 @@
1
+ # 任务切换
2
+
3
+ 仅在线程侧栏收起时显示入口;展开时移除入口并关闭浮层与遮罩。入口随 Codex 标题栏布局切换:
4
+ - 普通/分栏:在主 Chat 标题前显示原生 list-bullet 图标;整段标题可点击。
5
+ - 全宽标签页:在 Chat 标签页前注入独立的任务图标按钮。
6
+ - 移除扩展铃铛,不修改原生 Sidebar 图标及通知蓝点。
7
+ - 点击打开或关闭;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。移开鼠标不关闭。
8
+
9
+ 样式使用原生字体、text-secondary、radius-lg、primary-ghost-hover 与 ring token。入口仅 hover 显示背景,不保持选中底色。标题区域 no-drag,其他标题栏区域保留窗口拖拽。
10
+
11
+ 浮层宽320px(窄窗口缩小),最大高660px(约6条,受视口高度限制);沿用 dropdown 背景、border、radius-2xl、shadow-xl-spread。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
12
+
13
+ 列表集合为工作中、最近1小时打开过、未读的会话,按 Thread ID 去重。统一按消息时间倒序,当前会话仅标记选中,不置顶。原生流式摘要变化时更新观测消息时间,不因菜单重绘更新时间。最近访问记录限定当前窗口运行期间。每条显示:
14
+ 1. 标题、本地/远程设备图标、相对时间;工作中用原生 loading 替代时间。
15
+ 2. 最多两行省略的 AI 回答摘要。
16
+ 3. 所属 Project 图标和名称(若有),不合并分组。
17
+
18
+ 时间使用原生 hover card 的 recencyAt,缺失时 updatedAt;每30秒刷新。工作状态读取原生 statusState.type=loading。缓存会话数据与原生导航回调,复用导航和已读更新,不缓存 DOM 行,不发送消息。
19
+
20
+ 打开时临时启用 Activity 以取得完整列表,关闭或卸载时恢复原先模式。只读取原生数据,预览不标记已读。界面及无障碍文案适配中英文。
21
+
22
+ 入口只显示任务图标,不显示数量。浮层打开时使用原生 --modal-backdrop-background 遮罩覆盖底层界面;遮罩位于浮层下方,点击关闭并返回入口焦点,卸载时移除。
23
+
24
+ 标题固定、条目容器独立滚动;浮层底部保留3个 spacing 单位(12px)固定内边距,滚动时仍保留。
25
+
26
+ 点击后先同步显示浮层与可用数据,首帧绘制后再切换原生 Activity 补全条目;关闭或卸载会取消尚未开始的切换。
27
+
28
+ 消息刷新时保留列表滚动位置与条目键盘焦点。当前身份消失时清除选中状态,最近访问缓存按一小时过期。
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "unread-inbox",
4
+ "version": "0.1.0",
5
+ "label": {
6
+ "en": "Task Switcher",
7
+ "zh-CN": "任务切换"
8
+ },
9
+ "description": {
10
+ "en": "Switch between working and unread chats from the chat title or task button.",
11
+ "zh-CN": "点击聊天标题或任务按钮,切换工作中与未读会话。"
12
+ },
13
+ "entry": {
14
+ "kind": "page-script",
15
+ "script": "src/unread-inbox.mjs"
16
+ },
17
+ "placement": {
18
+ "order": 70
19
+ }
20
+ }
@@ -0,0 +1,360 @@
1
+ const taskGlyph = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M13.3336 11.4749C13.6234 11.475 13.858 11.7104 13.858 12.0002C13.8579 12.2899 13.6233 12.5255 13.3336 12.5256H6.66663C6.37678 12.5256 6.1414 12.2901 6.14124 12.0002C6.14124 11.7103 6.37668 11.4749 6.66663 11.4749H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 10.9827C3.50002 10.9827 3.84226 11.3241 3.84241 11.7454C3.84241 12.1667 3.50011 12.509 3.07874 12.509C2.65748 12.5089 2.31604 12.1666 2.31604 11.7454C2.31619 11.3242 2.65758 10.9828 3.07874 10.9827Z\" fill=\"currentColor\"/> <path d=\"M13.3336 7.47485C13.6234 7.47503 13.858 7.7104 13.858 8.00024C13.8579 8.28995 13.6233 8.52546 13.3336 8.52563H6.66663C6.37678 8.52563 6.1414 8.29005 6.14124 8.00024C6.14124 7.71029 6.37668 7.47485 6.66663 7.47485H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 6.98267C3.50002 6.98267 3.84226 7.32411 3.84241 7.74536C3.84241 8.16674 3.50011 8.50903 3.07874 8.50903C2.65748 8.50888 2.31604 8.16665 2.31604 7.74536C2.31619 7.3242 2.65758 6.98281 3.07874 6.98267Z\" fill=\"currentColor\"/> <path d=\"M13.3336 3.47485C13.6234 3.47503 13.858 3.7104 13.858 4.00024C13.8579 4.28995 13.6233 4.52546 13.3336 4.52563H6.66663C6.37678 4.52563 6.1414 4.29005 6.14124 4.00024C6.14124 3.71029 6.37668 3.47485 6.66663 3.47485H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 2.98267C3.50002 2.98267 3.84226 3.32411 3.84241 3.74536C3.84241 4.16674 3.50011 4.50903 3.07874 4.50903C2.65748 4.50888 2.31604 4.16665 2.31604 3.74536C2.31619 3.3242 2.65758 2.98281 3.07874 2.98267Z\" fill=\"currentColor\"/></svg>";
2
+ const deviceGlyphs = {"local": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.6667 2.47461C13.6928 2.47479 14.525 3.30694 14.5251 4.33301V10.1445C15.0248 10.184 15.3787 10.6177 15.3787 11.0928V11.9355C15.3785 12.9036 14.6274 13.7373 13.6433 13.7373H2.34644C1.36254 13.7371 0.611273 12.9035 0.611084 11.9355V11.0928C0.611084 10.6149 0.970003 10.1796 1.47437 10.1445V4.33301C1.47454 3.30702 2.3068 2.47492 3.33276 2.47461H12.6667ZM1.66187 11.9355C1.66205 12.3765 1.99397 12.6863 2.34644 12.6865H13.6433C13.9959 12.6865 14.3287 12.3767 14.3289 11.9355V11.1924H1.66187V11.9355ZM3.33276 3.52539C2.8867 3.5257 2.52532 3.88692 2.52515 4.33301V10.1416H13.4744V4.33301C13.4742 3.88684 13.1129 3.52557 12.6667 3.52539H3.33276Z\" fill=\"currentColor\"/></svg>", "remote": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11.5005 1.1416C12.4346 1.14184 13.1919 1.89985 13.1919 2.83398V6.55859H13.3335C13.8259 6.55859 14.1917 6.9676 14.1919 7.42969V8.2959C14.1914 9.14015 13.5269 9.85938 12.6665 9.85938H3.3335C2.47306 9.85938 1.80856 9.14015 1.80811 8.2959V7.42969C1.80834 6.9677 2.17428 6.55877 2.6665 6.55859H2.80811V2.83398C2.80811 1.8997 3.56621 1.1416 4.50049 1.1416H11.5005ZM2.85889 8.2959C2.85933 8.59786 3.08989 8.80859 3.3335 8.80859H12.6665C12.9101 8.80859 13.1416 8.59786 13.1421 8.2959V7.60938H2.85889V8.2959ZM4.50049 2.19238C4.14611 2.19238 3.85889 2.4796 3.85889 2.83398V6.55859H12.1421V2.83398C12.1421 2.47975 11.8547 2.19262 11.5005 2.19238H4.50049Z\" fill=\"currentColor\"/> <path d=\"M2.8595 13.4307C3.28863 13.4308 3.63684 13.7788 3.63684 14.208C3.63677 14.6371 3.28859 14.9852 2.8595 14.9854C2.43029 14.9854 2.08222 14.6372 2.08215 14.208C2.08215 13.7788 2.43025 13.4307 2.8595 13.4307Z\" fill=\"currentColor\"/> <path d=\"M5.42981 13.4307C5.859 13.4307 6.20715 13.7788 6.20715 14.208C6.20709 14.6372 5.85896 14.9853 5.42981 14.9854C5.0006 14.9854 4.65253 14.6372 4.65247 14.208C4.65247 13.7788 5.00056 13.4307 5.42981 13.4307Z\" fill=\"currentColor\"/> <path d=\"M8.00012 13.4307C8.42937 13.4307 8.77747 13.7788 8.77747 14.208C8.7774 14.6372 8.42933 14.9854 8.00012 14.9854C7.57092 14.9854 7.22284 14.6372 7.22278 14.208C7.22278 13.7788 7.57087 13.4307 8.00012 13.4307Z\" fill=\"currentColor\"/> <path d=\"M10.5704 13.4307C10.9997 13.4307 11.3478 13.7788 11.3478 14.208C11.3477 14.6372 10.9996 14.9854 10.5704 14.9854C10.1414 14.9852 9.79413 14.6371 9.79407 14.208C9.79407 13.7789 10.1414 13.4309 10.5704 13.4307Z\" fill=\"currentColor\"/> <path d=\"M13.1417 13.4307C13.5707 13.4309 13.9181 13.7789 13.9181 14.208C13.918 14.637 13.5707 14.9851 13.1417 14.9854C12.7125 14.9854 12.3644 14.6372 12.3644 14.208C12.3644 13.7788 12.7125 13.4307 13.1417 13.4307Z\" fill=\"currentColor\"/> <path d=\"M8.00012 10.8809C8.42925 10.8809 8.77727 11.2281 8.77747 11.6572C8.77747 12.0865 8.42937 12.4346 8.00012 12.4346C7.57087 12.4346 7.22278 12.0865 7.22278 11.6572C7.22298 11.2281 7.571 10.8809 8.00012 10.8809Z\" fill=\"currentColor\"/></svg>"};
3
+ const text = (en, cn) => (document.documentElement.lang || navigator.language || "en").startsWith("zh") ? cn : en;
4
+ const projectIcons = new Map();
5
+ const folderGlyph = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.94946 2.66821C7.58311 2.66821 8.20067 2.86877 8.71313 3.24146L9.77759 4.01587C10.0627 4.22319 10.4065 4.33521 10.759 4.33521H14.9583C16.6142 4.33521 17.9563 5.67732 17.9563 7.33325V14.3333C17.9563 15.9892 16.6142 17.3313 14.9583 17.3313H5.04126C3.38547 17.3311 2.04321 15.9891 2.04321 14.3333V5.66626C2.04339 4.01058 3.38558 2.66839 5.04126 2.66821H6.94946ZM3.37329 9.83228V14.3333C3.37329 15.2545 4.12001 16.001 5.04126 16.0012H14.9583C15.8796 16.0012 16.6262 15.2546 16.6262 14.3333V9.83228H3.37329ZM5.04126 3.99829C4.12012 3.99847 3.37347 4.74512 3.37329 5.66626V8.5022H16.6262V7.33325C16.6262 6.41186 15.8796 5.66528 14.9583 5.66528H10.759C10.1254 5.66528 9.50784 5.46377 8.99536 5.09106L7.93091 4.31763C7.64576 4.11024 7.30205 3.99829 6.94946 3.99829H5.04126Z\" fill=\"currentColor\"/></svg>";
6
+ const marker = "data-minecodex-unread";
7
+ const style = document.createElement("style");
8
+ style.textContent = `
9
+ [data-minecodex-task-title] { pointer-events:auto; -webkit-app-region:no-drag; cursor:pointer; border-radius:var(--radius-lg); min-height:32px; padding-inline:6px; }
10
+ [data-minecodex-task-title]:hover { background:var(--color-background-primary-ghost-hover); }
11
+ [data-minecodex-task-title]:focus-visible { outline:2px solid var(--color-ring); outline-offset:-2px; }
12
+ [data-minecodex-task-badge] { display:inline-flex; flex-shrink:0; align-items:center; gap:4px; color:var(--color-text-secondary); font-family:var(--font-sans-default); font-size:var(--text-xs); font-variant-numeric:tabular-nums; }
13
+ [data-minecodex-unread=trigger] { -webkit-app-region:no-drag; }
14
+ [${marker}=scrim] { position:fixed; inset:0; z-index:2147482999; background:var(--modal-backdrop-background); -webkit-app-region:no-drag; }
15
+ [${marker}=scrim][hidden] { display:none; }
16
+ [${marker}=panel] { position:fixed; z-index:2147483000; box-sizing:border-box; width:min(320px,calc(100vw - 16px)); max-height:min(660px,calc(100vh - 64px)); display:flex; flex-direction:column; overflow:hidden; padding:var(--app-menu-gutter,4px); padding-bottom:calc(var(--spacing,4px) * 3); border:1px solid var(--color-border); border-radius:var(--radius-2xl); background:var(--color-token-dropdown-background); color:var(--color-text-primary); box-shadow:var(--shadow-xl-spread); font-family:var(--font-sans-default); font-size:var(--text-sm); }
17
+ [${marker}=panel][hidden] { display:none; }
18
+ [${marker}=panel] .mcx-thread-list { min-height:0; overflow-y:auto; overscroll-behavior:contain; }
19
+ [${marker}=panel] h2 { flex-shrink:0; margin:8px 10px; font-size:var(--text-sm); font-weight:500; color:var(--color-text-secondary); }
20
+ [${marker}=panel] [data-unread-row] { display:flex; flex-direction:column; gap:6px; width:100%; padding:10px; box-sizing:border-box; border:0; border-radius:var(--radius-xl); background:transparent; text-align:start; color:var(--color-text-primary); font:inherit; cursor:pointer; }
21
+ [${marker}=panel] [data-unread-row][aria-current=page] { background:var(--color-background-primary-ghost-hover); }
22
+ [${marker}=panel] [data-unread-row]:hover { background:var(--color-background-primary-ghost-hover); }
23
+ [${marker}=panel] [data-unread-row]:focus-visible { outline:2px solid var(--color-ring); outline-offset:-2px; }
24
+ [${marker}=panel] .mcx-thread-header { display:flex; align-items:center; gap:8px; width:100%; min-width:0; }
25
+ [${marker}=panel] .mcx-thread-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; font-weight:500; }
26
+ [${marker}=panel] .mcx-device { display:flex; flex-shrink:0; color:var(--color-text-tertiary); }
27
+ [${marker}=panel] .mcx-loading { margin-inline-start:auto; flex-shrink:0; display:flex; align-items:center; color:var(--color-text-tertiary); }
28
+ [${marker}=panel] time { margin-inline-start:auto; flex-shrink:0; font-size:var(--text-xs); color:var(--color-text-tertiary); }
29
+ [${marker}=panel] .mcx-summary { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; white-space:normal; overflow-wrap:anywhere; font-size:var(--text-xs); line-height:var(--text-xs--line-height,1.4); color:var(--color-text-tertiary); }
30
+ [${marker}=panel] .mcx-project { display:flex; align-items:center; gap:6px; padding:0; min-width:0; font-size:var(--text-xs); color:var(--color-text-secondary); font-weight:500; }
31
+ [${marker}=panel] .mcx-project > span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
32
+ [${marker}=panel] .mcx-project svg { width:16px; height:16px; }
33
+ [${marker}=panel] .mcx-empty { margin:20px 12px; color:var(--color-text-tertiary); }
34
+ `;
35
+ document.head.append(style);
36
+ const panel = document.createElement("div");
37
+ panel.setAttribute(marker, "panel");
38
+ panel.id = "minecodex-unread-panel";
39
+ panel.setAttribute("role", "dialog");
40
+ panel.hidden = true;
41
+ document.body.append(panel);
42
+ const scrim = document.createElement("div");
43
+ scrim.setAttribute(marker, "scrim");
44
+ scrim.setAttribute("aria-hidden", "true");
45
+ scrim.hidden = true;
46
+ document.body.append(scrim);
47
+ scrim.addEventListener("click", () => close(true), { signal });
48
+ let trigger = null, badge = null, binding = null, standalone = false;
49
+ let activityFrame = null, activityTimer = null;
50
+ const recentRows = new Map();
51
+ const openedAt = new Map();
52
+ const messageUpdates = new Map();
53
+ let selectedKey = null;
54
+ let restoreActivity = false, signature = "", queued = false, disposed = false;
55
+ function activityButton() {
56
+ return [...document.querySelectorAll('#app-shell-sidebar button[aria-pressed]')].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
57
+ }
58
+ function isUnread(row) {
59
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
60
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
61
+ if (typeof fiber.memoizedProps?.isUnread === "boolean") return fiber.memoizedProps.isUnread;
62
+ }
63
+ return false;
64
+ }
65
+ function isWorking(row) {
66
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
67
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
68
+ const status = fiber.memoizedProps?.statusState;
69
+ if (status?.type) return status.type === "loading";
70
+ }
71
+ return false;
72
+ }
73
+ // 缓存数据与原生导航动作,不保留会被 React 卸载的会话 DOM。
74
+ function threadRecord(row) {
75
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
76
+ let activate = null;
77
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
78
+ const props = fiber.memoizedProps;
79
+ if (props?.statusState && typeof props.onClick === "function") { activate = props.onClick; break; }
80
+ }
81
+ return { ...threadDetails(row), key:row.dataset.appActionSidebarThreadId, unread:isUnread(row), activate, spinner:row.querySelector?.('[class~="motion-safe:animate-spin"]')?.outerHTML ?? "" };
82
+ }
83
+ function selectThreads(records, recent, now = Date.now()) {
84
+ return [...new Map(records.map(item => [item.key, item])).values()]
85
+ .filter(item => item.working || item.unread || now - (recent.get(item.key) ?? -Infinity) < 3600000)
86
+ .sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0));
87
+ }
88
+ function captureCurrentRow() {
89
+ const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
90
+ const id = document.querySelector('[data-above-composer-conversation-id]')?.getAttribute('data-above-composer-conversation-id');
91
+ selectedKey = selected?.dataset.appActionSidebarThreadId
92
+ ?? [...recentRows.keys()].find(key => id && key.endsWith(":" + id)) ?? null;
93
+ if (selected) recentRows.set(selectedKey, threadRecord(selected));
94
+ if (selectedKey) openedAt.set(selectedKey, Date.now());
95
+ for (const [key, time] of openedAt) if (Date.now() - time >= 3600000) {
96
+ openedAt.delete(key); recentRows.delete(key); messageUpdates.delete(key);
97
+ }
98
+ }
99
+ function projectForRow(row) {
100
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
101
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
102
+ const group = fiber.memoizedProps?.secondaryContent?.props?.projectGroup;
103
+ if (group?.projectId) return { id:group.projectId, name:group.label };
104
+ }
105
+ return null;
106
+ }
107
+ function threadDetails(row) {
108
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
109
+ let summary, hostConfig;
110
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
111
+ summary ||= fiber.memoizedProps?.threadSummary;
112
+ hostConfig ||= fiber.memoizedProps?.hoverCardHostConfig;
113
+ }
114
+ const hostId = summary?.hostId || row.dataset.appActionSidebarThreadHostId || "local";
115
+ const key = row.dataset.appActionSidebarThreadId;
116
+ const working = isWorking(row);
117
+ const message = row.querySelector("[data-thread-secondary-title]")?.textContent || "";
118
+ const nativeTime = summary?.recencyAt ?? summary?.updatedAt ?? null;
119
+ const previous = messageUpdates.get(key);
120
+ // 原生流式摘要发生变化时推进时间,不因重绘或打开菜单重置消息时间。
121
+ if (working && (!previous || previous.message !== message || !previous.working)) {
122
+ messageUpdates.set(key, {message, working, time:Math.max(nativeTime || 0, Date.now())});
123
+ } else if (previous) previous.working = working;
124
+ return {
125
+ title:row.dataset.appActionSidebarThreadTitle || row.querySelector("[data-thread-title]")?.textContent || "",
126
+ summary:row.querySelector("[data-thread-secondary-title]")?.textContent || "",
127
+ // 与原生 Thread hover card 相同,优先使用任务 recency 时间。
128
+ timestamp:Math.max(nativeTime || 0, messageUpdates.get(key)?.time || 0) || null,
129
+ hostId, hostName:hostConfig?.display_name || hostId,
130
+ project:projectForRow(row),
131
+ working:isWorking(row),
132
+ };
133
+ }
134
+ function relativeTime(timestamp, now = Date.now()) {
135
+ if (!Number.isFinite(timestamp)) return "";
136
+ const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
137
+ if (seconds < 60) return text("Just now", "刚刚");
138
+ const units = seconds < 3600 ? [60, "m", "分钟"] : seconds < 86400 ? [3600, "h", "小时"] : [86400, "d", "天"];
139
+ const value = Math.floor(seconds / units[0]);
140
+ return text(value + units[1], value + units[2] + "前");
141
+ }
142
+ function cacheProjectIcons() {
143
+ for (const row of document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-project-row]')) {
144
+ const glyph = row.querySelector("span.relative.inline-flex");
145
+ if (glyph) projectIcons.set(row.dataset.appActionSidebarProjectId, glyph.cloneNode(true));
146
+ }
147
+ }
148
+ function close(restoreFocus = false) {
149
+ cancelAnimationFrame(activityFrame); clearTimeout(activityTimer);
150
+ activityFrame = null; activityTimer = null;
151
+ panel.hidden = true;
152
+ scrim.hidden = true;
153
+ if (trigger?.getAttribute("aria-expanded") !== "false") trigger?.setAttribute("aria-expanded", "false");
154
+ if (restoreActivity) {
155
+ restoreActivity = false;
156
+ const native = activityButton();
157
+ if (native?.getAttribute("aria-pressed") === "true") native.click();
158
+ }
159
+ if (restoreFocus) trigger?.focus();
160
+ }
161
+ function position() {
162
+ if (!trigger) return;
163
+ const rect = trigger.getBoundingClientRect();
164
+ const width = Math.min(320, innerWidth - 16);
165
+ panel.style.left = `${Math.max(8, Math.min(rect.left, innerWidth - width - 8))}px`;
166
+ panel.style.top = `${rect.bottom + 8}px`;
167
+ panel.style.maxWidth = `${width}px`;
168
+ panel.style.maxHeight = `${Math.max(0, Math.min(660, innerHeight - rect.bottom - 24))}px`;
169
+ }
170
+ function renderRows() {
171
+ captureCurrentRow();
172
+ const records = [...document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-thread-row]')].map(threadRecord);
173
+ const mounted = new Set(records.map(item => item.key));
174
+ for (const item of records) if (openedAt.has(item.key)) recentRows.set(item.key, item);
175
+ for (const [key, item] of recentRows) if (!mounted.has(key)) records.push({ ...item, working:false, unread:false });
176
+ const details = selectThreads(records, openedAt).map(item => ({ ...item, current:item.key === selectedKey }));
177
+ const next = JSON.stringify([text("Priority", "优先事项"), details, Math.floor(Date.now() / 60000)]);
178
+ if (signature === next) return;
179
+ signature = next;
180
+ const scrollTop = panel.querySelector(".mcx-thread-list")?.scrollTop ?? 0;
181
+ const focusedKey = panel.contains(document.activeElement) ? document.activeElement?.dataset.threadKey : null;
182
+ panel.replaceChildren();
183
+ panel.setAttribute("aria-label", text("Priority", "优先事项"));
184
+ const heading = document.createElement("h2");
185
+ heading.textContent = text("Priority", "优先事项");
186
+ panel.append(heading);
187
+ const list = document.createElement("div");
188
+ list.className = "mcx-thread-list";
189
+ panel.append(list);
190
+ details.forEach(item => {
191
+ const card = document.createElement("button");
192
+ card.type = "button";
193
+ card.dataset.unreadRow = "";
194
+ card.dataset.threadKey = item.key;
195
+ if (item.current) card.setAttribute("aria-current", "page");
196
+ const header = document.createElement("span");
197
+ header.className = "mcx-thread-header";
198
+ const title = document.createElement("span");
199
+ title.className = "mcx-thread-title";
200
+ title.textContent = item.title;
201
+ const device = document.createElement("span");
202
+ device.className = "mcx-device";
203
+ const remote = item.hostId !== "local";
204
+ device.innerHTML = deviceGlyphs[remote ? "remote" : "local"];
205
+ device.title = remote ? item.hostName : text("This computer", "本机");
206
+ device.setAttribute("aria-label", device.title);
207
+ header.append(title, device);
208
+ const elapsed = relativeTime(item.timestamp);
209
+ if (item.working) {
210
+ const loading = document.createElement("span");
211
+ loading.className = "mcx-loading";
212
+ loading.setAttribute("role", "status");
213
+ loading.setAttribute("aria-label", text("Working", "正在工作"));
214
+ // 直接复用当前会话原生 loading,保持动画与 reduced-motion 行为一致。
215
+ loading.innerHTML = item.spinner;
216
+ header.append(loading);
217
+ } else if (elapsed) {
218
+ const time = document.createElement("time");
219
+ time.dateTime = new Date(item.timestamp).toISOString();
220
+ time.textContent = elapsed;
221
+ time.title = new Date(item.timestamp).toLocaleString();
222
+ header.append(time);
223
+ }
224
+ card.append(header);
225
+ if (item.summary) {
226
+ const summary = document.createElement("span");
227
+ summary.className = "mcx-summary";
228
+ summary.textContent = item.summary;
229
+ card.append(summary);
230
+ }
231
+ if (item.project) {
232
+ const project = document.createElement("span");
233
+ project.className = "mcx-project";
234
+ const glyph = projectIcons.get(item.project.id)?.cloneNode(true);
235
+ if (glyph) project.append(glyph);
236
+ else project.innerHTML = folderGlyph;
237
+ const name = document.createElement("span");
238
+ name.textContent = item.project.name;
239
+ project.append(name);
240
+ card.append(project);
241
+ }
242
+ card.addEventListener("click", () => {
243
+ if (!item.current) {
244
+ const native = [...document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-thread-row]')]
245
+ .find(row => row.dataset.appActionSidebarThreadId === item.key);
246
+ const activate = native ? threadRecord(native).activate : item.activate;
247
+ if (!activate) return;
248
+ activate();
249
+ }
250
+ close();
251
+ });
252
+ list.append(card);
253
+ });
254
+ if (!details.length) {
255
+ const empty = document.createElement("p");
256
+ empty.className = "mcx-empty";
257
+ empty.textContent = text("You're all caught up", "暂无未读会话");
258
+ list.append(empty);
259
+ }
260
+ if (focusedKey) [...list.children].find(card => card.dataset.threadKey === focusedKey)?.focus({ preventScroll:true });
261
+ list.scrollTop = scrollTop;
262
+ }
263
+ function open() {
264
+ if (!trigger) return;
265
+ cacheProjectIcons();
266
+ captureCurrentRow();
267
+ const native = activityButton();
268
+ if (!native) return;
269
+ panel.hidden = false;
270
+ scrim.hidden = false;
271
+ trigger.setAttribute("aria-expanded", "true");
272
+ renderRows(); position();
273
+ // 先呈现浮层,下一帧之后再更新原生 Activity,避免同步 React 切换阻塞点击反馈。
274
+ if (native.getAttribute("aria-pressed") !== "true") {
275
+ activityFrame = requestAnimationFrame(() => {
276
+ activityFrame = null;
277
+ activityTimer = setTimeout(() => {
278
+ activityTimer = null;
279
+ if (disposed || panel.hidden) return;
280
+ const latest = activityButton();
281
+ if (latest?.getAttribute("aria-pressed") === "false") {
282
+ restoreActivity = true; latest.click();
283
+ }
284
+ }, 0);
285
+ });
286
+ }
287
+ }
288
+ function visible(element) {
289
+ return element && !element.closest('[aria-hidden="true"], [inert]') && element.checkVisibility();
290
+ }
291
+ function detachTrigger() {
292
+ binding?.abort(); binding = null;
293
+ if (standalone) trigger?.remove();
294
+ else if (trigger) {
295
+ for (const name of ["data-minecodex-task-title", "data-minecodex-unread", "role", "tabindex", "aria-haspopup", "aria-controls", "aria-expanded", "aria-label"]) trigger.removeAttribute(name);
296
+ }
297
+ badge?.remove(); badge = null; trigger = null;
298
+ }
299
+ function sync() {
300
+ if (disposed) return;
301
+ cacheProjectIcons();
302
+ captureCurrentRow();
303
+ const sidebarToggle = [...document.querySelectorAll("[data-app-shell-sidebar-trigger]")].find(visible);
304
+ if (!sidebarToggle || sidebarToggle.getAttribute("aria-expanded") !== "false") {
305
+ close(); detachTrigger(); return;
306
+ }
307
+ const title = [...document.querySelectorAll('[data-app-shell-titlebar-slot="main"] [data-app-shell-titlebar-content]')]
308
+ .find(e => visible(e) && !e.querySelector("button, [role=tab], [data-app-shell-tab-row]"));
309
+ const tabs = [...document.querySelectorAll('[data-app-shell-tab-row][role=presentation]')].find(visible);
310
+ const target = title || tabs;
311
+ if (!target) { close(); detachTrigger(); return; }
312
+ const needsStandalone = !title;
313
+ if (!trigger?.isConnected || standalone !== needsStandalone || (title ? trigger !== title : trigger.parentElement !== target)) {
314
+ close(); detachTrigger(); standalone = needsStandalone;
315
+ binding = new AbortController();
316
+ if (title) {
317
+ trigger = title;
318
+ trigger.setAttribute("data-minecodex-task-title", "");
319
+ trigger.setAttribute("role", "button");
320
+ trigger.tabIndex = 0;
321
+ } else {
322
+ trigger = document.createElement("button");
323
+ trigger.type = "button";
324
+ trigger.className = "no-drag pointer-events-auto flex items-center justify-center gap-1 rounded-lg text-tertiary hover:bg-primary-ghost-hover button-toolbar px-2";
325
+ target.prepend(trigger);
326
+ }
327
+ trigger.setAttribute(marker, "trigger");
328
+ trigger.setAttribute("aria-haspopup", "dialog");
329
+ trigger.setAttribute("aria-controls", panel.id);
330
+ trigger.setAttribute("aria-expanded", "false");
331
+ badge = document.createElement("span");
332
+ badge.setAttribute("data-minecodex-task-badge", "");
333
+ badge.innerHTML = taskGlyph;
334
+ trigger.prepend(badge);
335
+ trigger.addEventListener("click", event => { event.stopPropagation(); panel.hidden ? open() : close(); }, {signal:binding.signal});
336
+ trigger.addEventListener("keydown", event => {
337
+ if (["Enter", " ", "ArrowDown"].includes(event.key)) {
338
+ event.preventDefault(); event.stopPropagation();
339
+ if (event.key === "ArrowDown" || panel.hidden) open(); else close();
340
+ }
341
+ }, {signal:binding.signal});
342
+ }
343
+ const label = text("Switch tasks", "切换任务");
344
+ if (trigger.getAttribute("aria-label") !== label) trigger.setAttribute("aria-label", label);
345
+ if (!panel.hidden) { renderRows(); position(); }
346
+ }
347
+ const observer = new MutationObserver(() => {
348
+ if (queued) return;
349
+ queued = true;
350
+ queueMicrotask(() => { queued = false; sync(); });
351
+ });
352
+ observer.observe(document.documentElement, { childList:true, subtree:true, attributes:true, attributeFilter:["aria-expanded", "aria-pressed", "aria-hidden", "data-app-shell-header-layout", "data-app-action-sidebar-thread-selected", "lang"] });
353
+ document.addEventListener("pointerdown", event => { if (event.target !== scrim && !trigger?.contains(event.target) && !panel.contains(event.target)) close(); }, { signal });
354
+ document.addEventListener("keydown", event => { if (event.key === "Escape" && !panel.hidden) { event.preventDefault(); close(true); } }, { signal });
355
+ window.addEventListener("resize", position, { signal });
356
+ const refreshTimer = setInterval(() => { if (!panel.hidden) renderRows(); }, 30000);
357
+ sync();
358
+ return () => {
359
+ disposed = true; clearInterval(refreshTimer); observer.disconnect(); close(); detachTrigger(); panel.remove(); scrim.remove(); style.remove();
360
+ };