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.
- package/README.md +9 -3
- package/features/images/README.md +16 -98
- package/features/images/codex-feature.json +6 -23
- package/features/images/src/library-prompt-page.mjs +239 -0
- 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 +11 -3
- package/features/session-status/README.md +8 -4
- package/features/session-status/src/session-status.mjs +272 -25
- 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 +36 -0
- package/features/unread-inbox/codex-feature.json +20 -0
- package/features/unread-inbox/src/unread-inbox.mjs +446 -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 +17 -5
- package/packages/cli/src/config.mjs +4 -1
- package/packages/cli/src/control-server.mjs +2 -2
- package/packages/runtime-host/src/codex-injection.mjs +38 -3
- package/packages/runtime-host/src/codex-runtime.mjs +41 -20
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/runtime-host/src/session-status-reader.mjs +40 -16
- 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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// 订阅原生额度缓存;不另行轮询,也不复制账号信息到插件服务。
|
|
2
|
+
const MARKER = "data-minecodex-usage-meter";
|
|
3
|
+
const TIP_ID = "minecodex-usage-tooltip";
|
|
4
|
+
const STYLE_ID = "minecodex-usage-style";
|
|
5
|
+
let record = null;
|
|
6
|
+
let client = null;
|
|
7
|
+
let unsubscribe = null;
|
|
8
|
+
let queued = false;
|
|
9
|
+
let hoverTimer = null;
|
|
10
|
+
|
|
11
|
+
function remainingWindows(data) {
|
|
12
|
+
const windows = [data?.rate_limit?.primary_window, data?.rate_limit?.secondary_window];
|
|
13
|
+
const remaining = seconds => {
|
|
14
|
+
const window = windows.find(window => window?.limit_window_seconds === seconds);
|
|
15
|
+
return typeof window?.used_percent === "number" && Number.isFinite(window.used_percent)
|
|
16
|
+
? Math.round(Math.max(0, Math.min(100, 100 - window.used_percent))) : null;
|
|
17
|
+
};
|
|
18
|
+
const weekly = remaining(604800), fiveHour = remaining(18000);
|
|
19
|
+
return weekly == null ? null : { weekly, fiveHour, primary: fiveHour ?? weekly };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nativeContext(rail) {
|
|
23
|
+
const key = Object.keys(rail).find(key => key.startsWith("__reactFiber$"));
|
|
24
|
+
let fiber = key ? rail[key] : null;
|
|
25
|
+
const result = { auth: null, client: null, router: null };
|
|
26
|
+
for (let depth = 0; fiber && depth < 200; depth++, fiber = fiber.return) {
|
|
27
|
+
const props = fiber.memoizedProps;
|
|
28
|
+
if (!props) continue;
|
|
29
|
+
for (const value of [props.auth, props.value]) {
|
|
30
|
+
if (!result.auth && value && typeof value === "object" && "authMethod" in value && "userId" in value && "accountId" in value) result.auth = value;
|
|
31
|
+
}
|
|
32
|
+
for (const value of [props.queryClient, props.client, props.value]) {
|
|
33
|
+
if (!result.client && typeof value?.getQueryCache === "function" && typeof value?.getQueryData === "function") result.client = value;
|
|
34
|
+
}
|
|
35
|
+
const router = props.router ?? props.value?.router;
|
|
36
|
+
if (!result.router && typeof router?.navigate === "function") result.router = router;
|
|
37
|
+
if (result.auth && result.client && result.router) break;
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function currentWindows(context) {
|
|
43
|
+
const { auth, client } = context;
|
|
44
|
+
if (auth?.isLoading || !auth?.userId || !auth?.accountId || !["chatgpt", "chatgptAuthTokens"].includes(auth.authMethod)) return null;
|
|
45
|
+
const data = client?.getQueryData(["rate-limit-status"]);
|
|
46
|
+
// 账号与缓存必须同时一致;旧账号的最后一次响应不能留在导航栏。
|
|
47
|
+
if (!data || data.user_id !== auth.userId || data.account_id !== auth.accountId) return null;
|
|
48
|
+
return remainingWindows(data);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const style = document.createElement("style");
|
|
52
|
+
style.id = STYLE_ID;
|
|
53
|
+
style.textContent = `
|
|
54
|
+
[${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; -webkit-app-region:no-drag; }
|
|
55
|
+
[${MARKER}] .mcx-usage-ring { display:block; width:24px; height:24px; overflow:visible; }
|
|
56
|
+
[${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:inherit; font-family:var(--font-sans-default); font-size:calc(var(--text-xs) * 2 / 3); line-height:1; font-weight:500; letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
|
|
57
|
+
[${MARKER}] [data-window="weekly"] { color:inherit; }
|
|
58
|
+
[${MARKER}] [data-window="fiveHour"] { color:var(--color-text-secondary); }
|
|
59
|
+
#${TIP_ID} { position:fixed; z-index:2147483000; width:max-content; max-width:calc(100vw - 16px); pointer-events:none; user-select:none; padding:5px 12px; border:1px solid var(--color-border-tooltip); border-radius:var(--radius-2xl); background:var(--color-background-tooltip); color:var(--color-text-tooltip); box-shadow:var(--shadow-tooltip); font-family:var(--font-sans-default); font-size:var(--text-sm); line-height:18px; font-weight:var(--tooltip-compact-font-weight); letter-spacing:var(--tracking-tooltip); }
|
|
60
|
+
#${TIP_ID}[hidden] { display:none; }
|
|
61
|
+
#${TIP_ID} dl { display:grid; grid-template-columns:auto auto; gap:2px 16px; margin:0; }
|
|
62
|
+
#${TIP_ID} dt { text-align:left; }
|
|
63
|
+
#${TIP_ID} dd { margin:0; text-align:right; font-variant-numeric:tabular-nums; }
|
|
64
|
+
`;
|
|
65
|
+
document.head.append(style);
|
|
66
|
+
|
|
67
|
+
function svgNode(name, attributes) {
|
|
68
|
+
const node = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
69
|
+
for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));
|
|
70
|
+
return node;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function draw(windows) {
|
|
74
|
+
const svg = svgNode("svg", { viewBox: "0 0 32 32", class: "mcx-usage-ring", "aria-hidden": "true" });
|
|
75
|
+
for (const [key, radius] of [["weekly", 43 / 3], ["fiveHour", 10.5]]) {
|
|
76
|
+
if (windows[key] == null) continue;
|
|
77
|
+
const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 10 / 3 });
|
|
78
|
+
group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, opacity: 0.18 }));
|
|
79
|
+
if (windows[key] > 0) group.append(svgNode("circle", {
|
|
80
|
+
cx: 16, cy: 16, r: radius, pathLength: 100,
|
|
81
|
+
"stroke-dasharray": `${windows[key]} 100`, "stroke-linecap": "round", transform: "rotate(-90 16 16)",
|
|
82
|
+
}));
|
|
83
|
+
svg.append(group);
|
|
84
|
+
}
|
|
85
|
+
const number = document.createElement("span");
|
|
86
|
+
number.className = "mcx-usage-number";
|
|
87
|
+
number.textContent = String(windows.primary);
|
|
88
|
+
number.setAttribute("data-dual", String(windows.fiveHour != null));
|
|
89
|
+
number.setAttribute("data-digits", String(number.textContent.length));
|
|
90
|
+
record.button.replaceChildren(svg, number);
|
|
91
|
+
const list = document.createElement("dl");
|
|
92
|
+
const rows = windows.fiveHour == null ? [] : [["5h Limit", windows.fiveHour]];
|
|
93
|
+
rows.push(["Weekly Limit", windows.weekly]);
|
|
94
|
+
for (const [label, percent] of rows) {
|
|
95
|
+
const term = document.createElement("dt"), value = document.createElement("dd");
|
|
96
|
+
term.textContent = label;
|
|
97
|
+
value.textContent = `${percent}%`;
|
|
98
|
+
list.append(term, value);
|
|
99
|
+
}
|
|
100
|
+
record.tooltip.replaceChildren(list);
|
|
101
|
+
record.button.setAttribute("aria-label", rows.map(([label, percent]) => `${label} ${percent}%`).join(", "));
|
|
102
|
+
record.button.setAttribute("aria-description", "Open Usage settings");
|
|
103
|
+
if (!record.tooltip.hidden) positionTooltip();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function positionTooltip() {
|
|
107
|
+
if (!record) return;
|
|
108
|
+
const rect = record.button.getBoundingClientRect();
|
|
109
|
+
const tooltip = record.tooltip;
|
|
110
|
+
// 按物理视口定位,兼容原生 app-shell 的 CSS zoom。
|
|
111
|
+
tooltip.style.left = `${Math.max(8, Math.min(rect.right + 2, window.innerWidth - tooltip.offsetWidth - 8))}px`;
|
|
112
|
+
tooltip.style.top = `${Math.max(8, Math.min(rect.top + (rect.height - tooltip.offsetHeight) / 2, window.innerHeight - tooltip.offsetHeight - 8))}px`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function showTooltip() {
|
|
116
|
+
clearTimeout(hoverTimer);
|
|
117
|
+
if (!record) return;
|
|
118
|
+
record.tooltip.hidden = false;
|
|
119
|
+
record.button.setAttribute("aria-describedby", TIP_ID);
|
|
120
|
+
positionTooltip();
|
|
121
|
+
}
|
|
122
|
+
function hideTooltip() {
|
|
123
|
+
clearTimeout(hoverTimer);
|
|
124
|
+
if (!record) return;
|
|
125
|
+
record.tooltip.hidden = true;
|
|
126
|
+
record.button.removeAttribute("aria-describedby");
|
|
127
|
+
}
|
|
128
|
+
function removeControl() {
|
|
129
|
+
hideTooltip();
|
|
130
|
+
record?.button.remove();
|
|
131
|
+
record?.tooltip.remove();
|
|
132
|
+
record = null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function mount(help, rail) {
|
|
136
|
+
// 只克隆按钮的外观属性,不携带原生 Help 的菜单行为或 id。
|
|
137
|
+
const button = help.cloneNode(false);
|
|
138
|
+
for (const attribute of [...button.attributes]) {
|
|
139
|
+
if (attribute.name.startsWith("aria-") || attribute.name === "id" || attribute.name === "data-state") button.removeAttribute(attribute.name);
|
|
140
|
+
}
|
|
141
|
+
button.setAttribute(MARKER, "");
|
|
142
|
+
const tooltip = document.createElement("div");
|
|
143
|
+
tooltip.id = TIP_ID;
|
|
144
|
+
tooltip.setAttribute("role", "tooltip");
|
|
145
|
+
tooltip.hidden = true;
|
|
146
|
+
const anchor = help.parentElement.classList.contains("contents") ? help.parentElement : help;
|
|
147
|
+
anchor.after(button);
|
|
148
|
+
document.body.append(tooltip);
|
|
149
|
+
record = { button, tooltip, rail, help, signature: null };
|
|
150
|
+
button.addEventListener("pointerenter", () => { hoverTimer = setTimeout(showTooltip, 350); });
|
|
151
|
+
button.addEventListener("pointerleave", hideTooltip);
|
|
152
|
+
button.addEventListener("focus", showTooltip);
|
|
153
|
+
button.addEventListener("blur", hideTooltip);
|
|
154
|
+
button.addEventListener("keydown", event => { if (event.key === "Escape") { event.stopPropagation(); hideTooltip(); } });
|
|
155
|
+
button.addEventListener("click", () => {
|
|
156
|
+
hideTooltip();
|
|
157
|
+
const { router } = nativeContext(rail);
|
|
158
|
+
router?.navigate("/settings/usage?tab=overview", { state: router.state?.location?.state });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function sync() {
|
|
163
|
+
queued = false;
|
|
164
|
+
if (signal.aborted) return;
|
|
165
|
+
const rail = [...document.querySelectorAll('nav[data-app-navigation-rail="true"]')].find(rail => rail.getBoundingClientRect().width > 0);
|
|
166
|
+
const help = rail?.querySelector('button[aria-label="Help menu"],button[aria-label="帮助菜单"]');
|
|
167
|
+
const context = rail ? nativeContext(rail) : {};
|
|
168
|
+
if (client !== context.client) {
|
|
169
|
+
unsubscribe?.();
|
|
170
|
+
client = context.client;
|
|
171
|
+
unsubscribe = client?.getQueryCache().subscribe(event => {
|
|
172
|
+
if (event.query?.queryKey?.[0] === "rate-limit-status") schedule();
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
const windows = currentWindows(context);
|
|
176
|
+
if (!help || !context.router || !windows) { removeControl(); return; }
|
|
177
|
+
if (record?.help !== help || !record?.button.isConnected) { removeControl(); mount(help, rail); }
|
|
178
|
+
const signature = JSON.stringify(windows);
|
|
179
|
+
if (record.signature !== signature) { record.signature = signature; draw(windows); }
|
|
180
|
+
}
|
|
181
|
+
function schedule() {
|
|
182
|
+
if (queued || signal.aborted) return;
|
|
183
|
+
queued = true;
|
|
184
|
+
queueMicrotask(sync);
|
|
185
|
+
}
|
|
186
|
+
const observer = new MutationObserver(schedule);
|
|
187
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["aria-label"] });
|
|
188
|
+
window.addEventListener("resize", positionTooltip);
|
|
189
|
+
window.addEventListener("focus", schedule);
|
|
190
|
+
const cleanup = () => {
|
|
191
|
+
observer.disconnect();
|
|
192
|
+
unsubscribe?.();
|
|
193
|
+
window.removeEventListener("resize", positionTooltip);
|
|
194
|
+
window.removeEventListener("focus", schedule);
|
|
195
|
+
removeControl();
|
|
196
|
+
style.remove();
|
|
197
|
+
};
|
|
198
|
+
signal.addEventListener("abort", cleanup, { once: true });
|
|
199
|
+
sync();
|
|
200
|
+
return cleanup;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
"features/model-slider/THIRD_PARTY_NOTICES.md",
|
|
24
24
|
"features/model-slider/vendor",
|
|
25
25
|
"features/notes/src",
|
|
26
|
-
"features/notes/web",
|
|
27
26
|
"features/notes/assets",
|
|
28
27
|
"features/notes/codex-feature.json",
|
|
28
|
+
"features/notes/DESIGN.md",
|
|
29
|
+
"features/notes/README.md",
|
|
29
30
|
"features/images/src",
|
|
30
31
|
"features/images/web",
|
|
31
32
|
"features/images/codex-feature.json",
|
|
@@ -41,17 +42,28 @@
|
|
|
41
42
|
"apps/dashboard/favicon.svg",
|
|
42
43
|
"apps/dashboard/index.html",
|
|
43
44
|
"apps/dashboard/styles.css",
|
|
44
|
-
"README.md"
|
|
45
|
+
"README.md",
|
|
46
|
+
"features/usage-meter/src",
|
|
47
|
+
"features/usage-meter/codex-feature.json",
|
|
48
|
+
"features/usage-meter/DESIGN.md",
|
|
49
|
+
"features/usage-meter/README.md",
|
|
50
|
+
"features/unread-inbox/src",
|
|
51
|
+
"features/unread-inbox/codex-feature.json",
|
|
52
|
+
"features/unread-inbox/DESIGN.md",
|
|
53
|
+
"features/sidebar-focus/src",
|
|
54
|
+
"features/sidebar-focus/codex-feature.json",
|
|
55
|
+
"features/sidebar-focus/DESIGN.md",
|
|
56
|
+
"features/sidebar-focus/vendor/lucide-LICENSE"
|
|
45
57
|
],
|
|
46
58
|
"engines": {
|
|
47
59
|
"node": ">=22.5.0"
|
|
48
60
|
},
|
|
49
61
|
"scripts": {
|
|
50
62
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
51
|
-
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs apps/dashboard/test/*.test.mjs",
|
|
63
|
+
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/sidebar-focus/test/*.test.mjs",
|
|
52
64
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
53
65
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
54
|
-
"check": "node
|
|
66
|
+
"check": "node scripts/check-syntax.mjs",
|
|
55
67
|
"build:service": "scripts/build-service-app.zsh",
|
|
56
68
|
"pack:check": "npm pack --dry-run",
|
|
57
69
|
"pack:audit": "node scripts/audit-package.mjs",
|
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status"]);
|
|
5
|
+
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "unread-inbox", "sidebar-focus"]);
|
|
6
6
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
7
7
|
schemaVersion: 1,
|
|
8
8
|
features: Object.freeze({
|
|
@@ -11,6 +11,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
11
11
|
images: true,
|
|
12
12
|
"hide-upsell-banner": true,
|
|
13
13
|
"session-status": true,
|
|
14
|
+
"usage-meter": true,
|
|
15
|
+
"unread-inbox": true,
|
|
16
|
+
"sidebar-focus": true,
|
|
14
17
|
}),
|
|
15
18
|
});
|
|
16
19
|
|
|
@@ -196,8 +196,8 @@ async function iconResponse(id, paths, response) {
|
|
|
196
196
|
if (id === "model-slider") {
|
|
197
197
|
iconPath = path.join(paths.featuresRoot, id, "assets", "ui", "sparkles.svg");
|
|
198
198
|
} else if (id === "notes") {
|
|
199
|
-
iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "
|
|
200
|
-
} else if (["images", "hide-upsell-banner", "session-status"].includes(id)) {
|
|
199
|
+
iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "book-open.svg");
|
|
200
|
+
} else if (["images", "hide-upsell-banner", "session-status", "usage-meter"].includes(id)) {
|
|
201
201
|
const manifest = JSON.parse(await readFile(path.join(paths.featuresRoot, id, "codex-feature.json"), "utf8"));
|
|
202
202
|
const viewBox = manifest.icon?.viewBox ?? "0 0 24 24";
|
|
203
203
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
|
|
@@ -390,12 +390,14 @@ export function createInjectionSource(features, {
|
|
|
390
390
|
"config",
|
|
391
391
|
`"use strict";\n${source}`,
|
|
392
392
|
);
|
|
393
|
+
const pageConfig = feature.pageScript.config ??= {};
|
|
394
|
+
pageConfig.requestHostAction = (action, payload) => requestPageHostAction(feature.id, action, payload);
|
|
393
395
|
const cleanup = installer(
|
|
394
396
|
lifetime.signal,
|
|
395
397
|
window,
|
|
396
398
|
document,
|
|
397
399
|
MutationObserver,
|
|
398
|
-
|
|
400
|
+
pageConfig,
|
|
399
401
|
);
|
|
400
402
|
if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
|
|
401
403
|
} catch (error) {
|
|
@@ -2779,9 +2781,10 @@ export function createInjectionSource(features, {
|
|
|
2779
2781
|
}
|
|
2780
2782
|
// 原生开合的临时 min-width 钉定与弹层水平居中都基于对 Measurement
|
|
2781
2783
|
// 探针的测量;把探针文本同步为 MCX 显示标签,原生机制即可测得
|
|
2782
|
-
//
|
|
2784
|
+
// 真实显示宽度。保留 React 持有的子节点,避免切模型时卸载失败。
|
|
2783
2785
|
const measurement = trigger.querySelector('[class*="_ModelPickerTriggerMeasurement_"]');
|
|
2784
|
-
|
|
2786
|
+
const measurementText = measurement && firstVisibleTextNode(measurement);
|
|
2787
|
+
if (measurementText && measurementText.nodeValue !== label) measurementText.nodeValue = label;
|
|
2785
2788
|
}
|
|
2786
2789
|
|
|
2787
2790
|
function ensureModelSelector() {
|
|
@@ -4525,10 +4528,36 @@ export function createInjectionSource(features, {
|
|
|
4525
4528
|
}
|
|
4526
4529
|
}
|
|
4527
4530
|
|
|
4531
|
+
function requestPageHostAction(featureId, action, payload) {
|
|
4532
|
+
if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
|
|
4533
|
+
if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
|
|
4534
|
+
return new Promise((resolve, reject) => {
|
|
4535
|
+
const requestId = crypto.randomUUID();
|
|
4536
|
+
const timer = setTimeout(() => {
|
|
4537
|
+
pendingHostActions.delete(requestId);
|
|
4538
|
+
reject(new Error("Host request timed out"));
|
|
4539
|
+
}, 10_000);
|
|
4540
|
+
pendingHostActions.set(requestId, { kind: "page-script", resolve, reject, timer });
|
|
4541
|
+
try {
|
|
4542
|
+
globalThis[config.bindingName](JSON.stringify({ token: config.bindingToken, featureId, requestId, action, payload }));
|
|
4543
|
+
} catch (error) {
|
|
4544
|
+
clearTimeout(timer);
|
|
4545
|
+
pendingHostActions.delete(requestId);
|
|
4546
|
+
reject(error);
|
|
4547
|
+
}
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4528
4551
|
function resolveHostAction(requestId, response) {
|
|
4529
4552
|
const pending = pendingHostActions.get(requestId);
|
|
4530
4553
|
if (!pending) return false;
|
|
4531
4554
|
pendingHostActions.delete(requestId);
|
|
4555
|
+
if (pending.kind === "page-script") {
|
|
4556
|
+
clearTimeout(pending.timer);
|
|
4557
|
+
if (response.ok) pending.resolve(response.result);
|
|
4558
|
+
else pending.reject(new Error(response.error?.message ?? "Host request failed"));
|
|
4559
|
+
return true;
|
|
4560
|
+
}
|
|
4532
4561
|
const record = surfaceRecords.get(pending.recordKey);
|
|
4533
4562
|
if (!record) return false;
|
|
4534
4563
|
if (pending.kind === "surface-load") {
|
|
@@ -4711,6 +4740,12 @@ export function createInjectionSource(features, {
|
|
|
4711
4740
|
resolveHostAction,
|
|
4712
4741
|
dispose: () => {
|
|
4713
4742
|
lifetime.abort();
|
|
4743
|
+
for (const [id, pending] of pendingHostActions) {
|
|
4744
|
+
if (pending.kind !== "page-script") continue;
|
|
4745
|
+
clearTimeout(pending.timer);
|
|
4746
|
+
pending.reject(new Error("Runtime disposed"));
|
|
4747
|
+
pendingHostActions.delete(id);
|
|
4748
|
+
}
|
|
4714
4749
|
for (const cleanup of pageScriptCleanups.splice(0)) {
|
|
4715
4750
|
try { cleanup(); } catch (error) {
|
|
4716
4751
|
console.warn("[codex-personal] page-script cleanup failed", error);
|
|
@@ -47,15 +47,11 @@ export {
|
|
|
47
47
|
} from "./codex-injection.mjs";
|
|
48
48
|
|
|
49
49
|
export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (card?.getAttribute('data-composer-placement') === 'home') return null;
|
|
56
|
-
const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
|
|
57
|
-
return selected?.getAttribute('data-app-action-sidebar-thread-id')
|
|
58
|
-
?? location.pathname + location.search + location.hash;
|
|
50
|
+
return [...new Set([...document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')].map(composer => {
|
|
51
|
+
const card = composer.closest('[data-codex-composer-root]');
|
|
52
|
+
if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
|
|
53
|
+
return card.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
|
|
54
|
+
}).filter(Boolean))];
|
|
59
55
|
})()`;
|
|
60
56
|
|
|
61
57
|
export class CodexRuntime {
|
|
@@ -550,6 +546,29 @@ export class CodexRuntime {
|
|
|
550
546
|
});
|
|
551
547
|
return;
|
|
552
548
|
}
|
|
549
|
+
if (feature.id === "notes" && request.action === "quick-prompts") {
|
|
550
|
+
try {
|
|
551
|
+
const { route, method = "GET", body } = request.payload ?? {};
|
|
552
|
+
const allowed = (route === "/api/state" && method === "GET")
|
|
553
|
+
|| (["/api/prompts", "/api/categories", "/api/reorder"].includes(route) && method === "POST")
|
|
554
|
+
|| (/^\/api\/categories\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
|
|
555
|
+
|| (/^\/api\/prompts\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
|
|
556
|
+
|| (/^\/api\/prompts\/[a-zA-Z0-9-]+\/use$/.test(route) && method === "POST");
|
|
557
|
+
const origin = pageScriptServiceOrigin(feature);
|
|
558
|
+
if (!allowed || !origin) throw new Error("Prompt request is not allowed");
|
|
559
|
+
const response = await this.fetchImpl(new URL(route, origin), {
|
|
560
|
+
method, headers: { "Content-Type": "application/json", Origin: CODEX_APP_ORIGIN },
|
|
561
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
562
|
+
signal: AbortSignal.timeout(8_000),
|
|
563
|
+
});
|
|
564
|
+
const result = response.status === 204 ? null : await response.json();
|
|
565
|
+
if (!response.ok) throw new Error(result?.error?.message ?? "Prompt request failed");
|
|
566
|
+
await this.resolveHostAction(client, request.requestId, { ok: true, result });
|
|
567
|
+
} catch (error) {
|
|
568
|
+
await this.resolveHostAction(client, request.requestId, { ok: false, error: { code: "PROMPT_REQUEST_FAILED", message: error.message } });
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
553
572
|
if (request.action === "import-generated-image") {
|
|
554
573
|
await this.importGeneratedImage(feature, request.payload);
|
|
555
574
|
return;
|
|
@@ -741,17 +760,19 @@ export class CodexRuntime {
|
|
|
741
760
|
expression: SESSION_STATUS_THREAD_ID_EXPRESSION,
|
|
742
761
|
returnByValue: true,
|
|
743
762
|
}, { timeoutMs: 3_000 });
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
threadId
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
763
|
+
for (const value of response.result?.value ?? []) {
|
|
764
|
+
const threadId = threadIdFromLocation(value);
|
|
765
|
+
const data = threadId ? await this.sessionStatusReader.read(threadId) : null;
|
|
766
|
+
const snapshot = data ?? (threadId ? {
|
|
767
|
+
threadId,
|
|
768
|
+
retryAfterMs: this.sessionStatusReader.retryAfterMs?.(threadId) ?? 500,
|
|
769
|
+
} : null);
|
|
770
|
+
if (this.stopping || this.clients.get(targetId) !== client) return;
|
|
771
|
+
await client.send("Runtime.evaluate", {
|
|
772
|
+
expression: `window.__minecodexSessionStatus?.update(${JSON.stringify(snapshot)}, ${revision})`,
|
|
773
|
+
returnByValue: true,
|
|
774
|
+
}, { timeoutMs: 3_000 });
|
|
775
|
+
}
|
|
755
776
|
} catch (error) {
|
|
756
777
|
this.logger.warn?.("Session Status update failed", error.message);
|
|
757
778
|
if (this.clients.get(targetId) === client) {
|
|
@@ -70,9 +70,7 @@ function emptyState(filePath) {
|
|
|
70
70
|
contextWindow: null,
|
|
71
71
|
firstOutputAt: null,
|
|
72
72
|
pendingToolIntervals: [],
|
|
73
|
-
|
|
74
|
-
decodeMs: 0,
|
|
75
|
-
speedSamples: 0,
|
|
73
|
+
speedMeasurements: [],
|
|
76
74
|
ttftTotalMs: 0,
|
|
77
75
|
ttftSamples: 0,
|
|
78
76
|
toolMs: 0,
|
|
@@ -110,6 +108,9 @@ export function applySessionStatusEvent(state, event) {
|
|
|
110
108
|
const payload = event?.payload ?? {};
|
|
111
109
|
if (event?.type === "compacted") {
|
|
112
110
|
state.compactionRecords += 1;
|
|
111
|
+
state.last = null;
|
|
112
|
+
state.firstOutputAt = null;
|
|
113
|
+
state.pendingToolIntervals = [];
|
|
113
114
|
if (Number.isSafeInteger(payload.window_number) && payload.window_number >= 0) {
|
|
114
115
|
state.compactionWindow = Math.max(state.compactionWindow, payload.window_number);
|
|
115
116
|
}
|
|
@@ -134,12 +135,21 @@ export function applySessionStatusEvent(state, event) {
|
|
|
134
135
|
state.baseInstructionTokens = estimatedTextTokens(payload.base_instructions?.text);
|
|
135
136
|
}
|
|
136
137
|
if (event?.type === "event_msg") {
|
|
137
|
-
if (payload.type === "context_compacted")
|
|
138
|
+
if (payload.type === "context_compacted") {
|
|
139
|
+
state.compactionEvents += 1;
|
|
140
|
+
state.last = null;
|
|
141
|
+
state.firstOutputAt = null;
|
|
142
|
+
state.pendingToolIntervals = [];
|
|
143
|
+
}
|
|
138
144
|
if (payload.type === "task_started" && payload.turn_id) {
|
|
145
|
+
state.firstOutputAt = null;
|
|
146
|
+
state.pendingToolIntervals = [];
|
|
139
147
|
state.turns.add(payload.turn_id);
|
|
140
148
|
state.runningTurns.add(payload.turn_id);
|
|
141
149
|
}
|
|
142
150
|
if (["task_complete", "task_interrupted", "turn_aborted"].includes(payload.type)) {
|
|
151
|
+
state.firstOutputAt = null;
|
|
152
|
+
state.pendingToolIntervals = [];
|
|
143
153
|
if (payload.turn_id) state.runningTurns.delete(payload.turn_id);
|
|
144
154
|
else state.runningTurns.clear();
|
|
145
155
|
}
|
|
@@ -158,13 +168,17 @@ export function applySessionStatusEvent(state, event) {
|
|
|
158
168
|
if (TOOL_ITEMS.has(itemType) && Number.isFinite(start) && Number.isFinite(end)) {
|
|
159
169
|
state.toolMs += Math.max(0, end - start);
|
|
160
170
|
state.pendingToolIntervals.push([start, end]);
|
|
171
|
+
// 工具完成事件可能晚于用量记录落盘,补扣已记录样本中的重叠区间。
|
|
172
|
+
for (const sample of state.speedMeasurements) {
|
|
173
|
+
if (start < sample.end && end > sample.start) sample.tools.push([start, end]);
|
|
174
|
+
}
|
|
161
175
|
}
|
|
162
176
|
}
|
|
163
177
|
if (payload.type === "token_count") {
|
|
164
178
|
if (!state.hasResponseUsage && validUsage(payload.info?.total_token_usage)) {
|
|
165
179
|
state.total = payload.info.total_token_usage;
|
|
166
180
|
}
|
|
167
|
-
if (
|
|
181
|
+
if (validUsage(payload.info?.last_token_usage)) {
|
|
168
182
|
state.last = payload.info.last_token_usage;
|
|
169
183
|
}
|
|
170
184
|
if (Number.isFinite(payload.info?.model_context_window)) {
|
|
@@ -174,18 +188,18 @@ export function applySessionStatusEvent(state, event) {
|
|
|
174
188
|
}
|
|
175
189
|
if (event?.type === "token_usage_record") {
|
|
176
190
|
const responseId = payload.response_id;
|
|
177
|
-
if (responseId &&
|
|
191
|
+
if (responseId && state.responses.has(responseId)) return;
|
|
192
|
+
if (responseId) {
|
|
178
193
|
state.responses.add(responseId);
|
|
179
194
|
const responseEnd = Date.parse(event.timestamp);
|
|
180
|
-
const elapsed = state.firstOutputAt == null ? null : responseEnd - state.firstOutputAt
|
|
181
|
-
- overlappingToolMs(state.pendingToolIntervals, state.firstOutputAt, responseEnd);
|
|
182
195
|
const output = payload.usage?.output_tokens;
|
|
183
|
-
//
|
|
196
|
+
// 起止点来自会话事件,并非模型首末 token 时间;只能提供观测约值。
|
|
184
197
|
if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null
|
|
185
|
-
&& Number.isFinite(
|
|
186
|
-
state.
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
&& Number.isFinite(responseEnd) && responseEnd > state.firstOutputAt) {
|
|
199
|
+
state.speedMeasurements.push({
|
|
200
|
+
start: state.firstOutputAt, end: responseEnd, output,
|
|
201
|
+
tools: state.pendingToolIntervals.slice(),
|
|
202
|
+
});
|
|
189
203
|
}
|
|
190
204
|
}
|
|
191
205
|
state.firstOutputAt = null;
|
|
@@ -200,6 +214,16 @@ export function applySessionStatusEvent(state, event) {
|
|
|
200
214
|
}
|
|
201
215
|
|
|
202
216
|
export function sessionStatusSnapshot(state, threadId) {
|
|
217
|
+
let decodeTokens = 0;
|
|
218
|
+
let decodeMs = 0;
|
|
219
|
+
let speedSamples = 0;
|
|
220
|
+
for (const sample of state.speedMeasurements) {
|
|
221
|
+
const elapsed = sample.end - sample.start - overlappingToolMs(sample.tools, sample.start, sample.end);
|
|
222
|
+
if (elapsed <= 0 || elapsed >= 600_000) continue;
|
|
223
|
+
decodeTokens += sample.output;
|
|
224
|
+
decodeMs += elapsed;
|
|
225
|
+
speedSamples += 1;
|
|
226
|
+
}
|
|
203
227
|
const total = state.total;
|
|
204
228
|
const input = total?.input_tokens ?? 0;
|
|
205
229
|
const cached = total?.cached_input_tokens ?? 0;
|
|
@@ -218,9 +242,9 @@ export function sessionStatusSnapshot(state, threadId) {
|
|
|
218
242
|
uncachedInputTokens: total ? Math.max(0, input - cached - cacheWrite) : null,
|
|
219
243
|
outputTokens: total?.output_tokens ?? null,
|
|
220
244
|
cacheHitPercent: input > 0 ? Math.round(cached / input * 100) : null,
|
|
221
|
-
observedTps:
|
|
222
|
-
speedSamples
|
|
223
|
-
observedModelMs:
|
|
245
|
+
observedTps: decodeMs > 0 ? Math.round(decodeTokens / (decodeMs / 1000)) : null,
|
|
246
|
+
speedSamples,
|
|
247
|
+
observedModelMs: decodeMs,
|
|
224
248
|
toolMs: state.toolMs,
|
|
225
249
|
averageFirstTokenMs: state.ttftSamples > 0 ? Math.round(state.ttftTotalMs / state.ttftSamples) : null,
|
|
226
250
|
contextUsedTokens: contextUsed,
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-circle"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<circle cx="12" cy="12" r="10" />
|
|
15
|
-
</svg>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-ellipsis"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<circle cx="12" cy="12" r="1" />
|
|
15
|
-
<circle cx="19" cy="12" r="1" />
|
|
16
|
-
<circle cx="5" cy="12" r="1" />
|
|
17
|
-
</svg>
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-external-link"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M15 3h6v6" />
|
|
15
|
-
<path d="M10 14 21 3" />
|
|
16
|
-
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
17
|
-
</svg>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
<!-- @license lucide-static v1.31.0 - ISC -->
|
|
2
|
-
<svg
|
|
3
|
-
class="lucide lucide-list-todo"
|
|
4
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
-
width="24"
|
|
6
|
-
height="24"
|
|
7
|
-
viewBox="0 0 24 24"
|
|
8
|
-
fill="none"
|
|
9
|
-
stroke="currentColor"
|
|
10
|
-
stroke-width="2"
|
|
11
|
-
stroke-linecap="round"
|
|
12
|
-
stroke-linejoin="round"
|
|
13
|
-
>
|
|
14
|
-
<path d="M13 5h8" />
|
|
15
|
-
<path d="M13 12h8" />
|
|
16
|
-
<path d="M13 19h8" />
|
|
17
|
-
<path d="m3 17 2 2 4-4" />
|
|
18
|
-
<rect x="3" y="4" width="6" height="6" rx="1" />
|
|
19
|
-
</svg>
|