minecodex 1.0.7 → 1.0.9
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/features/model-slider/README.md +7 -4
- package/features/model-slider/codex-feature.json +0 -7
- package/features/notes/src/http-server.mjs +3 -0
- package/features/notes/web/app.js +9 -303
- package/features/notes/web/file-icons.mjs +68 -0
- package/features/notes/web/i18n.mjs +237 -0
- package/features/notes/web/todo-drag.mjs +18 -0
- package/package.json +1 -1
- package/packages/runtime-host/src/codex-cdp.mjs +153 -0
- package/packages/runtime-host/src/codex-design-contract.mjs +116 -0
- package/packages/runtime-host/src/codex-efforts.mjs +47 -0
- package/packages/runtime-host/src/codex-injection.mjs +4545 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/feature-registry.mjs +1 -13
- package/packages/runtime-host/src/host-actions.mjs +221 -0
- package/packages/runtime-host/src/main.mjs +7 -9
- package/packages/runtime-host/src/model-capabilities.mjs +0 -86
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// i18n for CodexNotes web surfaces. Single source of all user-visible copy.
|
|
2
|
+
// Locale state is initialised by app.js via setLocale(); this module never touches DOM.
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_LOCALES = new Set(["en", "zh-CN"]);
|
|
5
|
+
|
|
6
|
+
function normalizeLocale(value) {
|
|
7
|
+
const candidate = String(value ?? "").trim();
|
|
8
|
+
if (candidate.toLowerCase().startsWith("zh")) return "zh-CN";
|
|
9
|
+
return SUPPORTED_LOCALES.has(candidate) ? candidate : "en";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let locale = "en";
|
|
13
|
+
|
|
14
|
+
export function getLocale() {
|
|
15
|
+
return locale;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function setLocale(value) {
|
|
19
|
+
const next = normalizeLocale(value);
|
|
20
|
+
if (next === locale) return false;
|
|
21
|
+
locale = next;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const MESSAGES = Object.freeze({
|
|
26
|
+
en: {
|
|
27
|
+
requestFailed: ({ status }) => `Request failed (${status})`,
|
|
28
|
+
temporarilyUnavailable: "CodexNotes is temporarily unavailable",
|
|
29
|
+
unavailable: "CodexNotes is unavailable",
|
|
30
|
+
retry: "Retry",
|
|
31
|
+
hostUnavailable: "Codex host is not connected",
|
|
32
|
+
hostTimeout: "Codex did not respond",
|
|
33
|
+
hostActionFailed: "Host action failed",
|
|
34
|
+
restoreTodo: "Restore Todo",
|
|
35
|
+
completeTodo: "Complete Todo",
|
|
36
|
+
edit: "Edit",
|
|
37
|
+
delete: "Delete",
|
|
38
|
+
editTodo: "Edit Todo",
|
|
39
|
+
newTodo: "New Todo",
|
|
40
|
+
todoText: "Todo text",
|
|
41
|
+
todoPlaceholder: "What do you want to remember?",
|
|
42
|
+
saveTodo: "Save Todo",
|
|
43
|
+
addTodo: "Add Todo",
|
|
44
|
+
cancel: "Cancel",
|
|
45
|
+
editPrompt: "Edit Prompt",
|
|
46
|
+
newPrompt: "New Prompt",
|
|
47
|
+
optionalTitle: "Optional title",
|
|
48
|
+
promptTitle: "Prompt title",
|
|
49
|
+
promptBody: "Prompt body",
|
|
50
|
+
promptPlaceholder: "Write a reusable prompt…",
|
|
51
|
+
savePrompt: "Save Prompt",
|
|
52
|
+
addPrompt: "Add Prompt",
|
|
53
|
+
insertPrompt: "Insert Prompt",
|
|
54
|
+
morePromptActions: "More prompt actions",
|
|
55
|
+
star: "Star",
|
|
56
|
+
unstar: "Unstar",
|
|
57
|
+
showInFolder: "Show in folder",
|
|
58
|
+
attachToChat: "Attach to chat",
|
|
59
|
+
viewAll: "View all",
|
|
60
|
+
todo: "Todo",
|
|
61
|
+
noTodo: "No Todo yet",
|
|
62
|
+
prompts: "Prompts",
|
|
63
|
+
noPrompts: "No prompts yet",
|
|
64
|
+
files: "Files",
|
|
65
|
+
pinFiles: "Pin files",
|
|
66
|
+
noPinnedFiles: "No pinned files",
|
|
67
|
+
active: "Active",
|
|
68
|
+
add: "Add",
|
|
69
|
+
nothingActive: "Nothing active",
|
|
70
|
+
completed: "Completed",
|
|
71
|
+
deleteAll: "Delete all",
|
|
72
|
+
deleteCompletedTitle: "Delete all completed Todo",
|
|
73
|
+
noCompletedTodo: "No completed Todo",
|
|
74
|
+
starred: "Starred",
|
|
75
|
+
noStarredPrompts: "No starred prompts",
|
|
76
|
+
pinnedFiles: "Pinned files",
|
|
77
|
+
loading: "Loading CodexNotes…",
|
|
78
|
+
insertedAtCaret: "Inserted at the Composer caret",
|
|
79
|
+
addedToComposer: "Added to Composer",
|
|
80
|
+
insertedFilePath: "Inserted file path",
|
|
81
|
+
filesPinned: ({ count }) => `${count} file${count === 1 ? "" : "s"} pinned`,
|
|
82
|
+
alreadyPinned: "Already pinned",
|
|
83
|
+
todoEmpty: "Todo text cannot be empty",
|
|
84
|
+
promptEmpty: "Prompt body cannot be empty",
|
|
85
|
+
editorItemMissing: "This item no longer exists.",
|
|
86
|
+
close: "Close",
|
|
87
|
+
invalidRequest: "The request is invalid",
|
|
88
|
+
invalidContentType: "The request must use JSON",
|
|
89
|
+
invalidJson: "The request body is not valid JSON",
|
|
90
|
+
bodyTooLarge: "The request is too large",
|
|
91
|
+
notFound: "The item was not found",
|
|
92
|
+
invalidOrder: "The Todo order is no longer current",
|
|
93
|
+
invalidUse: "The requested action could not be recorded",
|
|
94
|
+
fileUnavailable: "The file is unavailable",
|
|
95
|
+
fileMissing: "The source file no longer exists",
|
|
96
|
+
directoryNotAllowed: "Folders cannot be pinned; choose regular files",
|
|
97
|
+
pathNotAbsolute: "Only absolute paths can be pinned",
|
|
98
|
+
pickerUnavailable: "The system file picker is unavailable",
|
|
99
|
+
pickerFailed: "The system file picker could not be opened",
|
|
100
|
+
revealUnavailable: "Show in folder is unavailable on this platform",
|
|
101
|
+
revealFailed: "The file could not be shown in the system file manager",
|
|
102
|
+
previewUnavailable: "This file does not have an image preview",
|
|
103
|
+
previewTooLarge: "The image is too large to preview",
|
|
104
|
+
internalError: "CodexNotes could not complete the request",
|
|
105
|
+
actionNotAllowed: "This action is not available",
|
|
106
|
+
},
|
|
107
|
+
"zh-CN": {
|
|
108
|
+
requestFailed: ({ status }) => `请求失败(${status})`,
|
|
109
|
+
temporarilyUnavailable: "CodexNotes 暂时不可用",
|
|
110
|
+
unavailable: "CodexNotes 不可用",
|
|
111
|
+
retry: "重试",
|
|
112
|
+
hostUnavailable: "尚未连接 Codex 宿主",
|
|
113
|
+
hostTimeout: "Codex 未响应",
|
|
114
|
+
hostActionFailed: "宿主操作失败",
|
|
115
|
+
restoreTodo: "恢复待办",
|
|
116
|
+
completeTodo: "完成待办",
|
|
117
|
+
edit: "编辑",
|
|
118
|
+
delete: "删除",
|
|
119
|
+
editTodo: "编辑待办",
|
|
120
|
+
newTodo: "新建待办",
|
|
121
|
+
todoText: "待办内容",
|
|
122
|
+
todoPlaceholder: "你想记住什么?",
|
|
123
|
+
saveTodo: "保存待办",
|
|
124
|
+
addTodo: "添加待办",
|
|
125
|
+
cancel: "取消",
|
|
126
|
+
editPrompt: "编辑提示词",
|
|
127
|
+
newPrompt: "新建提示词",
|
|
128
|
+
optionalTitle: "可选标题",
|
|
129
|
+
promptTitle: "提示词标题",
|
|
130
|
+
promptBody: "提示词正文",
|
|
131
|
+
promptPlaceholder: "编写一条可复用的提示词…",
|
|
132
|
+
savePrompt: "保存提示词",
|
|
133
|
+
addPrompt: "添加提示词",
|
|
134
|
+
insertPrompt: "插入提示词",
|
|
135
|
+
morePromptActions: "更多提示词操作",
|
|
136
|
+
star: "收藏",
|
|
137
|
+
unstar: "取消收藏",
|
|
138
|
+
showInFolder: "在文件夹中显示",
|
|
139
|
+
attachToChat: "添加到对话",
|
|
140
|
+
viewAll: "查看全部",
|
|
141
|
+
todo: "待办",
|
|
142
|
+
noTodo: "暂无待办",
|
|
143
|
+
prompts: "提示词",
|
|
144
|
+
noPrompts: "暂无提示词",
|
|
145
|
+
files: "文件",
|
|
146
|
+
pinFiles: "固定文件",
|
|
147
|
+
noPinnedFiles: "暂无固定文件",
|
|
148
|
+
active: "进行中",
|
|
149
|
+
add: "添加",
|
|
150
|
+
nothingActive: "暂无进行中的待办",
|
|
151
|
+
completed: "已完成",
|
|
152
|
+
deleteAll: "全部删除",
|
|
153
|
+
deleteCompletedTitle: "删除全部已完成待办",
|
|
154
|
+
noCompletedTodo: "暂无已完成待办",
|
|
155
|
+
starred: "已收藏",
|
|
156
|
+
noStarredPrompts: "暂无收藏的提示词",
|
|
157
|
+
pinnedFiles: "已固定文件",
|
|
158
|
+
loading: "正在加载 CodexNotes…",
|
|
159
|
+
insertedAtCaret: "已插入到消息输入框光标处",
|
|
160
|
+
addedToComposer: "已添加到消息输入框",
|
|
161
|
+
insertedFilePath: "已插入文件路径",
|
|
162
|
+
filesPinned: ({ count }) => `已固定 ${count} 个文件`,
|
|
163
|
+
alreadyPinned: "文件已固定",
|
|
164
|
+
todoEmpty: "待办内容不能为空",
|
|
165
|
+
promptEmpty: "提示词正文不能为空",
|
|
166
|
+
editorItemMissing: "该项目已不存在。",
|
|
167
|
+
close: "关闭",
|
|
168
|
+
invalidRequest: "请求无效",
|
|
169
|
+
invalidContentType: "请求必须使用 JSON",
|
|
170
|
+
invalidJson: "请求正文不是有效的 JSON",
|
|
171
|
+
bodyTooLarge: "请求内容过大",
|
|
172
|
+
notFound: "找不到该项目",
|
|
173
|
+
invalidOrder: "待办顺序已发生变化",
|
|
174
|
+
invalidUse: "无法记录这次操作",
|
|
175
|
+
fileUnavailable: "文件不可用",
|
|
176
|
+
fileMissing: "源文件已不存在",
|
|
177
|
+
directoryNotAllowed: "不能固定文件夹,请选择普通文件",
|
|
178
|
+
pathNotAbsolute: "只能固定绝对路径",
|
|
179
|
+
pickerUnavailable: "系统文件选择器不可用",
|
|
180
|
+
pickerFailed: "无法打开系统文件选择器",
|
|
181
|
+
revealUnavailable: "当前平台不支持在文件夹中显示",
|
|
182
|
+
revealFailed: "无法在系统文件管理器中显示该文件",
|
|
183
|
+
previewUnavailable: "该文件没有图片预览",
|
|
184
|
+
previewTooLarge: "图片过大,无法预览",
|
|
185
|
+
internalError: "CodexNotes 无法完成请求",
|
|
186
|
+
actionNotAllowed: "当前操作不可用",
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
export function t(key, params = {}) {
|
|
191
|
+
const value = MESSAGES[locale]?.[key] ?? MESSAGES.en[key] ?? key;
|
|
192
|
+
return typeof value === "function" ? value(params) : value;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const ERROR_MESSAGE_KEYS = Object.freeze({
|
|
196
|
+
ACTION_NOT_ALLOWED: "actionNotAllowed",
|
|
197
|
+
BODY_TOO_LARGE: "bodyTooLarge",
|
|
198
|
+
DIRECTORY_NOT_ALLOWED: "directoryNotAllowed",
|
|
199
|
+
EMPTY_BODY: "invalidRequest",
|
|
200
|
+
FILE_MISSING: "fileMissing",
|
|
201
|
+
FILE_UNAVAILABLE: "fileUnavailable",
|
|
202
|
+
HOST_ACTION_FAILED: "hostActionFailed",
|
|
203
|
+
HOST_NOT_ALLOWED: "actionNotAllowed",
|
|
204
|
+
HOST_TIMEOUT: "hostTimeout",
|
|
205
|
+
HOST_UNAVAILABLE: "hostUnavailable",
|
|
206
|
+
INTERNAL_ERROR: "internalError",
|
|
207
|
+
INVALID_BODY: "invalidRequest",
|
|
208
|
+
INVALID_CHANGES: "invalidRequest",
|
|
209
|
+
INVALID_COMPLETED: "invalidRequest",
|
|
210
|
+
INVALID_CONTENT_TYPE: "invalidContentType",
|
|
211
|
+
INVALID_ID: "invalidRequest",
|
|
212
|
+
INVALID_JSON: "invalidJson",
|
|
213
|
+
INVALID_ORDER: "invalidOrder",
|
|
214
|
+
INVALID_PATHS: "invalidRequest",
|
|
215
|
+
INVALID_REQUEST: "invalidRequest",
|
|
216
|
+
INVALID_SURFACE: "invalidRequest",
|
|
217
|
+
INVALID_ORIGIN: "actionNotAllowed",
|
|
218
|
+
INVALID_STAR: "invalidRequest",
|
|
219
|
+
INVALID_TITLE: "invalidRequest",
|
|
220
|
+
INVALID_USE: "invalidUse",
|
|
221
|
+
NOT_FOUND: "notFound",
|
|
222
|
+
ORIGIN_NOT_ALLOWED: "actionNotAllowed",
|
|
223
|
+
PATH_NOT_ABSOLUTE: "pathNotAbsolute",
|
|
224
|
+
PICKER_FAILED: "pickerFailed",
|
|
225
|
+
PICKER_UNAVAILABLE: "pickerUnavailable",
|
|
226
|
+
PREVIEW_TOO_LARGE: "previewTooLarge",
|
|
227
|
+
PREVIEW_UNAVAILABLE: "previewUnavailable",
|
|
228
|
+
REVEAL_FAILED: "revealFailed",
|
|
229
|
+
REVEAL_UNAVAILABLE: "revealUnavailable",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
export function localizedErrorMessage(code, fallback, params = {}) {
|
|
233
|
+
const key = ERROR_MESSAGE_KEYS[code];
|
|
234
|
+
return key ? t(key, params) : fallback;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export { SUPPORTED_LOCALES, normalizeLocale };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Todo 拖拽排序纯算法:drop 位置判定与顺序重排。
|
|
2
|
+
// 不触碰 DOM/状态,仅输入计算;app.js 的拖拽事件处理调用它们。
|
|
3
|
+
|
|
4
|
+
function todoDropPosition(event, row) {
|
|
5
|
+
const rect = row.getBoundingClientRect();
|
|
6
|
+
return event.clientY < rect.top + rect.height / 2 ? "before" : "after";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function reorderedTodoIds(ids, draggedId, targetId, position) {
|
|
10
|
+
if (!ids.includes(draggedId) || !ids.includes(targetId) || draggedId === targetId) return ids;
|
|
11
|
+
const reordered = ids.filter((id) => id !== draggedId);
|
|
12
|
+
const targetIndex = reordered.indexOf(targetId);
|
|
13
|
+
const insertAt = targetIndex + (position === "after" ? 1 : 0);
|
|
14
|
+
reordered.splice(insertAt, 0, draggedId);
|
|
15
|
+
return reordered;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { todoDropPosition, reorderedTodoIds };
|
package/package.json
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// CDP (Chrome DevTools Protocol) 客户端:WebSocket 连接、表达式求值与进程等待。
|
|
2
|
+
// 与注入内容无关;CodexRuntime 通过这里的 connectClient 与 Codex Renderer 通信。
|
|
3
|
+
export async function connect(url) {
|
|
4
|
+
const socket = new WebSocket(url);
|
|
5
|
+
const pending = new Map();
|
|
6
|
+
const eventWaiters = new Map();
|
|
7
|
+
const eventListeners = new Map();
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
|
|
10
|
+
await new Promise((resolve, reject) => {
|
|
11
|
+
socket.addEventListener("open", resolve, { once: true });
|
|
12
|
+
socket.addEventListener("error", reject, { once: true });
|
|
13
|
+
});
|
|
14
|
+
socket.addEventListener("message", (event) => {
|
|
15
|
+
const message = JSON.parse(String(event.data));
|
|
16
|
+
if (!message.id) {
|
|
17
|
+
for (const listener of eventListeners.get(message.method) ?? []) listener(message.params);
|
|
18
|
+
const waiters = eventWaiters.get(message.method) ?? [];
|
|
19
|
+
eventWaiters.delete(message.method);
|
|
20
|
+
for (const waiter of waiters) waiter.resolve(message.params);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const request = pending.get(message.id);
|
|
24
|
+
if (!request) return;
|
|
25
|
+
pending.delete(message.id);
|
|
26
|
+
if (message.error) request.reject(new Error(message.error.message));
|
|
27
|
+
else request.resolve(message.result);
|
|
28
|
+
});
|
|
29
|
+
socket.addEventListener("close", () => {
|
|
30
|
+
const error = new Error("CDP WebSocket closed");
|
|
31
|
+
for (const request of pending.values()) request.reject(error);
|
|
32
|
+
pending.clear();
|
|
33
|
+
for (const waiters of eventWaiters.values()) {
|
|
34
|
+
for (const waiter of waiters) waiter.reject(error);
|
|
35
|
+
}
|
|
36
|
+
eventWaiters.clear();
|
|
37
|
+
eventListeners.clear();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
socket,
|
|
42
|
+
send(method, params = {}) {
|
|
43
|
+
const id = nextId++;
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
pending.set(id, { resolve, reject });
|
|
46
|
+
socket.send(JSON.stringify({ id, method, params }));
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
on(method, listener) {
|
|
50
|
+
const listeners = eventListeners.get(method) ?? new Set();
|
|
51
|
+
listeners.add(listener);
|
|
52
|
+
eventListeners.set(method, listeners);
|
|
53
|
+
return () => listeners.delete(listener);
|
|
54
|
+
},
|
|
55
|
+
waitFor(method, timeoutMs) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const waiters = eventWaiters.get(method) ?? [];
|
|
58
|
+
const waiter = {
|
|
59
|
+
resolve(value) {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
resolve(value);
|
|
62
|
+
},
|
|
63
|
+
reject,
|
|
64
|
+
};
|
|
65
|
+
const timeout = setTimeout(() => {
|
|
66
|
+
const remaining = (eventWaiters.get(method) ?? []).filter((item) => item !== waiter);
|
|
67
|
+
if (remaining.length) eventWaiters.set(method, remaining);
|
|
68
|
+
else eventWaiters.delete(method);
|
|
69
|
+
reject(new Error(`Timed out waiting for CDP event ${method}`));
|
|
70
|
+
}, timeoutMs);
|
|
71
|
+
waiters.push(waiter);
|
|
72
|
+
eventWaiters.set(method, waiters);
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function waitForExpression(client, expression, timeoutMs = 20_000) {
|
|
79
|
+
const deadline = Date.now() + timeoutMs;
|
|
80
|
+
while (Date.now() < deadline) {
|
|
81
|
+
try {
|
|
82
|
+
const result = await client.send("Runtime.evaluate", { expression, returnByValue: true });
|
|
83
|
+
if (result.result?.value) return;
|
|
84
|
+
} catch {
|
|
85
|
+
// Reloading briefly destroys the previous execution context.
|
|
86
|
+
}
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Timed out waiting for renderer state: ${expression}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function evaluationValue(result) {
|
|
93
|
+
if (result.exceptionDetails) {
|
|
94
|
+
throw new Error(result.exceptionDetails.exception?.description ?? result.exceptionDetails.text);
|
|
95
|
+
}
|
|
96
|
+
return result.result?.value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function requireCdpWebSocketUrl(version, expectedCdpPort) {
|
|
100
|
+
const value = typeof version?.webSocketDebuggerUrl === "string"
|
|
101
|
+
? version.webSocketDebuggerUrl.trim()
|
|
102
|
+
: "";
|
|
103
|
+
if (!value) {
|
|
104
|
+
throw new Error("Codex CDP /json/version is missing webSocketDebuggerUrl");
|
|
105
|
+
}
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = new URL(value);
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error("Codex CDP webSocketDebuggerUrl is invalid");
|
|
111
|
+
}
|
|
112
|
+
if (!new Set(["ws:", "wss:"]).has(parsed.protocol) || !parsed.host) {
|
|
113
|
+
throw new Error("Codex CDP webSocketDebuggerUrl must use ws: or wss:");
|
|
114
|
+
}
|
|
115
|
+
const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
116
|
+
if (!new Set(["127.0.0.1", "localhost", "::1"]).has(hostname)) {
|
|
117
|
+
throw new Error("Codex CDP webSocketDebuggerUrl must use a loopback host");
|
|
118
|
+
}
|
|
119
|
+
const configuredPort = Number(expectedCdpPort);
|
|
120
|
+
const effectivePort = parsed.port
|
|
121
|
+
? Number(parsed.port)
|
|
122
|
+
: parsed.protocol === "wss:" ? 443 : 80;
|
|
123
|
+
if (!Number.isInteger(configuredPort) || effectivePort !== configuredPort) {
|
|
124
|
+
throw new Error(`Codex CDP webSocketDebuggerUrl must use configured CDP port ${expectedCdpPort}`);
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function processChildHasExited(child) {
|
|
130
|
+
return child && (
|
|
131
|
+
(child.exitCode !== null && child.exitCode !== undefined)
|
|
132
|
+
|| (child.signalCode !== null && child.signalCode !== undefined)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function waitForProcessChildExit(child, timeoutMs) {
|
|
137
|
+
if (processChildHasExited(child)) return Promise.resolve(true);
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
let settled = false;
|
|
140
|
+
let timer;
|
|
141
|
+
const finish = (exited) => {
|
|
142
|
+
if (settled) return;
|
|
143
|
+
settled = true;
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
child.removeListener?.("exit", onExit);
|
|
146
|
+
resolve(exited || processChildHasExited(child));
|
|
147
|
+
};
|
|
148
|
+
const onExit = () => finish(true);
|
|
149
|
+
child.once?.("exit", onExit);
|
|
150
|
+
timer = setTimeout(() => finish(false), timeoutMs);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Codex 原生设计契约:token 名单与派生规则的单一事实来源。
|
|
2
|
+
// 注入到 Codex Renderer 的 install 脚本必须自包含(install.toString() 序列化
|
|
3
|
+
// 后经 CDP 注入),因此 install 内通过 config.themeTokens 拿到本模块的名单;
|
|
4
|
+
// 名单只在这里维护,install 不再持有自己的字面量副本。
|
|
5
|
+
|
|
6
|
+
export const CODEX_THEME_TOKENS = Object.freeze([
|
|
7
|
+
"--codex-base-accent",
|
|
8
|
+
"--codex-base-ink",
|
|
9
|
+
"--codex-base-surface",
|
|
10
|
+
"--codex-corner-radius-scale",
|
|
11
|
+
"--codex-corner-shape",
|
|
12
|
+
"--spacing",
|
|
13
|
+
"--radius-3xl-base",
|
|
14
|
+
"--elevation-prominent",
|
|
15
|
+
"--elevation-stroke",
|
|
16
|
+
"--color-token-main-surface-primary",
|
|
17
|
+
"--color-token-dropdown-background",
|
|
18
|
+
"--color-token-foreground",
|
|
19
|
+
"--color-token-text-secondary",
|
|
20
|
+
"--color-token-text-tertiary",
|
|
21
|
+
"--color-token-border",
|
|
22
|
+
"--color-token-focus-border",
|
|
23
|
+
"--color-token-conversation-summary-trailing",
|
|
24
|
+
"--color-token-bg-primary",
|
|
25
|
+
"--color-token-bg-secondary",
|
|
26
|
+
"--color-token-bg-tertiary",
|
|
27
|
+
"--color-surface-tertiary",
|
|
28
|
+
"--color-surface-elevated-secondary",
|
|
29
|
+
"--color-background-elevated-primary-opaque",
|
|
30
|
+
"--color-background-primary-ghost-hover",
|
|
31
|
+
"--color-chart-blue",
|
|
32
|
+
"--color-chart-purple",
|
|
33
|
+
"--color-background-panel",
|
|
34
|
+
"--color-background-control",
|
|
35
|
+
"--color-background-primary-solid",
|
|
36
|
+
"--color-text-primary-solid",
|
|
37
|
+
"--color-background-button-primary",
|
|
38
|
+
"--color-background-button-primary-hover",
|
|
39
|
+
"--color-background-button-secondary",
|
|
40
|
+
"--color-background-button-secondary-hover",
|
|
41
|
+
"--color-background-button-tertiary-hover",
|
|
42
|
+
"--color-text-button-tertiary",
|
|
43
|
+
"--color-token-button-background",
|
|
44
|
+
"--color-token-button-foreground",
|
|
45
|
+
"--color-text-foreground",
|
|
46
|
+
"--color-text-foreground-secondary",
|
|
47
|
+
"--color-text-foreground-tertiary",
|
|
48
|
+
"--color-text-inverse",
|
|
49
|
+
"--color-text-on-accent",
|
|
50
|
+
"--color-text-button-primary",
|
|
51
|
+
"--color-text-accent",
|
|
52
|
+
"--color-token-text-link-foreground",
|
|
53
|
+
"--color-border",
|
|
54
|
+
"--color-border-subtle",
|
|
55
|
+
"--color-border-heavy",
|
|
56
|
+
"--color-border-focus",
|
|
57
|
+
"--color-token-scrollbar-slider-background",
|
|
58
|
+
"--color-token-scrollbar-slider-hover-background",
|
|
59
|
+
"--color-token-scrollbar-slider-active-background",
|
|
60
|
+
"--color-icon-primary",
|
|
61
|
+
"--color-icon-secondary",
|
|
62
|
+
"--color-icon-tertiary",
|
|
63
|
+
"--color-token-list-hover-background",
|
|
64
|
+
"--color-token-input-background",
|
|
65
|
+
"--color-token-input-border",
|
|
66
|
+
"--color-token-input-foreground",
|
|
67
|
+
"--color-token-input-placeholder-foreground",
|
|
68
|
+
"--font-sans-default",
|
|
69
|
+
"--font-weight-normal",
|
|
70
|
+
"--vscode-font-weight",
|
|
71
|
+
"--default-mono-font-family",
|
|
72
|
+
"--text-heading-lg",
|
|
73
|
+
"--text-heading-md",
|
|
74
|
+
"--text-base",
|
|
75
|
+
"--text-base--line-height",
|
|
76
|
+
"--text-sm",
|
|
77
|
+
"--text-sm--line-height",
|
|
78
|
+
"--text-xs",
|
|
79
|
+
"--spacing-token-button-composer",
|
|
80
|
+
"--radius-sm",
|
|
81
|
+
"--radius-md",
|
|
82
|
+
"--radius-lg",
|
|
83
|
+
"--radius-xl",
|
|
84
|
+
"--radius-2xl",
|
|
85
|
+
"--shadow-md",
|
|
86
|
+
"--shadow-lg",
|
|
87
|
+
"--shadow-xl",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
// images 图片编辑器可用性依赖的宿主 token 子集。
|
|
91
|
+
// 与 features/images/web/app.js 的 editorTokens 保持同一来源(一致性由测试锁定)。
|
|
92
|
+
export const EDITOR_TOKEN_NAMES = Object.freeze([
|
|
93
|
+
"--color-surface-tertiary",
|
|
94
|
+
"--color-surface-elevated-secondary",
|
|
95
|
+
"--color-border-subtle",
|
|
96
|
+
"--color-background-primary-ghost-hover",
|
|
97
|
+
"--color-chart-blue",
|
|
98
|
+
"--color-text-inverse",
|
|
99
|
+
"--color-token-main-surface-primary",
|
|
100
|
+
"--color-token-input-foreground",
|
|
101
|
+
"--color-text-foreground",
|
|
102
|
+
"--color-text-foreground-tertiary",
|
|
103
|
+
"--color-text-on-accent",
|
|
104
|
+
"--color-border",
|
|
105
|
+
"--color-border-focus",
|
|
106
|
+
"--color-token-list-hover-background",
|
|
107
|
+
"--color-text-accent",
|
|
108
|
+
"--font-sans-default",
|
|
109
|
+
"--font-weight-normal",
|
|
110
|
+
"--text-sm",
|
|
111
|
+
"--text-sm--line-height",
|
|
112
|
+
"--radius-md",
|
|
113
|
+
"--radius-xl",
|
|
114
|
+
"--shadow-md",
|
|
115
|
+
"--shadow-xl",
|
|
116
|
+
]);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// Model Slider 思考强度来源说明(为什么需要这个模块)
|
|
6
|
+
//
|
|
7
|
+
// OpenCodex 把第三方模型路由给 Codex 时,会在 catalog 里给所有“有思考强度”的
|
|
8
|
+
// 模型额外广告 mock 的 max/ultra(即使上游并不支持)。GLM 之类被 OpenCodex 测得
|
|
9
|
+
// 准的模型没有这个问题;而像 Qwen/Grok/MiMo 这类 OpenCodex 没内置档位的模型,
|
|
10
|
+
// 只能通过用户的 OpenCodex 配置声明真实档位。
|
|
11
|
+
//
|
|
12
|
+
// 这个模块只做一件事:读取 OpenCodex 配置(~/.opencodex/config.json)中按
|
|
13
|
+
// provider 声明的 modelReasoningEfforts,作为 Model Slider 的“真实档位表”。
|
|
14
|
+
// 它不产生、不猜测、不修改任何档位;配置里没有的模型保持原样。
|
|
15
|
+
function canonicalEfforts(values) {
|
|
16
|
+
if (!Array.isArray(values)) return null;
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const out = values
|
|
19
|
+
.map((value) => String(value).trim().toLowerCase())
|
|
20
|
+
.filter((value) => {
|
|
21
|
+
if (!value || seen.has(value)) return false;
|
|
22
|
+
seen.add(value);
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
return out.length ? out : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 读取 OpenCodex 配置中按 provider 声明的真实思考强度档位。OpenCodex 同步 catalog
|
|
29
|
+
// 时会额外广告 mock 的 max/ultra;这里只透传 provider 自己声明的档位,供 Model
|
|
30
|
+
// Slider 精确显示,避免 UI 出现上游并不支持的档位。
|
|
31
|
+
export async function loadOpenCodexRealEfforts({
|
|
32
|
+
opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
|
|
33
|
+
read = readFile,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const configPayload = await read(opencodexConfigPath, "utf8")
|
|
36
|
+
.then((value) => JSON.parse(value))
|
|
37
|
+
.catch(() => null);
|
|
38
|
+
const entries = {};
|
|
39
|
+
for (const [provider, providerConfig] of Object.entries(configPayload?.providers ?? {})) {
|
|
40
|
+
for (const [modelId, efforts] of Object.entries(providerConfig?.modelReasoningEfforts ?? {})) {
|
|
41
|
+
const levels = canonicalEfforts(efforts);
|
|
42
|
+
if (!levels) continue;
|
|
43
|
+
entries[`${provider}/${modelId}`.toLowerCase()] = levels;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
}
|