minecodex 2.0.0 → 2.0.2

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.
@@ -178,7 +178,7 @@ async function scan() {
178
178
  copy.type = "button";
179
179
  function copyFeedback(copied = false, failed = false) {
180
180
  const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : label("Copy prompt", "复制提示词");
181
- copy.title = text;
181
+ copy.setAttribute("data-minecodex-tooltip", text);
182
182
  copy.setAttribute("aria-label", text);
183
183
  copy.replaceChildren(copyGlyph(copied));
184
184
  }
@@ -6,7 +6,7 @@ Prompts 页面负责新增、编辑、分类、Emoji、排序和统计;Quick P
6
6
  ## 原生依据
7
7
  2026-09-27 在隔离 Codex 实测导航扩展区、Library 页面及 Add files and more。
8
8
  - Prompts 导航项位于固定扩展区末尾,顺序跟随扩展区而非硬编码 Projects。仅定位 `data-app-navigation-rail`,不匹配设置菜单。
9
- - Quick Prompts 位于 Add 与 Change permissions 之间,克隆 Add 的按钮 classes 与响应式图标容器。入口采用 Lucide book-open;20px 图标描边折算 1.75px(24 viewBox / stroke-width 2.1)。
9
+ - Quick Prompts 位于 Add 与 Change permissions 之间,克隆 Add 的按钮 classes 与响应式图标容器。入口采用 Lucide book-open;保留 16 / 20px 图标容器,viewBox 扩为 27,将图形缩小约 11%;non-scaling-stroke 保持 1.5px 笔画,热区仍为 28px。
10
10
  - 页面沿用 Library 的大标题和胶囊 Tabs;分类图标取自当前 Codex 原生 SymbolBrowser / project appearance catalogue,记录于 `assets/native-prompt-icons.json`,不手绘路径。
11
11
 
12
12
  | 角色 | Token / 权威来源 | 值或用途 |
@@ -29,7 +29,7 @@ Prompts 页面负责新增、编辑、分类、Emoji、排序和统计;Quick P
29
29
  分类颜色复用原生选择器的亮 / 暗模式色值:blue #0285ff / #339cff、green #04b84c / #40c977、orange #fb6a22 / #ff8549、pink #ff66ad / #ff8cc1、purple #924ff7 / #ad7bf9、red #fa423e / #ff6764、yellow #ffc300 / #ffd240;black 使用原生次级文字 token。
30
30
 
31
31
  ## 页面
32
- Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻搜索框、New prompt。搜索框直接采用原生 Library 的透明背景、1px outline、32px 高、胶囊圆角、12px 水平 padding 和 16px 搜索 SVG。分类管理使用原生 sliders 图标,不再放在 Tabs 行。排序菜单沿用原生 Library 的 4px gutter、20px radius、5px / 8px item padding、90% elevated surface 与 shadow-xl-spread。Tabs 仅包含 All、分类和 Other。默认分类生图、编程、文案、研究、翻译、效率,允许新增、改名、删除、换图标和颜色。删除分类仅解除归属,保留提示词和统计。
32
+ Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻搜索框、New prompt。搜索框直接采用原生 Library 的透明背景、1px outline、32px 高、胶囊圆角、12px 水平 padding 和 16px 搜索 SVG。分类管理使用原生 sliders 图标,不再放在 Tabs 行。排序菜单沿用原生 Library 的 4px gutter、20px radius、5px / 8px item padding、90% elevated surface 与 shadow-xl-spread。Tabs 仅包含 All、分类和 Other。默认分类生图、编程、文案、研究、翻译、效率、思考、学习,允许新增、改名、删除、换图标和颜色。删除分类仅解除归属,保留提示词和统计。
33
33
 
34
34
  分类管理在 440px 宽居中模态面板中完成,复用原生 New folder 弹窗的 20px padding、25px radius、弱描边与背景遮罩。分类编辑保留箭头返回和关闭按钮,不使用文字 Back 或整页替换。模态内限制 Tab 焦点,Escape 关闭后恢复入口焦点。提示词编辑使用同一结构,宽 520px。
35
35
 
@@ -64,3 +64,9 @@ Category 下拉面板与触发栏等宽、左右对齐,宽度包含内边距
64
64
 
65
65
  ## 语言
66
66
  界面名称、操作、Tooltip、无障碍标签、空状态、错误提示与图标/颜色名称按 Codex 的语言显示中英文;日期按对应 locale 格式化。用户保存的提示词与分类名称原样展示,不随界面语言改写。
67
+
68
+ ## 内置提示词与 Tooltip
69
+
70
+ 内置 24 条来自 dsh-prompt 0.1.13 的 MIT 提示词,正文保持原样,按思考、学习、编程、效率分类并配 Emoji。来源与许可保存在 assets/dsh-prompt-LICENSE.txt。首次启动或旧库升级时单次导入;随后允许编辑和删除,重启不还原,也不重建被删除的分类。用户个人内容只保留在本机数据库,不纳入默认资源。
71
+
72
+ 所有简短悬浮提示使用运行时的 data-minecodex-tooltip 标记,取原生 background/text/border-tooltip、radius-2xl、shadow-tooltip、tooltip-compact-font-weight 和 tracking-tooltip;padding 5px 12px、行高 18px。支持悬浮与键盘聚焦、Escape 关闭及卸载清理,不再使用浏览器 title 气泡。
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 管理页面
6
6
  - All 与分类 Tabs,搜索、新增、编辑、删除。
7
- - 默认分类:生图、编程、文案、研究、翻译、效率;支持新增、改名、删除与排序。
7
+ - 默认分类:生图、编程、文案、研究、翻译、效率、思考、学习;支持新增、改名、删除与排序。
8
8
  - 分类图标和颜色复用 Codex 原生 SymbolBrowser 的资源。单条提示词可设置一个 Emoji,否则继承分类图标。
9
9
  - 使用右侧三横手柄拖拽调整组内顺序,也可按调用次数、最近调用排序。
10
10
  - 删除分类会将提示词移至其他,保留正文、Emoji 和统计。
@@ -23,3 +23,5 @@
23
23
  运行入口为 `src/main.mjs` → `quick-prompts-server.mjs`,仅开放提示词、分类、排序、调用记录及健康检查。页面经已认证 Runtime Host binding 访问固定允许的接口。旧 HTTP 服务、网页、Todo / Files 操作及文件清理代码已删除。`prompt-schema.mjs` 只负责向前迁移;`product.mjs` 负责提示词、分类和统计的事务读写。历史 Todo / Files 表保持原样。
24
24
 
25
25
  服务仅监听 loopback,默认端口 47832。沿用 CODEX_NOTES_PORT、CODEX_NOTES_DATA_DIR、CODEX_NOTES_DATABASE。设计依据见 [DESIGN.md](./DESIGN.md)。入口采用 Lucide book-open,拖拽手柄采用 Lucide menu。分类 SVG 来自当前 Codex 原生图标目录,未手绘替代路径。
26
+
27
+ 内置 24 条已分类、带 Emoji 的快捷提示词,来自 MIT 授权的 dsh-prompt 0.1.13。首次启动或升级仅导入一次,后续编辑与删除不会被覆盖。个人提示词不随包发布。来源和完整许可见 [dsh-prompt-LICENSE.txt](./assets/dsh-prompt-LICENSE.txt)。
@@ -0,0 +1,170 @@
1
+ [
2
+ {
3
+ "id": "dsh-fp",
4
+ "title": "第一性原理拆解",
5
+ "body": "请用第一性原理分析以下主题,按编号结构化输出,结论先行:\n1. 这个问题的基本组成单元是什么?\n2. 哪些是真正不可改变的真理(而非假设)?\n3. 如果去掉所有既有假设,问题本身还剩下什么?\n4. 有哪些被大多数人默认为常识、但其实可以颠覆的?\n5. 基于以上推演给出你的原创见解,并说明它与主流看法的差异。\n主题:",
6
+ "categoryId": "thinking",
7
+ "emoji": "🧱"
8
+ },
9
+ {
10
+ "id": "dsh-socratic",
11
+ "title": "苏格拉底式追问",
12
+ "body": "请用苏格拉底式追问帮助我思考,按编号结构化输出:\n1. 先复述我当前的观点或核心假设;\n2. 提出 3 个最有力的质疑;\n3. 找出一个我尚未考虑到的视角;\n4. 如果你是深思熟虑的对话者,你会问我哪几个我没想到的问题?\n5. 追问结束后,把整个过程浓缩成一个更精准的问题。\n主题:",
13
+ "categoryId": "thinking",
14
+ "emoji": "❓"
15
+ },
16
+ {
17
+ "id": "dsh-deep",
18
+ "title": "深度多维分析",
19
+ "body": "请从多个维度深度分析以下主题,按编号结构化输出,结论先行:\n1. 本质是什么?\n2. 有哪些常见的误解?\n3. 支持和反对的证据分别是什么?\n4. 这个问题的边界在哪里(什么情况下它不再成立)?\n5. 如果你必须做出一个判断,会是什么?\n6. 你最强的那个结论,弱点在哪里?\n主题:",
20
+ "categoryId": "thinking",
21
+ "emoji": "🔬"
22
+ },
23
+ {
24
+ "id": "dsh-adversarial",
25
+ "title": "对抗式审查",
26
+ "body": "请对以下方案/结论做对抗式审查(红队角色),先给一段结论摘要,再按编号展开:\n1. 找出其中最强的 5 个假设,逐一论证它们可能如何失效;\n2. 构造最可能推翻它的 3 个反例或失败场景;\n3. 指出其中隐藏的认知偏差与利益相关方视角的盲区;\n4. 给出「如果这个方案失败,最可能的原因是什么」的排序清单;\n5. 最后给出:在何种条件下你才会认为它是错的——以及何种证据能让你改变立场。\n方案 / 结论:",
27
+ "categoryId": "thinking",
28
+ "emoji": "🛡️"
29
+ },
30
+ {
31
+ "id": "dsh-decision",
32
+ "title": "决策博弈分析",
33
+ "body": "你是一位冷静的决策分析师。请分析我的处境,按编号结构化输出(你的选择仅作参考,重点是分析过程):\n1. 列出每个选项最坏的结果是什么;\n2. 每个选项最好的结果是什么;\n3. 最可能的结果是什么;\n4. 如果 10 年后回头看,我可能会后悔什么;\n5. 有没有哪条路是「没有回头路」的;\n6. 给出你本人的选择并说明理由;再指出我可能忽略的第三个选项。\n我的处境(选项与背景):",
34
+ "categoryId": "thinking",
35
+ "emoji": "⚖️"
36
+ },
37
+ {
38
+ "id": "dsh-premortem",
39
+ "title": "事前验尸",
40
+ "body": "假设以下方案/项目已经在一年后彻底失败。请以「事后回溯」的方式写失败分析,按编号结构化输出:\n1. 按可能性从高到低列出 8 个失败原因;\n2. 对前 3 个原因,给出现在就能做的预防措施;\n3. 指出哪些早期信号现在就该监控;\n4. 最后说明:这个方案是否从一开始就有致命缺陷。\n方案描述:",
41
+ "categoryId": "thinking",
42
+ "emoji": "🚧"
43
+ },
44
+ {
45
+ "id": "dsh-feynman",
46
+ "title": "费曼技巧",
47
+ "body": "请用费曼技巧确认我是否真正理解了以下概念,按编号结构化输出:\n1. 用最简单的语言、面向完全不懂的人解释这个概念(禁止术语);\n2. 给出 2 个生动类比和 2 个反例;\n3. 指出我理解中的错误或不准确之处;\n4. 设计 3 个判断题检验我是否真的懂了。\n概念:\n我的理解(可选):",
48
+ "categoryId": "learning",
49
+ "emoji": "💡"
50
+ },
51
+ {
52
+ "id": "dsh-fivewhys",
53
+ "title": "五个为什么",
54
+ "body": "针对以下问题,请连续追问五个「为什么」,每层基于上一层的事实回答,最后区分根本原因与表层触发因素并给出解决方向:\n问题描述:",
55
+ "categoryId": "thinking",
56
+ "emoji": "🔎"
57
+ },
58
+ {
59
+ "id": "dsh-mece",
60
+ "title": "MECE 拆解",
61
+ "body": "请用 MECE 原则拆解以下问题,按编号结构化输出:\n1. 给出 3~6 个相互独立(Mutually Exclusive)的分类维度;\n2. 每个维度下列出所有子项,确保完全穷尽(Collectively Exhaustive);\n3. 指出拆解中可能的遗漏与重叠;\n4. 给出按重要性排序后的关键子问题清单。\n问题:",
62
+ "categoryId": "thinking",
63
+ "emoji": "🧩"
64
+ },
65
+ {
66
+ "id": "dsh-bias",
67
+ "title": "认知偏差检查",
68
+ "body": "请审查以下推理/计划/结论中的认知偏差,按编号结构化输出:\n1. 列出可能影响它的偏差(确认偏误、沉没成本、可得性、锚定、乐观/悲观偏差、群体思维等),并指出具体证据;\n2. 对每条偏差,给出「如何检验是否真的受影响」的方法;\n3. 给出一个反事实检验:如果结论被证明是错的,最可能因为哪条偏差?\n推理 / 计划 / 结论:",
69
+ "categoryId": "thinking",
70
+ "emoji": "🪞"
71
+ },
72
+ {
73
+ "id": "dsh-learnpath",
74
+ "title": "学习路线设计",
75
+ "body": "请为以下领域/技能设计一条从零到可实战的学习路线,按编号结构化输出:\n1. 按阶段拆分(入门→进阶→实战),每个阶段给出学习目标;\n2. 每个阶段推荐 2~3 个核心主题和练习方式;\n3. 指出常见的「伪学习陷阱」(只收藏不练习等);\n4. 给出一个 30 天内可完成的最小闭环,以及检验掌握程度的基准。\n领域 / 技能:\n当前水平(可选):\n目标(可选):",
76
+ "categoryId": "learning",
77
+ "emoji": "🗺️"
78
+ },
79
+ {
80
+ "id": "dsh-concept",
81
+ "title": "概念澄清",
82
+ "body": "请帮我彻底理解以下概念,按编号结构化输出:\n1. 一句话定义 + 一个最小例子;\n2. 它与常见相近概念的本质区别(各给一个对比场景);\n3. 这个概念在什么情况下不适用(边界);\n4. 给出 3 个由浅入深的练习场景让我检验理解。\n概念:\n相近概念(可选,不知道可留空,AI 自行选常见相近概念对比):",
83
+ "categoryId": "learning",
84
+ "emoji": "📖"
85
+ },
86
+ {
87
+ "id": "dsh-quiz",
88
+ "title": "AI 反向考验",
89
+ "body": "请通过出题检验我是否真正理解了以下主题/交付内容:\n1. 设计选择题/判断题/简答题/案例题(由浅入深);\n2. 我作答后指出错误与盲区;\n3. 直到回答正确、知识点无盲区后再确认交付。\n主题 / 交付内容:",
90
+ "categoryId": "learning",
91
+ "emoji": "🎯"
92
+ },
93
+ {
94
+ "id": "dsh-codereview",
95
+ "title": "代码审查",
96
+ "body": "请审查以下代码,先给总体结论,再按严重程度排序输出,每个问题附具体修改建议(含示例代码):\n1. 正确性:潜在 bug、边界条件、并发/异步问题;\n2. 安全:注入、鉴权、敏感信息、依赖风险;\n3. 可维护性:命名、结构、重复、可读性;\n4. 性能:明显的低效点。\n代码过长时可分段审查,但最终汇总为一页结论。\n代码 / 文件 / 技术栈:",
97
+ "categoryId": "code",
98
+ "emoji": "🔍"
99
+ },
100
+ {
101
+ "id": "dsh-testdesign",
102
+ "title": "测试用例设计",
103
+ "body": "请为以下功能设计测试用例,按编号结构化输出:\n1. 正常路径用例(含输入/预期输出);\n2. 边界与极端值用例;\n3. 异常与错误处理用例;\n4. 并发/重复/幂等场景(如适用);\n5. 指出哪些用例最容易漏、以及对应的风险。\n功能描述:",
104
+ "categoryId": "code",
105
+ "emoji": "🧪"
106
+ },
107
+ {
108
+ "id": "dsh-refactor",
109
+ "title": "重构方案",
110
+ "body": "请为以下代码提出重构方案,按编号结构化输出:\n1. 指出当前结构的主要问题(职责、耦合、可测试性);\n2. 给出目标结构(抽象与拆分建议,含示例);\n3. 按「小步重构」顺序列出步骤,每一步可独立提交且行为不变;\n4. 指出哪些重构有风险、需要测试保护。\n代码 / 模块:",
111
+ "categoryId": "code",
112
+ "emoji": "🛠️"
113
+ },
114
+ {
115
+ "id": "dsh-explain",
116
+ "title": "解释代码",
117
+ "body": "请解释以下代码,按编号结构化输出:\n1. 这段代码做什么(一句话);\n2. 逐段讲解执行流程与关键数据结构;\n3. 指出其中「为什么这样写」的设计取舍;\n4. 指出潜在问题与改进点;\n5. 用一句话概括它在我更大的系统里扮演的角色。\n代码:",
118
+ "categoryId": "code",
119
+ "emoji": "💻"
120
+ },
121
+ {
122
+ "id": "dsh-blindspot",
123
+ "title": "盲区检查",
124
+ "body": "在我开始以下任务之前,请帮我做一次盲区检查,按编号结构化输出:\n1. 我可能遗漏的关键变量有哪些?\n2. 潜在的风险与最容易忽略的影响因素?\n3. 哪些点需要提前决策、哪些可以后置?\n4. 给我一个 3 分钟内可完成的快速检查清单。\n我的计划(目标 / 约束):",
125
+ "categoryId": "productivity",
126
+ "emoji": "👁️"
127
+ },
128
+ {
129
+ "id": "dsh-snapshot",
130
+ "title": "零丢失快照",
131
+ "body": "请执行一次「零丢失快照」:\n1. 把我们从会话开始至今的关键信息,按「目的地 / 约束与偏好 / 已确认的决定 / 待决问题 / 雾区(隐约可见但还不清晰)」五类逐条列出——不压缩、不合并,宁可啰嗦不可省略;\n2. 每条标注出处(引用我原话);\n3. 单独列一节「可疑遗漏」:凡是我提过但你觉得与主线无关、太模糊或像执行细节而没纳入的,全部摆出并写明理由;\n4. 列完后停下等我逐条核对,确认后再落盘。",
132
+ "categoryId": "productivity",
133
+ "emoji": "📸"
134
+ },
135
+ {
136
+ "id": "dsh-kickoff",
137
+ "title": "任务启动",
138
+ "body": "在开始以下任务之前,请先确认起点并澄清细节,按编号结构化输出:\n1. 最终目标(要做什么);\n2. 当前进度(目前想到哪一步);\n3. 经验水平(新手 / 有经验 / 专家);\n4. 不熟悉的地方;\n5. 成功的标准;\n6. 通过提问澄清细节(成功标准之外还有哪些约束、有哪些风险现在就该知道)。\n最终目标:\n当前进度(可选):\n经验水平(新手 / 有经验 / 专家):\n不熟悉的地方(可选):",
139
+ "categoryId": "productivity",
140
+ "emoji": "🚀"
141
+ },
142
+ {
143
+ "id": "dsh-prototype",
144
+ "title": "原型验证",
145
+ "body": "请为以下主题/想法做原型验证,每轮输出保持克制(方向概要即可,不要太长):\n1. 给出 2~4 个方向差距很大的候选方案;\n2. 每个方向给出最小的验证方式(怎么快速试错);\n3. 由我选择和反馈,几轮后逐步收敛到满意的方向;\n4. 收敛后给出下一步的执行计划要点。\n主题 / 想法:",
146
+ "categoryId": "productivity",
147
+ "emoji": "🧫"
148
+ },
149
+ {
150
+ "id": "dsh-plan",
151
+ "title": "执行计划",
152
+ "body": "请为以下目标制定执行计划,按编号结构化输出(时间安排是可调整的草案):\n1. 明确步骤(含顺序与依赖);\n2. 时间安排(每步预计时长/里程碑);\n3. 所需资源和工具;\n4. 交付物和验收标准;\n5. 风险和应对方案。\n目标:\n约束(可选):",
153
+ "categoryId": "productivity",
154
+ "emoji": "📋"
155
+ },
156
+ {
157
+ "id": "dsh-deviation",
158
+ "title": "记录偏离",
159
+ "body": "请担任我的过程记录员(多轮持续角色):从现在起,在每次交互后更新一份简洁的偏离记录,按编号结构化:\n1. 遇到了什么新的情况?\n2. 为什么改变了方案?\n3. 做了哪些关键决定?\n4. 对结果有什么影响?\n每次只输出「新增/变更」条目,不要重复已记录内容。",
160
+ "categoryId": "productivity",
161
+ "emoji": "📝"
162
+ },
163
+ {
164
+ "id": "dsh-retro",
165
+ "title": "过程复盘",
166
+ "body": "请帮我复盘刚刚完成的以下任务/项目,按编号结构化输出:\n1. 整体步骤回顾(实际 vs 计划的差异);\n2. 使用的资料和工具;\n3. 遇到的困难;\n4. 解决思路与可复用经验;\n5. 如果重来一次,哪些地方会做得不同?\n任务 / 项目:",
167
+ "categoryId": "productivity",
168
+ "emoji": "🔄"
169
+ }
170
+ ]
@@ -0,0 +1,24 @@
1
+ Default prompt text from dsh-prompt v0.1.13
2
+ https://github.com/FeatherHunter/dsh-prompt
3
+
4
+ MIT License
5
+
6
+ Copyright (c) 2026 王辰浩
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in all
16
+ copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ SOFTWARE.
@@ -0,0 +1,29 @@
1
+ import prompts from "../assets/default-prompts.json" with { type: "json" };
2
+
3
+ // 单次导入后全部归用户管理,删除和编辑不会在下次启动时被还原。
4
+ export function seedDefaultPrompts(db, now) {
5
+ db.exec("BEGIN IMMEDIATE");
6
+ try {
7
+ if (!db.prepare("SELECT 1 FROM metadata WHERE key = 'default_prompts_v1'").get()) {
8
+ const categoryOrder = db.prepare("SELECT COALESCE(MAX(sort_order), -1) + 1 AS n FROM prompt_categories").get().n;
9
+ const categories = [
10
+ ["thinking", "思考", "flask", "purple"],
11
+ ["learning", "学习", "book", "green"],
12
+ ["code", "编程", "terminal", "blue"],
13
+ ["productivity", "效率", "logs", "black"],
14
+ ];
15
+ categories.forEach((row, i) => db.prepare("INSERT OR IGNORE INTO prompt_categories VALUES (?, ?, ?, ?, ?)").run(...row, categoryOrder + i));
16
+ const order = db.prepare("SELECT COALESCE(MAX(sort_order), -1) + 1 AS n FROM prompts").get().n;
17
+ const insert = db.prepare(`INSERT INTO prompts
18
+ (id,title,body,starred,created_at,updated_at,last_used_at,category_id,emoji,sort_order)
19
+ VALUES (?,?,?,0,?,?,?,?,?,?)`);
20
+ prompts.forEach((p, i) => insert.run(p.id, p.title, p.body, now, now, 0, p.categoryId, p.emoji, order + i));
21
+ db.prepare("INSERT INTO metadata VALUES ('default_prompts_v1', 1)").run();
22
+ db.prepare("UPDATE metadata SET value = value + 1 WHERE key = 'revision'").run();
23
+ }
24
+ db.exec("COMMIT");
25
+ } catch (error) {
26
+ db.exec("ROLLBACK");
27
+ throw error;
28
+ }
29
+ }
@@ -1,3 +1,4 @@
1
+ import { seedDefaultPrompts } from "./default-prompts.mjs";
1
2
  import { migratePromptSchema } from "./prompt-schema.mjs";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { chmod, mkdir, open as openFile } from "node:fs/promises";
@@ -120,6 +121,7 @@ export class CodexNotesProduct {
120
121
  );
121
122
  try {
122
123
  migratePromptSchema(this.db);
124
+ seedDefaultPrompts(this.db, this.clock());
123
125
  } catch (error) {
124
126
  this.close();
125
127
  throw error;
@@ -1,6 +1,6 @@
1
1
  const ICONS = {
2
2
  prompt:
3
- '<svg\n class="lucide lucide-book-open"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2.1"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M12 7v14" />\n <path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />\n</svg>',
3
+ '<svg\n class="lucide lucide-book-open"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="-1.5 -1.5 27 27"\n fill="none"\n stroke="currentColor"\n stroke-width="1.5"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path vector-effect="non-scaling-stroke" d="M12 7v14" />\n <path vector-effect="non-scaling-stroke" d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />\n</svg>',
4
4
  chevron:
5
5
  '<!-- @license lucide-static v0.577.0 - ISC -->\n<svg\n class="lucide lucide-chevron-down"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="m6 9 6 6 6-6" />\n</svg>\n',
6
6
  search:
@@ -194,7 +194,7 @@ function control(label, handler, className = "qp-command", iconName = null) {
194
194
  node.setAttribute("aria-label", label);
195
195
  if (iconName) {
196
196
  node.append(icon(iconName));
197
- node.title = label;
197
+ node.setAttribute("data-minecodex-tooltip", label);
198
198
  }
199
199
  node.addEventListener("click", handler);
200
200
  return node;
@@ -395,11 +395,13 @@ function sync() {
395
395
  close();
396
396
  if (!button) {
397
397
  button = add.cloneNode(false);
398
+ button.removeAttribute("title");
399
+ button.removeAttribute("aria-describedby");
398
400
  button.removeAttribute("id");
399
401
  button.removeAttribute("data-composer-navigation-target");
400
402
  button.setAttribute(MARKER, "button");
401
403
  button.setAttribute("aria-label", text("Quick Prompts", "快捷提示词"));
402
- button.title = text("Quick Prompts", "快捷提示词");
404
+ button.setAttribute("data-minecodex-tooltip", text("Quick Prompts", "快捷提示词"));
403
405
  button.setAttribute("aria-haspopup", "dialog");
404
406
  button.setAttribute("aria-expanded", "false");
405
407
  button.setAttribute("data-state", "closed");
@@ -549,7 +551,7 @@ let pagePopup = null;
549
551
  let popupTrigger = null;
550
552
  function pageIcon(label, name, handler) {
551
553
  const button = control(label, handler, "pm-icon", name);
552
- button.title = label;
554
+ button.setAttribute("data-minecodex-tooltip", label);
553
555
  return button;
554
556
  }
555
557
  function closePagePopup(restoreFocus = false) {
@@ -859,10 +861,10 @@ function orderControls(item, ordered, isCategory, row) {
859
861
  () => {},
860
862
  );
861
863
  handle.classList.add("pm-drag");
862
- handle.title = text(
864
+ handle.setAttribute("data-minecodex-tooltip", text(
863
865
  "Drag to reorder · Alt + ↑ / ↓",
864
866
  "拖拽排序 · Alt + ↑ / ↓",
865
- );
867
+ ));
866
868
  row.dataset.orderId = item.id;
867
869
  const group = isCategory ? "categories" : `prompts:${item.categoryId ?? ""}`;
868
870
  row.dataset.orderGroup = group;
@@ -1115,7 +1117,7 @@ function editPrompt(item = null) {
1115
1117
  emojiButton.replaceChildren(
1116
1118
  categoryIcon({ categoryId: category.value || null, emoji: emoji.value }),
1117
1119
  );
1118
- emojiButton.title = text("Choose icon", "选择图标");
1120
+ emojiButton.setAttribute("data-minecodex-tooltip", text("Choose icon", "选择图标"));
1119
1121
  }
1120
1122
  refreshAppearance();
1121
1123
  const body = element("textarea");
@@ -1311,7 +1313,7 @@ function editCategory(category = null) {
1311
1313
  ).documentElement;
1312
1314
  btn.replaceChildren(svg);
1313
1315
  btn.setAttribute("aria-pressed", String(key === selectedIcon));
1314
- btn.title = appearanceLabel(key);
1316
+ btn.setAttribute("data-minecodex-tooltip", appearanceLabel(key));
1315
1317
  btn.setAttribute("aria-label", appearanceLabel(key));
1316
1318
  icons.append(btn);
1317
1319
  }
@@ -1334,7 +1336,7 @@ function editCategory(category = null) {
1334
1336
  swatch.append(element("span", "pm-swatch"));
1335
1337
  btn.replaceChildren(swatch);
1336
1338
  btn.setAttribute("aria-pressed", String(key === selectedColor));
1337
- btn.title = appearanceLabel(key);
1339
+ btn.setAttribute("data-minecodex-tooltip", appearanceLabel(key));
1338
1340
  btn.setAttribute("aria-label", appearanceLabel(key));
1339
1341
  colors.append(btn);
1340
1342
  }
@@ -1455,13 +1457,16 @@ function syncNavigation() {
1455
1457
  }
1456
1458
  if (!navButton?.isConnected) {
1457
1459
  navButton = source.cloneNode(false);
1460
+ navButton.removeAttribute("id");
1461
+ navButton.removeAttribute("title");
1462
+ navButton.removeAttribute("aria-describedby");
1458
1463
  navButton.removeAttribute("data-sidebar-destination");
1459
1464
  navButton.removeAttribute("data-selected");
1460
1465
  navButton.removeAttribute("aria-current");
1461
1466
  navButton.removeAttribute("data-suppress-active-style");
1462
1467
  navButton.setAttribute(MARKER, "nav");
1463
1468
  navButton.setAttribute("aria-label", text("Prompts", "提示词"));
1464
- navButton.title = text("Prompts", "提示词");
1469
+ navButton.setAttribute("data-minecodex-tooltip", text("Prompts", "提示词"));
1465
1470
  const inner = element("span", source.firstElementChild?.className ?? "");
1466
1471
  const glyph = icon("prompt-nav");
1467
1472
  glyph.setAttribute("aria-hidden", "true");
@@ -18,7 +18,7 @@
18
18
  | 字体 | `--font-sans-default`、`--text-xs` | 与原生辅助信息一致 |
19
19
  | 圆角与阴影 | `--radius-xl`、`--shadow-lg` | 明细弹层 |
20
20
 
21
- 状态行和输入框共用内容宽度,单行高度为 24px,顶部留 2px 内边距;每个状态按钮高 22px,内边距为上下 3px、左右 7px。宽度不足时自然换行,不覆盖输入框或悬浮控件。统计图标采用 Lucide Gauge,用量图标采用 Phosphor Database 单路径版本,两者以 14px 绘制;上下文使用与 Codex 原生相同参数的 12px 百分环。弹层由按钮锚定,键盘可聚焦,Escape 和点击外部关闭。遵守系统减少动态效果设置,不添加持续动画。
21
+ 状态行和输入框共用内容宽度,单行高度为 24px,顶部留 2px 内边距;每个状态按钮高 22px,内边距为上下 3px、左右 6px。按钮之间 gap 为 4px,两组可见内容间距为 16px(6 + 4 + 6);按钮内图标与文字 gap 为 6px。状态行展开且可见时,将包含它的原生 `.bottom-0.pb-4` 容器底部 padding 从 16px 覆盖为 4px;收起、隐藏或卸载后自动恢复原生留白,展开的状态行使用 -4px 顶部外边距,将输入框下方原生 8px gap 的实际距离收为4px,收起时恢复。宽度不足时自然换行,不覆盖输入框或悬浮控件。统计图标采用 Lucide Gauge,用量图标采用 Phosphor Database 单路径版本,两者以 14px 绘制;上下文使用与 Codex 原生相同参数的 12px 百分环。弹层由按钮锚定,键盘可聚焦,Escape 和点击外部关闭。遵守系统减少动态效果设置,不添加持续动画。
22
22
 
23
23
  状态行以当前输入框所在的会话为准,不依赖侧栏中可能因项目折叠而卸载的 Thread 行。新聊天没有会话 ID 时隐藏旧数据;首条消息建立会话后默认展开详细状态行;若用户曾收起,则沿用其选择。尚未取得的指标用 `—` 占位,不等待日志或 Token 用量;切换会话时立即清空旧数值,详情展开状态保持不变。
24
24
 
@@ -29,14 +29,17 @@ document.getElementById(STYLE_ID)?.remove();
29
29
  const style = document.createElement("style");
30
30
  style.id = STYLE_ID;
31
31
  style.textContent = `
32
- [${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 12px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--font-sans-default,inherit); font-size:var(--text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--color-text-tertiary); }
32
+ [${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 4px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--font-sans-default,inherit); font-size:var(--text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--color-text-tertiary); }
33
33
  [${MARKER}][hidden] { display:none; }
34
34
  [data-minecodex-session-reveal] { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows 200ms ease,opacity 160ms ease; }
35
35
  [data-minecodex-session-reveal][data-expanded] { grid-template-rows:1fr; opacity:1; }
36
36
  @starting-style { [data-minecodex-session-reveal][data-expanded] { grid-template-rows:0fr; opacity:0; } }
37
+ [data-minecodex-session-reveal][data-expanded]:not([hidden]) { margin-top:-4px; }
37
38
  [data-minecodex-session-reveal] > div { min-height:0; overflow:hidden; }
38
39
  [data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
39
40
  [data-minecodex-session-reveal][hidden] { display:none; }
41
+ /* 仅展开状态行时缩减原生底部留白;收起或卸载后自动恢复。 */
42
+ .bottom-0.pb-4:has([data-minecodex-session-reveal][data-expanded]:not([hidden])) { padding-bottom:4px; }
40
43
  [data-minecodex-context-trigger] { display:inline-flex; align-items:center; justify-content:center; flex:none; width:24px; height:var(--height-token-button-composer,28px); padding:0; border:0; border-radius:var(--radius-full); background:transparent; color:var(--color-text-tertiary); cursor:pointer; }
41
44
  [data-minecodex-context-trigger][hidden] { display:none; }
42
45
  [data-context-progress] { transition:stroke-dashoffset 120ms ease-out; }
@@ -44,9 +47,9 @@ style.textContent = `
44
47
  [data-minecodex-context-trigger]:focus-visible { outline:2px solid var(--color-ring); }
45
48
  [data-minecodex-context-trigger] svg { width:12px; height:12px; }
46
49
  [data-minecodex-native-context] { display:none !important; }
47
- [data-minecodex-context-tooltip] { position:fixed; z-index:2147483000; pointer-events:none; width:max-content; 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; text-align:center; white-space:pre-line; }
50
+ [data-minecodex-context-tooltip] { position:fixed; z-index:2147483000; pointer-events:none; width:max-content; 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); text-align:center; white-space:pre-line; }
48
51
  @media (prefers-reduced-motion:reduce) { [data-minecodex-session-reveal], [data-context-progress] { transition:none; } }
49
- [${MARKER}] button { box-sizing:border-box; display:inline-flex; align-items:center; gap:5px; height:22px; color:inherit; font:inherit; border:0; border-radius:var(--radius-xl,12px); background:transparent; padding:3px 7px; cursor:pointer; white-space:nowrap; }
52
+ [${MARKER}] button { box-sizing:border-box; display:inline-flex; align-items:center; gap:6px; height:22px; color:inherit; font:inherit; border:0; border-radius:var(--radius-xl,12px); background:transparent; padding:3px 6px; cursor:pointer; white-space:nowrap; }
50
53
  [${MARKER}] svg, [data-minecodex-session-status-panel] h3 svg { width:14px; height:14px; flex:none; stroke-width:1.75; }
51
54
  [${MARKER}] [data-kind="context"] svg, [data-minecodex-session-status-panel] h3 .context-ring { width:12px; height:12px; stroke-width:2; }
52
55
  [${MARKER}] button:hover, [${MARKER}] button[aria-expanded="true"] { background:var(--color-token-list-hover-background); }
@@ -285,7 +288,7 @@ function appendMixBar(dl, title, parts) {
285
288
  const segment = document.createElement("span");
286
289
  segment.dataset.tone = tone;
287
290
  segment.style.width = `${amount / total * 100}%`;
288
- segment.title = `${name} ${percent(amount)}%`;
291
+ segment.setAttribute("data-minecodex-tooltip", `${name} ${percent(amount)}%`);
289
292
  bar.append(segment);
290
293
  }
291
294
  dl.append(bar);
@@ -348,7 +351,7 @@ function showPanel(kind) {
348
351
  const segment = document.createElement("span");
349
352
  segment.dataset.segment = kind;
350
353
  segment.style.width = `${amount / snapshot.contextWindow * 100}%`;
351
- segment.title = copy[kind === "tools" ? "toolHistory" : kind];
354
+ segment.setAttribute("data-minecodex-tooltip", copy[kind === "tools" ? "toolHistory" : kind]);
352
355
  bar.append(segment);
353
356
  }
354
357
  panel.insertBefore(head, dl);
@@ -1,28 +1,36 @@
1
1
  # 任务切换
2
2
 
3
- 仅在线程侧栏收起时显示入口;展开时移除入口并关闭浮层与遮罩。入口随 Codex 标题栏布局切换:
3
+ 仅在线程侧栏收起时显示入口;展开时移除入口并关闭浮层。入口随 Codex 标题栏布局切换:
4
4
  - 普通/分栏:在主 Chat 标题前显示原生 list-bullet 图标;整段标题可点击。
5
5
  - 全宽标签页:在 Chat 标签页前注入独立的任务图标按钮。
6
- - 移除扩展铃铛,不修改原生 Sidebar 图标及通知蓝点。
7
- - 点击打开或关闭;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。移开鼠标不关闭。
6
+ - 移除扩展铃铛,注入入口存在时,用原生完整 SVG 显示无蓝点的 Sidebar 图标;保留 React 原节点,卸载或展开侧栏时移除替代图标并恢复原节点。
7
+ - 悬停入口150ms展开;鼠标离开入口与浮层250ms后关闭,跨越8px间隙进入浮层会取消关闭。点击入口确保打开;点击外部、Escape 关闭。Enter、空格、ArrowDown 可打开。
8
8
 
9
9
  样式使用原生字体、text-secondary、radius-lg、primary-ghost-hover 与 ring token。入口仅 hover 显示背景,不保持选中底色。标题区域 no-drag,其他标题栏区域保留窗口拖拽。
10
10
 
11
- 浮层宽320px(窄窗口缩小),最大高660px(约6条,受视口高度限制);沿用 dropdown 背景、border、radius-2xl、shadow-xl-spread。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
11
+ 浮层宽320px(窄窗口缩小),最大高440px(约5条,受视口高度限制);沿用 dropdown 背景与 border,圆角16px、shadow-md,外层上下 padding 6px、左右0;滚动容器左右 padding 6px,使滚动条靠近浮层边缘。位置对齐标题触发区域左侧、底缘下方8px,并受窗口边界约束。
12
12
 
13
- 列表集合为工作中、最近1小时打开过、未读的会话,按 Thread ID 去重。统一按消息时间倒序,当前会话仅标记选中,不置顶。原生流式摘要变化时更新观测消息时间,不因菜单重绘更新时间。最近访问记录限定当前窗口运行期间。每条显示:
14
- 1. 标题、本地/远程设备图标、相对时间;工作中用原生 loading 替代时间。
15
- 2. 最多两行省略的 AI 回答摘要。
16
- 3. 所属 Project 图标和名称(若有),不合并分组。
13
+ 列表集合为等待回复/批准、工作中、最近1小时打开过、未读的会话,按 Thread ID 去重。按需要回复、等待批准、未读、执行中、空闲排序,同级按消息时间倒序;排除当前会话。原生流式摘要变化时更新观测消息时间,不因菜单重绘更新时间。最近访问记录限定当前窗口运行期间。每条显示:
14
+ 1. 单行标题(13px/18px),远程标识紧随标题且不显示主机名;右侧状态优先级为待确认标签、原生 loading、相对时间。背景仅 hover 时显示。
15
+ 2. 单行省略的 AI 回答摘要(12px/16px)。
16
+ 3. 所属 Project 原生图标(12px)与名称(若有),11px/16px 弱化文字,不合并分组。条目 padding 10px 12px,行间 gap 4px,圆角10px。
17
17
 
18
18
  时间使用原生 hover card 的 recencyAt,缺失时 updatedAt;每30秒刷新。工作状态读取原生 statusState.type=loading。缓存会话数据与原生导航回调,复用导航和已读更新,不缓存 DOM 行,不发送消息。
19
19
 
20
20
  打开时临时启用 Activity 以取得完整列表,关闭或卸载时恢复原先模式。只读取原生数据,预览不标记已读。界面及无障碍文案适配中英文。
21
21
 
22
- 入口只显示任务图标,不显示数量。浮层打开时使用原生 --modal-backdrop-background 遮罩覆盖底层界面;遮罩位于浮层下方,点击关闭并返回入口焦点,卸载时移除。
22
+ 入口只显示任务图标,不显示数量。浮层标题为 Recent activity/最近活动,反映工作中、未读与最近访问的集合;不将最近访问误称为待处理。浮层无页面遮罩,点击外部关闭,Escape 关闭并返回入口焦点。
23
23
 
24
- 标题固定、条目容器独立滚动;浮层底部保留3个 spacing 单位(12px)固定内边距,滚动时仍保留。
24
+ 标题固定、条目容器独立滚动;上下保留6px内边距;条目内容左右保留6px,滚动条不受外层横向内边距挤压。
25
25
 
26
26
  点击后先同步显示浮层与可用数据,首帧绘制后再切换原生 Activity 补全条目;关闭或卸载会取消尚未开始的切换。
27
27
 
28
28
  消息刷新时保留列表滚动位置与条目键盘焦点。当前身份消失时清除选中状态,最近访问缓存按一小时过期。
29
+
30
+ 待确认状态读取原生行的 `statusPill.label.props.id=codex.localTaskRow.needsInput` 或 `chips[].id=awaiting-approval`,不根据摘要推断。Needs input/需要回复沿用原生 chart-blue 15% 背景,Awaiting approval/等待批准沿用 chart-green 20% 背景与圆角胶囊;适配紧凑行使用12px/16px文字。点击条目仍仅导航,不执行批准或原生 Snooze 动作。
31
+
32
+ 滚动条与原生 overflow 容器一致:thin,静止使用 `--color-border`,hover/active 使用 `--color-border-strong`,轨道透明;不使用全局较深的 `--scrollbar-color`。
33
+
34
+ 入口提醒点:排除当前会话后,有需要回复或批准时为红色 `#FF3B30`,否则存在未读为原生蓝色 `--blue-400`;只有执行中或空闲则无点。点直径6px。无其他命中会话时不打开浮层,包括仅当前会话命中的情况。原生侧栏图标与 MineCodex 提醒随注入/卸载同步恢复。
35
+
36
+ 浮层标题颜色与原生 Sidebar 类别标题一致,使用 `--color-text-tertiary`。
@@ -1,3 +1,5 @@
1
+ // 原生完整侧栏图标,取自当前 Codex 16px/20px 资源。
2
+ const sidebarGlyphs = {"16": "<svg aria-hidden=\"true\" class=\"_Icon_uttcz_1\" focusable=\"false\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\"><path d=\"M4.66699 4.80859C4.95683 4.80873 5.19238 5.04412 5.19238 5.33398V10.667C5.19238 10.9569 4.95683 11.1923 4.66699 11.1924C4.37704 11.1924 4.1416 10.9569 4.1416 10.667V5.33398C4.1416 5.04403 4.37704 4.80859 4.66699 4.80859Z\" fill=\"currentColor\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11.5 2.30762C13.1707 2.30762 14.5254 3.66235 14.5254 5.33301V10.666C14.5254 12.3367 13.1707 13.6914 11.5 13.6914H4.5C2.82934 13.6914 1.47461 12.3367 1.47461 10.666V5.33301C1.47461 3.66235 2.82934 2.30762 4.5 2.30762H11.5ZM4.5 3.3584C3.40924 3.3584 2.52539 4.24225 2.52539 5.33301V10.666C2.52539 11.7568 3.40924 12.6416 4.5 12.6416H11.5C12.5908 12.6416 13.4746 11.7568 13.4746 10.666V5.33301C13.4746 4.24225 12.5908 3.3584 11.5 3.3584H4.5Z\" fill=\"currentColor\"></path></svg>", "20": "<svg aria-hidden=\"true\" class=\"_Icon_uttcz_1\" focusable=\"false\" height=\"20\" viewBox=\"0 0 20 20\" width=\"20\"><path d=\"M5.83301 6.00098C6.20028 6.00098 6.49805 6.29875 6.49805 6.66602V13.333C6.49787 13.7001 6.20017 13.998 5.83301 13.998C5.46596 13.9979 5.16814 13.7 5.16797 13.333V6.66602C5.16797 6.29883 5.46585 6.00111 5.83301 6.00098Z\" fill=\"currentColor\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.5 2.87695C16.5241 2.87695 18.165 4.51787 18.165 6.54199V13.459C18.1649 15.483 16.524 17.124 14.5 17.124H5.5C3.47598 17.124 1.83514 15.483 1.83496 13.459V6.54199C1.83496 4.51787 3.47588 2.87695 5.5 2.87695H14.5ZM5.5 4.20703C4.21042 4.20703 3.16504 5.25241 3.16504 6.54199V13.459C3.16521 14.7484 4.21052 15.7939 5.5 15.7939H14.5C15.7895 15.7939 16.8348 14.7484 16.835 13.459V6.54199C16.835 5.25241 15.7896 4.20703 14.5 4.20703H5.5Z\" fill=\"currentColor\"></path></svg>"};
1
3
  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
4
  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
5
  const text = (en, cn) => (document.documentElement.lang || navigator.language || "en").startsWith("zh") ? cn : en;
@@ -9,27 +11,32 @@ style.textContent = `
9
11
  [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
12
  [data-minecodex-task-title]:hover { background:var(--color-background-primary-ghost-hover); }
11
13
  [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; }
14
+ [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; position:relative; }
15
+ [data-minecodex-task-badge][data-notification]::after { content:""; position:absolute; width:6px; height:6px; right:-2px; top:-1px; border-radius:50%; background:var(--blue-400,#3A83F7); box-shadow:0 0 0 1.5px var(--color-token-dropdown-background); }
16
+ [data-minecodex-task-badge][data-notification=red]::after { background:#FF3B30; }
17
+ [data-minecodex-sidebar-original] { display:none !important; }
13
18
  [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); }
19
+ [${marker}=panel] { position:fixed; z-index:2147483000; box-sizing:border-box; width:min(320px,calc(100vw - 16px)); max-height:min(440px,calc(100vh - 64px)); display:flex; flex-direction:column; overflow:hidden; padding:6px 0; border:1px solid var(--color-border); border-radius:16px; background:var(--color-token-dropdown-background); color:var(--color-text-primary); box-shadow:var(--shadow-md,0 4px 16px rgba(0,0,0,.12)); font-family:var(--font-sans-default); font-size:var(--text-sm); }
17
20
  [${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); }
21
+ [${marker}=panel] .mcx-thread-list { min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:0 6px; scrollbar-width:thin; scrollbar-color:var(--color-border) transparent; }
22
+ [${marker}=panel] .mcx-thread-list:hover, [${marker}=panel] .mcx-thread-list:active { scrollbar-color:var(--color-border-strong) transparent; }
23
+ [${marker}=panel] h2 { flex-shrink:0; margin:8px 16px; font-size:var(--text-sm); font-weight:500; color:var(--color-text-tertiary); }
24
+ [${marker}=panel] [data-unread-row] { display:flex; flex-direction:column; gap:4px; width:100%; padding:10px 12px; box-sizing:border-box; border:0; border-radius:10px; background:transparent; text-align:start; color:var(--color-text-primary); font:inherit; cursor:pointer; }
25
+ [${marker}=panel] [data-unread-row][aria-current=page] .mcx-thread-title::before { content:""; display:inline-block; width:5px; height:5px; margin-inline-end:6px; border-radius:50%; background:currentColor; vertical-align:middle; }
22
26
  [${marker}=panel] [data-unread-row]:hover { background:var(--color-background-primary-ghost-hover); }
23
27
  [${marker}=panel] [data-unread-row]:focus-visible { outline:2px solid var(--color-ring); outline-offset:-2px; }
24
28
  [${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; }
29
+ [${marker}=panel] .mcx-thread-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; font-size:13px; line-height:18px; font-weight:500; }
26
30
  [${marker}=panel] .mcx-device { display:flex; flex-shrink:0; color:var(--color-text-tertiary); }
27
31
  [${marker}=panel] .mcx-loading { margin-inline-start:auto; flex-shrink:0; display:flex; align-items:center; color:var(--color-text-tertiary); }
28
32
  [${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; }
33
+ [${marker}=panel] .mcx-summary { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; line-height:16px; color:var(--color-text-tertiary); }
34
+ [${marker}=panel] .mcx-project { display:flex; align-items:center; gap:6px; padding:0; min-width:0; font-size:11px; line-height:16px; color:var(--color-text-tertiary); font-weight:400; }
31
35
  [${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; }
36
+ [${marker}=panel] .mcx-project svg, [${marker}=panel] .mcx-project-icon { width:12px; height:12px; flex:none; }
37
+ [${marker}=panel] .mcx-project-icon > * { width:12px; height:12px; }
38
+ [${marker}=panel] .mcx-attention { margin-inline-start:auto; flex:none; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border-radius:999px; padding:2px 10px 2px 8px; font-size:12px; line-height:16px; color:var(--color-chart-blue); background:color-mix(in srgb,var(--color-chart-blue) 15%,transparent); }
39
+ [${marker}=panel] .mcx-attention[data-attention="approval"] { color:var(--color-chart-green); background:color-mix(in srgb,var(--color-chart-green) 20%,transparent); }
33
40
  [${marker}=panel] .mcx-empty { margin:20px 12px; color:var(--color-text-tertiary); }
34
41
  `;
35
42
  document.head.append(style);
@@ -39,14 +46,23 @@ panel.id = "minecodex-unread-panel";
39
46
  panel.setAttribute("role", "dialog");
40
47
  panel.hidden = true;
41
48
  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
49
  let trigger = null, badge = null, binding = null, standalone = false;
49
50
  let activityFrame = null, activityTimer = null;
51
+ let hoverOpenTimer = null, hoverCloseTimer = null;
52
+ function cancelHoverTimers() {
53
+ clearTimeout(hoverOpenTimer); clearTimeout(hoverCloseTimer);
54
+ }
55
+ function scheduleHoverOpen() {
56
+ cancelHoverTimers();
57
+ if (panel.hidden) hoverOpenTimer = setTimeout(open, 150);
58
+ }
59
+ function scheduleHoverClose() {
60
+ cancelHoverTimers();
61
+ // 留出跨越入口与浮层间隙的时间。
62
+ hoverCloseTimer = setTimeout(() => close(), 250);
63
+ }
64
+ panel.addEventListener("pointerenter", cancelHoverTimers, { signal });
65
+ panel.addEventListener("pointerleave", scheduleHoverClose, { signal });
50
66
  const recentRows = new Map();
51
67
  const openedAt = new Map();
52
68
  const messageUpdates = new Map();
@@ -80,10 +96,14 @@ function threadRecord(row) {
80
96
  }
81
97
  return { ...threadDetails(row), key:row.dataset.appActionSidebarThreadId, unread:isUnread(row), activate, spinner:row.querySelector?.('[class~="motion-safe:animate-spin"]')?.outerHTML ?? "" };
82
98
  }
83
- function selectThreads(records, recent, now = Date.now()) {
99
+ function notificationColor(records) {
100
+ return records.some(item => item.attention) ? "red" : records.some(item => item.unread) ? "blue" : null;
101
+ }
102
+ function selectThreads(records, recent, now = Date.now(), currentKey = null) {
103
+ const rank = item => item.attention === "input" ? 0 : item.attention === "approval" ? 1 : item.unread ? 2 : item.working ? 3 : 4;
84
104
  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));
105
+ .filter(item => item.key !== currentKey && (item.attention || item.working || item.unread || now - (recent.get(item.key) ?? -Infinity) < 3600000))
106
+ .sort((a, b) => rank(a) - rank(b) || (b.timestamp ?? 0) - (a.timestamp ?? 0));
87
107
  }
88
108
  function captureCurrentRow() {
89
109
  const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
@@ -104,6 +124,16 @@ function projectForRow(row) {
104
124
  }
105
125
  return null;
106
126
  }
127
+ function attentionState(row) {
128
+ let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
129
+ for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
130
+ const props = fiber.memoizedProps;
131
+ // 复用原生状态契约,不从摘要文案推断待确认状态。
132
+ if (props?.chips?.some(chip => chip.id === "awaiting-approval")) return "approval";
133
+ if (props?.statusPill?.label?.props?.id === "codex.localTaskRow.needsInput") return "input";
134
+ }
135
+ return null;
136
+ }
107
137
  function threadDetails(row) {
108
138
  let fiber = row[Object.keys(row).find(key => key.startsWith("__reactFiber"))];
109
139
  let summary, hostConfig;
@@ -129,6 +159,7 @@ function threadDetails(row) {
129
159
  hostId, hostName:hostConfig?.display_name || hostId,
130
160
  project:projectForRow(row),
131
161
  working:isWorking(row),
162
+ attention:attentionState(row),
132
163
  };
133
164
  }
134
165
  function relativeTime(timestamp, now = Date.now()) {
@@ -146,10 +177,10 @@ function cacheProjectIcons() {
146
177
  }
147
178
  }
148
179
  function close(restoreFocus = false) {
180
+ cancelHoverTimers();
149
181
  cancelAnimationFrame(activityFrame); clearTimeout(activityTimer);
150
182
  activityFrame = null; activityTimer = null;
151
183
  panel.hidden = true;
152
- scrim.hidden = true;
153
184
  if (trigger?.getAttribute("aria-expanded") !== "false") trigger?.setAttribute("aria-expanded", "false");
154
185
  if (restoreActivity) {
155
186
  restoreActivity = false;
@@ -165,24 +196,33 @@ function position() {
165
196
  panel.style.left = `${Math.max(8, Math.min(rect.left, innerWidth - width - 8))}px`;
166
197
  panel.style.top = `${rect.bottom + 8}px`;
167
198
  panel.style.maxWidth = `${width}px`;
168
- panel.style.maxHeight = `${Math.max(0, Math.min(660, innerHeight - rect.bottom - 24))}px`;
199
+ panel.style.maxHeight = `${Math.max(0, Math.min(440, innerHeight - rect.bottom - 24))}px`;
169
200
  }
170
- function renderRows() {
201
+ function activityRecords() {
171
202
  captureCurrentRow();
172
203
  const records = [...document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-thread-row]')].map(threadRecord);
173
204
  const mounted = new Set(records.map(item => item.key));
174
205
  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)]);
206
+ for (const [key, item] of recentRows) if (!mounted.has(key)) records.push({ ...item, working:false, unread:false, attention:null });
207
+ return selectThreads(records, openedAt, Date.now(), selectedKey);
208
+ }
209
+ function syncNotification(details) {
210
+ const color = notificationColor(details);
211
+ if (color) { if (badge?.dataset.notification !== color) badge?.setAttribute("data-notification", color); }
212
+ else badge?.removeAttribute("data-notification");
213
+ }
214
+ function renderRows(details = activityRecords()) {
215
+ syncNotification(details);
216
+ if (!details.length) { close(); return; }
217
+ const next = JSON.stringify([text("Recent activity", "最近活动"), details, Math.floor(Date.now() / 60000)]);
178
218
  if (signature === next) return;
179
219
  signature = next;
180
220
  const scrollTop = panel.querySelector(".mcx-thread-list")?.scrollTop ?? 0;
181
221
  const focusedKey = panel.contains(document.activeElement) ? document.activeElement?.dataset.threadKey : null;
182
222
  panel.replaceChildren();
183
- panel.setAttribute("aria-label", text("Priority", "优先事项"));
223
+ panel.setAttribute("aria-label", text("Recent activity", "最近活动"));
184
224
  const heading = document.createElement("h2");
185
- heading.textContent = text("Priority", "优先事项");
225
+ heading.textContent = text("Recent activity", "最近活动");
186
226
  panel.append(heading);
187
227
  const list = document.createElement("div");
188
228
  list.className = "mcx-thread-list";
@@ -202,11 +242,18 @@ function renderRows() {
202
242
  device.className = "mcx-device";
203
243
  const remote = item.hostId !== "local";
204
244
  device.innerHTML = deviceGlyphs[remote ? "remote" : "local"];
205
- device.title = remote ? item.hostName : text("This computer", "本机");
245
+ device.setAttribute("data-minecodex-tooltip", text("Remote", "远程"));
206
246
  device.setAttribute("aria-label", device.title);
207
- header.append(title, device);
247
+ header.append(title);
248
+ if (remote) header.append(device);
208
249
  const elapsed = relativeTime(item.timestamp);
209
- if (item.working) {
250
+ if (item.attention) {
251
+ const label = document.createElement("span");
252
+ label.className = "mcx-attention";
253
+ label.dataset.attention = item.attention;
254
+ label.textContent = item.attention === "approval" ? text("Awaiting approval", "等待批准") : text("Needs input", "需要回复");
255
+ header.append(label);
256
+ } else if (item.working) {
210
257
  const loading = document.createElement("span");
211
258
  loading.className = "mcx-loading";
212
259
  loading.setAttribute("role", "status");
@@ -218,7 +265,7 @@ function renderRows() {
218
265
  const time = document.createElement("time");
219
266
  time.dateTime = new Date(item.timestamp).toISOString();
220
267
  time.textContent = elapsed;
221
- time.title = new Date(item.timestamp).toLocaleString();
268
+ time.setAttribute("data-minecodex-tooltip", new Date(item.timestamp).toLocaleString());
222
269
  header.append(time);
223
270
  }
224
271
  card.append(header);
@@ -231,9 +278,12 @@ function renderRows() {
231
278
  if (item.project) {
232
279
  const project = document.createElement("span");
233
280
  project.className = "mcx-project";
281
+ const icon = document.createElement("span");
282
+ icon.className = "mcx-project-icon";
234
283
  const glyph = projectIcons.get(item.project.id)?.cloneNode(true);
235
- if (glyph) project.append(glyph);
236
- else project.innerHTML = folderGlyph;
284
+ if (glyph) icon.append(glyph);
285
+ else icon.innerHTML = folderGlyph;
286
+ project.append(icon);
237
287
  const name = document.createElement("span");
238
288
  name.textContent = item.project.name;
239
289
  project.append(name);
@@ -266,10 +316,12 @@ function open() {
266
316
  captureCurrentRow();
267
317
  const native = activityButton();
268
318
  if (!native) return;
319
+ const details = activityRecords();
320
+ syncNotification(details);
321
+ if (!details.length) return;
269
322
  panel.hidden = false;
270
- scrim.hidden = false;
271
323
  trigger.setAttribute("aria-expanded", "true");
272
- renderRows(); position();
324
+ renderRows(details); position();
273
325
  // 先呈现浮层,下一帧之后再更新原生 Activity,避免同步 React 切换阻塞点击反馈。
274
326
  if (native.getAttribute("aria-pressed") !== "true") {
275
327
  activityFrame = requestAnimationFrame(() => {
@@ -288,7 +340,36 @@ function open() {
288
340
  function visible(element) {
289
341
  return element && !element.closest('[aria-hidden="true"], [inert]') && element.checkVisibility();
290
342
  }
343
+ const nativeSidebarIcons = new Map();
344
+ function restoreSidebarIcons() {
345
+ for (const [original, replacement] of nativeSidebarIcons) {
346
+ original.removeAttribute("data-minecodex-sidebar-original"); replacement.remove();
347
+ }
348
+ nativeSidebarIcons.clear();
349
+ }
350
+ function syncSidebarIcons(toggle) {
351
+ for (const [original, replacement] of nativeSidebarIcons) {
352
+ if (!original.isConnected || !toggle.contains(original)) {
353
+ original.removeAttribute("data-minecodex-sidebar-original"); replacement.remove(); nativeSidebarIcons.delete(original);
354
+ }
355
+ }
356
+ for (const original of toggle.querySelectorAll('svg:not([data-minecodex-sidebar-replacement])')) {
357
+ if (nativeSidebarIcons.has(original)) continue;
358
+ const glyph = sidebarGlyphs[original.getAttribute("width")];
359
+ if (!glyph) continue;
360
+ const container = document.createElement("span");
361
+ container.innerHTML = glyph;
362
+ const replacement = container.firstElementChild;
363
+ replacement.setAttribute("class", original.getAttribute("class") || "");
364
+ replacement.setAttribute("data-minecodex-sidebar-replacement", "");
365
+ replacement.setAttribute("data-no-autosize", "true");
366
+ // 不移除 React 所有的 SVG,只切换显示并插入独立资源。
367
+ original.setAttribute("data-minecodex-sidebar-original", "");
368
+ original.after(replacement); nativeSidebarIcons.set(original, replacement);
369
+ }
370
+ }
291
371
  function detachTrigger() {
372
+ restoreSidebarIcons();
292
373
  binding?.abort(); binding = null;
293
374
  if (standalone) trigger?.remove();
294
375
  else if (trigger) {
@@ -332,7 +413,9 @@ function sync() {
332
413
  badge.setAttribute("data-minecodex-task-badge", "");
333
414
  badge.innerHTML = taskGlyph;
334
415
  trigger.prepend(badge);
335
- trigger.addEventListener("click", event => { event.stopPropagation(); panel.hidden ? open() : close(); }, {signal:binding.signal});
416
+ trigger.addEventListener("click", event => { event.stopPropagation(); if (panel.hidden) open(); }, {signal:binding.signal});
417
+ trigger.addEventListener("pointerenter", scheduleHoverOpen, { signal:binding.signal });
418
+ trigger.addEventListener("pointerleave", scheduleHoverClose, { signal:binding.signal });
336
419
  trigger.addEventListener("keydown", event => {
337
420
  if (["Enter", " ", "ArrowDown"].includes(event.key)) {
338
421
  event.preventDefault(); event.stopPropagation();
@@ -340,9 +423,12 @@ function sync() {
340
423
  }
341
424
  }, {signal:binding.signal});
342
425
  }
343
- const label = text("Switch tasks", "切换任务");
426
+ syncSidebarIcons(sidebarToggle);
427
+ const details = activityRecords();
428
+ syncNotification(details);
429
+ const label = text("Recent activity", "最近活动");
344
430
  if (trigger.getAttribute("aria-label") !== label) trigger.setAttribute("aria-label", label);
345
- if (!panel.hidden) { renderRows(); position(); }
431
+ if (!panel.hidden) { renderRows(details); position(); }
346
432
  }
347
433
  const observer = new MutationObserver(() => {
348
434
  if (queued) return;
@@ -350,11 +436,11 @@ const observer = new MutationObserver(() => {
350
436
  queueMicrotask(() => { queued = false; sync(); });
351
437
  });
352
438
  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 });
439
+ document.addEventListener("pointerdown", event => { if (!trigger?.contains(event.target) && !panel.contains(event.target)) close(); }, { signal });
354
440
  document.addEventListener("keydown", event => { if (event.key === "Escape" && !panel.hidden) { event.preventDefault(); close(true); } }, { signal });
355
441
  window.addEventListener("resize", position, { signal });
356
- const refreshTimer = setInterval(() => { if (!panel.hidden) renderRows(); }, 30000);
442
+ const refreshTimer = setInterval(sync, 30000);
357
443
  sync();
358
444
  return () => {
359
- disposed = true; clearInterval(refreshTimer); observer.disconnect(); close(); detachTrigger(); panel.remove(); scrim.remove(); style.remove();
445
+ disposed = true; clearInterval(refreshTimer); observer.disconnect(); close(); detachTrigger(); panel.remove(); style.remove();
360
446
  };
@@ -8,9 +8,9 @@
8
8
 
9
9
  ## 数据契约
10
10
 
11
- 已在 2026-09-27 的隔离 Codex 中确认:原生 QueryClient 缓存
12
- `["rate-limit-status", userId, accountId]`,由 Codex 自行通过流/轮询更新。
13
- 插件只订阅该缓存,按当前原生 auth 的 userId/accountId 精确读取,不自行请求
11
+ 已在 2026-09-29 的运行中 Codex 确认:原生 QueryClient 的成功额度数据缓存键为
12
+ `["rate-limit-status"]`,由 Codex 自行更新。
13
+ 插件只订阅该缓存,校验响应的 user_id/account_id 与当前原生 auth 一致,不自行请求
14
14
  接口,不持久化账号或额度数据。切换账号、退出登录、额度数据缺失时隐藏旧值。
15
15
 
16
16
  窗口按 `limit_window_seconds` 识别:604800 为周、18000 为五小时;不依赖
@@ -42,7 +42,7 @@ function nativeContext(rail) {
42
42
  function currentWindows(context) {
43
43
  const { auth, client } = context;
44
44
  if (auth?.isLoading || !auth?.userId || !auth?.accountId || !["chatgpt", "chatgptAuthTokens"].includes(auth.authMethod)) return null;
45
- const data = client?.getQueryData(["rate-limit-status", auth.userId, auth.accountId]);
45
+ const data = client?.getQueryData(["rate-limit-status"]);
46
46
  // 账号与缓存必须同时一致;旧账号的最后一次响应不能留在导航栏。
47
47
  if (!data || data.user_id !== auth.userId || data.account_id !== auth.accountId) return null;
48
48
  return remainingWindows(data);
@@ -53,7 +53,7 @@ style.id = STYLE_ID;
53
53
  style.textContent = `
54
54
  [${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; -webkit-app-region:no-drag; }
55
55
  [${MARKER}] .mcx-usage-ring { display:block; width:24px; height:24px; overflow:visible; }
56
- [${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:inherit; font-family:var(--font-sans-default); font-size:calc(var(--text-xs) * 2 / 3); line-height:1; font-weight:500; letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
56
+ [${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:inherit; font-family:var(--font-sans-default); font-size:calc(var(--text-xs) * 2 / 3); line-height:1; font-weight:var(--font-weight-medium,500); letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
57
57
  [${MARKER}] [data-window="weekly"] { color:inherit; }
58
58
  [${MARKER}] [data-window="fiveHour"] { color:var(--color-text-secondary); }
59
59
  #${TIP_ID} { position:fixed; z-index:2147483000; width:max-content; max-width:calc(100vw - 16px); pointer-events:none; user-select: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); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -2,10 +2,11 @@
2
2
  // 该字符串经 CDP 注入后独立执行,因此 install 函数体必须自包含;
3
3
  // CodexRuntime 与 CDP 客户端在 codex-runtime.mjs / codex-cdp.mjs。
4
4
 
5
+ import { installFeatureTooltips } from "./feature-tooltips.mjs";
5
6
  import { randomBytes } from "node:crypto";
6
7
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES } from "./codex-design-contract.mjs";
7
8
 
8
- export const RUNTIME_VERSION = 101;
9
+ export const RUNTIME_VERSION = 102;
9
10
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
10
11
  export const CSP_BOOTSTRAP_VERSION = 1;
11
12
  export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
@@ -119,14 +120,14 @@ export function createInjectionSource(features, {
119
120
  bindingToken = "test-binding-token",
120
121
  runtimeSessionId = randomBytes(16).toString("hex"),
121
122
  } = {}) {
122
- function install(config) {
123
+ function install(config, installTooltips) {
123
124
  if (window.top !== window) return;
124
125
  if (!document.documentElement) {
125
126
  // The document bootstrap runs before the root element exists. Defer the
126
127
  // whole install until the DOM is ready instead of crashing on a null
127
128
  // documentElement (which previously left the runtime permanently absent
128
129
  // after a renderer reload).
129
- document.addEventListener("DOMContentLoaded", () => install(config), { once: true });
130
+ document.addEventListener("DOMContentLoaded", () => install(config, installTooltips), { once: true });
130
131
  return;
131
132
  }
132
133
  const currentRuntime = window.__codexPersonalRuntime;
@@ -138,6 +139,7 @@ export function createInjectionSource(features, {
138
139
  currentRuntime?.dispose?.();
139
140
 
140
141
  const lifetime = new AbortController();
142
+ installTooltips(lifetime.signal);
141
143
  const rightPanelRouteKinds = new Set(config.rightPanelRouteKinds);
142
144
 
143
145
  const entryMarker = "data-codex-personal-feature";
@@ -1497,7 +1499,7 @@ export function createInjectionSource(features, {
1497
1499
  button.replaceChildren(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
1498
1500
  const itemLabel = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? label;
1499
1501
  button.setAttribute("aria-label", selector.strings[starred ? "unstar" : "star"].replace("{model}", itemLabel));
1500
- button.setAttribute("title", selector.strings[starred ? "unstar" : "star"].replace("{model}", itemLabel));
1502
+ button.setAttribute("data-minecodex-tooltip", selector.strings[starred ? "unstar" : "star"].replace("{model}", itemLabel));
1501
1503
  }
1502
1504
  renderEnhancedModelMenu(menu, selector);
1503
1505
  }
@@ -1559,7 +1561,7 @@ export function createInjectionSource(features, {
1559
1561
  star.type = "button";
1560
1562
  star.setAttribute("data-codex-model-slider-star", starred ? "unstar" : "star");
1561
1563
  star.setAttribute("aria-label", selector.strings[starred ? "unstar" : "star"].replace("{model}", display));
1562
- star.setAttribute("title", selector.strings[starred ? "unstar" : "star"].replace("{model}", display));
1564
+ star.setAttribute("data-minecodex-tooltip", selector.strings[starred ? "unstar" : "star"].replace("{model}", display));
1563
1565
  star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
1564
1566
  row.append(star);
1565
1567
  item.replaceChildren(row);
@@ -3285,7 +3287,7 @@ export function createInjectionSource(features, {
3285
3287
  const pressed = String(open);
3286
3288
  const state = open ? "open" : "closed";
3287
3289
  if (entry.getAttribute("aria-label") !== label) entry.setAttribute("aria-label", label);
3288
- if (entry.getAttribute("title") !== label) entry.setAttribute("title", label);
3290
+ if (entry.getAttribute("data-minecodex-tooltip") !== label) entry.setAttribute("data-minecodex-tooltip", label);
3289
3291
  if (entry.getAttribute("aria-pressed") !== pressed) entry.setAttribute("aria-pressed", pressed);
3290
3292
  if (entry.getAttribute("aria-expanded") !== pressed) entry.setAttribute("aria-expanded", pressed);
3291
3293
  if (entry.getAttribute("data-state") !== state) entry.setAttribute("data-state", state);
@@ -4262,8 +4264,10 @@ export function createInjectionSource(features, {
4262
4264
  if (!button) return null;
4263
4265
  const label = summaryButtonLabel(feature);
4264
4266
  button.setAttribute(entryMarker, feature.id);
4267
+ button.removeAttribute("title");
4268
+ button.removeAttribute("aria-describedby");
4265
4269
  button.setAttribute("aria-label", label);
4266
- button.setAttribute("title", label);
4270
+ button.setAttribute("data-minecodex-tooltip", label);
4267
4271
  button.setAttribute("aria-pressed", "false");
4268
4272
  button.setAttribute("aria-expanded", "false");
4269
4273
  button.setAttribute("data-state", "closed");
@@ -4808,7 +4812,7 @@ export function createInjectionSource(features, {
4808
4812
  themeTokens: CODEX_THEME_TOKENS,
4809
4813
  themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
4810
4814
  features: runtimeFeatures,
4811
- })})`;
4815
+ })}, ${installFeatureTooltips.toString()})`;
4812
4816
  }
4813
4817
 
4814
4818
  export function createRuntimeReadyContract({
@@ -0,0 +1,65 @@
1
+ // 自包含函数随运行时注入;只处理 MineCodex 标记,不接管原生 tooltip。
2
+ export function installFeatureTooltips(signal) {
3
+ const attribute = "data-minecodex-tooltip";
4
+ const tip = document.createElement("div");
5
+ tip.id = "minecodex-feature-tooltip";
6
+ tip.setAttribute("role", "tooltip");
7
+ tip.hidden = true;
8
+ tip.style.cssText = `position:fixed;z-index:100;pointer-events:none;box-sizing:border-box;
9
+ zoom:var(--codex-window-zoom,1);max-width:min(320px,calc(100vw - 16px));
10
+ padding:5px 12px;border:1px solid var(--color-border-tooltip);border-radius:var(--radius-2xl);
11
+ background:var(--color-background-tooltip);color:var(--color-text-tooltip);box-shadow:var(--shadow-tooltip);
12
+ font-family:var(--font-sans-default);font-size:var(--text-sm);line-height:18px;
13
+ font-weight:var(--tooltip-compact-font-weight);letter-spacing:var(--tracking-tooltip);
14
+ text-align:center;overflow-wrap:anywhere;user-select:none;`;
15
+ let anchor = null;
16
+ let timer;
17
+ let frame;
18
+ function hide() {
19
+ clearTimeout(timer);
20
+ cancelAnimationFrame(frame);
21
+ if (anchor) {
22
+ const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(id => id && id !== tip.id);
23
+ if (ids.length) anchor.setAttribute("aria-describedby", ids.join(" "));
24
+ else anchor.removeAttribute("aria-describedby");
25
+ }
26
+ anchor = null;
27
+ tip.hidden = true;
28
+ }
29
+ function position() {
30
+ if (!anchor?.isConnected || !anchor.getClientRects().length || anchor.getAttribute("aria-expanded") === "true") return hide();
31
+ const zoom = Number.parseFloat(getComputedStyle(tip).zoom) || 1;
32
+ const r = anchor.getBoundingClientRect();
33
+ const w = tip.offsetWidth, h = tip.offsetHeight;
34
+ const top = r.top / zoom - h - 8;
35
+ tip.style.left = `${Math.max(8, Math.min((r.left + r.width / 2) / zoom - w / 2, innerWidth / zoom - w - 8))}px`;
36
+ tip.style.top = `${top >= 8 ? top : r.bottom / zoom + 8}px`;
37
+ frame = requestAnimationFrame(position);
38
+ }
39
+ function show(target, immediate = false) {
40
+ if (!target || target === anchor) return;
41
+ hide();
42
+ if (target.getAttribute("aria-expanded") === "true" || !target.getAttribute(attribute)) return;
43
+ anchor = target;
44
+ timer = setTimeout(() => {
45
+ if (!anchor?.isConnected) return hide();
46
+ if (!tip.isConnected) document.body.append(tip);
47
+ tip.textContent = anchor.getAttribute(attribute);
48
+ tip.hidden = false;
49
+ const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean);
50
+ anchor.setAttribute("aria-describedby", [...new Set([...ids, tip.id])].join(" "));
51
+ position();
52
+ }, immediate ? 0 : 450);
53
+ }
54
+ const targetOf = event => event.target instanceof Element ? event.target.closest(`[${attribute}]`) : null;
55
+ document.addEventListener("pointerover", event => show(targetOf(event)), { signal });
56
+ document.addEventListener("pointerout", event => {
57
+ if (anchor && !anchor.contains(event.relatedTarget)) hide();
58
+ }, { signal });
59
+ document.addEventListener("focusin", event => show(targetOf(event), true), { signal });
60
+ document.addEventListener("focusout", hide, { signal });
61
+ document.addEventListener("pointerdown", hide, { signal, capture: true });
62
+ document.addEventListener("keydown", event => { if (event.key === "Escape") hide(); }, { signal });
63
+ window.addEventListener("blur", hide, { signal });
64
+ signal.addEventListener("abort", () => { hide(); tip.remove(); }, { once: true });
65
+ }