minecodex 1.2.2 → 2.0.1

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 +11 -3
  19. package/features/session-status/README.md +8 -4
  20. package/features/session-status/src/session-status.mjs +272 -25
  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 +36 -0
  26. package/features/unread-inbox/codex-feature.json +20 -0
  27. package/features/unread-inbox/src/unread-inbox.mjs +446 -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
@@ -18,7 +18,6 @@ function selectedCopy() {
18
18
  const language = document.documentElement.lang || navigator.languages?.[0] || navigator.language || "en";
19
19
  return String(language).toLowerCase().startsWith("zh") ? copyByLocale["zh-CN"] : copyByLocale.en;
20
20
  }
21
- let copy = selectedCopy();
22
21
 
23
22
  // 图标来自 Lucide Static 与 Phosphor Icons;许可见 vendor/。
24
23
  const icons = {
@@ -30,12 +29,27 @@ document.getElementById(STYLE_ID)?.remove();
30
29
  const style = document.createElement("style");
31
30
  style.id = STYLE_ID;
32
31
  style.textContent = `
33
- [${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 12px; margin:-2px auto -6px; 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); }
34
33
  [${MARKER}][hidden] { display:none; }
35
- [${MARKER}]:not([hidden]) { animation:mcx-session-status-enter 160ms ease-out both; }
36
- @keyframes mcx-session-status-enter { from { opacity:0; transform:translateY(4px); } to { opacity:1; transform:translateY(0); } }
37
- @media (prefers-reduced-motion: reduce) { [${MARKER}]:not([hidden]) { animation:none; } }
38
- [${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; }
34
+ [data-minecodex-session-reveal] { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows 200ms ease,opacity 160ms ease; }
35
+ [data-minecodex-session-reveal][data-expanded] { grid-template-rows:1fr; opacity:1; }
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; }
38
+ [data-minecodex-session-reveal] > div { min-height:0; overflow:hidden; }
39
+ [data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
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; }
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; }
44
+ [data-minecodex-context-trigger][hidden] { display:none; }
45
+ [data-context-progress] { transition:stroke-dashoffset 120ms ease-out; }
46
+ [data-minecodex-context-trigger]:hover { background:var(--color-background-primary-ghost-hover); }
47
+ [data-minecodex-context-trigger]:focus-visible { outline:2px solid var(--color-ring); }
48
+ [data-minecodex-context-trigger] svg { width:12px; height:12px; }
49
+ [data-minecodex-native-context] { display:none !important; }
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; text-align:center; white-space:pre-line; }
51
+ @media (prefers-reduced-motion:reduce) { [data-minecodex-session-reveal], [data-context-progress] { transition:none; } }
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; }
39
53
  [${MARKER}] svg, [data-minecodex-session-status-panel] h3 svg { width:14px; height:14px; flex:none; stroke-width:1.75; }
40
54
  [${MARKER}] [data-kind="context"] svg, [data-minecodex-session-status-panel] h3 .context-ring { width:12px; height:12px; stroke-width:2; }
41
55
  [${MARKER}] button:hover, [${MARKER}] button[aria-expanded="true"] { background:var(--color-token-list-hover-background); }
@@ -64,6 +78,33 @@ style.textContent = `
64
78
  `;
65
79
  document.head.append(style);
66
80
 
81
+ const instances = new Map();
82
+ const expansionKey = "minecodex:session-status:expanded";
83
+ let globalExpanded = localStorage.getItem(expansionKey) !== "false";
84
+ let instanceSequence = 0;
85
+ function setGlobalExpanded(value) {
86
+ globalExpanded = value;
87
+ localStorage.setItem(expansionKey, String(value));
88
+ for (const instance of instances.values()) instance.setExpanded(value);
89
+ }
90
+ window.addEventListener("storage", (event) => {
91
+ if (event.key !== expansionKey) return;
92
+ globalExpanded = event.newValue !== "false";
93
+ for (const instance of instances.values()) instance.setExpanded(globalExpanded);
94
+ }, { signal });
95
+ // Codex 会把离屏输入框搬到 Quick Chat 浮层;节点仍连接不代表当前页面是聊天。
96
+ function isActiveComposer(composer) {
97
+ const card = composer.closest('[data-codex-composer-root]');
98
+ return Boolean(composer.isConnected
99
+ && card?.getAttribute('data-composer-placement') === 'thread'
100
+ && !composer.closest('[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
101
+ && composer.checkVisibility({ visibilityProperty: true }));
102
+ }
103
+ function createInstance(composerElement) {
104
+ let copy = selectedCopy();
105
+ const controller = new AbortController();
106
+ const signal = controller.signal;
107
+ const instanceId = ++instanceSequence;
67
108
  const root = document.createElement("div");
68
109
  root.setAttribute(MARKER, "");
69
110
  root.hidden = true;
@@ -90,6 +131,71 @@ function createContextRing() {
90
131
  }
91
132
  return svg;
92
133
  }
134
+ const reveal = document.createElement("div");
135
+ reveal.dataset.minecodexSessionReveal = "";
136
+ const clip = document.createElement("div");
137
+ clip.append(root);
138
+ reveal.append(clip);
139
+ root.id = `minecodex-session-details-${instanceId}`;
140
+ const trigger = document.createElement("button");
141
+ trigger.type = "button";
142
+ trigger.dataset.minecodexContextTrigger = "";
143
+ trigger.setAttribute("aria-controls", root.id);
144
+ trigger.append(createContextRing());
145
+ const tooltip = document.createElement("div");
146
+ tooltip.id = `minecodex-context-tooltip-${instanceId}`;
147
+ tooltip.dataset.minecodexContextTooltip = "";
148
+ tooltip.setAttribute("role", "tooltip");
149
+ tooltip.hidden = true;
150
+ document.body.append(tooltip);
151
+ let expanded = globalExpanded;
152
+ let collapseTimer = null;
153
+ let collapsing = false;
154
+ const nativeIndicators = new Set();
155
+ function setExpanded(value) {
156
+ clearTimeout(collapseTimer);
157
+ collapsing = expanded && !value && !matchMedia("(prefers-reduced-motion: reduce)").matches;
158
+ expanded = value;
159
+ if (collapsing) collapseTimer = setTimeout(() => {
160
+ collapsing = false;
161
+ syncVisibility(findComposer());
162
+ }, 200);
163
+ reveal.toggleAttribute("data-expanded", expanded);
164
+ clip.inert = !expanded;
165
+ trigger.setAttribute("aria-expanded", String(expanded));
166
+ tooltip.hidden = true;
167
+ if (expanded) reveal.hidden = false;
168
+ syncVisibility(findComposer());
169
+ if (!expanded) closePanel();
170
+ }
171
+ trigger.addEventListener("click", () => setGlobalExpanded(!expanded), { signal });
172
+ function hideTooltip() { tooltip.hidden = true; trigger.removeAttribute("aria-describedby"); }
173
+ function showContextTooltip() {
174
+ const value = snapshot;
175
+ const percent = value?.contextPercent;
176
+ const zh = copy === copyByLocale["zh-CN"];
177
+ const status = percent == null ? "—" : percent >= 50
178
+ ? (zh ? `已占用 ${percent}%` : `${percent}% full`)
179
+ : (zh ? `已使用 ${percent}%(剩余 ${100 - percent}%)` : `${percent}% used (${100 - percent}% left)`);
180
+ const tokens = value?.contextWindow == null ? "" : `${zh ? "已使用 " : ""}${Math.round(value.contextUsedTokens / 1000)}k / ${Math.round(value.contextWindow / 1000)}k ${zh ? "Token" : "tokens used"}`;
181
+ tooltip.replaceChildren();
182
+ for (const [text, muted] of [[zh ? "上下文窗口:" : "Context window:", true], [status, percent >= 50], [tokens, false]]) {
183
+ if (!text) continue;
184
+ const line = document.createElement("div");
185
+ line.textContent = text;
186
+ if (muted) line.style.opacity = "0.6";
187
+ tooltip.append(line);
188
+ }
189
+ tooltip.hidden = false;
190
+ trigger.setAttribute("aria-describedby", tooltip.id);
191
+ const rect = trigger.getBoundingClientRect();
192
+ tooltip.style.left = `${Math.max(8, Math.min(innerWidth - tooltip.offsetWidth - 8, rect.left + (rect.width - tooltip.offsetWidth) / 2))}px`;
193
+ tooltip.style.top = `${rect.top - tooltip.offsetHeight - 4}px`;
194
+ }
195
+ trigger.addEventListener("pointerenter", showContextTooltip, { signal });
196
+ trigger.addEventListener("focus", showContextTooltip, { signal });
197
+ trigger.addEventListener("pointerleave", hideTooltip, { signal });
198
+ trigger.addEventListener("blur", hideTooltip, { signal });
93
199
  const buttons = ["statistics", "usage", "context"].map((kind) => {
94
200
  const button = document.createElement("button");
95
201
  button.type = "button";
@@ -104,6 +210,12 @@ const buttons = ["statistics", "usage", "context"].map((kind) => {
104
210
  root.append(button);
105
211
  return button;
106
212
  });
213
+ const collapse = document.createElement("button");
214
+ collapse.type = "button";
215
+ // Lucide chevron-up,与状态栏其余线性图标保持相同笔画。
216
+ collapse.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>';
217
+ collapse.addEventListener("click", () => { setGlobalExpanded(false); trigger.focus(); hideTooltip(); }, { signal });
218
+ root.append(collapse);
107
219
  let snapshot = null;
108
220
  let panel = null;
109
221
  let openKind = null;
@@ -114,6 +226,11 @@ let refreshSequence = 0;
114
226
  let disposed = false;
115
227
  let lastRevision = 0;
116
228
 
229
+ function cachePercent(value) {
230
+ const percent = value?.inputTokens > 0 && value.cachedInputTokens != null
231
+ ? value.cachedInputTokens / value.inputTokens * 100 : value?.cacheHitPercent;
232
+ return percent == null ? "—" : `${percent > 99 ? percent.toFixed(1) : Math.round(percent)}%`;
233
+ }
117
234
  function number(value) { return value == null ? "—" : new Intl.NumberFormat().format(value); }
118
235
  function compact(value) {
119
236
  if (value == null) return "—";
@@ -216,7 +333,7 @@ function showPanel(kind) {
216
333
  row(dl, copy.input, tokenValue(otherInput)).dataset.tone = "blue";
217
334
  row(dl, copy.cached, tokenValue(snapshot.cachedInputTokens)).dataset.tone = "green";
218
335
  divider(dl);
219
- row(dl, copy.cacheDetail, snapshot.cacheHitPercent == null ? "—" : `${snapshot.cacheHitPercent}%`);
336
+ row(dl, copy.cacheDetail, cachePercent(snapshot));
220
337
  } else {
221
338
  if (snapshot.contextPercent != null) {
222
339
  const head = document.createElement("div");
@@ -266,20 +383,91 @@ function showPanel(kind) {
266
383
  }
267
384
 
268
385
  function findComposer() {
269
- return document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
270
- ?? document.querySelector('[contenteditable="true"][role="textbox"]');
386
+ return isActiveComposer(composerElement) ? composerElement : null;
271
387
  }
272
388
  function currentThreadId(composer) {
273
389
  if (!composer) return null;
274
- const card = composer?.closest('[data-codex-composer-root]');
390
+ const card = composer.closest('[data-codex-composer-root]');
275
391
  const portal = card?.querySelector('[data-above-composer-portal]');
276
- let value;
277
- if (portal) value = portal.getAttribute('data-above-composer-conversation-id');
278
- else if (card?.getAttribute('data-composer-placement') === 'home') return null;
279
- else value = document.querySelector('[data-app-action-sidebar-thread-selected="true"]')?.getAttribute('data-app-action-sidebar-thread-id')
280
- ?? window.location.pathname + window.location.search + window.location.hash;
392
+ // 只认 Codex 输入框绑定的会话;侧栏和 URL 在切换模式时可能仍指向旧会话。
393
+ if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
394
+ const value = portal?.getAttribute('data-above-composer-conversation-id');
281
395
  return value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
282
396
  }
397
+ // 与原生 Context 指示器订阅同一会话状态;不以日志百分比覆盖它。
398
+ const emptyNativeContext = () => ({ contextUsedTokens: null, contextWindow: null, contextPercent: null });
399
+ let nativeContext = emptyNativeContext();
400
+ let nativeContextFamily = null;
401
+ let nativeContextSubscription = null;
402
+ let nativeContextThread = null;
403
+ function findNativeScope(element) {
404
+ let fiber;
405
+ for (let node = element; node && !fiber; node = node.parentElement) {
406
+ fiber = node[Object.keys(node).find((key) => key.startsWith("__reactFiber"))];
407
+ }
408
+ for (let depth = 0; fiber && depth < 200; depth++, fiber = fiber.return) {
409
+ const chain = fiber.memoizedProps?.value;
410
+ if (!(chain instanceof Map)) continue;
411
+ for (const node of chain.values()) {
412
+ if (node?.token?.__scopeBrand === "AppScope" && typeof node.store?.sub === "function") {
413
+ return { node, chain };
414
+ }
415
+ }
416
+ }
417
+ return null;
418
+ }
419
+ function contextFromNativeUsage(usage) {
420
+ const windowSize = usage?.modelContextWindow;
421
+ const total = usage?.last?.totalTokens;
422
+ if (!Number.isFinite(windowSize) || windowSize <= 0 || !Number.isFinite(total) || total < 0) {
423
+ return emptyNativeContext();
424
+ }
425
+ const used = Math.min(total, windowSize);
426
+ return { contextUsedTokens: used, contextWindow: windowSize, contextPercent: Math.round(used / windowSize * 100) };
427
+ }
428
+ function syncNativeContext(composer) {
429
+ if (nativeContextThread === activeThreadId && nativeContextSubscription) return;
430
+ nativeContextSubscription?.();
431
+ nativeContextSubscription = null;
432
+ nativeContextThread = null;
433
+ nativeContext = emptyNativeContext();
434
+ if (!activeThreadId || !nativeContextFamily) return;
435
+ const scope = findNativeScope(composer);
436
+ if (!scope) return;
437
+ try {
438
+ const threadId = activeThreadId;
439
+ const descriptor = nativeContextFamily.resolve(scope.node, scope.chain, threadId);
440
+ const atom = descriptor.resolve(scope.node, scope.chain);
441
+ const update = () => {
442
+ if (disposed || activeThreadId !== threadId) return;
443
+ nativeContext = contextFromNativeUsage(scope.node.store.get(atom));
444
+ render();
445
+ };
446
+ nativeContextThread = threadId;
447
+ nativeContextSubscription = scope.node.store.sub(atom, update);
448
+ update();
449
+ } catch {
450
+ nativeContextThread = null;
451
+ }
452
+ }
453
+ async function loadNativeContextFamily() {
454
+ try {
455
+ const url = [...document.querySelectorAll("link[rel=modulepreload]")].map((link) => link.href)
456
+ .find((name) => /\/app-shared-[^/]+\.js(?:$|\?)/.test(name));
457
+ if (!url) return;
458
+ const source = await (await fetch(url, { signal })).text();
459
+ // 从当前构建解析导出,避免依赖每次更新都会变化的压缩符号或资源哈希。
460
+ const local = source.match(/([\w$]+)=[\w$]+\(\(\{latestTokenUsageInfo:/)?.[1];
461
+ if (!local) return;
462
+ const exported = [...source.matchAll(/([\w$]+) as ([\w$]+)/g)]
463
+ .find((match) => match[1] === local)?.[2];
464
+ if (!exported) return;
465
+ const module = await import(url);
466
+ if (signal.aborted || disposed) return;
467
+ nativeContextFamily = module[exported];
468
+ syncNativeContext(findComposer());
469
+ } catch { /* 原生接口不可用时保留占位,不显示过期的日志百分比。 */ }
470
+ }
283
471
  function requestRefresh() {
284
472
  if (!activeThreadId || signal.aborted) return;
285
473
  if (typeof window[config.bindingName] === "function") {
@@ -307,7 +495,10 @@ function scheduleRefresh() {
307
495
  function syncVisibility(composer) {
308
496
  const threadId = currentThreadId(composer);
309
497
  root.hidden = !threadId || snapshot?.threadId !== threadId;
310
- if (root.hidden) closePanel();
498
+ // 收起时退出布局,避免空 grid 仍触发原生 flex gap。
499
+ reveal.hidden = root.hidden || (!expanded && !collapsing);
500
+ trigger.hidden = root.hidden || expanded;
501
+ if (root.hidden) { closePanel(); hideTooltip(); }
311
502
  }
312
503
  function ensurePosition() {
313
504
  if (disposed || signal.aborted) return;
@@ -315,6 +506,7 @@ function ensurePosition() {
315
506
  const threadId = currentThreadId(composer);
316
507
  if (threadId !== activeThreadId) {
317
508
  activeThreadId = threadId;
509
+ nativeContext = emptyNativeContext();
318
510
  // 会话建立后先展示占位,不让日志落盘速度决定状态栏是否出现。
319
511
  snapshot = threadId ? { threadId } : null;
320
512
  closePanel();
@@ -322,24 +514,47 @@ function ensurePosition() {
322
514
  requestRefresh();
323
515
  scheduleRefresh();
324
516
  }
325
- if (!composer) { root.remove(); closePanel(); return; }
517
+ syncNativeContext(composer);
518
+ if (!composer) { reveal.remove(); trigger.remove(); closePanel(); return; }
326
519
  const form = composer.closest("form");
327
520
  const card = composer.closest("[data-codex-composer-root]") ?? form ?? composer.parentElement?.parentElement;
328
521
  if (!card?.parentElement) return;
329
- if (root.previousElementSibling !== card || root.parentElement !== card.parentElement) {
330
- card.after(root);
522
+ const model = card.querySelector('[data-composer-navigation-target="reasoning"]');
523
+ // 沿模型按钮的包装层找到横向控制组,不依赖本地化文案。
524
+ let anchor = model;
525
+ while (anchor?.parentElement && !anchor.parentElement.classList.contains("items-center")) anchor = anchor.parentElement;
526
+ if (anchor && trigger.nextElementSibling !== anchor) anchor.before(trigger);
527
+ for (const indicator of nativeIndicators) {
528
+ if (!threadId || !indicator.isConnected) {
529
+ indicator.removeAttribute("data-minecodex-native-context");
530
+ nativeIndicators.delete(indicator);
531
+ }
532
+ }
533
+ for (const indicator of threadId ? card.querySelectorAll('[role="img"][aria-label]') : []) {
534
+ if (!indicator.querySelector('svg circle')) continue;
535
+ indicator.setAttribute("data-minecodex-native-context", "");
536
+ nativeIndicators.add(indicator);
537
+ }
538
+ if (reveal.previousElementSibling !== card || reveal.parentElement !== card.parentElement) {
539
+ card.after(reveal);
331
540
  closePanel();
332
541
  }
333
542
  syncVisibility(composer);
334
543
  }
335
544
  function render() {
545
+ if (snapshot) snapshot = { ...snapshot, ...nativeContext };
336
546
  syncVisibility(findComposer());
337
547
  if (root.hidden) return;
548
+ trigger.querySelector("[data-context-progress]").setAttribute("stroke-dashoffset", String(100 - (snapshot.contextPercent ?? 0)));
549
+ trigger.setAttribute("aria-label", `${copy.context}: ${snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`}`);
550
+ collapse.setAttribute("aria-label", copy === copyByLocale["zh-CN"] ? "收起会话状态" : "Collapse session status");
551
+ if (!tooltip.hidden) showContextTooltip();
338
552
  buttons[0].querySelector(".status-label").textContent = `${number(snapshot.turns)} ${copy.turns} ${number(snapshot.steps)} ${copy.steps} · ${snapshot.observedTps == null ? "—" : number(snapshot.observedTps)} tok/s`;
339
- buttons[1].querySelector(".status-label").textContent = `${compact(snapshot.totalTokens)} · ${copy.cache} ${snapshot.cacheHitPercent == null ? "—" : `${snapshot.cacheHitPercent}%`}`;
553
+ buttons[1].querySelector(".status-label").textContent = `${compact(snapshot.totalTokens)} · ${copy.cache} ${cachePercent(snapshot)}`;
340
554
  buttons[2].querySelector(".status-label").textContent = snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`;
341
555
  buttons[2].querySelector("[data-context-progress]").setAttribute("stroke-dashoffset", String(100 - (snapshot.contextPercent ?? 0)));
342
556
  buttons[2].setAttribute("aria-label", `${copy.context}: ${snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`}`);
557
+ for (const ring of [trigger, buttons[2]]) ring.querySelector("[data-context-progress]").setAttribute("opacity", snapshot.contextPercent > 0 ? "1" : "0");
343
558
  if (openKind) showPanel(openKind);
344
559
  }
345
560
  for (const button of buttons) button.addEventListener("click", () => {
@@ -360,7 +575,7 @@ const observer = new MutationObserver(() => {
360
575
  });
361
576
  observer.observe(document.documentElement, {
362
577
  childList: true, subtree: true, attributes: true,
363
- attributeFilter: ["data-above-composer-conversation-id", "data-composer-placement", "data-app-action-sidebar-thread-selected"],
578
+ attributeFilter: ["data-above-composer-conversation-id", "data-composer-placement"],
364
579
  });
365
580
  const localeObserver = new MutationObserver(() => {
366
581
  const nextCopy = selectedCopy();
@@ -369,8 +584,9 @@ const localeObserver = new MutationObserver(() => {
369
584
  render();
370
585
  });
371
586
  localeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
587
+ setExpanded(globalExpanded);
372
588
  ensurePosition();
373
- const previous = window.__minecodexSessionStatus;
589
+ void loadNativeContextFamily();
374
590
  const receiver = {
375
591
  update(value, revision) {
376
592
  ensurePosition();
@@ -385,14 +601,45 @@ const receiver = {
385
601
  scheduleRefresh();
386
602
  },
387
603
  };
388
- window.__minecodexSessionStatus = receiver;
389
- return () => {
604
+ return { update: receiver.update, setExpanded, dispose() {
605
+ controller.abort();
390
606
  disposed = true;
607
+ nativeContextSubscription?.();
391
608
  clearTimeout(refreshTimer);
609
+ clearTimeout(collapseTimer);
392
610
  observer.disconnect();
393
611
  localeObserver.disconnect();
394
612
  closePanel();
395
- root.remove();
613
+ reveal.remove();
614
+ trigger.remove();
615
+ tooltip.remove();
616
+ for (const indicator of nativeIndicators) indicator.removeAttribute("data-minecodex-native-context");
617
+ } };
618
+ }
619
+ function syncInstances() {
620
+ for (const [composer, instance] of instances) {
621
+ if (!isActiveComposer(composer)) { instance.dispose(); instances.delete(composer); }
622
+ }
623
+ for (const composer of document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')) {
624
+ if (isActiveComposer(composer) && !instances.has(composer)) instances.set(composer, createInstance(composer));
625
+ }
626
+ }
627
+ const previous = window.__minecodexSessionStatus;
628
+ const receiver = { update(value, revision) {
629
+ syncInstances();
630
+ for (const instance of instances.values()) instance.update(value, revision);
631
+ } };
632
+ window.__minecodexSessionStatus = receiver;
633
+ const instanceObserver = new MutationObserver(syncInstances);
634
+ instanceObserver.observe(document.documentElement, {
635
+ childList:true, subtree:true, attributes:true,
636
+ attributeFilter:["class", "style", "hidden", "inert", "aria-hidden", "data-composer-placement"],
637
+ });
638
+ syncInstances();
639
+ return () => {
640
+ instanceObserver.disconnect();
641
+ for (const instance of instances.values()) instance.dispose();
642
+ instances.clear();
396
643
  style.remove();
397
644
  if (window.__minecodexSessionStatus === receiver) window.__minecodexSessionStatus = previous;
398
645
  };
@@ -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.