minecodex 1.2.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -23
  4. package/features/images/src/library-prompt-page.mjs +239 -0
  5. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  6. package/features/notes/DESIGN.md +66 -0
  7. package/features/notes/README.md +18 -99
  8. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  9. package/features/notes/assets/icons/book-open.svg +16 -0
  10. package/features/notes/assets/native-emojis.json +1 -0
  11. package/features/notes/assets/native-prompt-icons.json +28 -0
  12. package/features/notes/codex-feature.json +12 -70
  13. package/features/notes/src/main.mjs +2 -2
  14. package/features/notes/src/product.mjs +287 -613
  15. package/features/notes/src/prompt-schema.mjs +117 -0
  16. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  17. package/features/notes/src/quick-prompts-server.mjs +167 -0
  18. package/features/session-status/DESIGN.md +11 -3
  19. package/features/session-status/README.md +8 -4
  20. package/features/session-status/src/session-status.mjs +272 -25
  21. package/features/sidebar-focus/DESIGN.md +16 -0
  22. package/features/sidebar-focus/codex-feature.json +20 -0
  23. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  24. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  25. package/features/unread-inbox/DESIGN.md +36 -0
  26. package/features/unread-inbox/codex-feature.json +20 -0
  27. package/features/unread-inbox/src/unread-inbox.mjs +446 -0
  28. package/features/usage-meter/DESIGN.md +49 -0
  29. package/features/usage-meter/README.md +18 -0
  30. package/features/usage-meter/codex-feature.json +13 -0
  31. package/features/usage-meter/src/usage-meter.mjs +200 -0
  32. package/package.json +17 -5
  33. package/packages/cli/src/config.mjs +4 -1
  34. package/packages/cli/src/control-server.mjs +2 -2
  35. package/packages/runtime-host/src/codex-injection.mjs +38 -3
  36. package/packages/runtime-host/src/codex-runtime.mjs +41 -20
  37. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  38. package/packages/runtime-host/src/session-status-reader.mjs +40 -16
  39. package/features/notes/assets/icons/circle.svg +0 -15
  40. package/features/notes/assets/icons/ellipsis.svg +0 -17
  41. package/features/notes/assets/icons/external-link.svg +0 -17
  42. package/features/notes/assets/icons/list-todo.svg +0 -19
  43. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  44. package/features/notes/assets/icons/paperclip.svg +0 -15
  45. package/features/notes/assets/icons/star-off.svg +0 -17
  46. package/features/notes/assets/icons/star.svg +0 -15
  47. package/features/notes/assets/icons/sticky-note.svg +0 -16
  48. package/features/notes/assets/icons/trash-2.svg +0 -19
  49. package/features/notes/src/http-server.mjs +0 -580
  50. package/features/notes/web/app.js +0 -1202
  51. package/features/notes/web/codex-artifact-document.svg +0 -1
  52. package/features/notes/web/codex-attachment.svg +0 -1
  53. package/features/notes/web/codex-build.svg +0 -1
  54. package/features/notes/web/codex-code.svg +0 -1
  55. package/features/notes/web/codex-cplusplus.svg +0 -1
  56. package/features/notes/web/codex-css.svg +0 -1
  57. package/features/notes/web/codex-document.svg +0 -1
  58. package/features/notes/web/codex-file.svg +0 -1
  59. package/features/notes/web/codex-folder.svg +0 -1
  60. package/features/notes/web/codex-hashes.svg +0 -1
  61. package/features/notes/web/codex-html.svg +0 -1
  62. package/features/notes/web/codex-image.svg +0 -1
  63. package/features/notes/web/codex-java.svg +0 -1
  64. package/features/notes/web/codex-javascript.svg +0 -1
  65. package/features/notes/web/codex-json.svg +0 -1
  66. package/features/notes/web/codex-notebook.svg +0 -1
  67. package/features/notes/web/codex-pdf.svg +0 -1
  68. package/features/notes/web/codex-pencil.svg +0 -3
  69. package/features/notes/web/codex-php.svg +0 -1
  70. package/features/notes/web/codex-plus.svg +0 -3
  71. package/features/notes/web/codex-presentation.svg +0 -1
  72. package/features/notes/web/codex-python.svg +0 -1
  73. package/features/notes/web/codex-react.svg +0 -1
  74. package/features/notes/web/codex-rust.svg +0 -1
  75. package/features/notes/web/codex-shell.svg +0 -1
  76. package/features/notes/web/codex-skill.svg +0 -1
  77. package/features/notes/web/codex-spreadsheet.svg +0 -1
  78. package/features/notes/web/codex-task.svg +0 -3
  79. package/features/notes/web/codex-terminal.svg +0 -1
  80. package/features/notes/web/codex-toml.svg +0 -1
  81. package/features/notes/web/codex-typescript.svg +0 -1
  82. package/features/notes/web/codex-view-all.svg +0 -3
  83. package/features/notes/web/file-icons.mjs +0 -68
  84. package/features/notes/web/i18n.mjs +0 -237
  85. package/features/notes/web/index.html +0 -15
  86. package/features/notes/web/styles.css +0 -1155
  87. package/features/notes/web/todo-drag.mjs +0 -18
@@ -1,1202 +0,0 @@
1
- const submitShortcut = /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘↩" : "Ctrl+Enter";
2
- const params = new URL(document.baseURI).searchParams;
3
- import { setLocale, getLocale, t, localizedErrorMessage } from "./i18n.mjs";
4
- import { CODEX_COLOR_FILE_ICONS, codexFileIconKind } from "./file-icons.mjs";
5
- import { todoDropPosition, reorderedTodoIds } from "./todo-drag.mjs";
6
-
7
- setLocale(params.get("locale") ?? navigator.languages?.[0] ?? navigator.language);
8
- document.documentElement.lang = getLocale();
9
-
10
-
11
- const requestedSurface = params.get("surface") === "notes" ? "prompts" : params.get("surface");
12
- const surface = new Set(["summary", "todo", "prompts", "files", "editor"]).has(requestedSurface)
13
- ? requestedSurface
14
- : "summary";
15
- const editorKind = params.get("kind") === "todo" ? "todo" : "prompt";
16
- const editorId = params.get("id") || null;
17
-
18
- document.body.dataset.surface = surface;
19
-
20
- const app = document.querySelector("#app");
21
- const toast = document.querySelector("#toast");
22
- const SUMMARY_ITEM_LIMIT = 5;
23
- const PROMPT_PREVIEW_POINTER_QUERY = "(hover: hover) and (pointer: fine)";
24
- const DOMAIN_ICON_BY_SURFACE = Object.freeze({
25
- todo: "list-todo",
26
- prompt: "message-square-quote",
27
- prompts: "message-square-quote",
28
- files: "paperclip",
29
- });
30
- const ui = {
31
- state: null,
32
- todoById: new Map(),
33
- promptById: new Map(),
34
- fileById: new Map(),
35
- loading: true,
36
- error: null,
37
- editingTodo: null,
38
- editingPrompt: null,
39
- editorInitialized: false,
40
- draggingTodo: null,
41
- todoDropTarget: null,
42
- todoDragPreview: null,
43
- promptPreviewId: null,
44
- requestId: 0,
45
- hostRequests: new Map(),
46
- hostOrigin: null,
47
- refreshPromise: null,
48
- pendingRevision: 0,
49
- refreshShouldReportErrors: false,
50
- eventSource: null,
51
- eventReconnectTimer: null,
52
- editorMissing: false,
53
- editorSubmitting: false,
54
- };
55
-
56
- const escapeHtml = (value) => String(value ?? "")
57
- .replaceAll("&", "&")
58
- .replaceAll("<", "&lt;")
59
- .replaceAll(">", "&gt;")
60
- .replaceAll('"', "&quot;")
61
- .replaceAll("'", "&#039;");
62
-
63
- function postToHost(message) {
64
- window.parent.postMessage(message, ui.hostOrigin ?? "*");
65
- }
66
-
67
- function indexState(state) {
68
- ui.todoById = new Map(
69
- [...state.todos.active, ...state.todos.completed].map((item) => [item.id, item]),
70
- );
71
- ui.promptById = new Map(state.prompts.map((item) => [item.id, item]));
72
- ui.fileById = new Map(state.files.map((item) => [item.id, item]));
73
- }
74
-
75
- function showToast(message, tone = "neutral") {
76
- toast.textContent = message;
77
- toast.dataset.tone = tone;
78
- toast.classList.add("is-visible");
79
- clearTimeout(showToast.timeout);
80
- showToast.timeout = setTimeout(() => toast.classList.remove("is-visible"), 2600);
81
- }
82
-
83
- async function api(path, options = {}) {
84
- const response = await fetch(path, {
85
- ...options,
86
- headers: options.body
87
- ? { "Content-Type": "application/json", ...(options.headers ?? {}) }
88
- : options.headers,
89
- });
90
- if (!response.ok) {
91
- const payload = await response.json().catch(() => null);
92
- const fallback = payload?.error?.message ?? t("requestFailed", { status: response.status });
93
- const error = new Error(localizedErrorMessage(payload?.error?.code, fallback, payload?.error?.details));
94
- error.code = payload?.error?.code;
95
- error.status = response.status;
96
- error.details = payload?.error?.details;
97
- throw error;
98
- }
99
- if (response.status === 204) return null;
100
- return response.json();
101
- }
102
-
103
- function syncEditorMissingState() {
104
- if (surface !== "editor" || !ui.editorInitialized) return false;
105
- const draft = editorKind === "todo" ? ui.editingTodo : ui.editingPrompt;
106
- if (!draft?.id) return false;
107
- const exists = editorKind === "todo" ? ui.todoById.has(draft.id) : ui.promptById.has(draft.id);
108
- const nextMissing = !exists;
109
- const changed = ui.editorMissing !== nextMissing;
110
- ui.editorMissing = nextMissing;
111
- return changed;
112
- }
113
-
114
- function stateRequest() {
115
- const query = `surface=${encodeURIComponent(surface)}`;
116
- if (surface === "editor") {
117
- return `/api/state?${query}&kind=${encodeURIComponent(editorKind)}&id=${encodeURIComponent(editorId ?? "")}`;
118
- }
119
- return `/api/state?${query}`;
120
- }
121
-
122
- function changeAffectsSurface(change) {
123
- if (!change?.reason) return true;
124
- if (Array.isArray(change.surfaces)) {
125
- const target = surface === "editor" ? `editor:${editorKind}` : surface;
126
- if (change.surfaces.includes(target)) return true;
127
- }
128
- const domain = String(change.reason).split(":", 1)[0];
129
- if (surface === "summary") return ["todo", "prompt", "file", "files"].includes(domain);
130
- if (surface === "editor") return domain === editorKind;
131
- return domain === (surface === "prompts" ? "prompt" : surface);
132
- }
133
-
134
- async function performRefresh() {
135
- const previousRevision = ui.state?.revision ?? -1;
136
- const hadState = Boolean(ui.state);
137
- const hadError = Boolean(ui.error);
138
- try {
139
- const next = await api(stateRequest());
140
- const stateChanged = !ui.state || next.revision > ui.state.revision;
141
- if (stateChanged) {
142
- ui.state = next;
143
- indexState(next);
144
- }
145
- ui.error = null;
146
- ui.loading = false;
147
- const editorMissingChanged = stateChanged ? syncEditorMissingState() : false;
148
- if (!hadState || hadError || (stateChanged && (surface !== "editor" || editorMissingChanged))) render();
149
- return ui.state;
150
- } catch (error) {
151
- ui.error = t("temporarilyUnavailable");
152
- ui.loading = false;
153
- if (ui.refreshShouldReportErrors) showToast(error.message, "danger");
154
- if (!hadState || !hadError) render();
155
- return ui.state;
156
- } finally {
157
- ui.refreshShouldReportErrors = false;
158
- const shouldRetry = ui.pendingRevision > Math.max(previousRevision, ui.state?.revision ?? -1);
159
- ui.refreshPromise = null;
160
- if (shouldRetry) queueMicrotask(() => refreshState({ quiet: true }));
161
- }
162
- }
163
-
164
- function refreshState({ quiet = false, revision = 0 } = {}) {
165
- ui.pendingRevision = Math.max(ui.pendingRevision, Number(revision) || 0);
166
- if (!quiet) ui.refreshShouldReportErrors = true;
167
- if (!ui.refreshPromise) ui.refreshPromise = performRefresh();
168
- return ui.refreshPromise;
169
- }
170
-
171
- function connectEvents() {
172
- clearTimeout(ui.eventReconnectTimer);
173
- ui.eventReconnectTimer = null;
174
- ui.eventSource?.close();
175
- const events = new EventSource("/api/events");
176
- ui.eventSource = events;
177
- const handleRevision = (event) => {
178
- let change;
179
- try {
180
- change = JSON.parse(event.data);
181
- } catch {
182
- return;
183
- }
184
- if (!ui.state || change.revision > ui.state.revision) {
185
- if (!changeAffectsSurface(change)) {
186
- ui.pendingRevision = Math.max(ui.pendingRevision, change.revision);
187
- return;
188
- }
189
- refreshState({ quiet: true, revision: change.revision });
190
- }
191
- };
192
- events.addEventListener("ready", handleRevision);
193
- events.addEventListener("change", (event) => {
194
- handleRevision(event);
195
- });
196
- events.onerror = () => {
197
- events.close();
198
- if (ui.eventSource === events) ui.eventSource = null;
199
- if (!ui.eventReconnectTimer) ui.eventReconnectTimer = setTimeout(connectEvents, 1200);
200
- };
201
- }
202
-
203
- function hostRequest(action, payload = {}, timeoutMs = 8000) {
204
- if (window.parent === window) {
205
- return Promise.reject(Object.assign(new Error(t("hostUnavailable")), { code: "HOST_UNAVAILABLE" }));
206
- }
207
- const requestId = `codexnotes-${Date.now()}-${++ui.requestId}`;
208
- return new Promise((resolve, reject) => {
209
- const timeout = setTimeout(() => {
210
- ui.hostRequests.delete(requestId);
211
- reject(Object.assign(new Error(t("hostTimeout")), { code: "HOST_TIMEOUT" }));
212
- }, timeoutMs);
213
- ui.hostRequests.set(requestId, { resolve, reject, timeout });
214
- postToHost({
215
- type: "codex-personal:host-action",
216
- feature: "notes",
217
- surface,
218
- requestId,
219
- action,
220
- payload,
221
- });
222
- });
223
- }
224
-
225
- window.addEventListener("message", (event) => {
226
- if (event.source !== window.parent) return;
227
- if (ui.hostOrigin && event.origin !== ui.hostOrigin) return;
228
- const message = event.data;
229
- if (!message || typeof message !== "object") return;
230
- if (!ui.hostOrigin && event.origin && event.origin !== "null") ui.hostOrigin = event.origin;
231
- if (message.type === "codex-personal:theme") {
232
- if (message.locale && setLocale(message.locale)) {
233
- document.documentElement.lang = getLocale();
234
- if (ui.state) render();
235
- }
236
- const tokens = message.tokens ?? message.theme?.tokens ?? {};
237
- for (const [name, value] of Object.entries(tokens)) {
238
- if (typeof value === "string" && (name.startsWith("--") || name.startsWith("codex-"))) {
239
- document.documentElement.style.setProperty(name.startsWith("--") ? name : `--${name}`, value);
240
- }
241
- }
242
- if (message.theme?.mode || message.mode) {
243
- document.documentElement.dataset.theme = message.theme?.mode ?? message.mode;
244
- }
245
- reportSummaryMetrics();
246
- return;
247
- }
248
- if (message.type === "codex-personal:focus") {
249
- requestAnimationFrame(() => app.querySelector("[autofocus]")?.focus());
250
- return;
251
- }
252
- if (message.type !== "codex-personal:host-result" || message.feature !== "notes") return;
253
- const request = ui.hostRequests.get(message.requestId);
254
- if (!request) return;
255
- clearTimeout(request.timeout);
256
- ui.hostRequests.delete(message.requestId);
257
- if (message.ok) request.resolve(message.result ?? {});
258
- else request.reject(Object.assign(new Error(localizedErrorMessage(
259
- message.error?.code,
260
- message.error?.message ?? t("hostActionFailed"),
261
- )), {
262
- code: message.error?.code ?? "HOST_ACTION_FAILED",
263
- }));
264
- });
265
-
266
- function iconMarkup(name, className = "") {
267
- return `<span class="icon icon-${name}${className ? ` ${className}` : ""}" aria-hidden="true"></span>`;
268
- }
269
-
270
- function domainIconName(key) {
271
- return DOMAIN_ICON_BY_SURFACE[key] ?? "attachment";
272
- }
273
-
274
- function iconName(action) {
275
- if (action.startsWith("add-")) return action === "add-file" ? "attachment" : "plus";
276
- if (action.startsWith("delete-")) return "trash-2";
277
- return {
278
- "edit-prompt": "edit-pencil",
279
- "edit-todo": "edit-pencil",
280
- more: "ellipsis",
281
- "reveal-file": "external-link",
282
- }[action] ?? null;
283
- }
284
-
285
- function fileIconMarkup(kind) {
286
- if (CODEX_COLOR_FILE_ICONS.has(kind)) {
287
- return `<img class="file-kind is-color" src="/icons/file-${kind}.svg" alt="" aria-hidden="true">`;
288
- }
289
- return iconMarkup(`file-${kind}`, "file-kind");
290
- }
291
-
292
- function actionButton(action, label, { quiet = false, title = label } = {}) {
293
- const icon = quiet ? iconName(action) : null;
294
- 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>`;
295
- }
296
-
297
- function todoRow(todo, { completed = false, draggable = true } = {}) {
298
- const toggleLabel = completed ? t("restoreTodo") : t("completeTodo");
299
- return `<li class="row todo-row${completed ? " is-completed" : ""}" data-id="${escapeHtml(todo.id)}" ${draggable && !completed ? 'draggable="true"' : ""}>
300
- ${draggable && !completed ? '<span class="todo-drop-indicator" aria-hidden="true" role="presentation"></span>' : ""}
301
- <label class="check-control" title="${escapeHtml(toggleLabel)}">
302
- <input type="checkbox" data-action="toggle-todo" data-id="${escapeHtml(todo.id)}" aria-label="${escapeHtml(toggleLabel)}" ${completed ? "checked" : ""}>
303
- <span class="todo-check-icon" aria-hidden="true"></span>
304
- </label>
305
- <span class="row-primary todo-label">${escapeHtml(todo.body)}</span>
306
- <div class="row-actions">
307
- ${actionButton("edit-todo", t("edit"), { quiet: true })}
308
- ${actionButton("delete-todo", t("delete"), { quiet: true })}
309
- </div>
310
- </li>`;
311
- }
312
-
313
- function initializeEditor() {
314
- if (surface !== "editor" || ui.editorInitialized) return;
315
- ui.editorInitialized = true;
316
- if (editorKind === "todo") {
317
- const todo = ui.todoById.get(editorId);
318
- ui.editorMissing = Boolean(editorId && !todo);
319
- ui.editingTodo = {
320
- id: todo?.id ?? editorId,
321
- body: todo?.body ?? "",
322
- };
323
- return;
324
- }
325
- const prompt = ui.promptById.get(editorId);
326
- ui.editorMissing = Boolean(editorId && !prompt);
327
- ui.editingPrompt = {
328
- id: prompt?.id ?? editorId,
329
- title: prompt?.title ?? "",
330
- body: prompt?.body ?? "",
331
- };
332
- }
333
-
334
- function modalCommand(label, { action = null, submit = false, shortcut = "", disabled = false } = {}) {
335
- const behavior = submit
336
- ? 'type="submit"'
337
- : `type="button" data-action="${action}"`;
338
- return `<button ${behavior} class="modal-command${submit ? " is-default" : ""}"${disabled ? " disabled" : ""}>
339
- <span>${escapeHtml(label)}</span>
340
- ${shortcut ? `<kbd>${escapeHtml(shortcut)}</kbd>` : ""}
341
- </button>`;
342
- }
343
-
344
- function modalEditorTitle(kind, label) {
345
- return `<header class="modal-editor-title">
346
- ${iconMarkup(domainIconName(kind), "modal-editor-title-icon")}
347
- <span>${escapeHtml(label)}</span>
348
- </header>`;
349
- }
350
-
351
- function syncEditorSubmitState(form) {
352
- const submit = form?.querySelector('button[type="submit"]');
353
- const body = form?.elements.namedItem("body");
354
- if (submit) submit.disabled = !body?.value.trim();
355
- }
356
-
357
- function renderEditor() {
358
- initializeEditor();
359
- if (ui.editorMissing) {
360
- return `<main class="editor-modal-shell" data-editor-shell>
361
- <div class="modal-editor" role="alert">
362
- ${modalEditorTitle(editorKind, editorKind === "todo" ? t("editTodo") : t("editPrompt"))}
363
- <div class="status-state is-error"><p>${escapeHtml(t("editorItemMissing"))}</p></div>
364
- <div class="modal-actions">${modalCommand(t("close"), { action: "close-editor", shortcut: "esc" })}</div>
365
- </div>
366
- </main>`;
367
- }
368
- if (editorKind === "todo") {
369
- const draft = ui.editingTodo;
370
- const editing = Boolean(draft?.id);
371
- return `<main class="editor-modal-shell" data-editor-shell>
372
- <form class="modal-editor" data-form="save-todo">
373
- ${modalEditorTitle("todo", editing ? t("editTodo") : t("newTodo"))}
374
- <div class="modal-editor-fields is-todo">
375
- <textarea name="body" aria-label="${escapeHtml(t("todoText"))}" placeholder="${escapeHtml(t("todoPlaceholder"))}" autofocus>${escapeHtml(draft?.body)}</textarea>
376
- </div>
377
- <div class="modal-actions">
378
- ${modalCommand(editing ? t("saveTodo") : t("addTodo"), { submit: true, shortcut: submitShortcut, disabled: !draft?.body.trim() })}
379
- ${modalCommand(t("cancel"), { action: "close-editor", shortcut: "esc" })}
380
- </div>
381
- </form>
382
- </main>`;
383
- }
384
- const draft = ui.editingPrompt;
385
- const editing = Boolean(draft?.id);
386
- return `<main class="editor-modal-shell" data-editor-shell>
387
- <form class="modal-editor" data-form="save-prompt">
388
- ${modalEditorTitle("prompt", editing ? t("editPrompt") : t("newPrompt"))}
389
- <div class="modal-editor-fields is-prompt">
390
- <input name="title" value="${escapeHtml(draft?.title)}" placeholder="${escapeHtml(t("optionalTitle"))}" aria-label="${escapeHtml(t("promptTitle"))}" autocomplete="off">
391
- <textarea name="body" aria-label="${escapeHtml(t("promptBody"))}" placeholder="${escapeHtml(t("promptPlaceholder"))}" autofocus>${escapeHtml(draft?.body)}</textarea>
392
- </div>
393
- <div class="modal-actions">
394
- ${modalCommand(editing ? t("savePrompt") : t("addPrompt"), { submit: true, shortcut: submitShortcut, disabled: !draft?.body.trim() })}
395
- ${modalCommand(t("cancel"), { action: "close-editor", shortcut: "esc" })}
396
- </div>
397
- </form>
398
- </main>`;
399
- }
400
-
401
- function promptLeading(prompt) {
402
- return `<span class="summary-leading prompt-star-leading${prompt.starred ? " is-starred" : " is-placeholder"}" aria-hidden="true">
403
- ${prompt.starred ? iconMarkup("star", "star-mark") : ""}
404
- </span>`;
405
- }
406
-
407
- function promptRow(prompt, { showLeading = true } = {}) {
408
- return `<li class="row prompt-row" data-id="${escapeHtml(prompt.id)}">
409
- ${showLeading ? promptLeading(prompt) : ""}
410
- <button type="button" class="row-primary prompt-label" data-action="insert-prompt" data-id="${escapeHtml(prompt.id)}" title="${escapeHtml(t("insertPrompt"))}">
411
- <span>${escapeHtml(prompt.summary)}</span>
412
- </button>
413
- <div class="row-actions">
414
- ${actionButton("edit-prompt", t("edit"), { quiet: true })}
415
- <details class="row-menu">
416
- <summary class="quiet-button" aria-label="${escapeHtml(t("morePromptActions"))}" title="${escapeHtml(t("morePromptActions"))}">${iconMarkup("ellipsis")}</summary>
417
- <div class="menu-popover">
418
- <button type="button" class="menu-item" data-action="toggle-prompt-star" data-id="${escapeHtml(prompt.id)}">
419
- ${iconMarkup(prompt.starred ? "star-off" : "star", "menu-item-icon")}
420
- <span>${prompt.starred ? t("unstar") : t("star")}</span>
421
- </button>
422
- <button type="button" class="menu-item" data-action="delete-prompt" data-id="${escapeHtml(prompt.id)}">
423
- ${iconMarkup("trash-2", "menu-item-icon")}
424
- <span>${escapeHtml(t("delete"))}</span>
425
- </button>
426
- </div>
427
- </details>
428
- </div>
429
- </li>`;
430
- }
431
-
432
- function fileLeading(file) {
433
- const kind = codexFileIconKind(file);
434
- const fallback = fileIconMarkup(kind);
435
- if (kind !== "image") {
436
- return `<span class="summary-leading file-leading">${fallback}</span>`;
437
- }
438
- return `<span class="summary-leading file-leading has-thumbnail">
439
- <img class="file-thumbnail" src="/api/files/${encodeURIComponent(file.id)}/preview" alt="" loading="lazy" data-file-thumbnail>
440
- <span class="file-thumbnail-fallback">${fallback}</span>
441
- </span>`;
442
- }
443
-
444
- function fileRow(file) {
445
- return `<li class="row file-row" data-id="${escapeHtml(file.id)}" title="${escapeHtml(file.path)}">
446
- ${fileLeading(file)}
447
- <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>
448
- <div class="row-actions">
449
- ${actionButton("reveal-file", t("showInFolder"), { quiet: true })}
450
- ${actionButton("delete-file", t("delete"), { quiet: true })}
451
- </div>
452
- </li>`;
453
- }
454
-
455
- function emptyRow(label) {
456
- return `<li class="empty-row">${escapeHtml(label)}</li>`;
457
- }
458
-
459
- function summarySection({ key, title, items, addLabel, rows, emptyLabel, totalCount = items.length }) {
460
- const visible = items.slice(0, SUMMARY_ITEM_LIMIT);
461
- const summaryCounts = ui.state?.counts;
462
- const inferredCount = key === "todo"
463
- ? (summaryCounts ? summaryCounts.todos.active + summaryCounts.todos.completed : items.length)
464
- : (summaryCounts?.[key] ?? items.length);
465
- const hasOverflow = totalCount > SUMMARY_ITEM_LIMIT || inferredCount > SUMMARY_ITEM_LIMIT;
466
- return `<section class="summary-section" data-section="${key}">
467
- <header class="section-header">
468
- <span>${title}</span>
469
- ${actionButton(`add-${key}`, addLabel, { quiet: true, title: addLabel })}
470
- </header>
471
- <ul class="flat-list">${visible.length ? visible.map(rows).join("") : emptyRow(emptyLabel)}</ul>
472
- ${hasOverflow ? `<button type="button" class="view-all" data-action="view-all" data-surface="${key === "todo" ? "todo" : key}">
473
- ${iconMarkup(domainIconName(key), "view-all-icon")}
474
- <span class="view-all-label">${escapeHtml(t("viewAll"))}</span>
475
- </button>` : ""}
476
- </section>`;
477
- }
478
-
479
- function renderSummary() {
480
- const active = ui.state.todos.active;
481
- const completed = ui.state.todos.completed;
482
- const prompts = ui.state.prompts;
483
- const files = ui.state.files;
484
- const counts = ui.state.counts ?? {
485
- todos: { active: active.length, completed: completed.length },
486
- prompts: prompts.length,
487
- files: files.length,
488
- };
489
- const visibleTodos = active.length >= SUMMARY_ITEM_LIMIT
490
- ? active.slice(0, SUMMARY_ITEM_LIMIT)
491
- : [...active, ...completed.slice(0, SUMMARY_ITEM_LIMIT - active.length)];
492
- return `<div class="summary-shell" data-summary-shell>
493
- <div class="summary-scroll" data-summary-scroll>
494
- ${summarySection({
495
- key: "todo",
496
- title: t("todo"),
497
- items: visibleTodos,
498
- totalCount: active.length + completed.length,
499
- addLabel: t("addTodo"),
500
- rows: (item) => todoRow(item, {
501
- completed: Boolean(item.completedAt),
502
- draggable: !item.completedAt,
503
- }),
504
- emptyLabel: t("noTodo"),
505
- })}
506
- ${summarySection({ key: "prompts", title: t("prompts"), items: prompts, addLabel: t("addPrompt"), rows: promptRow, emptyLabel: t("noPrompts") })}
507
- ${summarySection({ key: "files", title: t("files"), items: files, addLabel: t("pinFiles"), rows: fileRow, emptyLabel: t("noPinnedFiles") })}
508
- </div>
509
- </div>`;
510
- }
511
-
512
- function pageActionButton(action, label, icon, title = label) {
513
- return `<button type="button" class="page-action" data-action="${action}" title="${escapeHtml(title)}" aria-label="${escapeHtml(title)}">
514
- ${iconMarkup(icon, "page-action-icon")}
515
- <span>${escapeHtml(label)}</span>
516
- </button>`;
517
- }
518
-
519
- function pageSectionTitle(title, count, action = "") {
520
- return `<div class="page-section-title">
521
- <div class="page-section-heading"><h2>${title}</h2><span class="page-section-count">${count}</span></div>
522
- ${action}
523
- </div>`;
524
- }
525
-
526
- function renderTodoPage() {
527
- const { active, completed } = ui.state.todos;
528
- return `<div class="full-shell todo-page">
529
- <section class="page-section">
530
- ${pageSectionTitle(t("active"), active.length, pageActionButton("add-todo", t("add"), "plus"))}
531
- <ul class="flat-list full-list">${active.length ? active.map((item) => todoRow(item)).join("") : emptyRow(t("nothingActive"))}</ul>
532
- </section>
533
- <section class="page-section completed-section">
534
- ${pageSectionTitle(
535
- t("completed"),
536
- completed.length,
537
- completed.length ? pageActionButton("clear-completed", t("deleteAll"), "trash-2", t("deleteCompletedTitle")) : "",
538
- )}
539
- <ul class="flat-list full-list">${completed.length ? completed.map((item) => todoRow(item, { completed: true, draggable: false })).join("") : emptyRow(t("noCompletedTodo"))}</ul>
540
- </section>
541
- </div>`;
542
- }
543
-
544
- function renderPromptsPage() {
545
- const starred = ui.state.prompts.filter((prompt) => prompt.starred);
546
- const prompts = ui.state.prompts.filter((prompt) => !prompt.starred);
547
- return `<div class="full-shell prompts-page">
548
- <div class="prompts-page-actions">
549
- ${pageActionButton("add-prompts", t("add"), "plus")}
550
- </div>
551
- <section class="page-section">
552
- ${pageSectionTitle(t("starred"), starred.length)}
553
- <ul class="flat-list full-list full-prompt-list">${starred.length ? starred.map((prompt) => promptRow(prompt, { showLeading: false })).join("") : emptyRow(t("noStarredPrompts"))}</ul>
554
- </section>
555
- <section class="page-section">
556
- ${pageSectionTitle(t("prompts"), prompts.length)}
557
- <ul class="flat-list full-list full-prompt-list">${prompts.length ? prompts.map((prompt) => promptRow(prompt, { showLeading: false })).join("") : emptyRow(t("noPrompts"))}</ul>
558
- </section>
559
- </div>`;
560
- }
561
-
562
- function renderFilesPage() {
563
- return `<div class="full-shell files-page" data-file-drop-zone>
564
- <section class="page-section">
565
- ${pageSectionTitle(t("pinnedFiles"), ui.state.files.length, pageActionButton("add-files", t("add"), "plus", t("pinFiles")))}
566
- <ul class="flat-list full-list">${ui.state.files.length ? ui.state.files.map(fileRow).join("") : emptyRow(t("noPinnedFiles"))}</ul>
567
- </section>
568
- </div>`;
569
- }
570
-
571
- function syncPromptRowDensity() {
572
- if (surface !== "prompts") return;
573
- cancelAnimationFrame(syncPromptRowDensity.frame);
574
- syncPromptRowDensity.frame = requestAnimationFrame(() => {
575
- const measurements = [...app.querySelectorAll(".prompt-row")].map((row) => {
576
- const label = row.querySelector(".prompt-label");
577
- if (!label) return null;
578
- const lineHeight = Number.parseFloat(getComputedStyle(label).lineHeight);
579
- return { row, multiline: label.getBoundingClientRect().height > lineHeight * 1.5 };
580
- }).filter(Boolean);
581
- for (const { row, multiline } of measurements) row.classList.toggle("is-multiline", multiline);
582
- });
583
- }
584
-
585
- function render() {
586
- if (ui.loading && !ui.state) {
587
- app.innerHTML = `<div class="status-state"><span class="spinner"></span><p>${escapeHtml(t("loading"))}</p></div>`;
588
- return;
589
- }
590
- if (!ui.state) {
591
- 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>`;
592
- return;
593
- }
594
- if (surface === "summary") app.innerHTML = renderSummary();
595
- else if (surface === "todo") app.innerHTML = renderTodoPage();
596
- else if (surface === "prompts") app.innerHTML = renderPromptsPage();
597
- else if (surface === "files") app.innerHTML = renderFilesPage();
598
- else app.innerHTML = renderEditor();
599
- syncPromptRowDensity();
600
- requestAnimationFrame(() => {
601
- app.querySelector("[autofocus]")?.focus();
602
- reportSummaryMetrics();
603
- });
604
- }
605
-
606
- function reportSummaryMetrics() {
607
- if (surface !== "summary" || window.parent === window) return;
608
- cancelAnimationFrame(reportSummaryMetrics.frame);
609
- clearTimeout(reportSummaryMetrics.timeout);
610
- const publish = () => {
611
- cancelAnimationFrame(reportSummaryMetrics.frame);
612
- clearTimeout(reportSummaryMetrics.timeout);
613
- reportSummaryMetrics.frame = null;
614
- reportSummaryMetrics.timeout = null;
615
- const shell = app.querySelector("[data-summary-shell]");
616
- const scroll = app.querySelector("[data-summary-scroll]");
617
- if (!shell || !scroll) return;
618
- const shellStyle = getComputedStyle(shell);
619
- const chrome = Number.parseFloat(shellStyle.paddingTop) + Number.parseFloat(shellStyle.paddingBottom);
620
- const height = Math.ceil(scroll.scrollHeight + (Number.isFinite(chrome) ? chrome : 0));
621
- postToHost({
622
- type: "codex-personal:surface-metrics",
623
- feature: "notes",
624
- surface,
625
- height,
626
- });
627
- };
628
- reportSummaryMetrics.frame = requestAnimationFrame(publish);
629
- reportSummaryMetrics.timeout = setTimeout(publish, 50);
630
- }
631
-
632
- async function mutate(path, options, successMessage = null) {
633
- try {
634
- const result = await api(path, options);
635
- await refreshState({ quiet: true, revision: result?.revision });
636
- if (successMessage) showToast(successMessage, "success");
637
- return true;
638
- } catch (error) {
639
- showToast(error.message, "danger");
640
- await refreshState({ quiet: true });
641
- if (surface !== "editor" && ui.state) render();
642
- return false;
643
- }
644
- }
645
-
646
- async function openEditorModal(kind, id = null) {
647
- if (window.parent === window) {
648
- const url = new URL(location.href);
649
- url.searchParams.set("surface", "editor");
650
- url.searchParams.set("kind", kind);
651
- if (id) url.searchParams.set("id", id);
652
- else url.searchParams.delete("id");
653
- window.open(url, "codexnotes-editor", `popup,width=420,height=${kind === "prompt" ? 250 : 198}`);
654
- return;
655
- }
656
- await hostRequest("open-editor-modal", { kind, id });
657
- }
658
-
659
- function closeEditor() {
660
- if (window.parent !== window) {
661
- postToHost({ type: "codex-personal:close", feature: "notes", surface });
662
- } else {
663
- window.close();
664
- }
665
- }
666
-
667
- async function insertPrompt(id) {
668
- const prompt = ui.promptById.get(id);
669
- if (!prompt) return;
670
- try {
671
- await hostRequest("insert-text", { text: prompt.body });
672
- await api(`/api/prompts/${encodeURIComponent(id)}/use`, {
673
- method: "POST",
674
- body: JSON.stringify({ action: "insert", success: true }),
675
- });
676
- await refreshState({ quiet: true });
677
- showToast(t("insertedAtCaret"), "success");
678
- } catch (error) {
679
- showToast(error.message, "danger");
680
- }
681
- }
682
-
683
- function closeOpenPromptMenus(except = null) {
684
- for (const menu of app.querySelectorAll(".row-menu[open]")) {
685
- if (menu !== except) menu.open = false;
686
- }
687
- }
688
-
689
- function dismissPromptMenus({ releaseFocus = false } = {}) {
690
- const focusedMenu = document.activeElement?.closest?.(".row-menu");
691
- closeOpenPromptMenus();
692
- if (releaseFocus && focusedMenu && document.activeElement instanceof HTMLElement) {
693
- document.activeElement.blur();
694
- }
695
- }
696
-
697
- function postPromptPreview(phase, prompt = null, row = null) {
698
- if (surface !== "summary" || window.parent === window) return;
699
- const message = {
700
- type: "codex-personal:prompt-preview",
701
- feature: "notes",
702
- phase,
703
- };
704
- if (phase === "show" && prompt && row) {
705
- const rect = row.getBoundingClientRect();
706
- message.prompt = {
707
- id: prompt.id,
708
- title: prompt.title,
709
- body: prompt.body,
710
- };
711
- message.anchor = {
712
- top: rect.top,
713
- right: rect.right,
714
- bottom: rect.bottom,
715
- left: rect.left,
716
- width: rect.width,
717
- height: rect.height,
718
- };
719
- }
720
- postToHost(message);
721
- }
722
-
723
- function hidePromptPreview() {
724
- if (!ui.promptPreviewId) return;
725
- ui.promptPreviewId = null;
726
- postPromptPreview("hide");
727
- }
728
-
729
- function showPromptPreviewForRow(row) {
730
- if (
731
- surface !== "summary"
732
- || window.parent === window
733
- || !window.matchMedia(PROMPT_PREVIEW_POINTER_QUERY).matches
734
- ) return;
735
- const id = row?.dataset.id;
736
- if (!id || ui.promptPreviewId === id) return;
737
- if (!row.isConnected) return;
738
- const prompt = ui.promptById.get(id);
739
- if (!prompt) return;
740
- hidePromptPreview();
741
- ui.promptPreviewId = id;
742
- postPromptPreview("show", prompt, row);
743
- }
744
-
745
- function pointerTargetsPromptMenu(event, row) {
746
- const directMenu = event.target.closest?.(".row-menu");
747
- if (directMenu && row.contains(directMenu)) return true;
748
- const trigger = row.querySelector(".row-menu > summary");
749
- if (!trigger) return false;
750
- const rect = trigger.getBoundingClientRect();
751
- return event.clientX >= rect.left
752
- && event.clientX <= rect.right
753
- && event.clientY >= rect.top
754
- && event.clientY <= rect.bottom;
755
- }
756
-
757
- async function useFile(id) {
758
- const file = ui.fileById.get(id);
759
- if (!file) return;
760
- try {
761
- const result = await hostRequest("attach-file", { path: file.path, name: file.name }, 12_000);
762
- const mode = result.mode === "insert-path" ? "insert-path" : "attach";
763
- await api(`/api/files/${encodeURIComponent(id)}/use`, {
764
- method: "POST",
765
- body: JSON.stringify({ action: mode, success: true }),
766
- });
767
- await refreshState({ quiet: true });
768
- showToast(mode === "attach" ? t("addedToComposer") : t("insertedFilePath"), "success");
769
- } catch (error) {
770
- if (error.status === 410 || error.code === "FILE_MISSING") await refreshState({ quiet: true });
771
- showToast(error.message, "danger");
772
- }
773
- }
774
-
775
- async function pickFiles() {
776
- try {
777
- const result = await api("/api/files/pick", { method: "POST", body: "{}" });
778
- await refreshState({ quiet: true });
779
- if (result.cancelled) return;
780
- if (result.rejected?.length) {
781
- showToast(localizedErrorMessage(result.rejected[0].code, result.rejected[0].message), "danger");
782
- } else if (result.added?.length) showToast(t("filesPinned", { count: result.added.length }), "success");
783
- else if (result.existing?.length) showToast(t("alreadyPinned"));
784
- } catch (error) {
785
- showToast(error.message, "danger");
786
- }
787
- }
788
-
789
- async function pinDroppedFiles(files) {
790
- let paths = Array.from(files)
791
- .map((file) => file.path ?? file.absolutePath ?? "")
792
- .filter((filePath) => filePath.startsWith("/"));
793
- if (paths.length === 0) {
794
- try {
795
- const result = await hostRequest("resolve-file-paths", { files: Array.from(files) });
796
- paths = result.paths ?? [];
797
- } catch (error) {
798
- showToast(error.message, "danger");
799
- return;
800
- }
801
- }
802
- try {
803
- const result = await api("/api/files", {
804
- method: "POST",
805
- body: JSON.stringify({ paths }),
806
- });
807
- await refreshState({ quiet: true });
808
- if (result.rejected?.length) {
809
- showToast(localizedErrorMessage(result.rejected[0].code, result.rejected[0].message), "danger");
810
- } else showToast(t("filesPinned", { count: result.added.length }), "success");
811
- } catch (error) {
812
- showToast(error.message, "danger");
813
- }
814
- }
815
-
816
- document.addEventListener("pointerdown", (event) => {
817
- if (surface === "summary" && !event.target.closest?.(".row-menu")) dismissPromptMenus();
818
- }, true);
819
-
820
- app.addEventListener("click", async (event) => {
821
- hidePromptPreview();
822
- if (!event.target.closest(".row-menu")) dismissPromptMenus();
823
- const target = event.target.closest("[data-action]");
824
- if (!target) return;
825
- const { action } = target.dataset;
826
- const id = target.dataset.id ?? target.closest("[data-id]")?.dataset.id;
827
-
828
- if (action === "retry") return refreshState();
829
- if (action === "add-todo") {
830
- try { await openEditorModal("todo"); } catch (error) { showToast(error.message, "danger"); }
831
- return;
832
- }
833
- if (action === "edit-todo") {
834
- try { await openEditorModal("todo", id); } catch (error) { showToast(error.message, "danger"); }
835
- return;
836
- }
837
- if (action === "delete-todo") {
838
- await mutate(`/api/todos/${encodeURIComponent(id)}`, { method: "DELETE" });
839
- return;
840
- }
841
- if (action === "clear-completed") {
842
- await mutate("/api/todos/completed", { method: "DELETE" });
843
- return;
844
- }
845
- if (action === "add-prompts") {
846
- try { await openEditorModal("prompt"); } catch (error) { showToast(error.message, "danger"); }
847
- return;
848
- }
849
- if (action === "edit-prompt") {
850
- try { await openEditorModal("prompt", id); } catch (error) { showToast(error.message, "danger"); }
851
- return;
852
- }
853
- if (action === "close-editor") { closeEditor(); return; }
854
- if (action === "insert-prompt") { await insertPrompt(id); return; }
855
- if (action === "toggle-prompt-star") {
856
- const prompt = ui.promptById.get(id);
857
- if (prompt) await mutate(`/api/prompts/${encodeURIComponent(id)}`, { method: "PATCH", body: JSON.stringify({ starred: !prompt.starred }) });
858
- return;
859
- }
860
- if (action === "delete-prompt") {
861
- await mutate(`/api/prompts/${encodeURIComponent(id)}`, { method: "DELETE" });
862
- return;
863
- }
864
- if (action === "add-files") { await pickFiles(); return; }
865
- if (action === "add-file") { await useFile(id); return; }
866
- if (action === "reveal-file") {
867
- try {
868
- await api(`/api/files/${encodeURIComponent(id)}/reveal`, { method: "POST" });
869
- } catch (error) {
870
- showToast(error.message, "danger");
871
- }
872
- return;
873
- }
874
- if (action === "delete-file") {
875
- await mutate(`/api/files/${encodeURIComponent(id)}`, { method: "DELETE" });
876
- return;
877
- }
878
- if (action === "view-all") {
879
- if (window.parent === window) window.open(`/?surface=${target.dataset.surface}`, "_blank", "noopener");
880
- else hostRequest("open-detail-tab", { surface: target.dataset.surface }).catch((error) => showToast(error.message, "danger"));
881
- }
882
- });
883
-
884
- app.addEventListener("toggle", (event) => {
885
- const menu = event.target.closest?.(".row-menu");
886
- if (!menu) return;
887
- if (menu.open) {
888
- hidePromptPreview();
889
- closeOpenPromptMenus(menu);
890
- }
891
- }, true);
892
-
893
- app.addEventListener("pointerover", (event) => {
894
- const row = event.target.closest?.(".prompt-row");
895
- if (!row) return;
896
- if (pointerTargetsPromptMenu(event, row)) {
897
- hidePromptPreview();
898
- return;
899
- }
900
- showPromptPreviewForRow(row);
901
- });
902
-
903
- app.addEventListener("pointerout", (event) => {
904
- const row = event.target.closest?.(".prompt-row");
905
- if (!row) return;
906
- const next = event.relatedTarget;
907
- if (next instanceof Node && row.contains(next)) return;
908
- hidePromptPreview();
909
- });
910
-
911
- app.addEventListener("scroll", hidePromptPreview, true);
912
-
913
- app.addEventListener("change", async (event) => {
914
- const target = event.target;
915
- if (target.dataset.action !== "toggle-todo") return;
916
- await mutate(`/api/todos/${encodeURIComponent(target.dataset.id)}`, {
917
- method: "PATCH",
918
- body: JSON.stringify({ completed: target.checked }),
919
- });
920
- });
921
-
922
- app.addEventListener("error", (event) => {
923
- const thumbnail = event.target.closest?.("[data-file-thumbnail]");
924
- if (!thumbnail) return;
925
- thumbnail.closest(".file-leading")?.classList.add("is-fallback");
926
- }, true);
927
-
928
- app.addEventListener("input", (event) => {
929
- const editorForm = event.target.closest('[data-form="save-prompt"], [data-form="save-todo"]');
930
- if (event.target.closest('[data-form="save-prompt"]') && ui.editingPrompt) {
931
- ui.editingPrompt[event.target.name] = event.target.value;
932
- }
933
- if (event.target.closest('[data-form="save-todo"]') && ui.editingTodo) {
934
- ui.editingTodo.body = event.target.value;
935
- }
936
- syncEditorSubmitState(editorForm);
937
- });
938
-
939
- app.addEventListener("submit", async (event) => {
940
- event.preventDefault();
941
- if (ui.editorSubmitting || ui.editorMissing) return;
942
- const form = event.target;
943
- ui.editorSubmitting = true;
944
- form.setAttribute("aria-busy", "true");
945
- const submit = form.querySelector('button[type="submit"]');
946
- if (submit) submit.disabled = true;
947
- if (form.dataset.form === "save-todo") {
948
- if (!ui.editingTodo?.body.trim()) {
949
- ui.editorSubmitting = false;
950
- form.removeAttribute("aria-busy");
951
- syncEditorSubmitState(form);
952
- return showToast(t("todoEmpty"), "danger");
953
- }
954
- const body = JSON.stringify({ body: ui.editingTodo.body });
955
- const saved = ui.editingTodo.id
956
- ? await mutate(`/api/todos/${encodeURIComponent(ui.editingTodo.id)}`, { method: "PATCH", body })
957
- : await mutate("/api/todos", { method: "POST", body });
958
- ui.editorSubmitting = false;
959
- form.removeAttribute("aria-busy");
960
- if (saved) closeEditor();
961
- else syncEditorSubmitState(form);
962
- return;
963
- }
964
- if (form.dataset.form === "save-prompt") {
965
- if (!ui.editingPrompt?.body.trim()) {
966
- ui.editorSubmitting = false;
967
- form.removeAttribute("aria-busy");
968
- syncEditorSubmitState(form);
969
- return showToast(t("promptEmpty"), "danger");
970
- }
971
- const draft = { title: ui.editingPrompt.title, body: ui.editingPrompt.body };
972
- let saved;
973
- if (ui.editingPrompt.id) {
974
- saved = await mutate(`/api/prompts/${encodeURIComponent(ui.editingPrompt.id)}`, {
975
- method: "PATCH",
976
- body: JSON.stringify(draft),
977
- });
978
- } else {
979
- saved = await mutate("/api/prompts", { method: "POST", body: JSON.stringify(draft) });
980
- }
981
- ui.editorSubmitting = false;
982
- form.removeAttribute("aria-busy");
983
- if (saved) {
984
- ui.editingPrompt = null;
985
- if (surface === "editor") closeEditor();
986
- else render();
987
- } else syncEditorSubmitState(form);
988
- return;
989
- }
990
- ui.editorSubmitting = false;
991
- form.removeAttribute("aria-busy");
992
- });
993
-
994
- function clearTodoDropTarget() {
995
- ui.todoDropTarget = null;
996
- const row = app.querySelector(".todo-row[data-drop-position]");
997
- if (row) delete row.dataset.dropPosition;
998
- }
999
-
1000
- function setTodoDropTarget(row, position) {
1001
- if (ui.todoDropTarget?.id === row.dataset.id && ui.todoDropTarget.position === position) return;
1002
- const previous = app.querySelector(".todo-row[data-drop-position]");
1003
- if (previous && previous !== row) delete previous.dataset.dropPosition;
1004
- ui.todoDropTarget = { id: row.dataset.id, position };
1005
- row.dataset.dropPosition = position;
1006
- }
1007
-
1008
-
1009
- function dragLeavesApp(event) {
1010
- if (event.relatedTarget instanceof Node && app.contains(event.relatedTarget)) return false;
1011
- const rect = app.getBoundingClientRect();
1012
- return event.clientX <= rect.left
1013
- || event.clientX >= rect.right
1014
- || event.clientY <= rect.top
1015
- || event.clientY >= rect.bottom;
1016
- }
1017
-
1018
- function clearTodoDragPreview() {
1019
- ui.todoDragPreview?.remove();
1020
- ui.todoDragPreview = null;
1021
- }
1022
-
1023
- function moveTodoDragPreview(event) {
1024
- const preview = ui.todoDragPreview;
1025
- if (!preview || (event.clientX === 0 && event.clientY === 0)) return;
1026
- const offsetX = Number(preview.dataset.dragOffsetX);
1027
- const offsetY = Number(preview.dataset.dragOffsetY);
1028
- preview.style.left = `${event.clientX - offsetX}px`;
1029
- preview.style.top = `${event.clientY - offsetY}px`;
1030
- }
1031
-
1032
- function setTodoDragPreview(row, event) {
1033
- if (typeof event.dataTransfer?.setDragImage !== "function") return;
1034
- clearTodoDragPreview();
1035
- const rect = row.getBoundingClientRect();
1036
- const preview = document.createElement("div");
1037
- const content = row.cloneNode(true);
1038
- const width = rect.width;
1039
- preview.className = "todo-drag-preview";
1040
- preview.setAttribute("aria-hidden", "true");
1041
- preview.inert = true;
1042
- preview.style.setProperty("--todo-drag-preview-width", `${width}px`);
1043
- preview.style.left = `${rect.left}px`;
1044
- preview.style.top = `${rect.top}px`;
1045
- preview.dataset.dragOffsetX = String(event.clientX - rect.left);
1046
- preview.dataset.dragOffsetY = String(event.clientY - rect.top);
1047
- content.classList.add("todo-drag-preview-content");
1048
- content.removeAttribute("data-id");
1049
- content.removeAttribute("draggable");
1050
- content.removeAttribute("data-drop-position");
1051
- content.querySelector(".todo-drop-indicator")?.remove();
1052
- content.querySelector(".row-actions")?.remove();
1053
- preview.append(content);
1054
- document.body.append(preview);
1055
- ui.todoDragPreview = preview;
1056
- const dragImage = document.createElement("canvas");
1057
- dragImage.width = 1;
1058
- dragImage.height = 1;
1059
- document.body.append(dragImage);
1060
- try {
1061
- event.dataTransfer.setDragImage(dragImage, 0, 0);
1062
- } catch {
1063
- dragImage.remove();
1064
- clearTodoDragPreview();
1065
- return;
1066
- }
1067
- requestAnimationFrame(() => dragImage.remove());
1068
- setTimeout(() => dragImage.remove(), 100);
1069
- }
1070
-
1071
- function clearTodoDraggingState() {
1072
- clearTodoDragPreview();
1073
- app.querySelector(".todo-row.is-dragging")?.classList.remove("is-dragging");
1074
- ui.draggingTodo = null;
1075
- }
1076
-
1077
- app.addEventListener("dragstart", (event) => {
1078
- const row = event.target.closest('.todo-row[draggable="true"]');
1079
- if (!row) return;
1080
- clearTodoDropTarget();
1081
- clearTodoDraggingState();
1082
- setTodoDragPreview(row, event);
1083
- ui.draggingTodo = row.dataset.id;
1084
- event.dataTransfer.effectAllowed = "move";
1085
- event.dataTransfer.setData("text/plain", row.dataset.id);
1086
- row.classList.add("is-dragging");
1087
- });
1088
-
1089
- app.addEventListener("dragend", () => {
1090
- clearTodoDraggingState();
1091
- clearTodoDropTarget();
1092
- });
1093
-
1094
- app.addEventListener("drag", (event) => {
1095
- if (ui.draggingTodo) moveTodoDragPreview(event);
1096
- });
1097
-
1098
- app.addEventListener("dragover", (event) => {
1099
- if (event.dataTransfer?.types.includes("Files")) {
1100
- event.preventDefault();
1101
- document.body.classList.add("is-file-dragging");
1102
- return;
1103
- }
1104
- if (!ui.draggingTodo) return;
1105
- moveTodoDragPreview(event);
1106
- const target = event.target.closest('.todo-row[draggable="true"]');
1107
- if (!target || target.dataset.id === ui.draggingTodo) {
1108
- clearTodoDropTarget();
1109
- return;
1110
- }
1111
- event.preventDefault();
1112
- event.dataTransfer.dropEffect = "move";
1113
- setTodoDropTarget(target, todoDropPosition(event, target));
1114
- });
1115
-
1116
- app.addEventListener("dragleave", (event) => {
1117
- if (!dragLeavesApp(event)) return;
1118
- document.body.classList.remove("is-file-dragging");
1119
- clearTodoDropTarget();
1120
- });
1121
-
1122
- app.addEventListener("drop", async (event) => {
1123
- document.body.classList.remove("is-file-dragging");
1124
- if (event.dataTransfer?.files?.length) {
1125
- event.preventDefault();
1126
- clearTodoDropTarget();
1127
- await pinDroppedFiles(event.dataTransfer.files);
1128
- return;
1129
- }
1130
- const target = event.target.closest('.todo-row[draggable="true"]');
1131
- const draggedId = ui.draggingTodo;
1132
- const position = target?.dataset.dropPosition ?? ui.todoDropTarget?.position;
1133
- const targetId = target?.dataset.id;
1134
- if (!draggedId || !targetId || targetId === draggedId || !["before", "after"].includes(position)) return;
1135
- event.preventDefault();
1136
- clearTodoDropTarget();
1137
- clearTodoDraggingState();
1138
- const currentIds = ui.state.todos.active.map((item) => item.id);
1139
- const ids = reorderedTodoIds(currentIds, draggedId, targetId, position);
1140
- if (ids.every((id, index) => id === currentIds[index])) return;
1141
- await mutate("/api/todos/reorder", { method: "POST", body: JSON.stringify({ ids }) });
1142
- });
1143
-
1144
- document.addEventListener("keydown", (event) => {
1145
- if (surface === "summary" && event.key === "Escape") {
1146
- const menu = app.querySelector(".row-menu[open]");
1147
- if (menu) {
1148
- event.preventDefault();
1149
- menu.open = false;
1150
- menu.querySelector("summary")?.focus();
1151
- }
1152
- return;
1153
- }
1154
- if (surface !== "editor") return;
1155
- if (event.key === "Escape") {
1156
- event.preventDefault();
1157
- closeEditor();
1158
- return;
1159
- }
1160
- if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
1161
- event.preventDefault();
1162
- const form = app.querySelector("form");
1163
- const submit = form?.querySelector('button[type="submit"]');
1164
- if (!submit?.disabled) form?.requestSubmit(submit);
1165
- return;
1166
- }
1167
- if (event.key === "Tab") {
1168
- const focusable = [...app.querySelectorAll("input, textarea, button")]
1169
- .filter((element) => !element.disabled && element.getClientRects().length);
1170
- const first = focusable[0];
1171
- const last = focusable.at(-1);
1172
- if (!first || !last) return;
1173
- if (event.shiftKey && document.activeElement === first) {
1174
- event.preventDefault();
1175
- last.focus();
1176
- } else if (!event.shiftKey && document.activeElement === last) {
1177
- event.preventDefault();
1178
- first.focus();
1179
- }
1180
- }
1181
- });
1182
-
1183
- if (surface === "summary") {
1184
- window.addEventListener("resize", () => {
1185
- hidePromptPreview();
1186
- reportSummaryMetrics();
1187
- });
1188
- window.addEventListener("blur", () => {
1189
- hidePromptPreview();
1190
- dismissPromptMenus({ releaseFocus: true });
1191
- });
1192
- } else if (surface === "prompts") {
1193
- window.addEventListener("resize", syncPromptRowDensity);
1194
- }
1195
- document.fonts?.ready.then(() => {
1196
- syncPromptRowDensity();
1197
- reportSummaryMetrics();
1198
- });
1199
-
1200
- postToHost({ type: "codex-personal:ready", feature: "notes", surface });
1201
- refreshState();
1202
- connectEvents();