minecodex 1.0.8 → 1.0.10
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/notes/README.md +1 -1
- 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/README.md +5 -3
- 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-injection.mjs +4567 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/host-actions.mjs +221 -0
|
@@ -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
|
@@ -132,8 +132,8 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
|
|
|
132
132
|
- `attach-file`:用 CDP 填充临时 file input,再把真实 `File` drop 给 Composer;只有
|
|
133
133
|
观察到原生附件 chip/preview 才报告 `attach`,否则明确返回 `insert-path`。
|
|
134
134
|
- `open-detail-tab`:动态发现当前 Codex React right-panel controller 与当前 Task scope,
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
传入包含 `Component`、`isAvailable`、`kind` 和 `dropDestinations` 的标签描述对象,
|
|
136
|
+
用稳定的 `featureId-detailId` 聚焦或创建唯一原生 Tab;capability 不可用时返回明确错误。
|
|
137
137
|
- `resolve-file-paths`:通过 Codex preload 暴露的 Electron file bridge 解析 Finder
|
|
138
138
|
drop 的真实 `File` 对象。
|
|
139
139
|
|
|
@@ -145,6 +145,8 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
|
|
|
145
145
|
本地 Surface 不直接导航到 loopback URL:Host 先创建 `about:blank` iframe,再校验
|
|
146
146
|
manifest URL、health service/protocol/instance,抓取 HTML 并用 `Page.setDocumentContent`
|
|
147
147
|
写入目标 frame。Images/Notes 只向精确的 `app://-` Origin 开放 CORS。
|
|
148
|
+
- CDP 重连时同步轮换后的 binding token;保留已就绪的 Surface 和编辑草稿,
|
|
149
|
+
使用新 token 重试未就绪页面,避免弹窗停留在空白 iframe。
|
|
148
150
|
- 入口由 Renderer 内的 MutationObserver 幂等挂载,React 重绘不会产生重复入口。
|
|
149
151
|
- Summary 根据对话主区域宽度连续派生 `overlay / shift / gutter`:小于 1096px
|
|
150
152
|
使用临时 Popover;1096–1535px 预留 316px 并把对话内容左移 158px;更宽时
|
|
@@ -166,7 +168,7 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
|
|
|
166
168
|
| --- | --- | --- |
|
|
167
169
|
| loopback HTTP、manifest、Surface message schema | 项目自有稳定接口 | 可由测试直接覆盖 |
|
|
168
170
|
| `window.electronBridge.getPathForFile` | 当前稳定 internal/preload | 仅用于 Finder `File` 路径解析 |
|
|
169
|
-
| React right-panel `openTab`、Task scope、JSX runtime | private internal capability | 动态发现 bundle
|
|
171
|
+
| React right-panel `openTab`、Task scope、JSX runtime | private internal capability | 动态发现 bundle 并做能力检测;失败时返回明确的不可用错误 |
|
|
170
172
|
| toolbar、summary、tabs、Composer selectors | DOM inference | 统一留在本 Adapter |
|
|
171
173
|
| `Runtime.addBinding`、`DOM.setFileInputFiles`、注入脚本 | high-risk CDP | 必须使用隔离 profile 验收 |
|
|
172
174
|
| `togglePinnedSummary`、`composer.addFiles` 字符串 | 探索证据 | 当前实现不把它们当契约 |
|
|
@@ -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
|
+
]);
|