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.
- package/README.md +9 -3
- package/features/images/README.md +16 -98
- package/features/images/codex-feature.json +6 -14
- package/features/images/src/http-server.mjs +15 -5
- package/features/images/src/image-library.mjs +37 -9
- package/features/images/src/library-prompt-page.mjs +239 -0
- package/features/images/src/prompt-index.mjs +27 -24
- package/features/images/src/prompt-source.mjs +100 -0
- package/features/images/web/app.js +180 -67
- package/features/images/web/i18n.mjs +5 -0
- package/features/images/web/index.html +29 -24
- package/features/images/web/styles.css +169 -139
- package/features/model-slider/README.md +13 -1
- package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
- package/features/notes/DESIGN.md +66 -0
- package/features/notes/README.md +18 -99
- package/features/notes/assets/icons/book-open-nav.svg +15 -0
- package/features/notes/assets/icons/book-open.svg +16 -0
- package/features/notes/assets/native-emojis.json +1 -0
- package/features/notes/assets/native-prompt-icons.json +28 -0
- package/features/notes/codex-feature.json +12 -70
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/src/product.mjs +287 -613
- package/features/notes/src/prompt-schema.mjs +117 -0
- package/features/notes/src/quick-prompts-page.mjs +1559 -0
- package/features/notes/src/quick-prompts-server.mjs +167 -0
- package/features/session-status/DESIGN.md +10 -2
- package/features/session-status/README.md +13 -3
- package/features/session-status/src/session-status.mjs +329 -24
- package/features/sidebar-focus/DESIGN.md +16 -0
- package/features/sidebar-focus/codex-feature.json +20 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
- package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
- package/features/unread-inbox/DESIGN.md +28 -0
- package/features/unread-inbox/codex-feature.json +20 -0
- package/features/unread-inbox/src/unread-inbox.mjs +360 -0
- package/features/usage-meter/DESIGN.md +49 -0
- package/features/usage-meter/README.md +18 -0
- package/features/usage-meter/codex-feature.json +13 -0
- package/features/usage-meter/src/usage-meter.mjs +200 -0
- package/package.json +21 -6
- package/packages/cli/src/config.mjs +4 -1
- package/packages/cli/src/control-server.mjs +2 -2
- package/packages/runtime-host/src/cloud-images.mjs +123 -0
- package/packages/runtime-host/src/codex-cdp.mjs +42 -8
- package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
- package/packages/runtime-host/src/codex-injection.mjs +364 -116
- package/packages/runtime-host/src/codex-runtime.mjs +105 -28
- package/packages/runtime-host/src/feature-registry.mjs +3 -0
- package/packages/runtime-host/src/session-status-reader.mjs +65 -24
- package/features/notes/assets/icons/circle.svg +0 -15
- package/features/notes/assets/icons/ellipsis.svg +0 -17
- package/features/notes/assets/icons/external-link.svg +0 -17
- package/features/notes/assets/icons/list-todo.svg +0 -19
- package/features/notes/assets/icons/message-square-quote.svg +0 -17
- package/features/notes/assets/icons/paperclip.svg +0 -15
- package/features/notes/assets/icons/star-off.svg +0 -17
- package/features/notes/assets/icons/star.svg +0 -15
- package/features/notes/assets/icons/sticky-note.svg +0 -16
- package/features/notes/assets/icons/trash-2.svg +0 -19
- package/features/notes/src/http-server.mjs +0 -580
- package/features/notes/web/app.js +0 -1202
- package/features/notes/web/codex-artifact-document.svg +0 -1
- package/features/notes/web/codex-attachment.svg +0 -1
- package/features/notes/web/codex-build.svg +0 -1
- package/features/notes/web/codex-code.svg +0 -1
- package/features/notes/web/codex-cplusplus.svg +0 -1
- package/features/notes/web/codex-css.svg +0 -1
- package/features/notes/web/codex-document.svg +0 -1
- package/features/notes/web/codex-file.svg +0 -1
- package/features/notes/web/codex-folder.svg +0 -1
- package/features/notes/web/codex-hashes.svg +0 -1
- package/features/notes/web/codex-html.svg +0 -1
- package/features/notes/web/codex-image.svg +0 -1
- package/features/notes/web/codex-java.svg +0 -1
- package/features/notes/web/codex-javascript.svg +0 -1
- package/features/notes/web/codex-json.svg +0 -1
- package/features/notes/web/codex-notebook.svg +0 -1
- package/features/notes/web/codex-pdf.svg +0 -1
- package/features/notes/web/codex-pencil.svg +0 -3
- package/features/notes/web/codex-php.svg +0 -1
- package/features/notes/web/codex-plus.svg +0 -3
- package/features/notes/web/codex-presentation.svg +0 -1
- package/features/notes/web/codex-python.svg +0 -1
- package/features/notes/web/codex-react.svg +0 -1
- package/features/notes/web/codex-rust.svg +0 -1
- package/features/notes/web/codex-shell.svg +0 -1
- package/features/notes/web/codex-skill.svg +0 -1
- package/features/notes/web/codex-spreadsheet.svg +0 -1
- package/features/notes/web/codex-task.svg +0 -3
- package/features/notes/web/codex-terminal.svg +0 -1
- package/features/notes/web/codex-toml.svg +0 -1
- package/features/notes/web/codex-typescript.svg +0 -1
- package/features/notes/web/codex-view-all.svg +0 -3
- package/features/notes/web/file-icons.mjs +0 -68
- package/features/notes/web/i18n.mjs +0 -237
- package/features/notes/web/index.html +0 -15
- package/features/notes/web/styles.css +0 -1155
- 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
|
|
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
|
-
[
|
|
36
|
-
|
|
37
|
-
@
|
|
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
|
|
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
|
|
265
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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(); };
|