minecodex 2.0.2 → 2.0.3
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/DESIGN.md +149 -0
- package/apps/dashboard/DESIGN.md +45 -0
- package/features/images/DESIGN.md +71 -0
- package/features/images/README.md +2 -0
- package/features/images/codex-feature.json +2 -1
- package/features/images/src/adapters/codex/cloud-import.mjs +52 -0
- package/features/images/src/adapters/codex/image-prompt.mjs +22 -0
- package/features/images/src/adapters/codex/library-api.mjs +55 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +18 -0
- package/features/images/src/adapters/codex/prompt-resolver-source.mjs +8 -0
- package/features/images/src/backend/cloud-library.mjs +10 -0
- package/features/images/src/domain/library-prompt.mjs +7 -0
- package/features/images/src/library-prompt-page.mjs +7 -75
- package/features/images/src/library-prompt-source.mjs +10 -0
- package/features/images/web/app.js +9 -9
- package/features/images/web/styles.css +219 -219
- package/features/model-slider/DESIGN.md +34 -0
- package/features/model-slider/src/adapters/codex/controller.mjs +93 -0
- package/features/model-slider/src/domain/model-rules.mjs +143 -0
- package/features/model-slider/src/frontend/install.mjs +2110 -0
- package/features/model-slider/src/injection-source.mjs +10 -0
- package/features/notes/DESIGN.md +13 -15
- package/features/notes/README.md +7 -1
- package/features/notes/assets/icons/book-open-nav-fill.svg +1 -0
- package/features/notes/codex-feature.json +5 -1
- package/features/notes/src/{default-prompts.mjs → backend/default-prompts.mjs} +1 -1
- package/features/notes/src/{product.mjs → backend/product.mjs} +1 -1
- package/features/notes/src/{quick-prompts-server.mjs → backend/quick-prompts-server.mjs} +2 -2
- package/features/notes/src/{quick-prompts-page.mjs → frontend/entry.mjs} +64 -257
- package/features/notes/src/frontend/icons.json +13 -0
- package/features/notes/src/frontend/styles.css +110 -0
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/vendor/phosphor-LICENSE +21 -0
- package/features/session-status/DESIGN.md +7 -7
- package/features/session-status/README.md +4 -0
- package/features/session-status/codex-feature.json +1 -1
- package/features/session-status/src/adapters/codex/context-usage.mjs +39 -0
- package/features/session-status/src/adapters/codex/session-log.mjs +81 -0
- package/features/session-status/src/adapters/codex/session-reader.mjs +106 -0
- package/features/session-status/src/domain/session-stats.mjs +141 -0
- package/features/session-status/src/injection-source.mjs +6 -0
- package/features/session-status/src/session-status.mjs +41 -101
- package/features/sidebar-focus/DESIGN.md +6 -5
- package/features/sidebar-focus/codex-feature.json +2 -1
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +28 -0
- package/features/sidebar-focus/src/injection-source.mjs +6 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +19 -66
- package/features/sites/DESIGN.md +60 -0
- package/features/sites/README.md +27 -0
- package/features/sites/codex-feature.json +28 -0
- package/features/sites/src/adapters/codex/catalog.mjs +26 -0
- package/features/sites/src/adapters/codex/renderer.mjs +83 -0
- package/features/sites/src/backend/discovery.mjs +73 -0
- package/features/sites/src/backend/project-sites.mjs +34 -0
- package/features/sites/src/backend/site-launcher.mjs +54 -0
- package/features/sites/src/backend/sites-preview.mjs +53 -0
- package/features/sites/src/backend/sites-service.mjs +162 -0
- package/features/sites/src/domain/site.mjs +40 -0
- package/features/sites/src/frontend/entry.mjs +383 -0
- package/features/sites/src/frontend/icons.json +12 -0
- package/features/sites/src/frontend/styles.css +72 -0
- package/features/sites/src/injection-source.mjs +7 -0
- package/features/sites/vendor/lucide-LICENSE +35 -0
- package/features/unread-inbox/DESIGN.md +6 -2
- package/features/unread-inbox/codex-feature.json +2 -1
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +66 -0
- package/features/unread-inbox/src/injection-source.mjs +6 -0
- package/features/unread-inbox/src/unread-inbox.mjs +34 -89
- package/features/usage-meter/DESIGN.md +9 -9
- package/features/usage-meter/README.md +2 -1
- package/features/usage-meter/codex-feature.json +3 -3
- package/features/usage-meter/src/adapters/codex/renderer.mjs +61 -0
- package/features/usage-meter/src/injection-source.mjs +6 -0
- package/features/usage-meter/src/usage-meter.mjs +27 -66
- package/package.json +17 -4
- package/packages/cli/src/config.mjs +2 -1
- package/packages/codex-adapter/README.md +15 -0
- package/packages/codex-adapter/package.json +1 -0
- package/packages/codex-adapter/src/renderer/environment.mjs +53 -0
- package/packages/runtime-host/README.md +27 -0
- package/packages/runtime-host/src/codex-injection.mjs +32 -2393
- package/packages/runtime-host/src/codex-runtime.mjs +21 -3
- package/packages/runtime-host/src/feature-registry.mjs +28 -3
- package/packages/ui/DESIGN.md +31 -0
- package/packages/ui/package.json +8 -0
- package/packages/ui/src/components/button.mjs +21 -0
- package/packages/ui/src/components/composer-menu.mjs +18 -0
- package/packages/ui/src/components/dialog.mjs +55 -0
- package/packages/ui/src/components/navigation-rail.mjs +77 -0
- package/packages/ui/src/components/page.mjs +8 -0
- package/packages/ui/src/components/tooltip.mjs +96 -0
- package/packages/ui/src/foundation/layers.mjs +14 -0
- package/packages/ui/src/foundation/surfaces.mjs +34 -0
- package/packages/ui/src/install.mjs +22 -0
- package/packages/ui/src/native/navigation-rail.mjs +33 -0
- package/packages/ui/src/native/theme.mjs +35 -0
- package/packages/{runtime-host/src/codex-design-contract.mjs → ui/src/native/tokens.mjs} +67 -24
- package/packages/runtime-host/src/cloud-images.mjs +0 -123
- package/packages/runtime-host/src/feature-tooltips.mjs +0 -65
- package/packages/runtime-host/src/session-status-reader.mjs +0 -340
- /package/features/notes/src/{prompt-schema.mjs → backend/prompt-schema.mjs} +0 -0
|
@@ -92,35 +92,78 @@ export const CODEX_THEME_TOKENS = Object.freeze([
|
|
|
92
92
|
"--radius-lg",
|
|
93
93
|
"--radius-xl",
|
|
94
94
|
"--radius-2xl",
|
|
95
|
+
"--radius-3xl",
|
|
95
96
|
"--shadow-md",
|
|
96
97
|
"--shadow-lg",
|
|
97
98
|
"--shadow-xl",
|
|
99
|
+
"--color-surface",
|
|
100
|
+
"--color-text-primary",
|
|
101
|
+
"--color-text-secondary",
|
|
102
|
+
"--color-text-tertiary",
|
|
103
|
+
"--color-border-overlay",
|
|
104
|
+
"--color-ring",
|
|
105
|
+
"--color-dialog-overlay",
|
|
106
|
+
"--dialog-backdrop-fade-filter",
|
|
107
|
+
"--modal-container-inner-padding",
|
|
108
|
+
"--dropdown-overlay-ring-width",
|
|
109
|
+
"--shadow-dropdown-overlay",
|
|
110
|
+
"--popover-overlay-ring-width",
|
|
111
|
+
"--shadow-popover-overlay",
|
|
112
|
+
"--suggestion-menu-border-width",
|
|
113
|
+
"--suggestion-menu-shadow",
|
|
114
|
+
"--radius-suggestion-menu",
|
|
115
|
+
"--radius-xl-base",
|
|
116
|
+
"--radius-full",
|
|
117
|
+
"--blur-sm",
|
|
118
|
+
"--blur-xl",
|
|
119
|
+
"--shadow-xl-spread",
|
|
120
|
+
"--control-size-lg",
|
|
121
|
+
"--control-font-size-md",
|
|
122
|
+
"--control-gutter-md",
|
|
123
|
+
"--control-gutter-pill-scaling",
|
|
124
|
+
"--button-gap-lg",
|
|
125
|
+
"--font-weight-semibold",
|
|
126
|
+
"--color-border-primary-outline",
|
|
127
|
+
"--color-background-primary-outline-hover",
|
|
128
|
+
"--color-background-primary-solid-hover",
|
|
129
|
+
"--color-background-tooltip",
|
|
130
|
+
"--color-text-tooltip",
|
|
131
|
+
"--color-border-tooltip",
|
|
132
|
+
"--shadow-tooltip",
|
|
133
|
+
"--tooltip-compact-font-weight",
|
|
134
|
+
"--tracking-tooltip",
|
|
135
|
+
"--color-background-danger-soft-alpha",
|
|
136
|
+
"--color-background-danger-soft-alpha-hover",
|
|
137
|
+
"--color-text-danger",
|
|
98
138
|
]);
|
|
99
139
|
|
|
100
|
-
// images
|
|
140
|
+
// images 图片编辑器可用性依赖的项目 token 子集。
|
|
101
141
|
// 与 features/images/web/app.js 的 editorTokens 保持同一来源(一致性由测试锁定)。
|
|
102
142
|
export const EDITOR_TOKEN_NAMES = Object.freeze([
|
|
103
|
-
"--color-surface-tertiary",
|
|
104
|
-
"--color-surface-elevated-secondary",
|
|
105
|
-
"--color-border-subtle",
|
|
106
|
-
"--color-background-primary-ghost-hover",
|
|
107
|
-
"--color-chart-blue",
|
|
108
|
-
"--color-text-inverse",
|
|
109
|
-
"--color-token-main-surface-primary",
|
|
110
|
-
"--color-token-input-foreground",
|
|
111
|
-
"--color-text-foreground",
|
|
112
|
-
"--color-text-foreground-tertiary",
|
|
113
|
-
"--color-text-on-accent",
|
|
114
|
-
"--color-border",
|
|
115
|
-
"--color-border-focus",
|
|
116
|
-
"--color-token-list-hover-background",
|
|
117
|
-
"--color-text-accent",
|
|
118
|
-
"--font-sans-default",
|
|
119
|
-
"--font-weight-normal",
|
|
120
|
-
"--text-sm",
|
|
121
|
-
"--text-sm--line-height",
|
|
122
|
-
"--radius-md",
|
|
123
|
-
"--radius-xl",
|
|
124
|
-
"--shadow-md",
|
|
125
|
-
"--shadow-xl",
|
|
143
|
+
"--mcx-color-surface-tertiary",
|
|
144
|
+
"--mcx-color-surface-elevated-secondary",
|
|
145
|
+
"--mcx-color-border-subtle",
|
|
146
|
+
"--mcx-color-background-primary-ghost-hover",
|
|
147
|
+
"--mcx-color-chart-blue",
|
|
148
|
+
"--mcx-color-text-inverse",
|
|
149
|
+
"--mcx-color-token-main-surface-primary",
|
|
150
|
+
"--mcx-color-token-input-foreground",
|
|
151
|
+
"--mcx-color-text-foreground",
|
|
152
|
+
"--mcx-color-text-foreground-tertiary",
|
|
153
|
+
"--mcx-color-text-on-accent",
|
|
154
|
+
"--mcx-color-border",
|
|
155
|
+
"--mcx-color-border-focus",
|
|
156
|
+
"--mcx-color-token-list-hover-background",
|
|
157
|
+
"--mcx-color-text-accent",
|
|
158
|
+
"--mcx-font-sans-default",
|
|
159
|
+
"--mcx-font-weight-normal",
|
|
160
|
+
"--mcx-text-sm",
|
|
161
|
+
"--mcx-text-sm--line-height",
|
|
162
|
+
"--mcx-radius-md",
|
|
163
|
+
"--mcx-radius-xl",
|
|
164
|
+
"--mcx-shadow-md",
|
|
165
|
+
"--mcx-shadow-xl",
|
|
126
166
|
]);
|
|
167
|
+
|
|
168
|
+
// 插件只消费 MineCodex 命名空间,原生名称变化集中在此映射。
|
|
169
|
+
export const MINECODEX_THEME_BINDINGS = Object.freeze(Object.fromEntries(CODEX_THEME_TOKENS.map(name => [name, "--mcx-" + name.slice(2)])));
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { evaluationValue } from "./codex-cdp.mjs";
|
|
2
|
-
|
|
3
|
-
// 在原生 Renderer 内使用现有登录态;凭据和临时下载地址不跨出 Renderer。
|
|
4
|
-
export async function cloudImagesInRenderer(operation, payload) {
|
|
5
|
-
const key = Symbol.for("minecodex.cloud-images.v1");
|
|
6
|
-
const state = window[key] ??= { files: new Map(), conversations: new Map() };
|
|
7
|
-
if (!state.capability) {
|
|
8
|
-
state.capability = (async () => {
|
|
9
|
-
const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
|
|
10
|
-
.map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
11
|
-
if (!initialUrl) throw new Error("Cloud Library is unavailable in this Codex version");
|
|
12
|
-
const initialSource = await (await fetch(initialUrl)).text();
|
|
13
|
-
const asset = name => {
|
|
14
|
-
const file = initialSource.match(new RegExp(`${name}-[\\w-]+\\.js`))?.[0];
|
|
15
|
-
if (!file) throw new Error(`Cloud Library capability ${name} is unavailable`);
|
|
16
|
-
return new URL(file, initialUrl).href;
|
|
17
|
-
};
|
|
18
|
-
const metadataUrl = asset("library-file-metadata");
|
|
19
|
-
const metadataSource = await (await fetch(metadataUrl)).text();
|
|
20
|
-
const apiLocal = metadataSource.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
|
|
21
|
-
const importBlock = [...metadataSource.matchAll(/import\{([^}]+)\}from["']([^"']*app-initial[^"']+)["']/g)][0];
|
|
22
|
-
const apiExport = importBlock?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)).find(parts => parts.at(-1) === apiLocal)?.[0];
|
|
23
|
-
if (!apiExport) throw new Error("Cloud Library API contract changed");
|
|
24
|
-
const metadataInit = metadataSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
25
|
-
const metadataExports = metadataSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
26
|
-
const initExport = metadataExports.find(parts => parts[0] === metadataInit)?.at(-1);
|
|
27
|
-
const metadata = await import(metadataUrl);
|
|
28
|
-
if (typeof metadata[initExport] !== "function") throw new Error("Cloud Library initialization contract changed");
|
|
29
|
-
metadata[initExport]();
|
|
30
|
-
const module = await import(initialUrl);
|
|
31
|
-
const api = module[apiExport];
|
|
32
|
-
if (!api?.safeGet || !api?.safePost) throw new Error("Cloud Library API is not ready");
|
|
33
|
-
const downloadUrl = asset("library-file-download");
|
|
34
|
-
const downloadSource = await (await fetch(downloadUrl)).text();
|
|
35
|
-
const exports = downloadSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
36
|
-
const responseLocal = downloadSource.match(/async function ([\w$]+)\([^)]*\)\{return [\w$]+\(await [\w$]+\([^;]+showInline:!0/)?.[1];
|
|
37
|
-
const initLocal = downloadSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
38
|
-
const exported = local => exports.find(parts => parts[0] === local)?.at(-1);
|
|
39
|
-
const download = await import(downloadUrl);
|
|
40
|
-
const initialize = download[exported(initLocal)], response = download[exported(responseLocal)];
|
|
41
|
-
if (typeof initialize !== "function" || typeof response !== "function") throw new Error("Cloud image download contract changed");
|
|
42
|
-
initialize();
|
|
43
|
-
return { api, response };
|
|
44
|
-
})().catch(error => { state.capability = null; throw error; });
|
|
45
|
-
}
|
|
46
|
-
const { api, response } = await state.capability;
|
|
47
|
-
if (operation === "list") {
|
|
48
|
-
if (!payload.cursor) { state.files.clear(); state.conversations.clear(); }
|
|
49
|
-
const result = await api.safePost("/files/library", {
|
|
50
|
-
requestBody: { limit: 50, cursor: payload.cursor ?? null, include_hidden_files: false, include_saved_entities: true },
|
|
51
|
-
signal: AbortSignal.timeout(25_000),
|
|
52
|
-
});
|
|
53
|
-
const files = result.items.filter(file => ["image/png", "image/jpeg", "image/webp"].includes(file.mime_type)
|
|
54
|
-
&& file.origination_thread_id && file.origination_message_id && !file.trashed_at);
|
|
55
|
-
for (const file of files) state.files.set(file.id, file);
|
|
56
|
-
return { items: files.map(file => ({ libraryId: file.id, version: `${file.current_version_number}:${file.updated_at}`, name: file.file_name })), cursor: result.cursor ?? null };
|
|
57
|
-
}
|
|
58
|
-
const file = state.files.get(payload.libraryId);
|
|
59
|
-
if (!file) throw new Error("Cloud image must come from the current Library listing");
|
|
60
|
-
const threadId = file.origination_thread_id;
|
|
61
|
-
if (!state.conversations.has(threadId)) {
|
|
62
|
-
const request = api.safeGet("/conversation/{conversation_id}", {
|
|
63
|
-
parameters: { path: { conversation_id: threadId } }, signal: AbortSignal.timeout(25_000),
|
|
64
|
-
}).catch(error => { state.conversations.delete(threadId); throw error; });
|
|
65
|
-
state.conversations.set(threadId, request);
|
|
66
|
-
}
|
|
67
|
-
const conversation = await state.conversations.get(threadId);
|
|
68
|
-
const node = conversation.mapping?.[file.origination_message_id];
|
|
69
|
-
const parts = node?.message?.content?.parts ?? [];
|
|
70
|
-
const part = parts.find(part => typeof part === "object" && part.content_type === "image_asset_pointer"
|
|
71
|
-
&& (part.asset_pointer === `sediment://${file.file_id}` || part.asset_pointer === file.file_id));
|
|
72
|
-
// 明确验证生成结果,避免把用户上传的参考图混入生成图库。
|
|
73
|
-
if (!part || !["assistant", "tool"].includes(node.message.author?.role)
|
|
74
|
-
|| !(part.metadata?.generation?.gen_id || part.metadata?.dalle?.gen_id)) return { skipped: true };
|
|
75
|
-
let prompt = part.metadata?.dalle?.prompt || null;
|
|
76
|
-
let promptKind = prompt ? "generation" : null;
|
|
77
|
-
if (!prompt) {
|
|
78
|
-
let ancestor = conversation.mapping[node.parent];
|
|
79
|
-
const visited = new Set();
|
|
80
|
-
while (ancestor && !visited.has(ancestor.id)) {
|
|
81
|
-
visited.add(ancestor.id);
|
|
82
|
-
if (ancestor.message?.author?.role === "user") {
|
|
83
|
-
prompt = ancestor.message.content?.parts?.filter(p => typeof p === "string").join("\n") || null;
|
|
84
|
-
promptKind = prompt ? "request" : null;
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
87
|
-
ancestor = conversation.mapping[ancestor.parent];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
const max = 32 * 1024 * 1024;
|
|
91
|
-
if (file.file_size_bytes > max) throw new Error("Cloud image exceeds 32 MB");
|
|
92
|
-
const imageBlob = await response(file, AbortSignal.timeout(60_000));
|
|
93
|
-
if (!(imageBlob instanceof Blob) || imageBlob.size > max) throw new Error("Cloud image download returned an invalid image");
|
|
94
|
-
const reader = imageBlob.stream().getReader(), chunks = [];
|
|
95
|
-
let length = 0;
|
|
96
|
-
try {
|
|
97
|
-
while (true) {
|
|
98
|
-
const {done, value} = await reader.read(); if (done) break;
|
|
99
|
-
length += value.length;
|
|
100
|
-
if (length > max) throw new Error("Cloud image exceeds 32 MB");
|
|
101
|
-
chunks.push(value);
|
|
102
|
-
}
|
|
103
|
-
} finally { await reader.cancel().catch(() => {}); }
|
|
104
|
-
let binary = "";
|
|
105
|
-
for (const chunk of chunks) for (let i = 0; i < chunk.length; i += 0x8000) binary += String.fromCharCode(...chunk.subarray(i, i + 0x8000));
|
|
106
|
-
return {
|
|
107
|
-
bytesBase64: btoa(binary), mimeType: imageBlob.type.split(";")[0] || file.mime_type,
|
|
108
|
-
itemId: file.file_id, conversationId: threadId, conversationTitle: conversation.title,
|
|
109
|
-
sourceKind: "chat", prompt, promptKind,
|
|
110
|
-
cloudLibraryId: file.id, cloudVersion: `${file.current_version_number}:${file.updated_at}`,
|
|
111
|
-
createdAt: new Date((node.message.create_time || conversation.create_time) * 1000).toISOString(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function cloudImagesAction(client, operation, payload = {}) {
|
|
116
|
-
if (!["list", "import"].includes(operation)) throw new Error("Unknown cloud image operation");
|
|
117
|
-
if (payload.cursor != null && (typeof payload.cursor !== "string" || payload.cursor.length > 4096)) throw new Error("Invalid Library cursor");
|
|
118
|
-
if (operation === "import" && (typeof payload.libraryId !== "string" || !/^libfile_[\w-]+$/.test(payload.libraryId))) throw new Error("Invalid Library file ID");
|
|
119
|
-
return evaluationValue(await client.send("Runtime.evaluate", {
|
|
120
|
-
expression: `(${cloudImagesInRenderer.toString()})(${JSON.stringify(operation)},${JSON.stringify(payload)})`,
|
|
121
|
-
awaitPromise: true, returnByValue: true,
|
|
122
|
-
}, { timeoutMs: 110_000 }));
|
|
123
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
// 自包含函数随运行时注入;只处理 MineCodex 标记,不接管原生 tooltip。
|
|
2
|
-
export function installFeatureTooltips(signal) {
|
|
3
|
-
const attribute = "data-minecodex-tooltip";
|
|
4
|
-
const tip = document.createElement("div");
|
|
5
|
-
tip.id = "minecodex-feature-tooltip";
|
|
6
|
-
tip.setAttribute("role", "tooltip");
|
|
7
|
-
tip.hidden = true;
|
|
8
|
-
tip.style.cssText = `position:fixed;z-index:100;pointer-events:none;box-sizing:border-box;
|
|
9
|
-
zoom:var(--codex-window-zoom,1);max-width:min(320px,calc(100vw - 16px));
|
|
10
|
-
padding:5px 12px;border:1px solid var(--color-border-tooltip);border-radius:var(--radius-2xl);
|
|
11
|
-
background:var(--color-background-tooltip);color:var(--color-text-tooltip);box-shadow:var(--shadow-tooltip);
|
|
12
|
-
font-family:var(--font-sans-default);font-size:var(--text-sm);line-height:18px;
|
|
13
|
-
font-weight:var(--tooltip-compact-font-weight);letter-spacing:var(--tracking-tooltip);
|
|
14
|
-
text-align:center;overflow-wrap:anywhere;user-select:none;`;
|
|
15
|
-
let anchor = null;
|
|
16
|
-
let timer;
|
|
17
|
-
let frame;
|
|
18
|
-
function hide() {
|
|
19
|
-
clearTimeout(timer);
|
|
20
|
-
cancelAnimationFrame(frame);
|
|
21
|
-
if (anchor) {
|
|
22
|
-
const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(id => id && id !== tip.id);
|
|
23
|
-
if (ids.length) anchor.setAttribute("aria-describedby", ids.join(" "));
|
|
24
|
-
else anchor.removeAttribute("aria-describedby");
|
|
25
|
-
}
|
|
26
|
-
anchor = null;
|
|
27
|
-
tip.hidden = true;
|
|
28
|
-
}
|
|
29
|
-
function position() {
|
|
30
|
-
if (!anchor?.isConnected || !anchor.getClientRects().length || anchor.getAttribute("aria-expanded") === "true") return hide();
|
|
31
|
-
const zoom = Number.parseFloat(getComputedStyle(tip).zoom) || 1;
|
|
32
|
-
const r = anchor.getBoundingClientRect();
|
|
33
|
-
const w = tip.offsetWidth, h = tip.offsetHeight;
|
|
34
|
-
const top = r.top / zoom - h - 8;
|
|
35
|
-
tip.style.left = `${Math.max(8, Math.min((r.left + r.width / 2) / zoom - w / 2, innerWidth / zoom - w - 8))}px`;
|
|
36
|
-
tip.style.top = `${top >= 8 ? top : r.bottom / zoom + 8}px`;
|
|
37
|
-
frame = requestAnimationFrame(position);
|
|
38
|
-
}
|
|
39
|
-
function show(target, immediate = false) {
|
|
40
|
-
if (!target || target === anchor) return;
|
|
41
|
-
hide();
|
|
42
|
-
if (target.getAttribute("aria-expanded") === "true" || !target.getAttribute(attribute)) return;
|
|
43
|
-
anchor = target;
|
|
44
|
-
timer = setTimeout(() => {
|
|
45
|
-
if (!anchor?.isConnected) return hide();
|
|
46
|
-
if (!tip.isConnected) document.body.append(tip);
|
|
47
|
-
tip.textContent = anchor.getAttribute(attribute);
|
|
48
|
-
tip.hidden = false;
|
|
49
|
-
const ids = (anchor.getAttribute("aria-describedby") || "").split(/\s+/).filter(Boolean);
|
|
50
|
-
anchor.setAttribute("aria-describedby", [...new Set([...ids, tip.id])].join(" "));
|
|
51
|
-
position();
|
|
52
|
-
}, immediate ? 0 : 450);
|
|
53
|
-
}
|
|
54
|
-
const targetOf = event => event.target instanceof Element ? event.target.closest(`[${attribute}]`) : null;
|
|
55
|
-
document.addEventListener("pointerover", event => show(targetOf(event)), { signal });
|
|
56
|
-
document.addEventListener("pointerout", event => {
|
|
57
|
-
if (anchor && !anchor.contains(event.relatedTarget)) hide();
|
|
58
|
-
}, { signal });
|
|
59
|
-
document.addEventListener("focusin", event => show(targetOf(event), true), { signal });
|
|
60
|
-
document.addEventListener("focusout", hide, { signal });
|
|
61
|
-
document.addEventListener("pointerdown", hide, { signal, capture: true });
|
|
62
|
-
document.addEventListener("keydown", event => { if (event.key === "Escape") hide(); }, { signal });
|
|
63
|
-
window.addEventListener("blur", hide, { signal });
|
|
64
|
-
signal.addEventListener("abort", () => { hide(); tip.remove(); }, { once: true });
|
|
65
|
-
}
|
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
import { createReadStream } from "node:fs";
|
|
2
|
-
import { readdir, stat } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
|
|
6
|
-
const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7
|
-
const MODEL_OUTPUT_ITEMS = new Set(["Reasoning", "AgentMessage"]);
|
|
8
|
-
const TOOL_ITEMS = new Set(["CommandExecution", "McpToolCall", "DynamicToolCall"]);
|
|
9
|
-
const IMAGE_TOKENS_ESTIMATE = 1844;
|
|
10
|
-
|
|
11
|
-
function estimatedTextTokens(value) {
|
|
12
|
-
return typeof value === "string" ? Math.ceil(Buffer.byteLength(value, "utf8") / 4) : 0;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function estimatedContentTokens(content) {
|
|
16
|
-
if (!Array.isArray(content)) return 0;
|
|
17
|
-
return content.reduce((total, item) => {
|
|
18
|
-
if (item?.type === "input_image" || item?.type === "output_image") return total + IMAGE_TOKENS_ESTIMATE;
|
|
19
|
-
return total + estimatedTextTokens(item?.text);
|
|
20
|
-
}, 0);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function estimatedEncryptedTokens(value) {
|
|
24
|
-
if (typeof value !== "string") return 0;
|
|
25
|
-
// 参考 Codex 的历史粗估:先去掉加密封装,再按约 4 字节/token 换算。
|
|
26
|
-
return Math.ceil(Math.max(0, Math.floor(value.length * 3 / 4) - 650) / 4);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function estimatedSections(item) {
|
|
30
|
-
const type = item?.type;
|
|
31
|
-
if (type === "message") {
|
|
32
|
-
const tokens = estimatedContentTokens(item.content);
|
|
33
|
-
return item.role === "developer" || item.role === "system"
|
|
34
|
-
? { instructions: tokens, messages: 0, tools: 0 }
|
|
35
|
-
: { instructions: 0, messages: tokens, tools: 0 };
|
|
36
|
-
}
|
|
37
|
-
if (type === "function_call" || type === "custom_tool_call") {
|
|
38
|
-
return { instructions: 0, messages: 0, tools: estimatedTextTokens(item.arguments ?? item.input) };
|
|
39
|
-
}
|
|
40
|
-
if (type === "function_call_output" || type === "custom_tool_call_output") {
|
|
41
|
-
return { instructions: 0, messages: 0, tools: estimatedContentTokens(item.output) };
|
|
42
|
-
}
|
|
43
|
-
if (type === "reasoning" || type === "compaction") {
|
|
44
|
-
return { instructions: 0, messages: estimatedContentTokens(item.summary)
|
|
45
|
-
+ estimatedEncryptedTokens(item.encrypted_content), tools: 0 };
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function addSections(target, sections) {
|
|
51
|
-
if (!sections) return;
|
|
52
|
-
for (const key of ["instructions", "messages", "tools"]) target[key] += sections[key];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function threadIdFromLocation(value) {
|
|
56
|
-
return String(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;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function emptyState(filePath) {
|
|
60
|
-
return {
|
|
61
|
-
filePath,
|
|
62
|
-
offset: 0,
|
|
63
|
-
pending: "",
|
|
64
|
-
turns: new Set(),
|
|
65
|
-
runningTurns: new Set(),
|
|
66
|
-
responses: new Set(),
|
|
67
|
-
hasResponseUsage: false,
|
|
68
|
-
total: null,
|
|
69
|
-
last: null,
|
|
70
|
-
contextWindow: null,
|
|
71
|
-
firstOutputAt: null,
|
|
72
|
-
pendingToolIntervals: [],
|
|
73
|
-
speedMeasurements: [],
|
|
74
|
-
ttftTotalMs: 0,
|
|
75
|
-
ttftSamples: 0,
|
|
76
|
-
toolMs: 0,
|
|
77
|
-
compactionRecords: 0,
|
|
78
|
-
compactionCompleted: 0,
|
|
79
|
-
compactionEvents: 0,
|
|
80
|
-
compactionWindow: 0,
|
|
81
|
-
baseInstructionTokens: 0,
|
|
82
|
-
visibleSections: { instructions: 0, messages: 0, tools: 0 },
|
|
83
|
-
hasVisibleHistory: false,
|
|
84
|
-
updatedAt: null,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function validUsage(value) {
|
|
89
|
-
return value && ["input_tokens", "cached_input_tokens", "output_tokens", "total_tokens"]
|
|
90
|
-
.every((key) => Number.isFinite(value[key]) && value[key] >= 0);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function overlappingToolMs(intervals, start, end) {
|
|
94
|
-
const clipped = intervals
|
|
95
|
-
.map(([toolStart, toolEnd]) => [Math.max(start, toolStart), Math.min(end, toolEnd)])
|
|
96
|
-
.filter(([left, right]) => right > left)
|
|
97
|
-
.sort((left, right) => left[0] - right[0]);
|
|
98
|
-
let elapsed = 0;
|
|
99
|
-
let rightEdge = start;
|
|
100
|
-
for (const [left, right] of clipped) {
|
|
101
|
-
elapsed += Math.max(0, right - Math.max(left, rightEdge));
|
|
102
|
-
rightEdge = Math.max(rightEdge, right);
|
|
103
|
-
}
|
|
104
|
-
return elapsed;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export function applySessionStatusEvent(state, event) {
|
|
108
|
-
const payload = event?.payload ?? {};
|
|
109
|
-
if (event?.type === "compacted") {
|
|
110
|
-
state.compactionRecords += 1;
|
|
111
|
-
state.last = null;
|
|
112
|
-
state.firstOutputAt = null;
|
|
113
|
-
state.pendingToolIntervals = [];
|
|
114
|
-
if (Number.isSafeInteger(payload.window_number) && payload.window_number >= 0) {
|
|
115
|
-
state.compactionWindow = Math.max(state.compactionWindow, payload.window_number);
|
|
116
|
-
}
|
|
117
|
-
// 压缩会替换活跃历史;不能继续累计压缩前的消息与工具记录。
|
|
118
|
-
state.visibleSections = { instructions: 0, messages: 0, tools: 0 };
|
|
119
|
-
state.hasVisibleHistory = false;
|
|
120
|
-
if (Array.isArray(payload.replacement_history)) {
|
|
121
|
-
for (const item of payload.replacement_history) addSections(state.visibleSections, estimatedSections(item));
|
|
122
|
-
state.hasVisibleHistory = true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
if (event?.type === "response_item") {
|
|
126
|
-
const sections = estimatedSections(payload);
|
|
127
|
-
if (sections) {
|
|
128
|
-
addSections(state.visibleSections, sections);
|
|
129
|
-
state.hasVisibleHistory = true;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (event?.type === "turn_context" && payload.turn_id) state.turns.add(payload.turn_id);
|
|
133
|
-
if (event?.type === "session_meta") {
|
|
134
|
-
if (Number.isFinite(payload.context_window)) state.contextWindow = payload.context_window;
|
|
135
|
-
state.baseInstructionTokens = estimatedTextTokens(payload.base_instructions?.text);
|
|
136
|
-
}
|
|
137
|
-
if (event?.type === "event_msg") {
|
|
138
|
-
if (payload.type === "context_compacted") {
|
|
139
|
-
state.compactionEvents += 1;
|
|
140
|
-
state.last = null;
|
|
141
|
-
state.firstOutputAt = null;
|
|
142
|
-
state.pendingToolIntervals = [];
|
|
143
|
-
}
|
|
144
|
-
if (payload.type === "task_started" && payload.turn_id) {
|
|
145
|
-
state.firstOutputAt = null;
|
|
146
|
-
state.pendingToolIntervals = [];
|
|
147
|
-
state.turns.add(payload.turn_id);
|
|
148
|
-
state.runningTurns.add(payload.turn_id);
|
|
149
|
-
}
|
|
150
|
-
if (["task_complete", "task_interrupted", "turn_aborted"].includes(payload.type)) {
|
|
151
|
-
state.firstOutputAt = null;
|
|
152
|
-
state.pendingToolIntervals = [];
|
|
153
|
-
if (payload.turn_id) state.runningTurns.delete(payload.turn_id);
|
|
154
|
-
else state.runningTurns.clear();
|
|
155
|
-
}
|
|
156
|
-
if (payload.type === "task_complete" && Number.isFinite(payload.time_to_first_token_ms)) {
|
|
157
|
-
state.ttftTotalMs += payload.time_to_first_token_ms;
|
|
158
|
-
state.ttftSamples += 1;
|
|
159
|
-
}
|
|
160
|
-
if (payload.type === "item_completed") {
|
|
161
|
-
const itemType = payload.item?.type;
|
|
162
|
-
if (itemType === "ContextCompaction") state.compactionCompleted += 1;
|
|
163
|
-
const start = payload.started_at_ms;
|
|
164
|
-
const end = payload.completed_at_ms;
|
|
165
|
-
if (MODEL_OUTPUT_ITEMS.has(itemType) && Number.isFinite(start)) {
|
|
166
|
-
state.firstOutputAt = Math.min(state.firstOutputAt ?? start, start);
|
|
167
|
-
}
|
|
168
|
-
if (TOOL_ITEMS.has(itemType) && Number.isFinite(start) && Number.isFinite(end)) {
|
|
169
|
-
state.toolMs += Math.max(0, end - start);
|
|
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
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (payload.type === "token_count") {
|
|
178
|
-
if (!state.hasResponseUsage && validUsage(payload.info?.total_token_usage)) {
|
|
179
|
-
state.total = payload.info.total_token_usage;
|
|
180
|
-
}
|
|
181
|
-
if (validUsage(payload.info?.last_token_usage)) {
|
|
182
|
-
state.last = payload.info.last_token_usage;
|
|
183
|
-
}
|
|
184
|
-
if (Number.isFinite(payload.info?.model_context_window)) {
|
|
185
|
-
state.contextWindow = payload.info.model_context_window;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (event?.type === "token_usage_record") {
|
|
190
|
-
const responseId = payload.response_id;
|
|
191
|
-
if (responseId && state.responses.has(responseId)) return;
|
|
192
|
-
if (responseId) {
|
|
193
|
-
state.responses.add(responseId);
|
|
194
|
-
const responseEnd = Date.parse(event.timestamp);
|
|
195
|
-
const output = payload.usage?.output_tokens;
|
|
196
|
-
// 起止点来自会话事件,并非模型首末 token 时间;只能提供观测约值。
|
|
197
|
-
if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null
|
|
198
|
-
&& Number.isFinite(responseEnd) && responseEnd > state.firstOutputAt) {
|
|
199
|
-
state.speedMeasurements.push({
|
|
200
|
-
start: state.firstOutputAt, end: responseEnd, output,
|
|
201
|
-
tools: state.pendingToolIntervals.slice(),
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
state.firstOutputAt = null;
|
|
206
|
-
state.pendingToolIntervals = [];
|
|
207
|
-
if (validUsage(payload.thread_token_usage)) {
|
|
208
|
-
state.total = payload.thread_token_usage;
|
|
209
|
-
state.hasResponseUsage = true;
|
|
210
|
-
}
|
|
211
|
-
if (validUsage(payload.usage)) state.last = payload.usage;
|
|
212
|
-
}
|
|
213
|
-
if (state.total || state.turns.size) state.updatedAt = event.timestamp ?? state.updatedAt;
|
|
214
|
-
}
|
|
215
|
-
|
|
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
|
-
}
|
|
227
|
-
const total = state.total;
|
|
228
|
-
const input = total?.input_tokens ?? 0;
|
|
229
|
-
const cached = total?.cached_input_tokens ?? 0;
|
|
230
|
-
const cacheWrite = total?.cache_write_input_tokens ?? 0;
|
|
231
|
-
const contextUsed = state.last?.total_tokens ?? null;
|
|
232
|
-
const contextWindow = state.contextWindow > 0 ? state.contextWindow : null;
|
|
233
|
-
return {
|
|
234
|
-
threadId,
|
|
235
|
-
isRunning: state.runningTurns.size > 0,
|
|
236
|
-
turns: state.turns.size,
|
|
237
|
-
steps: state.responses.size,
|
|
238
|
-
totalTokens: total?.total_tokens ?? null,
|
|
239
|
-
inputTokens: total?.input_tokens ?? null,
|
|
240
|
-
cachedInputTokens: total?.cached_input_tokens ?? null,
|
|
241
|
-
cacheWriteInputTokens: total?.cache_write_input_tokens ?? null,
|
|
242
|
-
uncachedInputTokens: total ? Math.max(0, input - cached - cacheWrite) : null,
|
|
243
|
-
outputTokens: total?.output_tokens ?? null,
|
|
244
|
-
cacheHitPercent: input > 0 ? Math.round(cached / input * 100) : null,
|
|
245
|
-
observedTps: decodeMs > 0 ? Math.round(decodeTokens / (decodeMs / 1000)) : null,
|
|
246
|
-
speedSamples,
|
|
247
|
-
observedModelMs: decodeMs,
|
|
248
|
-
toolMs: state.toolMs,
|
|
249
|
-
averageFirstTokenMs: state.ttftSamples > 0 ? Math.round(state.ttftTotalMs / state.ttftSamples) : null,
|
|
250
|
-
contextUsedTokens: contextUsed,
|
|
251
|
-
contextWindow,
|
|
252
|
-
contextPercent: contextUsed != null && contextWindow != null
|
|
253
|
-
? Math.min(100, Math.round(contextUsed / contextWindow * 100)) : null,
|
|
254
|
-
compactions: Math.max(state.compactionWindow, state.compactionRecords,
|
|
255
|
-
state.compactionCompleted, state.compactionEvents),
|
|
256
|
-
contextSections: state.hasVisibleHistory ? {
|
|
257
|
-
instructions: state.baseInstructionTokens + state.visibleSections.instructions,
|
|
258
|
-
messages: state.visibleSections.messages,
|
|
259
|
-
toolHistory: state.visibleSections.tools,
|
|
260
|
-
} : null,
|
|
261
|
-
updatedAt: state.updatedAt,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function findLog(directory, threadId) {
|
|
266
|
-
let entries;
|
|
267
|
-
try { entries = await readdir(directory, { withFileTypes: true }); }
|
|
268
|
-
catch (error) {
|
|
269
|
-
if (error.code === "ENOENT") return null;
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
for (const entry of entries) {
|
|
273
|
-
const filePath = path.join(directory, entry.name);
|
|
274
|
-
if (entry.isFile() && entry.name.endsWith(`-${threadId}.jsonl`)) return filePath;
|
|
275
|
-
if (entry.isDirectory()) {
|
|
276
|
-
const found = await findLog(filePath, threadId);
|
|
277
|
-
if (found) return found;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export class SessionStatusReader {
|
|
284
|
-
constructor({ sessionsDir = path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "sessions"), now = Date.now } = {}) {
|
|
285
|
-
this.sessionsDir = sessionsDir;
|
|
286
|
-
this.now = now;
|
|
287
|
-
this.states = new Map();
|
|
288
|
-
this.missingChecks = new Map();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
retryAfterMs(threadId) {
|
|
292
|
-
return Math.max(0, (this.missingChecks.get(threadId)?.nextCheckAt ?? 0) - this.now());
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async read(threadId) {
|
|
296
|
-
if (!THREAD_ID.test(threadId ?? "")) return null;
|
|
297
|
-
let state = this.states.get(threadId);
|
|
298
|
-
if (!state) {
|
|
299
|
-
if (this.retryAfterMs(threadId) > 0) return null;
|
|
300
|
-
const filePath = await findLog(this.sessionsDir, threadId);
|
|
301
|
-
if (!filePath) {
|
|
302
|
-
const attempts = this.missingChecks.get(threadId)?.attempts ?? 0;
|
|
303
|
-
const delay = [500, 1000, 2000][attempts] ?? 10_000;
|
|
304
|
-
this.missingChecks.set(threadId, { attempts: attempts + 1, nextCheckAt: this.now() + delay });
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
state = emptyState(filePath);
|
|
308
|
-
this.states.set(threadId, state);
|
|
309
|
-
this.missingChecks.delete(threadId);
|
|
310
|
-
}
|
|
311
|
-
let info;
|
|
312
|
-
try { info = await stat(state.filePath); }
|
|
313
|
-
catch (error) {
|
|
314
|
-
if (error.code !== "ENOENT") throw error;
|
|
315
|
-
this.states.delete(threadId);
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
if (info.size < state.offset) state = emptyState(state.filePath);
|
|
319
|
-
if (info.size > state.offset) {
|
|
320
|
-
const stream = createReadStream(state.filePath, {
|
|
321
|
-
start: state.offset,
|
|
322
|
-
end: info.size - 1,
|
|
323
|
-
encoding: "utf8",
|
|
324
|
-
});
|
|
325
|
-
for await (const chunk of stream) {
|
|
326
|
-
const lines = `${state.pending}${chunk}`.split("\n");
|
|
327
|
-
state.pending = lines.pop() ?? "";
|
|
328
|
-
for (const line of lines) {
|
|
329
|
-
if (!line.includes('"token_usage_record"') && !line.includes('"event_msg"')
|
|
330
|
-
&& !line.includes('"turn_context"') && !line.includes('"session_meta"')
|
|
331
|
-
&& !line.includes('"response_item"') && !line.includes('"compacted"')) continue;
|
|
332
|
-
try { applySessionStatusEvent(state, JSON.parse(line)); } catch { /* 忽略损坏的单行 */ }
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
state.offset = info.size;
|
|
336
|
-
this.states.set(threadId, state);
|
|
337
|
-
}
|
|
338
|
-
return sessionStatusSnapshot(state, threadId);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
File without changes
|