minecodex 1.2.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -14
  4. package/features/images/src/http-server.mjs +15 -5
  5. package/features/images/src/image-library.mjs +37 -9
  6. package/features/images/src/library-prompt-page.mjs +239 -0
  7. package/features/images/src/prompt-index.mjs +27 -24
  8. package/features/images/src/prompt-source.mjs +100 -0
  9. package/features/images/web/app.js +180 -67
  10. package/features/images/web/i18n.mjs +5 -0
  11. package/features/images/web/index.html +29 -24
  12. package/features/images/web/styles.css +169 -139
  13. package/features/model-slider/README.md +13 -1
  14. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  15. package/features/notes/DESIGN.md +66 -0
  16. package/features/notes/README.md +18 -99
  17. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  18. package/features/notes/assets/icons/book-open.svg +16 -0
  19. package/features/notes/assets/native-emojis.json +1 -0
  20. package/features/notes/assets/native-prompt-icons.json +28 -0
  21. package/features/notes/codex-feature.json +12 -70
  22. package/features/notes/src/main.mjs +2 -2
  23. package/features/notes/src/product.mjs +287 -613
  24. package/features/notes/src/prompt-schema.mjs +117 -0
  25. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  26. package/features/notes/src/quick-prompts-server.mjs +167 -0
  27. package/features/session-status/DESIGN.md +10 -2
  28. package/features/session-status/README.md +13 -3
  29. package/features/session-status/src/session-status.mjs +329 -24
  30. package/features/sidebar-focus/DESIGN.md +16 -0
  31. package/features/sidebar-focus/codex-feature.json +20 -0
  32. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  33. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  34. package/features/unread-inbox/DESIGN.md +28 -0
  35. package/features/unread-inbox/codex-feature.json +20 -0
  36. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  37. package/features/usage-meter/DESIGN.md +49 -0
  38. package/features/usage-meter/README.md +18 -0
  39. package/features/usage-meter/codex-feature.json +13 -0
  40. package/features/usage-meter/src/usage-meter.mjs +200 -0
  41. package/package.json +21 -6
  42. package/packages/cli/src/config.mjs +4 -1
  43. package/packages/cli/src/control-server.mjs +2 -2
  44. package/packages/runtime-host/src/cloud-images.mjs +123 -0
  45. package/packages/runtime-host/src/codex-cdp.mjs +42 -8
  46. package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
  47. package/packages/runtime-host/src/codex-injection.mjs +364 -116
  48. package/packages/runtime-host/src/codex-runtime.mjs +105 -28
  49. package/packages/runtime-host/src/feature-registry.mjs +3 -0
  50. package/packages/runtime-host/src/session-status-reader.mjs +65 -24
  51. package/features/notes/assets/icons/circle.svg +0 -15
  52. package/features/notes/assets/icons/ellipsis.svg +0 -17
  53. package/features/notes/assets/icons/external-link.svg +0 -17
  54. package/features/notes/assets/icons/list-todo.svg +0 -19
  55. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  56. package/features/notes/assets/icons/paperclip.svg +0 -15
  57. package/features/notes/assets/icons/star-off.svg +0 -17
  58. package/features/notes/assets/icons/star.svg +0 -15
  59. package/features/notes/assets/icons/sticky-note.svg +0 -16
  60. package/features/notes/assets/icons/trash-2.svg +0 -19
  61. package/features/notes/src/http-server.mjs +0 -580
  62. package/features/notes/web/app.js +0 -1202
  63. package/features/notes/web/codex-artifact-document.svg +0 -1
  64. package/features/notes/web/codex-attachment.svg +0 -1
  65. package/features/notes/web/codex-build.svg +0 -1
  66. package/features/notes/web/codex-code.svg +0 -1
  67. package/features/notes/web/codex-cplusplus.svg +0 -1
  68. package/features/notes/web/codex-css.svg +0 -1
  69. package/features/notes/web/codex-document.svg +0 -1
  70. package/features/notes/web/codex-file.svg +0 -1
  71. package/features/notes/web/codex-folder.svg +0 -1
  72. package/features/notes/web/codex-hashes.svg +0 -1
  73. package/features/notes/web/codex-html.svg +0 -1
  74. package/features/notes/web/codex-image.svg +0 -1
  75. package/features/notes/web/codex-java.svg +0 -1
  76. package/features/notes/web/codex-javascript.svg +0 -1
  77. package/features/notes/web/codex-json.svg +0 -1
  78. package/features/notes/web/codex-notebook.svg +0 -1
  79. package/features/notes/web/codex-pdf.svg +0 -1
  80. package/features/notes/web/codex-pencil.svg +0 -3
  81. package/features/notes/web/codex-php.svg +0 -1
  82. package/features/notes/web/codex-plus.svg +0 -3
  83. package/features/notes/web/codex-presentation.svg +0 -1
  84. package/features/notes/web/codex-python.svg +0 -1
  85. package/features/notes/web/codex-react.svg +0 -1
  86. package/features/notes/web/codex-rust.svg +0 -1
  87. package/features/notes/web/codex-shell.svg +0 -1
  88. package/features/notes/web/codex-skill.svg +0 -1
  89. package/features/notes/web/codex-spreadsheet.svg +0 -1
  90. package/features/notes/web/codex-task.svg +0 -3
  91. package/features/notes/web/codex-terminal.svg +0 -1
  92. package/features/notes/web/codex-toml.svg +0 -1
  93. package/features/notes/web/codex-typescript.svg +0 -1
  94. package/features/notes/web/codex-view-all.svg +0 -3
  95. package/features/notes/web/file-icons.mjs +0 -68
  96. package/features/notes/web/i18n.mjs +0 -237
  97. package/features/notes/web/index.html +0 -15
  98. package/features/notes/web/styles.css +0 -1155
  99. 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,11 +29,23 @@ 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 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); }
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; } }
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] > div { min-height:0; overflow:hidden; }
38
+ [data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
39
+ [data-minecodex-session-reveal][hidden] { display:none; }
40
+ [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
+ [data-minecodex-context-trigger][hidden] { display:none; }
42
+ [data-context-progress] { transition:stroke-dashoffset 120ms ease-out; }
43
+ [data-minecodex-context-trigger]:hover { background:var(--color-background-primary-ghost-hover); }
44
+ [data-minecodex-context-trigger]:focus-visible { outline:2px solid var(--color-ring); }
45
+ [data-minecodex-context-trigger] svg { width:12px; height:12px; }
46
+ [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; }
48
+ @media (prefers-reduced-motion:reduce) { [data-minecodex-session-reveal], [data-context-progress] { transition:none; } }
38
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; }
39
50
  [${MARKER}] svg, [data-minecodex-session-status-panel] h3 svg { width:14px; height:14px; flex:none; stroke-width:1.75; }
40
51
  [${MARKER}] [data-kind="context"] svg, [data-minecodex-session-status-panel] h3 .context-ring { width:12px; height:12px; stroke-width:2; }
@@ -64,6 +75,33 @@ style.textContent = `
64
75
  `;
65
76
  document.head.append(style);
66
77
 
78
+ const instances = new Map();
79
+ const expansionKey = "minecodex:session-status:expanded";
80
+ let globalExpanded = localStorage.getItem(expansionKey) !== "false";
81
+ let instanceSequence = 0;
82
+ function setGlobalExpanded(value) {
83
+ globalExpanded = value;
84
+ localStorage.setItem(expansionKey, String(value));
85
+ for (const instance of instances.values()) instance.setExpanded(value);
86
+ }
87
+ window.addEventListener("storage", (event) => {
88
+ if (event.key !== expansionKey) return;
89
+ globalExpanded = event.newValue !== "false";
90
+ for (const instance of instances.values()) instance.setExpanded(globalExpanded);
91
+ }, { signal });
92
+ // Codex 会把离屏输入框搬到 Quick Chat 浮层;节点仍连接不代表当前页面是聊天。
93
+ function isActiveComposer(composer) {
94
+ const card = composer.closest('[data-codex-composer-root]');
95
+ return Boolean(composer.isConnected
96
+ && card?.getAttribute('data-composer-placement') === 'thread'
97
+ && !composer.closest('[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
98
+ && composer.checkVisibility({ visibilityProperty: true }));
99
+ }
100
+ function createInstance(composerElement) {
101
+ let copy = selectedCopy();
102
+ const controller = new AbortController();
103
+ const signal = controller.signal;
104
+ const instanceId = ++instanceSequence;
67
105
  const root = document.createElement("div");
68
106
  root.setAttribute(MARKER, "");
69
107
  root.hidden = true;
@@ -90,6 +128,71 @@ function createContextRing() {
90
128
  }
91
129
  return svg;
92
130
  }
131
+ const reveal = document.createElement("div");
132
+ reveal.dataset.minecodexSessionReveal = "";
133
+ const clip = document.createElement("div");
134
+ clip.append(root);
135
+ reveal.append(clip);
136
+ root.id = `minecodex-session-details-${instanceId}`;
137
+ const trigger = document.createElement("button");
138
+ trigger.type = "button";
139
+ trigger.dataset.minecodexContextTrigger = "";
140
+ trigger.setAttribute("aria-controls", root.id);
141
+ trigger.append(createContextRing());
142
+ const tooltip = document.createElement("div");
143
+ tooltip.id = `minecodex-context-tooltip-${instanceId}`;
144
+ tooltip.dataset.minecodexContextTooltip = "";
145
+ tooltip.setAttribute("role", "tooltip");
146
+ tooltip.hidden = true;
147
+ document.body.append(tooltip);
148
+ let expanded = globalExpanded;
149
+ let collapseTimer = null;
150
+ let collapsing = false;
151
+ const nativeIndicators = new Set();
152
+ function setExpanded(value) {
153
+ clearTimeout(collapseTimer);
154
+ collapsing = expanded && !value && !matchMedia("(prefers-reduced-motion: reduce)").matches;
155
+ expanded = value;
156
+ if (collapsing) collapseTimer = setTimeout(() => {
157
+ collapsing = false;
158
+ syncVisibility(findComposer());
159
+ }, 200);
160
+ reveal.toggleAttribute("data-expanded", expanded);
161
+ clip.inert = !expanded;
162
+ trigger.setAttribute("aria-expanded", String(expanded));
163
+ tooltip.hidden = true;
164
+ if (expanded) reveal.hidden = false;
165
+ syncVisibility(findComposer());
166
+ if (!expanded) closePanel();
167
+ }
168
+ trigger.addEventListener("click", () => setGlobalExpanded(!expanded), { signal });
169
+ function hideTooltip() { tooltip.hidden = true; trigger.removeAttribute("aria-describedby"); }
170
+ function showContextTooltip() {
171
+ const value = snapshot;
172
+ const percent = value?.contextPercent;
173
+ const zh = copy === copyByLocale["zh-CN"];
174
+ const status = percent == null ? "—" : percent >= 50
175
+ ? (zh ? `已占用 ${percent}%` : `${percent}% full`)
176
+ : (zh ? `已使用 ${percent}%(剩余 ${100 - percent}%)` : `${percent}% used (${100 - percent}% left)`);
177
+ const tokens = value?.contextWindow == null ? "" : `${zh ? "已使用 " : ""}${Math.round(value.contextUsedTokens / 1000)}k / ${Math.round(value.contextWindow / 1000)}k ${zh ? "Token" : "tokens used"}`;
178
+ tooltip.replaceChildren();
179
+ for (const [text, muted] of [[zh ? "上下文窗口:" : "Context window:", true], [status, percent >= 50], [tokens, false]]) {
180
+ if (!text) continue;
181
+ const line = document.createElement("div");
182
+ line.textContent = text;
183
+ if (muted) line.style.opacity = "0.6";
184
+ tooltip.append(line);
185
+ }
186
+ tooltip.hidden = false;
187
+ trigger.setAttribute("aria-describedby", tooltip.id);
188
+ const rect = trigger.getBoundingClientRect();
189
+ tooltip.style.left = `${Math.max(8, Math.min(innerWidth - tooltip.offsetWidth - 8, rect.left + (rect.width - tooltip.offsetWidth) / 2))}px`;
190
+ tooltip.style.top = `${rect.top - tooltip.offsetHeight - 4}px`;
191
+ }
192
+ trigger.addEventListener("pointerenter", showContextTooltip, { signal });
193
+ trigger.addEventListener("focus", showContextTooltip, { signal });
194
+ trigger.addEventListener("pointerleave", hideTooltip, { signal });
195
+ trigger.addEventListener("blur", hideTooltip, { signal });
93
196
  const buttons = ["statistics", "usage", "context"].map((kind) => {
94
197
  const button = document.createElement("button");
95
198
  button.type = "button";
@@ -104,11 +207,27 @@ const buttons = ["statistics", "usage", "context"].map((kind) => {
104
207
  root.append(button);
105
208
  return button;
106
209
  });
210
+ const collapse = document.createElement("button");
211
+ collapse.type = "button";
212
+ // Lucide chevron-up,与状态栏其余线性图标保持相同笔画。
213
+ 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>';
214
+ collapse.addEventListener("click", () => { setGlobalExpanded(false); trigger.focus(); hideTooltip(); }, { signal });
215
+ root.append(collapse);
107
216
  let snapshot = null;
108
217
  let panel = null;
109
218
  let openKind = null;
110
219
  let rescanQueued = false;
220
+ let activeThreadId = null;
221
+ let refreshTimer = null;
222
+ let refreshSequence = 0;
223
+ let disposed = false;
224
+ let lastRevision = 0;
111
225
 
226
+ function cachePercent(value) {
227
+ const percent = value?.inputTokens > 0 && value.cachedInputTokens != null
228
+ ? value.cachedInputTokens / value.inputTokens * 100 : value?.cacheHitPercent;
229
+ return percent == null ? "—" : `${percent > 99 ? percent.toFixed(1) : Math.round(percent)}%`;
230
+ }
112
231
  function number(value) { return value == null ? "—" : new Intl.NumberFormat().format(value); }
113
232
  function compact(value) {
114
233
  if (value == null) return "—";
@@ -211,7 +330,7 @@ function showPanel(kind) {
211
330
  row(dl, copy.input, tokenValue(otherInput)).dataset.tone = "blue";
212
331
  row(dl, copy.cached, tokenValue(snapshot.cachedInputTokens)).dataset.tone = "green";
213
332
  divider(dl);
214
- row(dl, copy.cacheDetail, snapshot.cacheHitPercent == null ? "—" : `${snapshot.cacheHitPercent}%`);
333
+ row(dl, copy.cacheDetail, cachePercent(snapshot));
215
334
  } else {
216
335
  if (snapshot.contextPercent != null) {
217
336
  const head = document.createElement("div");
@@ -261,36 +380,178 @@ function showPanel(kind) {
261
380
  }
262
381
 
263
382
  function findComposer() {
264
- return document.querySelector('[data-codex-composer="true"][contenteditable="true"]')
265
- ?? document.querySelector('[contenteditable="true"][role="textbox"]');
383
+ return isActiveComposer(composerElement) ? composerElement : null;
384
+ }
385
+ function currentThreadId(composer) {
386
+ if (!composer) return null;
387
+ const card = composer.closest('[data-codex-composer-root]');
388
+ const portal = card?.querySelector('[data-above-composer-portal]');
389
+ // 只认 Codex 输入框绑定的会话;侧栏和 URL 在切换模式时可能仍指向旧会话。
390
+ if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
391
+ const value = portal?.getAttribute('data-above-composer-conversation-id');
392
+ 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;
393
+ }
394
+ // 与原生 Context 指示器订阅同一会话状态;不以日志百分比覆盖它。
395
+ const emptyNativeContext = () => ({ contextUsedTokens: null, contextWindow: null, contextPercent: null });
396
+ let nativeContext = emptyNativeContext();
397
+ let nativeContextFamily = null;
398
+ let nativeContextSubscription = null;
399
+ let nativeContextThread = null;
400
+ function findNativeScope(element) {
401
+ let fiber;
402
+ for (let node = element; node && !fiber; node = node.parentElement) {
403
+ fiber = node[Object.keys(node).find((key) => key.startsWith("__reactFiber"))];
404
+ }
405
+ for (let depth = 0; fiber && depth < 200; depth++, fiber = fiber.return) {
406
+ const chain = fiber.memoizedProps?.value;
407
+ if (!(chain instanceof Map)) continue;
408
+ for (const node of chain.values()) {
409
+ if (node?.token?.__scopeBrand === "AppScope" && typeof node.store?.sub === "function") {
410
+ return { node, chain };
411
+ }
412
+ }
413
+ }
414
+ return null;
415
+ }
416
+ function contextFromNativeUsage(usage) {
417
+ const windowSize = usage?.modelContextWindow;
418
+ const total = usage?.last?.totalTokens;
419
+ if (!Number.isFinite(windowSize) || windowSize <= 0 || !Number.isFinite(total) || total < 0) {
420
+ return emptyNativeContext();
421
+ }
422
+ const used = Math.min(total, windowSize);
423
+ return { contextUsedTokens: used, contextWindow: windowSize, contextPercent: Math.round(used / windowSize * 100) };
424
+ }
425
+ function syncNativeContext(composer) {
426
+ if (nativeContextThread === activeThreadId && nativeContextSubscription) return;
427
+ nativeContextSubscription?.();
428
+ nativeContextSubscription = null;
429
+ nativeContextThread = null;
430
+ nativeContext = emptyNativeContext();
431
+ if (!activeThreadId || !nativeContextFamily) return;
432
+ const scope = findNativeScope(composer);
433
+ if (!scope) return;
434
+ try {
435
+ const threadId = activeThreadId;
436
+ const descriptor = nativeContextFamily.resolve(scope.node, scope.chain, threadId);
437
+ const atom = descriptor.resolve(scope.node, scope.chain);
438
+ const update = () => {
439
+ if (disposed || activeThreadId !== threadId) return;
440
+ nativeContext = contextFromNativeUsage(scope.node.store.get(atom));
441
+ render();
442
+ };
443
+ nativeContextThread = threadId;
444
+ nativeContextSubscription = scope.node.store.sub(atom, update);
445
+ update();
446
+ } catch {
447
+ nativeContextThread = null;
448
+ }
449
+ }
450
+ async function loadNativeContextFamily() {
451
+ try {
452
+ const url = [...document.querySelectorAll("link[rel=modulepreload]")].map((link) => link.href)
453
+ .find((name) => /\/app-shared-[^/]+\.js(?:$|\?)/.test(name));
454
+ if (!url) return;
455
+ const source = await (await fetch(url, { signal })).text();
456
+ // 从当前构建解析导出,避免依赖每次更新都会变化的压缩符号或资源哈希。
457
+ const local = source.match(/([\w$]+)=[\w$]+\(\(\{latestTokenUsageInfo:/)?.[1];
458
+ if (!local) return;
459
+ const exported = [...source.matchAll(/([\w$]+) as ([\w$]+)/g)]
460
+ .find((match) => match[1] === local)?.[2];
461
+ if (!exported) return;
462
+ const module = await import(url);
463
+ if (signal.aborted || disposed) return;
464
+ nativeContextFamily = module[exported];
465
+ syncNativeContext(findComposer());
466
+ } catch { /* 原生接口不可用时保留占位,不显示过期的日志百分比。 */ }
467
+ }
468
+ function requestRefresh() {
469
+ if (!activeThreadId || signal.aborted) return;
470
+ if (typeof window[config.bindingName] === "function") {
471
+ window[config.bindingName](JSON.stringify({
472
+ token: config.bindingToken,
473
+ featureId: config.featureId,
474
+ requestId: `session-status:${++refreshSequence}`,
475
+ action: "refresh-session-status",
476
+ }));
477
+ }
478
+ }
479
+ function scheduleRefresh() {
480
+ clearTimeout(refreshTimer);
481
+ refreshTimer = null;
482
+ if (!activeThreadId || signal.aborted) return;
483
+ const waiting = snapshot?.turns == null || (snapshot.turns === 0 && snapshot.totalTokens == null);
484
+ const delay = waiting ? Math.max(500, snapshot?.retryAfterMs ?? 500) : snapshot?.isRunning ? 500 : 0;
485
+ if (!delay) return;
486
+ refreshTimer = setTimeout(() => {
487
+ refreshTimer = null;
488
+ requestRefresh();
489
+ scheduleRefresh();
490
+ }, delay);
266
491
  }
267
492
  function syncVisibility(composer) {
268
- const portal = composer?.closest('[data-codex-composer-root]')?.querySelector('[data-above-composer-portal]');
269
- const activeThreadId = portal?.getAttribute('data-above-composer-conversation-id');
270
- const hasData = snapshot && (snapshot.turns > 0 || snapshot.totalTokens != null);
271
- root.hidden = !hasData || (portal && snapshot.threadId !== activeThreadId);
272
- if (root.hidden) closePanel();
493
+ const threadId = currentThreadId(composer);
494
+ root.hidden = !threadId || snapshot?.threadId !== threadId;
495
+ // 收起时退出布局,避免空 grid 仍触发原生 flex gap。
496
+ reveal.hidden = root.hidden || (!expanded && !collapsing);
497
+ trigger.hidden = root.hidden || expanded;
498
+ if (root.hidden) { closePanel(); hideTooltip(); }
273
499
  }
274
500
  function ensurePosition() {
501
+ if (disposed || signal.aborted) return;
275
502
  const composer = findComposer();
276
- if (!composer) { root.remove(); closePanel(); return; }
503
+ const threadId = currentThreadId(composer);
504
+ if (threadId !== activeThreadId) {
505
+ activeThreadId = threadId;
506
+ nativeContext = emptyNativeContext();
507
+ // 会话建立后先展示占位,不让日志落盘速度决定状态栏是否出现。
508
+ snapshot = threadId ? { threadId } : null;
509
+ closePanel();
510
+ render();
511
+ requestRefresh();
512
+ scheduleRefresh();
513
+ }
514
+ syncNativeContext(composer);
515
+ if (!composer) { reveal.remove(); trigger.remove(); closePanel(); return; }
277
516
  const form = composer.closest("form");
278
517
  const card = composer.closest("[data-codex-composer-root]") ?? form ?? composer.parentElement?.parentElement;
279
518
  if (!card?.parentElement) return;
280
- if (root.previousElementSibling !== card || root.parentElement !== card.parentElement) {
281
- card.after(root);
519
+ const model = card.querySelector('[data-composer-navigation-target="reasoning"]');
520
+ // 沿模型按钮的包装层找到横向控制组,不依赖本地化文案。
521
+ let anchor = model;
522
+ while (anchor?.parentElement && !anchor.parentElement.classList.contains("items-center")) anchor = anchor.parentElement;
523
+ if (anchor && trigger.nextElementSibling !== anchor) anchor.before(trigger);
524
+ for (const indicator of nativeIndicators) {
525
+ if (!threadId || !indicator.isConnected) {
526
+ indicator.removeAttribute("data-minecodex-native-context");
527
+ nativeIndicators.delete(indicator);
528
+ }
529
+ }
530
+ for (const indicator of threadId ? card.querySelectorAll('[role="img"][aria-label]') : []) {
531
+ if (!indicator.querySelector('svg circle')) continue;
532
+ indicator.setAttribute("data-minecodex-native-context", "");
533
+ nativeIndicators.add(indicator);
534
+ }
535
+ if (reveal.previousElementSibling !== card || reveal.parentElement !== card.parentElement) {
536
+ card.after(reveal);
282
537
  closePanel();
283
538
  }
284
539
  syncVisibility(composer);
285
540
  }
286
541
  function render() {
542
+ if (snapshot) snapshot = { ...snapshot, ...nativeContext };
287
543
  syncVisibility(findComposer());
288
544
  if (root.hidden) return;
545
+ trigger.querySelector("[data-context-progress]").setAttribute("stroke-dashoffset", String(100 - (snapshot.contextPercent ?? 0)));
546
+ trigger.setAttribute("aria-label", `${copy.context}: ${snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`}`);
547
+ collapse.setAttribute("aria-label", copy === copyByLocale["zh-CN"] ? "收起会话状态" : "Collapse session status");
548
+ if (!tooltip.hidden) showContextTooltip();
289
549
  buttons[0].querySelector(".status-label").textContent = `${number(snapshot.turns)} ${copy.turns} ${number(snapshot.steps)} ${copy.steps} · ${snapshot.observedTps == null ? "—" : number(snapshot.observedTps)} tok/s`;
290
- buttons[1].querySelector(".status-label").textContent = `${compact(snapshot.totalTokens)} · ${copy.cache} ${snapshot.cacheHitPercent == null ? "—" : `${snapshot.cacheHitPercent}%`}`;
550
+ buttons[1].querySelector(".status-label").textContent = `${compact(snapshot.totalTokens)} · ${copy.cache} ${cachePercent(snapshot)}`;
291
551
  buttons[2].querySelector(".status-label").textContent = snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`;
292
552
  buttons[2].querySelector("[data-context-progress]").setAttribute("stroke-dashoffset", String(100 - (snapshot.contextPercent ?? 0)));
293
553
  buttons[2].setAttribute("aria-label", `${copy.context}: ${snapshot.contextPercent == null ? "—" : `${snapshot.contextPercent}%`}`);
554
+ for (const ring of [trigger, buttons[2]]) ring.querySelector("[data-context-progress]").setAttribute("opacity", snapshot.contextPercent > 0 ? "1" : "0");
294
555
  if (openKind) showPanel(openKind);
295
556
  }
296
557
  for (const button of buttons) button.addEventListener("click", () => {
@@ -309,7 +570,10 @@ const observer = new MutationObserver(() => {
309
570
  rescanQueued = true;
310
571
  queueMicrotask(() => { rescanQueued = false; ensurePosition(); });
311
572
  });
312
- observer.observe(document.documentElement, { childList: true, subtree: true });
573
+ observer.observe(document.documentElement, {
574
+ childList: true, subtree: true, attributes: true,
575
+ attributeFilter: ["data-above-composer-conversation-id", "data-composer-placement"],
576
+ });
313
577
  const localeObserver = new MutationObserver(() => {
314
578
  const nextCopy = selectedCopy();
315
579
  if (nextCopy === copy) return;
@@ -317,21 +581,62 @@ const localeObserver = new MutationObserver(() => {
317
581
  render();
318
582
  });
319
583
  localeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
584
+ setExpanded(globalExpanded);
320
585
  ensurePosition();
321
- const previous = window.__minecodexSessionStatus;
586
+ void loadNativeContextFamily();
322
587
  const receiver = {
323
- update(value) {
324
- snapshot = value;
588
+ update(value, revision) {
325
589
  ensurePosition();
590
+ // 切换会话期间的迟到快照不得覆盖新会话,也不清除其占位。
591
+ if (!activeThreadId || value?.threadId !== activeThreadId) return;
592
+ if (Number.isSafeInteger(revision)) {
593
+ if (revision <= lastRevision) return;
594
+ lastRevision = revision;
595
+ }
596
+ snapshot = value;
326
597
  render();
598
+ scheduleRefresh();
327
599
  },
328
600
  };
329
- window.__minecodexSessionStatus = receiver;
330
- return () => {
601
+ return { update: receiver.update, setExpanded, dispose() {
602
+ controller.abort();
603
+ disposed = true;
604
+ nativeContextSubscription?.();
605
+ clearTimeout(refreshTimer);
606
+ clearTimeout(collapseTimer);
331
607
  observer.disconnect();
332
608
  localeObserver.disconnect();
333
609
  closePanel();
334
- root.remove();
610
+ reveal.remove();
611
+ trigger.remove();
612
+ tooltip.remove();
613
+ for (const indicator of nativeIndicators) indicator.removeAttribute("data-minecodex-native-context");
614
+ } };
615
+ }
616
+ function syncInstances() {
617
+ for (const [composer, instance] of instances) {
618
+ if (!isActiveComposer(composer)) { instance.dispose(); instances.delete(composer); }
619
+ }
620
+ for (const composer of document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')) {
621
+ if (isActiveComposer(composer) && !instances.has(composer)) instances.set(composer, createInstance(composer));
622
+ }
623
+ }
624
+ const previous = window.__minecodexSessionStatus;
625
+ const receiver = { update(value, revision) {
626
+ syncInstances();
627
+ for (const instance of instances.values()) instance.update(value, revision);
628
+ } };
629
+ window.__minecodexSessionStatus = receiver;
630
+ const instanceObserver = new MutationObserver(syncInstances);
631
+ instanceObserver.observe(document.documentElement, {
632
+ childList:true, subtree:true, attributes:true,
633
+ attributeFilter:["class", "style", "hidden", "inert", "aria-hidden", "data-composer-placement"],
634
+ });
635
+ syncInstances();
636
+ return () => {
637
+ instanceObserver.disconnect();
638
+ for (const instance of instances.values()) instance.dispose();
639
+ instances.clear();
335
640
  style.remove();
336
641
  if (window.__minecodexSessionStatus === receiver) window.__minecodexSessionStatus = previous;
337
642
  };
@@ -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(); };