minecodex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/apps/dashboard/app.js +387 -0
  4. package/apps/dashboard/favicon.svg +18 -0
  5. package/apps/dashboard/index.html +70 -0
  6. package/apps/dashboard/styles.css +297 -0
  7. package/features/images/README.md +106 -0
  8. package/features/images/codex-feature.json +24 -0
  9. package/features/images/src/http-server.mjs +302 -0
  10. package/features/images/src/image-library.mjs +582 -0
  11. package/features/images/src/main.mjs +52 -0
  12. package/features/images/src/prompt-index.mjs +182 -0
  13. package/features/images/src/save-as.mjs +40 -0
  14. package/features/images/src/thread-metadata.mjs +93 -0
  15. package/features/images/web/app.js +1045 -0
  16. package/features/images/web/index.html +162 -0
  17. package/features/images/web/styles.css +994 -0
  18. package/features/model-slider/README.md +30 -0
  19. package/features/model-slider/THIRD_PARTY_NOTICES.md +24 -0
  20. package/features/model-slider/assets/providers/anthropic.svg +1 -0
  21. package/features/model-slider/assets/providers/deepseek.svg +1 -0
  22. package/features/model-slider/assets/providers/gemini.svg +1 -0
  23. package/features/model-slider/assets/providers/grok.svg +1 -0
  24. package/features/model-slider/assets/providers/hunyuan.svg +1 -0
  25. package/features/model-slider/assets/providers/kimi.svg +1 -0
  26. package/features/model-slider/assets/providers/minimax.svg +1 -0
  27. package/features/model-slider/assets/providers/openai.svg +1 -0
  28. package/features/model-slider/assets/providers/qwen.svg +1 -0
  29. package/features/model-slider/assets/providers/xiaomimimo.svg +1 -0
  30. package/features/model-slider/assets/providers/zhipu.svg +1 -0
  31. package/features/model-slider/assets/ui/sparkles.svg +18 -0
  32. package/features/model-slider/assets/ui/star-off.svg +17 -0
  33. package/features/model-slider/assets/ui/star.svg +15 -0
  34. package/features/model-slider/codex-feature.json +56 -0
  35. package/features/model-slider/vendor/cc-switch-LICENSE +21 -0
  36. package/features/model-slider/vendor/lucide-LICENSE +35 -0
  37. package/features/notes/README.md +106 -0
  38. package/features/notes/assets/icons/circle.svg +15 -0
  39. package/features/notes/assets/icons/ellipsis.svg +17 -0
  40. package/features/notes/assets/icons/external-link.svg +17 -0
  41. package/features/notes/assets/icons/list-todo.svg +19 -0
  42. package/features/notes/assets/icons/message-square-quote.svg +17 -0
  43. package/features/notes/assets/icons/paperclip.svg +15 -0
  44. package/features/notes/assets/icons/star-off.svg +17 -0
  45. package/features/notes/assets/icons/star.svg +15 -0
  46. package/features/notes/assets/icons/sticky-note.svg +16 -0
  47. package/features/notes/assets/icons/trash-2.svg +19 -0
  48. package/features/notes/codex-feature.json +90 -0
  49. package/features/notes/src/http-server.mjs +541 -0
  50. package/features/notes/src/main.mjs +54 -0
  51. package/features/notes/src/product.mjs +771 -0
  52. package/features/notes/web/app.js +1495 -0
  53. package/features/notes/web/codex-artifact-document.svg +1 -0
  54. package/features/notes/web/codex-attachment.svg +1 -0
  55. package/features/notes/web/codex-build.svg +1 -0
  56. package/features/notes/web/codex-code.svg +1 -0
  57. package/features/notes/web/codex-cplusplus.svg +1 -0
  58. package/features/notes/web/codex-css.svg +1 -0
  59. package/features/notes/web/codex-document.svg +1 -0
  60. package/features/notes/web/codex-file.svg +1 -0
  61. package/features/notes/web/codex-folder.svg +1 -0
  62. package/features/notes/web/codex-hashes.svg +1 -0
  63. package/features/notes/web/codex-html.svg +1 -0
  64. package/features/notes/web/codex-image.svg +1 -0
  65. package/features/notes/web/codex-java.svg +1 -0
  66. package/features/notes/web/codex-javascript.svg +1 -0
  67. package/features/notes/web/codex-json.svg +1 -0
  68. package/features/notes/web/codex-notebook.svg +1 -0
  69. package/features/notes/web/codex-pdf.svg +1 -0
  70. package/features/notes/web/codex-pencil.svg +3 -0
  71. package/features/notes/web/codex-php.svg +1 -0
  72. package/features/notes/web/codex-plus.svg +3 -0
  73. package/features/notes/web/codex-presentation.svg +1 -0
  74. package/features/notes/web/codex-python.svg +1 -0
  75. package/features/notes/web/codex-react.svg +1 -0
  76. package/features/notes/web/codex-rust.svg +1 -0
  77. package/features/notes/web/codex-shell.svg +1 -0
  78. package/features/notes/web/codex-skill.svg +1 -0
  79. package/features/notes/web/codex-spreadsheet.svg +1 -0
  80. package/features/notes/web/codex-task.svg +3 -0
  81. package/features/notes/web/codex-terminal.svg +1 -0
  82. package/features/notes/web/codex-toml.svg +1 -0
  83. package/features/notes/web/codex-typescript.svg +1 -0
  84. package/features/notes/web/codex-view-all.svg +3 -0
  85. package/features/notes/web/index.html +15 -0
  86. package/features/notes/web/styles.css +1155 -0
  87. package/package.json +67 -0
  88. package/packages/cli/bin/mcx.mjs +7 -0
  89. package/packages/cli/src/browser-exchange.mjs +31 -0
  90. package/packages/cli/src/commands.mjs +389 -0
  91. package/packages/cli/src/config.mjs +101 -0
  92. package/packages/cli/src/control-server.mjs +371 -0
  93. package/packages/cli/src/npm-adapter.mjs +40 -0
  94. package/packages/cli/src/paths.mjs +33 -0
  95. package/packages/cli/src/platform.mjs +368 -0
  96. package/packages/cli/src/runtime-manager.mjs +338 -0
  97. package/packages/runtime-host/README.md +171 -0
  98. package/packages/runtime-host/src/codex-runtime.mjs +4311 -0
  99. package/packages/runtime-host/src/feature-registry.mjs +827 -0
  100. package/packages/runtime-host/src/main.mjs +98 -0
  101. package/packages/runtime-host/src/pid-file.mjs +23 -0
@@ -0,0 +1,1495 @@
1
+ const params = new URLSearchParams(location.search);
2
+ const SUPPORTED_LOCALES = new Set(["en", "zh-CN"]);
3
+
4
+ function normalizeLocale(value) {
5
+ const candidate = String(value ?? "").trim();
6
+ if (candidate.toLowerCase().startsWith("zh")) return "zh-CN";
7
+ return SUPPORTED_LOCALES.has(candidate) ? candidate : "en";
8
+ }
9
+
10
+ const MESSAGES = Object.freeze({
11
+ en: {
12
+ requestFailed: ({ status }) => `Request failed (${status})`,
13
+ temporarilyUnavailable: "CodexNotes is temporarily unavailable",
14
+ unavailable: "CodexNotes is unavailable",
15
+ retry: "Retry",
16
+ hostUnavailable: "Codex host is not connected",
17
+ hostTimeout: "Codex did not respond",
18
+ hostActionFailed: "Host action failed",
19
+ restoreTodo: "Restore Todo",
20
+ completeTodo: "Complete Todo",
21
+ edit: "Edit",
22
+ delete: "Delete",
23
+ editTodo: "Edit Todo",
24
+ newTodo: "New Todo",
25
+ todoText: "Todo text",
26
+ todoPlaceholder: "What do you want to remember?",
27
+ saveTodo: "Save Todo",
28
+ addTodo: "Add Todo",
29
+ cancel: "Cancel",
30
+ editPrompt: "Edit Prompt",
31
+ newPrompt: "New Prompt",
32
+ optionalTitle: "Optional title",
33
+ promptTitle: "Prompt title",
34
+ promptBody: "Prompt body",
35
+ promptPlaceholder: "Write a reusable prompt…",
36
+ savePrompt: "Save Prompt",
37
+ addPrompt: "Add Prompt",
38
+ insertPrompt: "Insert Prompt",
39
+ morePromptActions: "More prompt actions",
40
+ star: "Star",
41
+ unstar: "Unstar",
42
+ showInFolder: "Show in folder",
43
+ attachToChat: "Attach to chat",
44
+ viewAll: "View all",
45
+ todo: "Todo",
46
+ noTodo: "No Todo yet",
47
+ prompts: "Prompts",
48
+ noPrompts: "No prompts yet",
49
+ files: "Files",
50
+ pinFiles: "Pin files",
51
+ noPinnedFiles: "No pinned files",
52
+ active: "Active",
53
+ add: "Add",
54
+ nothingActive: "Nothing active",
55
+ completed: "Completed",
56
+ deleteAll: "Delete all",
57
+ deleteCompletedTitle: "Delete all completed Todo",
58
+ noCompletedTodo: "No completed Todo",
59
+ starred: "Starred",
60
+ noStarredPrompts: "No starred prompts",
61
+ pinnedFiles: "Pinned files",
62
+ loading: "Loading CodexNotes…",
63
+ insertedAtCaret: "Inserted at the Composer caret",
64
+ addedToComposer: "Added to Composer",
65
+ insertedFilePath: "Inserted file path",
66
+ filesPinned: ({ count }) => `${count} file${count === 1 ? "" : "s"} pinned`,
67
+ alreadyPinned: "Already pinned",
68
+ todoEmpty: "Todo text cannot be empty",
69
+ promptEmpty: "Prompt body cannot be empty",
70
+ editorItemMissing: "This item no longer exists.",
71
+ close: "Close",
72
+ invalidRequest: "The request is invalid",
73
+ invalidContentType: "The request must use JSON",
74
+ invalidJson: "The request body is not valid JSON",
75
+ bodyTooLarge: "The request is too large",
76
+ notFound: "The item was not found",
77
+ invalidOrder: "The Todo order is no longer current",
78
+ invalidUse: "The requested action could not be recorded",
79
+ fileUnavailable: "The file is unavailable",
80
+ fileMissing: "The source file no longer exists",
81
+ directoryNotAllowed: "Folders cannot be pinned; choose regular files",
82
+ pathNotAbsolute: "Only absolute paths can be pinned",
83
+ pickerUnavailable: "The system file picker is unavailable",
84
+ pickerFailed: "The system file picker could not be opened",
85
+ revealUnavailable: "Show in folder is unavailable on this platform",
86
+ revealFailed: "The file could not be shown in the system file manager",
87
+ previewUnavailable: "This file does not have an image preview",
88
+ previewTooLarge: "The image is too large to preview",
89
+ internalError: "CodexNotes could not complete the request",
90
+ actionNotAllowed: "This action is not available",
91
+ },
92
+ "zh-CN": {
93
+ requestFailed: ({ status }) => `请求失败(${status})`,
94
+ temporarilyUnavailable: "CodexNotes 暂时不可用",
95
+ unavailable: "CodexNotes 不可用",
96
+ retry: "重试",
97
+ hostUnavailable: "尚未连接 Codex 宿主",
98
+ hostTimeout: "Codex 未响应",
99
+ hostActionFailed: "宿主操作失败",
100
+ restoreTodo: "恢复待办",
101
+ completeTodo: "完成待办",
102
+ edit: "编辑",
103
+ delete: "删除",
104
+ editTodo: "编辑待办",
105
+ newTodo: "新建待办",
106
+ todoText: "待办内容",
107
+ todoPlaceholder: "你想记住什么?",
108
+ saveTodo: "保存待办",
109
+ addTodo: "添加待办",
110
+ cancel: "取消",
111
+ editPrompt: "编辑提示词",
112
+ newPrompt: "新建提示词",
113
+ optionalTitle: "可选标题",
114
+ promptTitle: "提示词标题",
115
+ promptBody: "提示词正文",
116
+ promptPlaceholder: "编写一条可复用的提示词…",
117
+ savePrompt: "保存提示词",
118
+ addPrompt: "添加提示词",
119
+ insertPrompt: "插入提示词",
120
+ morePromptActions: "更多提示词操作",
121
+ star: "收藏",
122
+ unstar: "取消收藏",
123
+ showInFolder: "在文件夹中显示",
124
+ attachToChat: "添加到对话",
125
+ viewAll: "查看全部",
126
+ todo: "待办",
127
+ noTodo: "暂无待办",
128
+ prompts: "提示词",
129
+ noPrompts: "暂无提示词",
130
+ files: "文件",
131
+ pinFiles: "固定文件",
132
+ noPinnedFiles: "暂无固定文件",
133
+ active: "进行中",
134
+ add: "添加",
135
+ nothingActive: "暂无进行中的待办",
136
+ completed: "已完成",
137
+ deleteAll: "全部删除",
138
+ deleteCompletedTitle: "删除全部已完成待办",
139
+ noCompletedTodo: "暂无已完成待办",
140
+ starred: "已收藏",
141
+ noStarredPrompts: "暂无收藏的提示词",
142
+ pinnedFiles: "已固定文件",
143
+ loading: "正在加载 CodexNotes…",
144
+ insertedAtCaret: "已插入到消息输入框光标处",
145
+ addedToComposer: "已添加到消息输入框",
146
+ insertedFilePath: "已插入文件路径",
147
+ filesPinned: ({ count }) => `已固定 ${count} 个文件`,
148
+ alreadyPinned: "文件已固定",
149
+ todoEmpty: "待办内容不能为空",
150
+ promptEmpty: "提示词正文不能为空",
151
+ editorItemMissing: "该项目已不存在。",
152
+ close: "关闭",
153
+ invalidRequest: "请求无效",
154
+ invalidContentType: "请求必须使用 JSON",
155
+ invalidJson: "请求正文不是有效的 JSON",
156
+ bodyTooLarge: "请求内容过大",
157
+ notFound: "找不到该项目",
158
+ invalidOrder: "待办顺序已发生变化",
159
+ invalidUse: "无法记录这次操作",
160
+ fileUnavailable: "文件不可用",
161
+ fileMissing: "源文件已不存在",
162
+ directoryNotAllowed: "不能固定文件夹,请选择普通文件",
163
+ pathNotAbsolute: "只能固定绝对路径",
164
+ pickerUnavailable: "系统文件选择器不可用",
165
+ pickerFailed: "无法打开系统文件选择器",
166
+ revealUnavailable: "当前平台不支持在文件夹中显示",
167
+ revealFailed: "无法在系统文件管理器中显示该文件",
168
+ previewUnavailable: "该文件没有图片预览",
169
+ previewTooLarge: "图片过大,无法预览",
170
+ internalError: "CodexNotes 无法完成请求",
171
+ actionNotAllowed: "当前操作不可用",
172
+ },
173
+ });
174
+
175
+ let locale = normalizeLocale(params.get("locale") ?? navigator.languages?.[0] ?? navigator.language);
176
+ document.documentElement.lang = locale;
177
+
178
+ function t(key, params = {}) {
179
+ const value = MESSAGES[locale]?.[key] ?? MESSAGES.en[key] ?? key;
180
+ return typeof value === "function" ? value(params) : value;
181
+ }
182
+
183
+ const ERROR_MESSAGE_KEYS = Object.freeze({
184
+ ACTION_NOT_ALLOWED: "actionNotAllowed",
185
+ BODY_TOO_LARGE: "bodyTooLarge",
186
+ DIRECTORY_NOT_ALLOWED: "directoryNotAllowed",
187
+ EMPTY_BODY: "invalidRequest",
188
+ FILE_MISSING: "fileMissing",
189
+ FILE_UNAVAILABLE: "fileUnavailable",
190
+ HOST_ACTION_FAILED: "hostActionFailed",
191
+ HOST_NOT_ALLOWED: "actionNotAllowed",
192
+ HOST_TIMEOUT: "hostTimeout",
193
+ HOST_UNAVAILABLE: "hostUnavailable",
194
+ INTERNAL_ERROR: "internalError",
195
+ INVALID_BODY: "invalidRequest",
196
+ INVALID_CHANGES: "invalidRequest",
197
+ INVALID_COMPLETED: "invalidRequest",
198
+ INVALID_CONTENT_TYPE: "invalidContentType",
199
+ INVALID_ID: "invalidRequest",
200
+ INVALID_JSON: "invalidJson",
201
+ INVALID_ORDER: "invalidOrder",
202
+ INVALID_PATHS: "invalidRequest",
203
+ INVALID_REQUEST: "invalidRequest",
204
+ INVALID_SURFACE: "invalidRequest",
205
+ INVALID_ORIGIN: "actionNotAllowed",
206
+ INVALID_STAR: "invalidRequest",
207
+ INVALID_TITLE: "invalidRequest",
208
+ INVALID_USE: "invalidUse",
209
+ NOT_FOUND: "notFound",
210
+ ORIGIN_NOT_ALLOWED: "actionNotAllowed",
211
+ PATH_NOT_ABSOLUTE: "pathNotAbsolute",
212
+ PICKER_FAILED: "pickerFailed",
213
+ PICKER_UNAVAILABLE: "pickerUnavailable",
214
+ PREVIEW_TOO_LARGE: "previewTooLarge",
215
+ PREVIEW_UNAVAILABLE: "previewUnavailable",
216
+ REVEAL_FAILED: "revealFailed",
217
+ REVEAL_UNAVAILABLE: "revealUnavailable",
218
+ });
219
+
220
+ function localizedErrorMessage(code, fallback, params = {}) {
221
+ const key = ERROR_MESSAGE_KEYS[code];
222
+ return key ? t(key, params) : fallback;
223
+ }
224
+
225
+ const requestedSurface = params.get("surface") === "notes" ? "prompts" : params.get("surface");
226
+ const surface = new Set(["summary", "todo", "prompts", "files", "editor"]).has(requestedSurface)
227
+ ? requestedSurface
228
+ : "summary";
229
+ const editorKind = params.get("kind") === "todo" ? "todo" : "prompt";
230
+ const editorId = params.get("id") || null;
231
+
232
+ document.body.dataset.surface = surface;
233
+
234
+ const app = document.querySelector("#app");
235
+ const toast = document.querySelector("#toast");
236
+ const SUMMARY_ITEM_LIMIT = 5;
237
+ const PROMPT_PREVIEW_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
238
+ const DOMAIN_ICON_BY_SURFACE = Object.freeze({
239
+ todo: "list-todo",
240
+ prompt: "message-square-quote",
241
+ prompts: "message-square-quote",
242
+ files: "paperclip",
243
+ });
244
+ const CODEX_FILE_ICON_BY_EXTENSION = new Map([
245
+ ["ts", "typescript"],
246
+ ["tsx", "react"], ["jsx", "react"],
247
+ ["js", "javascript"], ["mjs", "javascript"], ["cjs", "javascript"], ["hs", "javascript"],
248
+ ["py", "python"],
249
+ ["java", "java"],
250
+ ["rs", "rust"],
251
+ ["php", "php"],
252
+ ["css", "css"], ["scss", "css"], ["less", "css"], ["sass", "css"],
253
+ ["cpp", "cplusplus"], ["cxx", "cplusplus"], ["cc", "cplusplus"], ["c", "cplusplus"],
254
+ ["hpp", "cplusplus"], ["hh", "cplusplus"], ["h", "cplusplus"],
255
+ ["rb", "code"], ["go", "code"], ["kt", "code"], ["swift", "code"], ["m", "code"],
256
+ ["mm", "code"], ["cs", "code"], ["sql", "code"],
257
+ ["json", "json"], ["jsonc", "json"],
258
+ ["md", "document"], ["mdx", "document"], ["markdown", "document"],
259
+ ["mkd", "document"], ["mdown", "document"],
260
+ ["html", "html"], ["htm", "html"],
261
+ ["yaml", "file"], ["yml", "file"],
262
+ ["toml", "toml"],
263
+ ["xml", "document"],
264
+ ["csv", "spreadsheet"], ["tsv", "spreadsheet"], ["xls", "spreadsheet"],
265
+ ["xlsm", "spreadsheet"], ["xlsx", "spreadsheet"],
266
+ ["doc", "artifact-document"], ["docx", "artifact-document"],
267
+ ["ipynb", "notebook"],
268
+ ["ppt", "presentation"], ["pptx", "presentation"],
269
+ ["sh", "shell"], ["bash", "shell"], ["zsh", "shell"], ["fish", "shell"], ["ps1", "shell"],
270
+ ["dockerfile", "terminal"],
271
+ ["env", "document"], ["dotenv", "document"], ["gitignore", "document"], ["lock", "document"],
272
+ ["png", "image"], ["jpg", "image"], ["jpeg", "image"], ["gif", "image"],
273
+ ["webp", "image"], ["bmp", "image"], ["svg", "image"], ["ico", "image"],
274
+ ["build", "build"], ["bazel", "build"], ["bzl", "build"], ["ninja", "build"],
275
+ ["gradle", "build"], ["mk", "build"], ["makefile", "build"],
276
+ ["sha", "hashes"], ["sha1", "hashes"], ["sha256", "hashes"],
277
+ ["md5", "hashes"], ["checksum", "hashes"], ["sum", "hashes"],
278
+ ["pdf", "pdf"],
279
+ ["zip", "folder"], ["gz", "folder"], ["tgz", "folder"], ["tar", "folder"],
280
+ ]);
281
+ const CODEX_COLOR_FILE_ICONS = new Set(["artifact-document", "pdf", "presentation", "spreadsheet"]);
282
+ const ui = {
283
+ state: null,
284
+ todoById: new Map(),
285
+ promptById: new Map(),
286
+ fileById: new Map(),
287
+ loading: true,
288
+ error: null,
289
+ editingTodo: null,
290
+ editingPrompt: null,
291
+ editorInitialized: false,
292
+ draggingTodo: null,
293
+ todoDropTarget: null,
294
+ todoDragPreview: null,
295
+ promptPreviewId: null,
296
+ requestId: 0,
297
+ hostRequests: new Map(),
298
+ hostOrigin: null,
299
+ refreshPromise: null,
300
+ pendingRevision: 0,
301
+ refreshShouldReportErrors: false,
302
+ eventSource: null,
303
+ eventReconnectTimer: null,
304
+ editorMissing: false,
305
+ editorSubmitting: false,
306
+ };
307
+
308
+ const escapeHtml = (value) => String(value ?? "")
309
+ .replaceAll("&", "&")
310
+ .replaceAll("<", "&lt;")
311
+ .replaceAll(">", "&gt;")
312
+ .replaceAll('"', "&quot;")
313
+ .replaceAll("'", "&#039;");
314
+
315
+ function setLocale(value) {
316
+ const next = normalizeLocale(value);
317
+ if (next === locale) return;
318
+ locale = next;
319
+ document.documentElement.lang = locale;
320
+ if (ui.state) render();
321
+ }
322
+
323
+ function postToHost(message) {
324
+ window.parent.postMessage(message, ui.hostOrigin ?? "*");
325
+ }
326
+
327
+ function indexState(state) {
328
+ ui.todoById = new Map(
329
+ [...state.todos.active, ...state.todos.completed].map((item) => [item.id, item]),
330
+ );
331
+ ui.promptById = new Map(state.prompts.map((item) => [item.id, item]));
332
+ ui.fileById = new Map(state.files.map((item) => [item.id, item]));
333
+ }
334
+
335
+ function showToast(message, tone = "neutral") {
336
+ toast.textContent = message;
337
+ toast.dataset.tone = tone;
338
+ toast.classList.add("is-visible");
339
+ clearTimeout(showToast.timeout);
340
+ showToast.timeout = setTimeout(() => toast.classList.remove("is-visible"), 2600);
341
+ }
342
+
343
+ async function api(path, options = {}) {
344
+ const response = await fetch(path, {
345
+ ...options,
346
+ headers: options.body
347
+ ? { "Content-Type": "application/json", ...(options.headers ?? {}) }
348
+ : options.headers,
349
+ });
350
+ if (!response.ok) {
351
+ const payload = await response.json().catch(() => null);
352
+ const fallback = payload?.error?.message ?? t("requestFailed", { status: response.status });
353
+ const error = new Error(localizedErrorMessage(payload?.error?.code, fallback, payload?.error?.details));
354
+ error.code = payload?.error?.code;
355
+ error.status = response.status;
356
+ error.details = payload?.error?.details;
357
+ throw error;
358
+ }
359
+ if (response.status === 204) return null;
360
+ return response.json();
361
+ }
362
+
363
+ function syncEditorMissingState() {
364
+ if (surface !== "editor" || !ui.editorInitialized) return false;
365
+ const draft = editorKind === "todo" ? ui.editingTodo : ui.editingPrompt;
366
+ if (!draft?.id) return false;
367
+ const exists = editorKind === "todo" ? ui.todoById.has(draft.id) : ui.promptById.has(draft.id);
368
+ const nextMissing = !exists;
369
+ const changed = ui.editorMissing !== nextMissing;
370
+ ui.editorMissing = nextMissing;
371
+ return changed;
372
+ }
373
+
374
+ function stateRequest() {
375
+ const query = `surface=${encodeURIComponent(surface)}`;
376
+ if (surface === "editor") {
377
+ return `/api/state?${query}&kind=${encodeURIComponent(editorKind)}&id=${encodeURIComponent(editorId ?? "")}`;
378
+ }
379
+ return `/api/state?${query}`;
380
+ }
381
+
382
+ function changeAffectsSurface(change) {
383
+ if (!change?.reason) return true;
384
+ if (Array.isArray(change.surfaces)) {
385
+ const target = surface === "editor" ? `editor:${editorKind}` : surface;
386
+ if (change.surfaces.includes(target)) return true;
387
+ }
388
+ const domain = String(change.reason).split(":", 1)[0];
389
+ if (surface === "summary") return ["todo", "prompt", "file", "files"].includes(domain);
390
+ if (surface === "editor") return domain === editorKind;
391
+ return domain === (surface === "prompts" ? "prompt" : surface);
392
+ }
393
+
394
+ async function performRefresh() {
395
+ const previousRevision = ui.state?.revision ?? -1;
396
+ const hadState = Boolean(ui.state);
397
+ const hadError = Boolean(ui.error);
398
+ try {
399
+ const next = await api(stateRequest());
400
+ const stateChanged = !ui.state || next.revision > ui.state.revision;
401
+ if (stateChanged) {
402
+ ui.state = next;
403
+ indexState(next);
404
+ }
405
+ ui.error = null;
406
+ ui.loading = false;
407
+ const editorMissingChanged = stateChanged ? syncEditorMissingState() : false;
408
+ if (!hadState || hadError || (stateChanged && (surface !== "editor" || editorMissingChanged))) render();
409
+ return ui.state;
410
+ } catch (error) {
411
+ ui.error = t("temporarilyUnavailable");
412
+ ui.loading = false;
413
+ if (ui.refreshShouldReportErrors) showToast(error.message, "danger");
414
+ if (!hadState || !hadError) render();
415
+ return ui.state;
416
+ } finally {
417
+ ui.refreshShouldReportErrors = false;
418
+ const shouldRetry = ui.pendingRevision > Math.max(previousRevision, ui.state?.revision ?? -1);
419
+ ui.refreshPromise = null;
420
+ if (shouldRetry) queueMicrotask(() => refreshState({ quiet: true }));
421
+ }
422
+ }
423
+
424
+ function refreshState({ quiet = false, revision = 0 } = {}) {
425
+ ui.pendingRevision = Math.max(ui.pendingRevision, Number(revision) || 0);
426
+ if (!quiet) ui.refreshShouldReportErrors = true;
427
+ if (!ui.refreshPromise) ui.refreshPromise = performRefresh();
428
+ return ui.refreshPromise;
429
+ }
430
+
431
+ function connectEvents() {
432
+ clearTimeout(ui.eventReconnectTimer);
433
+ ui.eventReconnectTimer = null;
434
+ ui.eventSource?.close();
435
+ const events = new EventSource("/api/events");
436
+ ui.eventSource = events;
437
+ const handleRevision = (event) => {
438
+ let change;
439
+ try {
440
+ change = JSON.parse(event.data);
441
+ } catch {
442
+ return;
443
+ }
444
+ if (!ui.state || change.revision > ui.state.revision) {
445
+ if (!changeAffectsSurface(change)) {
446
+ ui.pendingRevision = Math.max(ui.pendingRevision, change.revision);
447
+ return;
448
+ }
449
+ refreshState({ quiet: true, revision: change.revision });
450
+ }
451
+ };
452
+ events.addEventListener("ready", handleRevision);
453
+ events.addEventListener("change", (event) => {
454
+ handleRevision(event);
455
+ });
456
+ events.onerror = () => {
457
+ events.close();
458
+ if (ui.eventSource === events) ui.eventSource = null;
459
+ if (!ui.eventReconnectTimer) ui.eventReconnectTimer = setTimeout(connectEvents, 1200);
460
+ };
461
+ }
462
+
463
+ function hostRequest(action, payload = {}, timeoutMs = 8000) {
464
+ if (window.parent === window) {
465
+ return Promise.reject(Object.assign(new Error(t("hostUnavailable")), { code: "HOST_UNAVAILABLE" }));
466
+ }
467
+ const requestId = `codexnotes-${Date.now()}-${++ui.requestId}`;
468
+ return new Promise((resolve, reject) => {
469
+ const timeout = setTimeout(() => {
470
+ ui.hostRequests.delete(requestId);
471
+ reject(Object.assign(new Error(t("hostTimeout")), { code: "HOST_TIMEOUT" }));
472
+ }, timeoutMs);
473
+ ui.hostRequests.set(requestId, { resolve, reject, timeout });
474
+ postToHost({
475
+ type: "codex-personal:host-action",
476
+ feature: "notes",
477
+ surface,
478
+ requestId,
479
+ action,
480
+ payload,
481
+ });
482
+ });
483
+ }
484
+
485
+ window.addEventListener("message", (event) => {
486
+ if (event.source !== window.parent) return;
487
+ if (ui.hostOrigin && event.origin !== ui.hostOrigin) return;
488
+ const message = event.data;
489
+ if (!message || typeof message !== "object") return;
490
+ if (!ui.hostOrigin && event.origin && event.origin !== "null") ui.hostOrigin = event.origin;
491
+ if (message.type === "codex-personal:theme") {
492
+ if (message.locale) setLocale(message.locale);
493
+ const tokens = message.tokens ?? message.theme?.tokens ?? {};
494
+ for (const [name, value] of Object.entries(tokens)) {
495
+ if (typeof value === "string" && (name.startsWith("--") || name.startsWith("codex-"))) {
496
+ document.documentElement.style.setProperty(name.startsWith("--") ? name : `--${name}`, value);
497
+ }
498
+ }
499
+ if (message.theme?.mode || message.mode) {
500
+ document.documentElement.dataset.theme = message.theme?.mode ?? message.mode;
501
+ }
502
+ reportSummaryMetrics();
503
+ return;
504
+ }
505
+ if (message.type === "codex-personal:focus") {
506
+ requestAnimationFrame(() => app.querySelector("[autofocus]")?.focus());
507
+ return;
508
+ }
509
+ if (message.type !== "codex-personal:host-result" || message.feature !== "notes") return;
510
+ const request = ui.hostRequests.get(message.requestId);
511
+ if (!request) return;
512
+ clearTimeout(request.timeout);
513
+ ui.hostRequests.delete(message.requestId);
514
+ if (message.ok) request.resolve(message.result ?? {});
515
+ else request.reject(Object.assign(new Error(localizedErrorMessage(
516
+ message.error?.code,
517
+ message.error?.message ?? t("hostActionFailed"),
518
+ )), {
519
+ code: message.error?.code ?? "HOST_ACTION_FAILED",
520
+ }));
521
+ });
522
+
523
+ function iconMarkup(name, className = "") {
524
+ return `<span class="icon icon-${name}${className ? ` ${className}` : ""}" aria-hidden="true"></span>`;
525
+ }
526
+
527
+ function domainIconName(key) {
528
+ return DOMAIN_ICON_BY_SURFACE[key] ?? "attachment";
529
+ }
530
+
531
+ function iconName(action) {
532
+ if (action.startsWith("add-")) return action === "add-file" ? "attachment" : "plus";
533
+ if (action.startsWith("delete-")) return "trash-2";
534
+ return {
535
+ "edit-prompt": "edit-pencil",
536
+ "edit-todo": "edit-pencil",
537
+ more: "ellipsis",
538
+ "reveal-file": "external-link",
539
+ }[action] ?? null;
540
+ }
541
+
542
+ function codexFileIconKind(file) {
543
+ const rawPath = String(file.path || file.name || "").toLowerCase();
544
+ if (/[\\/]$/.test(rawPath)) return "folder";
545
+ const name = rawPath.split(/[\\/]/).at(-1) || "";
546
+ if (name === "skill.md") return "skill";
547
+ const dot = name.lastIndexOf(".");
548
+ const extension = dot > 0 && dot < name.length - 1
549
+ ? name.slice(dot + 1)
550
+ : dot === 0 && name.length > 1
551
+ ? name.slice(1)
552
+ : dot === -1
553
+ ? name
554
+ : null;
555
+ const extensionKind = extension ? CODEX_FILE_ICON_BY_EXTENSION.get(extension) : null;
556
+ if (extensionKind) return extensionKind;
557
+ const mimeType = String(file.mimeType || "").toLowerCase();
558
+ if (mimeType.startsWith("image/")) return "image";
559
+ if (mimeType.startsWith("text/")) return "document";
560
+ if (mimeType.startsWith("application/pdf")) return "pdf";
561
+ if (mimeType.startsWith("application/zip") || mimeType.startsWith("application/gzip")) return "folder";
562
+ return "file";
563
+ }
564
+
565
+ function fileIconMarkup(kind) {
566
+ if (CODEX_COLOR_FILE_ICONS.has(kind)) {
567
+ return `<img class="file-kind is-color" src="/icons/file-${kind}.svg" alt="" aria-hidden="true">`;
568
+ }
569
+ return iconMarkup(`file-${kind}`, "file-kind");
570
+ }
571
+
572
+ function actionButton(action, label, { quiet = false, title = label } = {}) {
573
+ const icon = quiet ? iconName(action) : null;
574
+ return `<button class="${quiet ? "quiet-button" : "text-button"}" type="button" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(label)}">${icon ? iconMarkup(icon) : escapeHtml(label)}</button>`;
575
+ }
576
+
577
+ function todoRow(todo, { completed = false, draggable = true } = {}) {
578
+ const toggleLabel = completed ? t("restoreTodo") : t("completeTodo");
579
+ return `<li class="row todo-row${completed ? " is-completed" : ""}" data-id="${escapeHtml(todo.id)}" ${draggable && !completed ? 'draggable="true"' : ""}>
580
+ ${draggable && !completed ? '<span class="todo-drop-indicator" aria-hidden="true" role="presentation"></span>' : ""}
581
+ <label class="check-control" title="${escapeHtml(toggleLabel)}">
582
+ <input type="checkbox" data-action="toggle-todo" data-id="${escapeHtml(todo.id)}" aria-label="${escapeHtml(toggleLabel)}" ${completed ? "checked" : ""}>
583
+ <span class="todo-check-icon" aria-hidden="true"></span>
584
+ </label>
585
+ <span class="row-primary todo-label">${escapeHtml(todo.body)}</span>
586
+ <div class="row-actions">
587
+ ${actionButton("edit-todo", t("edit"), { quiet: true })}
588
+ ${actionButton("delete-todo", t("delete"), { quiet: true })}
589
+ </div>
590
+ </li>`;
591
+ }
592
+
593
+ function initializeEditor() {
594
+ if (surface !== "editor" || ui.editorInitialized) return;
595
+ ui.editorInitialized = true;
596
+ if (editorKind === "todo") {
597
+ const todo = ui.todoById.get(editorId);
598
+ ui.editorMissing = Boolean(editorId && !todo);
599
+ ui.editingTodo = {
600
+ id: todo?.id ?? editorId,
601
+ body: todo?.body ?? "",
602
+ };
603
+ return;
604
+ }
605
+ const prompt = ui.promptById.get(editorId);
606
+ ui.editorMissing = Boolean(editorId && !prompt);
607
+ ui.editingPrompt = {
608
+ id: prompt?.id ?? editorId,
609
+ title: prompt?.title ?? "",
610
+ body: prompt?.body ?? "",
611
+ };
612
+ }
613
+
614
+ function modalCommand(label, { action = null, submit = false, shortcut = "", disabled = false } = {}) {
615
+ const behavior = submit
616
+ ? 'type="submit"'
617
+ : `type="button" data-action="${action}"`;
618
+ return `<button ${behavior} class="modal-command${submit ? " is-default" : ""}"${disabled ? " disabled" : ""}>
619
+ <span>${escapeHtml(label)}</span>
620
+ ${shortcut ? `<kbd>${escapeHtml(shortcut)}</kbd>` : ""}
621
+ </button>`;
622
+ }
623
+
624
+ function modalEditorTitle(kind, label) {
625
+ return `<header class="modal-editor-title">
626
+ ${iconMarkup(domainIconName(kind), "modal-editor-title-icon")}
627
+ <span>${escapeHtml(label)}</span>
628
+ </header>`;
629
+ }
630
+
631
+ function syncEditorSubmitState(form) {
632
+ const submit = form?.querySelector('button[type="submit"]');
633
+ const body = form?.elements.namedItem("body");
634
+ if (submit) submit.disabled = !body?.value.trim();
635
+ }
636
+
637
+ function renderEditor() {
638
+ initializeEditor();
639
+ if (ui.editorMissing) {
640
+ return `<main class="editor-modal-shell" data-editor-shell>
641
+ <div class="modal-editor" role="alert">
642
+ ${modalEditorTitle(editorKind, editorKind === "todo" ? t("editTodo") : t("editPrompt"))}
643
+ <div class="status-state is-error"><p>${escapeHtml(t("editorItemMissing"))}</p></div>
644
+ <div class="modal-actions">${modalCommand(t("close"), { action: "close-editor", shortcut: "esc" })}</div>
645
+ </div>
646
+ </main>`;
647
+ }
648
+ if (editorKind === "todo") {
649
+ const draft = ui.editingTodo;
650
+ const editing = Boolean(draft?.id);
651
+ return `<main class="editor-modal-shell" data-editor-shell>
652
+ <form class="modal-editor" data-form="save-todo">
653
+ ${modalEditorTitle("todo", editing ? t("editTodo") : t("newTodo"))}
654
+ <div class="modal-editor-fields is-todo">
655
+ <textarea name="body" aria-label="${escapeHtml(t("todoText"))}" placeholder="${escapeHtml(t("todoPlaceholder"))}" autofocus>${escapeHtml(draft?.body)}</textarea>
656
+ </div>
657
+ <div class="modal-actions">
658
+ ${modalCommand(editing ? t("saveTodo") : t("addTodo"), { submit: true, shortcut: "⌘↩", disabled: !draft?.body.trim() })}
659
+ ${modalCommand(t("cancel"), { action: "close-editor", shortcut: "esc" })}
660
+ </div>
661
+ </form>
662
+ </main>`;
663
+ }
664
+ const draft = ui.editingPrompt;
665
+ const editing = Boolean(draft?.id);
666
+ return `<main class="editor-modal-shell" data-editor-shell>
667
+ <form class="modal-editor" data-form="save-prompt">
668
+ ${modalEditorTitle("prompt", editing ? t("editPrompt") : t("newPrompt"))}
669
+ <div class="modal-editor-fields is-prompt">
670
+ <input name="title" value="${escapeHtml(draft?.title)}" placeholder="${escapeHtml(t("optionalTitle"))}" aria-label="${escapeHtml(t("promptTitle"))}" autocomplete="off">
671
+ <textarea name="body" aria-label="${escapeHtml(t("promptBody"))}" placeholder="${escapeHtml(t("promptPlaceholder"))}" autofocus>${escapeHtml(draft?.body)}</textarea>
672
+ </div>
673
+ <div class="modal-actions">
674
+ ${modalCommand(editing ? t("savePrompt") : t("addPrompt"), { submit: true, shortcut: "⌘↩", disabled: !draft?.body.trim() })}
675
+ ${modalCommand(t("cancel"), { action: "close-editor", shortcut: "esc" })}
676
+ </div>
677
+ </form>
678
+ </main>`;
679
+ }
680
+
681
+ function promptLeading(prompt) {
682
+ return `<span class="summary-leading prompt-star-leading${prompt.starred ? " is-starred" : " is-placeholder"}" aria-hidden="true">
683
+ ${prompt.starred ? iconMarkup("star", "star-mark") : ""}
684
+ </span>`;
685
+ }
686
+
687
+ function promptRow(prompt, { showLeading = true } = {}) {
688
+ return `<li class="row prompt-row" data-id="${escapeHtml(prompt.id)}">
689
+ ${showLeading ? promptLeading(prompt) : ""}
690
+ <button type="button" class="row-primary prompt-label" data-action="insert-prompt" data-id="${escapeHtml(prompt.id)}" title="${escapeHtml(t("insertPrompt"))}">
691
+ <span>${escapeHtml(prompt.summary)}</span>
692
+ </button>
693
+ <div class="row-actions">
694
+ ${actionButton("edit-prompt", t("edit"), { quiet: true })}
695
+ <details class="row-menu">
696
+ <summary class="quiet-button" aria-label="${escapeHtml(t("morePromptActions"))}" title="${escapeHtml(t("morePromptActions"))}">${iconMarkup("ellipsis")}</summary>
697
+ <div class="menu-popover">
698
+ <button type="button" class="menu-item" data-action="toggle-prompt-star" data-id="${escapeHtml(prompt.id)}">
699
+ ${iconMarkup(prompt.starred ? "star-off" : "star", "menu-item-icon")}
700
+ <span>${prompt.starred ? t("unstar") : t("star")}</span>
701
+ </button>
702
+ <button type="button" class="menu-item" data-action="delete-prompt" data-id="${escapeHtml(prompt.id)}">
703
+ ${iconMarkup("trash-2", "menu-item-icon")}
704
+ <span>${escapeHtml(t("delete"))}</span>
705
+ </button>
706
+ </div>
707
+ </details>
708
+ </div>
709
+ </li>`;
710
+ }
711
+
712
+ function fileLeading(file) {
713
+ const kind = codexFileIconKind(file);
714
+ const fallback = fileIconMarkup(kind);
715
+ if (kind !== "image") {
716
+ return `<span class="summary-leading file-leading">${fallback}</span>`;
717
+ }
718
+ return `<span class="summary-leading file-leading has-thumbnail">
719
+ <img class="file-thumbnail" src="/api/files/${encodeURIComponent(file.id)}/preview" alt="" loading="lazy" data-file-thumbnail>
720
+ <span class="file-thumbnail-fallback">${fallback}</span>
721
+ </span>`;
722
+ }
723
+
724
+ function fileRow(file) {
725
+ return `<li class="row file-row" data-id="${escapeHtml(file.id)}" title="${escapeHtml(file.path)}">
726
+ ${fileLeading(file)}
727
+ <button type="button" class="row-primary file-label" data-action="add-file" data-id="${escapeHtml(file.id)}" title="${escapeHtml(t("attachToChat"))}">${escapeHtml(file.name)}</button>
728
+ <div class="row-actions">
729
+ ${actionButton("reveal-file", t("showInFolder"), { quiet: true })}
730
+ ${actionButton("delete-file", t("delete"), { quiet: true })}
731
+ </div>
732
+ </li>`;
733
+ }
734
+
735
+ function emptyRow(label) {
736
+ return `<li class="empty-row">${escapeHtml(label)}</li>`;
737
+ }
738
+
739
+ function summarySection({ key, title, items, addLabel, rows, emptyLabel, totalCount = items.length }) {
740
+ const visible = items.slice(0, SUMMARY_ITEM_LIMIT);
741
+ const summaryCounts = ui.state?.counts;
742
+ const inferredCount = key === "todo"
743
+ ? (summaryCounts ? summaryCounts.todos.active + summaryCounts.todos.completed : items.length)
744
+ : (summaryCounts?.[key] ?? items.length);
745
+ const hasOverflow = totalCount > SUMMARY_ITEM_LIMIT || inferredCount > SUMMARY_ITEM_LIMIT;
746
+ return `<section class="summary-section" data-section="${key}">
747
+ <header class="section-header">
748
+ <span>${title}</span>
749
+ ${actionButton(`add-${key}`, addLabel, { quiet: true, title: addLabel })}
750
+ </header>
751
+ <ul class="flat-list">${visible.length ? visible.map(rows).join("") : emptyRow(emptyLabel)}</ul>
752
+ ${hasOverflow ? `<button type="button" class="view-all" data-action="view-all" data-surface="${key === "todo" ? "todo" : key}">
753
+ ${iconMarkup(domainIconName(key), "view-all-icon")}
754
+ <span class="view-all-label">${escapeHtml(t("viewAll"))}</span>
755
+ </button>` : ""}
756
+ </section>`;
757
+ }
758
+
759
+ function renderSummary() {
760
+ const active = ui.state.todos.active;
761
+ const completed = ui.state.todos.completed;
762
+ const prompts = ui.state.prompts;
763
+ const files = ui.state.files;
764
+ const counts = ui.state.counts ?? {
765
+ todos: { active: active.length, completed: completed.length },
766
+ prompts: prompts.length,
767
+ files: files.length,
768
+ };
769
+ const visibleTodos = active.length >= SUMMARY_ITEM_LIMIT
770
+ ? active.slice(0, SUMMARY_ITEM_LIMIT)
771
+ : [...active, ...completed.slice(0, SUMMARY_ITEM_LIMIT - active.length)];
772
+ return `<div class="summary-shell" data-summary-shell>
773
+ <div class="summary-scroll" data-summary-scroll>
774
+ ${summarySection({
775
+ key: "todo",
776
+ title: t("todo"),
777
+ items: visibleTodos,
778
+ totalCount: active.length + completed.length,
779
+ addLabel: t("addTodo"),
780
+ rows: (item) => todoRow(item, {
781
+ completed: Boolean(item.completedAt),
782
+ draggable: !item.completedAt,
783
+ }),
784
+ emptyLabel: t("noTodo"),
785
+ })}
786
+ ${summarySection({ key: "prompts", title: t("prompts"), items: prompts, addLabel: t("addPrompt"), rows: promptRow, emptyLabel: t("noPrompts") })}
787
+ ${summarySection({ key: "files", title: t("files"), items: files, addLabel: t("pinFiles"), rows: fileRow, emptyLabel: t("noPinnedFiles") })}
788
+ </div>
789
+ </div>`;
790
+ }
791
+
792
+ function pageActionButton(action, label, icon, title = label) {
793
+ return `<button type="button" class="page-action" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">
794
+ ${iconMarkup(icon, "page-action-icon")}
795
+ <span>${escapeHtml(label)}</span>
796
+ </button>`;
797
+ }
798
+
799
+ function pageSectionTitle(title, count, action = "") {
800
+ return `<div class="page-section-title">
801
+ <div class="page-section-heading"><h2>${title}</h2><span class="page-section-count">${count}</span></div>
802
+ ${action}
803
+ </div>`;
804
+ }
805
+
806
+ function renderTodoPage() {
807
+ const { active, completed } = ui.state.todos;
808
+ return `<div class="full-shell todo-page">
809
+ <section class="page-section">
810
+ ${pageSectionTitle(t("active"), active.length, pageActionButton("add-todo", t("add"), "plus"))}
811
+ <ul class="flat-list full-list">${active.length ? active.map((item) => todoRow(item)).join("") : emptyRow(t("nothingActive"))}</ul>
812
+ </section>
813
+ <section class="page-section completed-section">
814
+ ${pageSectionTitle(
815
+ t("completed"),
816
+ completed.length,
817
+ completed.length ? pageActionButton("clear-completed", t("deleteAll"), "trash-2", t("deleteCompletedTitle")) : "",
818
+ )}
819
+ <ul class="flat-list full-list">${completed.length ? completed.map((item) => todoRow(item, { completed: true, draggable: false })).join("") : emptyRow(t("noCompletedTodo"))}</ul>
820
+ </section>
821
+ </div>`;
822
+ }
823
+
824
+ function renderPromptsPage() {
825
+ const starred = ui.state.prompts.filter((prompt) => prompt.starred);
826
+ const prompts = ui.state.prompts.filter((prompt) => !prompt.starred);
827
+ return `<div class="full-shell prompts-page">
828
+ <div class="prompts-page-actions">
829
+ ${pageActionButton("add-prompts", t("add"), "plus")}
830
+ </div>
831
+ <section class="page-section">
832
+ ${pageSectionTitle(t("starred"), starred.length)}
833
+ <ul class="flat-list full-list full-prompt-list">${starred.length ? starred.map((prompt) => promptRow(prompt, { showLeading: false })).join("") : emptyRow(t("noStarredPrompts"))}</ul>
834
+ </section>
835
+ <section class="page-section">
836
+ ${pageSectionTitle(t("prompts"), prompts.length)}
837
+ <ul class="flat-list full-list full-prompt-list">${prompts.length ? prompts.map((prompt) => promptRow(prompt, { showLeading: false })).join("") : emptyRow(t("noPrompts"))}</ul>
838
+ </section>
839
+ </div>`;
840
+ }
841
+
842
+ function renderFilesPage() {
843
+ return `<div class="full-shell files-page" data-file-drop-zone>
844
+ <section class="page-section">
845
+ ${pageSectionTitle(t("pinnedFiles"), ui.state.files.length, pageActionButton("add-files", t("add"), "plus", t("pinFiles")))}
846
+ <ul class="flat-list full-list">${ui.state.files.length ? ui.state.files.map(fileRow).join("") : emptyRow(t("noPinnedFiles"))}</ul>
847
+ </section>
848
+ </div>`;
849
+ }
850
+
851
+ function syncPromptRowDensity() {
852
+ if (surface !== "prompts") return;
853
+ cancelAnimationFrame(syncPromptRowDensity.frame);
854
+ syncPromptRowDensity.frame = requestAnimationFrame(() => {
855
+ const measurements = [...app.querySelectorAll(".prompt-row")].map((row) => {
856
+ const label = row.querySelector(".prompt-label");
857
+ if (!label) return null;
858
+ const lineHeight = Number.parseFloat(getComputedStyle(label).lineHeight);
859
+ return { row, multiline: label.getBoundingClientRect().height > lineHeight * 1.5 };
860
+ }).filter(Boolean);
861
+ for (const { row, multiline } of measurements) row.classList.toggle("is-multiline", multiline);
862
+ });
863
+ }
864
+
865
+ function render() {
866
+ if (ui.loading && !ui.state) {
867
+ app.innerHTML = `<div class="status-state"><span class="spinner"></span><p>${escapeHtml(t("loading"))}</p></div>`;
868
+ return;
869
+ }
870
+ if (!ui.state) {
871
+ app.innerHTML = `<div class="status-state is-error"><p>${escapeHtml(ui.error ?? t("unavailable"))}</p><button type="button" class="text-button" data-action="retry">${escapeHtml(t("retry"))}</button></div>`;
872
+ return;
873
+ }
874
+ if (surface === "summary") app.innerHTML = renderSummary();
875
+ else if (surface === "todo") app.innerHTML = renderTodoPage();
876
+ else if (surface === "prompts") app.innerHTML = renderPromptsPage();
877
+ else if (surface === "files") app.innerHTML = renderFilesPage();
878
+ else app.innerHTML = renderEditor();
879
+ syncPromptRowDensity();
880
+ requestAnimationFrame(() => {
881
+ app.querySelector("[autofocus]")?.focus();
882
+ reportSummaryMetrics();
883
+ });
884
+ }
885
+
886
+ function reportSummaryMetrics() {
887
+ if (surface !== "summary" || window.parent === window) return;
888
+ cancelAnimationFrame(reportSummaryMetrics.frame);
889
+ clearTimeout(reportSummaryMetrics.timeout);
890
+ const publish = () => {
891
+ cancelAnimationFrame(reportSummaryMetrics.frame);
892
+ clearTimeout(reportSummaryMetrics.timeout);
893
+ reportSummaryMetrics.frame = null;
894
+ reportSummaryMetrics.timeout = null;
895
+ const shell = app.querySelector("[data-summary-shell]");
896
+ const scroll = app.querySelector("[data-summary-scroll]");
897
+ if (!shell || !scroll) return;
898
+ const shellStyle = getComputedStyle(shell);
899
+ const chrome = Number.parseFloat(shellStyle.paddingTop) + Number.parseFloat(shellStyle.paddingBottom);
900
+ const height = Math.ceil(scroll.scrollHeight + (Number.isFinite(chrome) ? chrome : 0));
901
+ postToHost({
902
+ type: "codex-personal:surface-metrics",
903
+ feature: "notes",
904
+ surface,
905
+ height,
906
+ });
907
+ };
908
+ reportSummaryMetrics.frame = requestAnimationFrame(publish);
909
+ reportSummaryMetrics.timeout = setTimeout(publish, 50);
910
+ }
911
+
912
+ async function mutate(path, options, successMessage = null) {
913
+ try {
914
+ const result = await api(path, options);
915
+ await refreshState({ quiet: true, revision: result?.revision });
916
+ if (successMessage) showToast(successMessage, "success");
917
+ return true;
918
+ } catch (error) {
919
+ showToast(error.message, "danger");
920
+ await refreshState({ quiet: true });
921
+ if (surface !== "editor" && ui.state) render();
922
+ return false;
923
+ }
924
+ }
925
+
926
+ async function openEditorModal(kind, id = null) {
927
+ if (window.parent === window) {
928
+ const url = new URL(location.href);
929
+ url.searchParams.set("surface", "editor");
930
+ url.searchParams.set("kind", kind);
931
+ if (id) url.searchParams.set("id", id);
932
+ else url.searchParams.delete("id");
933
+ window.open(url, "codexnotes-editor", `popup,width=420,height=${kind === "prompt" ? 250 : 198}`);
934
+ return;
935
+ }
936
+ await hostRequest("open-editor-modal", { kind, id });
937
+ }
938
+
939
+ function closeEditor() {
940
+ if (window.parent !== window) {
941
+ postToHost({ type: "codex-personal:close", feature: "notes", surface });
942
+ } else {
943
+ window.close();
944
+ }
945
+ }
946
+
947
+ async function insertPrompt(id) {
948
+ const prompt = ui.promptById.get(id);
949
+ if (!prompt) return;
950
+ try {
951
+ await hostRequest("insert-text", { text: prompt.body });
952
+ await api(`/api/prompts/${encodeURIComponent(id)}/use`, {
953
+ method: "POST",
954
+ body: JSON.stringify({ action: "insert", success: true }),
955
+ });
956
+ await refreshState({ quiet: true });
957
+ showToast(t("insertedAtCaret"), "success");
958
+ } catch (error) {
959
+ showToast(error.message, "danger");
960
+ }
961
+ }
962
+
963
+ function closeOpenPromptMenus(except = null) {
964
+ for (const menu of app.querySelectorAll(".row-menu[open]")) {
965
+ if (menu !== except) menu.open = false;
966
+ }
967
+ }
968
+
969
+ function dismissPromptMenus({ releaseFocus = false } = {}) {
970
+ const focusedMenu = document.activeElement?.closest?.(".row-menu");
971
+ closeOpenPromptMenus();
972
+ if (releaseFocus && focusedMenu && document.activeElement instanceof HTMLElement) {
973
+ document.activeElement.blur();
974
+ }
975
+ }
976
+
977
+ function postPromptPreview(phase, prompt = null, row = null) {
978
+ if (surface !== "summary" || window.parent === window) return;
979
+ const message = {
980
+ type: "codex-personal:prompt-preview",
981
+ feature: "notes",
982
+ phase,
983
+ };
984
+ if (phase === "show" && prompt && row) {
985
+ const rect = row.getBoundingClientRect();
986
+ message.prompt = {
987
+ id: prompt.id,
988
+ title: prompt.title,
989
+ body: prompt.body,
990
+ };
991
+ message.anchor = {
992
+ top: rect.top,
993
+ right: rect.right,
994
+ bottom: rect.bottom,
995
+ left: rect.left,
996
+ width: rect.width,
997
+ height: rect.height,
998
+ };
999
+ }
1000
+ postToHost(message);
1001
+ }
1002
+
1003
+ function hidePromptPreview() {
1004
+ if (!ui.promptPreviewId) return;
1005
+ ui.promptPreviewId = null;
1006
+ postPromptPreview("hide");
1007
+ }
1008
+
1009
+ function showPromptPreviewForRow(row) {
1010
+ if (
1011
+ surface !== "summary"
1012
+ || window.parent === window
1013
+ || !window.matchMedia(PROMPT_PREVIEW_POINTER_QUERY).matches
1014
+ ) return;
1015
+ const id = row?.dataset.id;
1016
+ if (!id || ui.promptPreviewId === id) return;
1017
+ if (!row.isConnected) return;
1018
+ const prompt = ui.promptById.get(id);
1019
+ if (!prompt) return;
1020
+ hidePromptPreview();
1021
+ ui.promptPreviewId = id;
1022
+ postPromptPreview("show", prompt, row);
1023
+ }
1024
+
1025
+ function pointerTargetsPromptMenu(event, row) {
1026
+ const directMenu = event.target.closest?.(".row-menu");
1027
+ if (directMenu && row.contains(directMenu)) return true;
1028
+ const trigger = row.querySelector(".row-menu > summary");
1029
+ if (!trigger) return false;
1030
+ const rect = trigger.getBoundingClientRect();
1031
+ return event.clientX >= rect.left
1032
+ && event.clientX <= rect.right
1033
+ && event.clientY >= rect.top
1034
+ && event.clientY <= rect.bottom;
1035
+ }
1036
+
1037
+ async function useFile(id) {
1038
+ const file = ui.fileById.get(id);
1039
+ if (!file) return;
1040
+ try {
1041
+ const result = await hostRequest("attach-file", { path: file.path, name: file.name }, 12_000);
1042
+ const mode = result.mode === "insert-path" ? "insert-path" : "attach";
1043
+ await api(`/api/files/${encodeURIComponent(id)}/use`, {
1044
+ method: "POST",
1045
+ body: JSON.stringify({ action: mode, success: true }),
1046
+ });
1047
+ await refreshState({ quiet: true });
1048
+ showToast(mode === "attach" ? t("addedToComposer") : t("insertedFilePath"), "success");
1049
+ } catch (error) {
1050
+ if (error.status === 410 || error.code === "FILE_MISSING") await refreshState({ quiet: true });
1051
+ showToast(error.message, "danger");
1052
+ }
1053
+ }
1054
+
1055
+ async function pickFiles() {
1056
+ try {
1057
+ const result = await api("/api/files/pick", { method: "POST", body: "{}" });
1058
+ await refreshState({ quiet: true });
1059
+ if (result.cancelled) return;
1060
+ if (result.rejected?.length) {
1061
+ showToast(localizedErrorMessage(result.rejected[0].code, result.rejected[0].message), "danger");
1062
+ } else if (result.added?.length) showToast(t("filesPinned", { count: result.added.length }), "success");
1063
+ else if (result.existing?.length) showToast(t("alreadyPinned"));
1064
+ } catch (error) {
1065
+ showToast(error.message, "danger");
1066
+ }
1067
+ }
1068
+
1069
+ async function pinDroppedFiles(files) {
1070
+ let paths = Array.from(files)
1071
+ .map((file) => file.path ?? file.absolutePath ?? "")
1072
+ .filter((filePath) => filePath.startsWith("/"));
1073
+ if (paths.length === 0) {
1074
+ try {
1075
+ const result = await hostRequest("resolve-file-paths", { files: Array.from(files) });
1076
+ paths = result.paths ?? [];
1077
+ } catch (error) {
1078
+ showToast(error.message, "danger");
1079
+ return;
1080
+ }
1081
+ }
1082
+ try {
1083
+ const result = await api("/api/files", {
1084
+ method: "POST",
1085
+ body: JSON.stringify({ paths }),
1086
+ });
1087
+ await refreshState({ quiet: true });
1088
+ if (result.rejected?.length) {
1089
+ showToast(localizedErrorMessage(result.rejected[0].code, result.rejected[0].message), "danger");
1090
+ } else showToast(t("filesPinned", { count: result.added.length }), "success");
1091
+ } catch (error) {
1092
+ showToast(error.message, "danger");
1093
+ }
1094
+ }
1095
+
1096
+ document.addEventListener("pointerdown", (event) => {
1097
+ if (surface === "summary" && !event.target.closest?.(".row-menu")) dismissPromptMenus();
1098
+ }, true);
1099
+
1100
+ app.addEventListener("click", async (event) => {
1101
+ hidePromptPreview();
1102
+ if (!event.target.closest(".row-menu")) dismissPromptMenus();
1103
+ const target = event.target.closest("[data-action]");
1104
+ if (!target) return;
1105
+ const { action } = target.dataset;
1106
+ const id = target.dataset.id ?? target.closest("[data-id]")?.dataset.id;
1107
+
1108
+ if (action === "retry") return refreshState();
1109
+ if (action === "add-todo") {
1110
+ try { await openEditorModal("todo"); } catch (error) { showToast(error.message, "danger"); }
1111
+ return;
1112
+ }
1113
+ if (action === "edit-todo") {
1114
+ try { await openEditorModal("todo", id); } catch (error) { showToast(error.message, "danger"); }
1115
+ return;
1116
+ }
1117
+ if (action === "delete-todo") {
1118
+ await mutate(`/api/todos/${encodeURIComponent(id)}`, { method: "DELETE" });
1119
+ return;
1120
+ }
1121
+ if (action === "clear-completed") {
1122
+ await mutate("/api/todos/completed", { method: "DELETE" });
1123
+ return;
1124
+ }
1125
+ if (action === "add-prompts") {
1126
+ try { await openEditorModal("prompt"); } catch (error) { showToast(error.message, "danger"); }
1127
+ return;
1128
+ }
1129
+ if (action === "edit-prompt") {
1130
+ try { await openEditorModal("prompt", id); } catch (error) { showToast(error.message, "danger"); }
1131
+ return;
1132
+ }
1133
+ if (action === "close-editor") { closeEditor(); return; }
1134
+ if (action === "insert-prompt") { await insertPrompt(id); return; }
1135
+ if (action === "toggle-prompt-star") {
1136
+ const prompt = ui.promptById.get(id);
1137
+ if (prompt) await mutate(`/api/prompts/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ starred: !prompt.starred }) });
1138
+ return;
1139
+ }
1140
+ if (action === "delete-prompt") {
1141
+ await mutate(`/api/prompts/${encodeURIComponent(id)}`, { method: "DELETE" });
1142
+ return;
1143
+ }
1144
+ if (action === "add-files") { await pickFiles(); return; }
1145
+ if (action === "add-file") { await useFile(id); return; }
1146
+ if (action === "reveal-file") {
1147
+ try {
1148
+ await api(`/api/files/${encodeURIComponent(id)}/reveal`, { method: "POST" });
1149
+ } catch (error) {
1150
+ showToast(error.message, "danger");
1151
+ }
1152
+ return;
1153
+ }
1154
+ if (action === "delete-file") {
1155
+ await mutate(`/api/files/${encodeURIComponent(id)}`, { method: "DELETE" });
1156
+ return;
1157
+ }
1158
+ if (action === "view-all") {
1159
+ if (window.parent === window) window.open(`/?surface=${target.dataset.surface}`, "_blank", "noopener");
1160
+ else hostRequest("open-detail-tab", { surface: target.dataset.surface }).catch((error) => showToast(error.message, "danger"));
1161
+ }
1162
+ });
1163
+
1164
+ app.addEventListener("toggle", (event) => {
1165
+ const menu = event.target.closest?.(".row-menu");
1166
+ if (!menu) return;
1167
+ if (menu.open) {
1168
+ hidePromptPreview();
1169
+ closeOpenPromptMenus(menu);
1170
+ }
1171
+ }, true);
1172
+
1173
+ app.addEventListener("pointerover", (event) => {
1174
+ const row = event.target.closest?.(".prompt-row");
1175
+ if (!row) return;
1176
+ if (pointerTargetsPromptMenu(event, row)) {
1177
+ hidePromptPreview();
1178
+ return;
1179
+ }
1180
+ showPromptPreviewForRow(row);
1181
+ });
1182
+
1183
+ app.addEventListener("pointerout", (event) => {
1184
+ const row = event.target.closest?.(".prompt-row");
1185
+ if (!row) return;
1186
+ const next = event.relatedTarget;
1187
+ if (next instanceof Node && row.contains(next)) return;
1188
+ hidePromptPreview();
1189
+ });
1190
+
1191
+ app.addEventListener("scroll", hidePromptPreview, true);
1192
+
1193
+ app.addEventListener("change", async (event) => {
1194
+ const target = event.target;
1195
+ if (target.dataset.action !== "toggle-todo") return;
1196
+ await mutate(`/api/todos/${encodeURIComponent(target.dataset.id)}`, {
1197
+ method: "PATCH",
1198
+ body: JSON.stringify({ completed: target.checked }),
1199
+ });
1200
+ });
1201
+
1202
+ app.addEventListener("error", (event) => {
1203
+ const thumbnail = event.target.closest?.("[data-file-thumbnail]");
1204
+ if (!thumbnail) return;
1205
+ thumbnail.closest(".file-leading")?.classList.add("is-fallback");
1206
+ }, true);
1207
+
1208
+ app.addEventListener("input", (event) => {
1209
+ const editorForm = event.target.closest('[data-form="save-prompt"], [data-form="save-todo"]');
1210
+ if (event.target.closest('[data-form="save-prompt"]') && ui.editingPrompt) {
1211
+ ui.editingPrompt[event.target.name] = event.target.value;
1212
+ }
1213
+ if (event.target.closest('[data-form="save-todo"]') && ui.editingTodo) {
1214
+ ui.editingTodo.body = event.target.value;
1215
+ }
1216
+ syncEditorSubmitState(editorForm);
1217
+ });
1218
+
1219
+ app.addEventListener("submit", async (event) => {
1220
+ event.preventDefault();
1221
+ if (ui.editorSubmitting || ui.editorMissing) return;
1222
+ const form = event.target;
1223
+ ui.editorSubmitting = true;
1224
+ form.setAttribute("aria-busy", "true");
1225
+ const submit = form.querySelector('button[type="submit"]');
1226
+ if (submit) submit.disabled = true;
1227
+ if (form.dataset.form === "save-todo") {
1228
+ if (!ui.editingTodo?.body.trim()) {
1229
+ ui.editorSubmitting = false;
1230
+ form.removeAttribute("aria-busy");
1231
+ syncEditorSubmitState(form);
1232
+ return showToast(t("todoEmpty"), "danger");
1233
+ }
1234
+ const body = JSON.stringify({ body: ui.editingTodo.body });
1235
+ const saved = ui.editingTodo.id
1236
+ ? await mutate(`/api/todos/${encodeURIComponent(ui.editingTodo.id)}`, { method: "PATCH", body })
1237
+ : await mutate("/api/todos", { method: "POST", body });
1238
+ ui.editorSubmitting = false;
1239
+ form.removeAttribute("aria-busy");
1240
+ if (saved) closeEditor();
1241
+ else syncEditorSubmitState(form);
1242
+ return;
1243
+ }
1244
+ if (form.dataset.form === "save-prompt") {
1245
+ if (!ui.editingPrompt?.body.trim()) {
1246
+ ui.editorSubmitting = false;
1247
+ form.removeAttribute("aria-busy");
1248
+ syncEditorSubmitState(form);
1249
+ return showToast(t("promptEmpty"), "danger");
1250
+ }
1251
+ const draft = { title: ui.editingPrompt.title, body: ui.editingPrompt.body };
1252
+ let saved;
1253
+ if (ui.editingPrompt.id) {
1254
+ saved = await mutate(`/api/prompts/${encodeURIComponent(ui.editingPrompt.id)}`, {
1255
+ method: "PATCH",
1256
+ body: JSON.stringify(draft),
1257
+ });
1258
+ } else {
1259
+ saved = await mutate("/api/prompts", { method: "POST", body: JSON.stringify(draft) });
1260
+ }
1261
+ ui.editorSubmitting = false;
1262
+ form.removeAttribute("aria-busy");
1263
+ if (saved) {
1264
+ ui.editingPrompt = null;
1265
+ if (surface === "editor") closeEditor();
1266
+ else render();
1267
+ } else syncEditorSubmitState(form);
1268
+ return;
1269
+ }
1270
+ ui.editorSubmitting = false;
1271
+ form.removeAttribute("aria-busy");
1272
+ });
1273
+
1274
+ function clearTodoDropTarget() {
1275
+ ui.todoDropTarget = null;
1276
+ const row = app.querySelector(".todo-row[data-drop-position]");
1277
+ if (row) delete row.dataset.dropPosition;
1278
+ }
1279
+
1280
+ function setTodoDropTarget(row, position) {
1281
+ if (ui.todoDropTarget?.id === row.dataset.id && ui.todoDropTarget.position === position) return;
1282
+ const previous = app.querySelector(".todo-row[data-drop-position]");
1283
+ if (previous && previous !== row) delete previous.dataset.dropPosition;
1284
+ ui.todoDropTarget = { id: row.dataset.id, position };
1285
+ row.dataset.dropPosition = position;
1286
+ }
1287
+
1288
+ function todoDropPosition(event, row) {
1289
+ const rect = row.getBoundingClientRect();
1290
+ return event.clientY < rect.top + rect.height / 2 ? "before" : "after";
1291
+ }
1292
+
1293
+ function dragLeavesApp(event) {
1294
+ if (event.relatedTarget instanceof Node && app.contains(event.relatedTarget)) return false;
1295
+ const rect = app.getBoundingClientRect();
1296
+ return event.clientX <= rect.left
1297
+ || event.clientX >= rect.right
1298
+ || event.clientY <= rect.top
1299
+ || event.clientY >= rect.bottom;
1300
+ }
1301
+
1302
+ function clearTodoDragPreview() {
1303
+ ui.todoDragPreview?.remove();
1304
+ ui.todoDragPreview = null;
1305
+ }
1306
+
1307
+ function moveTodoDragPreview(event) {
1308
+ const preview = ui.todoDragPreview;
1309
+ if (!preview || (event.clientX === 0 && event.clientY === 0)) return;
1310
+ const offsetX = Number(preview.dataset.dragOffsetX);
1311
+ const offsetY = Number(preview.dataset.dragOffsetY);
1312
+ preview.style.left = `${event.clientX - offsetX}px`;
1313
+ preview.style.top = `${event.clientY - offsetY}px`;
1314
+ }
1315
+
1316
+ function setTodoDragPreview(row, event) {
1317
+ if (typeof event.dataTransfer?.setDragImage !== "function") return;
1318
+ clearTodoDragPreview();
1319
+ const rect = row.getBoundingClientRect();
1320
+ const preview = document.createElement("div");
1321
+ const content = row.cloneNode(true);
1322
+ const width = rect.width;
1323
+ preview.className = "todo-drag-preview";
1324
+ preview.setAttribute("aria-hidden", "true");
1325
+ preview.inert = true;
1326
+ preview.style.setProperty("--todo-drag-preview-width", `${width}px`);
1327
+ preview.style.left = `${rect.left}px`;
1328
+ preview.style.top = `${rect.top}px`;
1329
+ preview.dataset.dragOffsetX = String(event.clientX - rect.left);
1330
+ preview.dataset.dragOffsetY = String(event.clientY - rect.top);
1331
+ content.classList.add("todo-drag-preview-content");
1332
+ content.removeAttribute("data-id");
1333
+ content.removeAttribute("draggable");
1334
+ content.removeAttribute("data-drop-position");
1335
+ content.querySelector(".todo-drop-indicator")?.remove();
1336
+ content.querySelector(".row-actions")?.remove();
1337
+ preview.append(content);
1338
+ document.body.append(preview);
1339
+ ui.todoDragPreview = preview;
1340
+ const dragImage = document.createElement("canvas");
1341
+ dragImage.width = 1;
1342
+ dragImage.height = 1;
1343
+ document.body.append(dragImage);
1344
+ try {
1345
+ event.dataTransfer.setDragImage(dragImage, 0, 0);
1346
+ } catch {
1347
+ dragImage.remove();
1348
+ clearTodoDragPreview();
1349
+ return;
1350
+ }
1351
+ requestAnimationFrame(() => dragImage.remove());
1352
+ setTimeout(() => dragImage.remove(), 100);
1353
+ }
1354
+
1355
+ function clearTodoDraggingState() {
1356
+ clearTodoDragPreview();
1357
+ app.querySelector(".todo-row.is-dragging")?.classList.remove("is-dragging");
1358
+ ui.draggingTodo = null;
1359
+ }
1360
+
1361
+ function reorderedTodoIds(ids, draggedId, targetId, position) {
1362
+ if (!ids.includes(draggedId) || !ids.includes(targetId) || draggedId === targetId) return ids;
1363
+ const reordered = ids.filter((id) => id !== draggedId);
1364
+ const targetIndex = reordered.indexOf(targetId);
1365
+ const insertAt = targetIndex + (position === "after" ? 1 : 0);
1366
+ reordered.splice(insertAt, 0, draggedId);
1367
+ return reordered;
1368
+ }
1369
+
1370
+ app.addEventListener("dragstart", (event) => {
1371
+ const row = event.target.closest('.todo-row[draggable="true"]');
1372
+ if (!row) return;
1373
+ clearTodoDropTarget();
1374
+ clearTodoDraggingState();
1375
+ setTodoDragPreview(row, event);
1376
+ ui.draggingTodo = row.dataset.id;
1377
+ event.dataTransfer.effectAllowed = "move";
1378
+ event.dataTransfer.setData("text/plain", row.dataset.id);
1379
+ row.classList.add("is-dragging");
1380
+ });
1381
+
1382
+ app.addEventListener("dragend", () => {
1383
+ clearTodoDraggingState();
1384
+ clearTodoDropTarget();
1385
+ });
1386
+
1387
+ app.addEventListener("drag", (event) => {
1388
+ if (ui.draggingTodo) moveTodoDragPreview(event);
1389
+ });
1390
+
1391
+ app.addEventListener("dragover", (event) => {
1392
+ if (event.dataTransfer?.types.includes("Files")) {
1393
+ event.preventDefault();
1394
+ document.body.classList.add("is-file-dragging");
1395
+ return;
1396
+ }
1397
+ if (!ui.draggingTodo) return;
1398
+ moveTodoDragPreview(event);
1399
+ const target = event.target.closest('.todo-row[draggable="true"]');
1400
+ if (!target || target.dataset.id === ui.draggingTodo) {
1401
+ clearTodoDropTarget();
1402
+ return;
1403
+ }
1404
+ event.preventDefault();
1405
+ event.dataTransfer.dropEffect = "move";
1406
+ setTodoDropTarget(target, todoDropPosition(event, target));
1407
+ });
1408
+
1409
+ app.addEventListener("dragleave", (event) => {
1410
+ if (!dragLeavesApp(event)) return;
1411
+ document.body.classList.remove("is-file-dragging");
1412
+ clearTodoDropTarget();
1413
+ });
1414
+
1415
+ app.addEventListener("drop", async (event) => {
1416
+ document.body.classList.remove("is-file-dragging");
1417
+ if (event.dataTransfer?.files?.length) {
1418
+ event.preventDefault();
1419
+ clearTodoDropTarget();
1420
+ await pinDroppedFiles(event.dataTransfer.files);
1421
+ return;
1422
+ }
1423
+ const target = event.target.closest('.todo-row[draggable="true"]');
1424
+ const draggedId = ui.draggingTodo;
1425
+ const position = target?.dataset.dropPosition ?? ui.todoDropTarget?.position;
1426
+ const targetId = target?.dataset.id;
1427
+ if (!draggedId || !targetId || targetId === draggedId || !["before", "after"].includes(position)) return;
1428
+ event.preventDefault();
1429
+ clearTodoDropTarget();
1430
+ clearTodoDraggingState();
1431
+ const currentIds = ui.state.todos.active.map((item) => item.id);
1432
+ const ids = reorderedTodoIds(currentIds, draggedId, targetId, position);
1433
+ if (ids.every((id, index) => id === currentIds[index])) return;
1434
+ await mutate("/api/todos/reorder", { method: "POST", body: JSON.stringify({ ids }) });
1435
+ });
1436
+
1437
+ document.addEventListener("keydown", (event) => {
1438
+ if (surface === "summary" && event.key === "Escape") {
1439
+ const menu = app.querySelector(".row-menu[open]");
1440
+ if (menu) {
1441
+ event.preventDefault();
1442
+ menu.open = false;
1443
+ menu.querySelector("summary")?.focus();
1444
+ }
1445
+ return;
1446
+ }
1447
+ if (surface !== "editor") return;
1448
+ if (event.key === "Escape") {
1449
+ event.preventDefault();
1450
+ closeEditor();
1451
+ return;
1452
+ }
1453
+ if (event.key === "Enter" && event.metaKey) {
1454
+ event.preventDefault();
1455
+ const form = app.querySelector("form");
1456
+ const submit = form?.querySelector('button[type="submit"]');
1457
+ if (!submit?.disabled) form?.requestSubmit(submit);
1458
+ return;
1459
+ }
1460
+ if (event.key === "Tab") {
1461
+ const focusable = [...app.querySelectorAll("input, textarea, button")]
1462
+ .filter((element) => !element.disabled && element.getClientRects().length);
1463
+ const first = focusable[0];
1464
+ const last = focusable.at(-1);
1465
+ if (!first || !last) return;
1466
+ if (event.shiftKey && document.activeElement === first) {
1467
+ event.preventDefault();
1468
+ last.focus();
1469
+ } else if (!event.shiftKey && document.activeElement === last) {
1470
+ event.preventDefault();
1471
+ first.focus();
1472
+ }
1473
+ }
1474
+ });
1475
+
1476
+ if (surface === "summary") {
1477
+ window.addEventListener("resize", () => {
1478
+ hidePromptPreview();
1479
+ reportSummaryMetrics();
1480
+ });
1481
+ window.addEventListener("blur", () => {
1482
+ hidePromptPreview();
1483
+ dismissPromptMenus({ releaseFocus: true });
1484
+ });
1485
+ } else if (surface === "prompts") {
1486
+ window.addEventListener("resize", syncPromptRowDensity);
1487
+ }
1488
+ document.fonts?.ready.then(() => {
1489
+ syncPromptRowDensity();
1490
+ reportSummaryMetrics();
1491
+ });
1492
+
1493
+ postToHost({ type: "codex-personal:ready", feature: "notes", surface });
1494
+ refreshState();
1495
+ connectEvents();