pinokiod 7.2.16 → 7.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/kernel/agent_instructions.js +166 -0
- package/kernel/api/index.js +137 -12
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/environment.js +23 -9
- package/kernel/plugin_sources.js +57 -4
- package/kernel/prototype.js +4 -0
- package/kernel/shell.js +2 -0
- package/kernel/watch/index.js +31 -4
- package/package.json +1 -1
- package/server/features/index.js +4 -4
- package/server/features/{drafts → notes}/index.js +9 -9
- package/server/features/{drafts → notes}/parser.js +12 -7
- package/server/features/notes/public/notes.css +955 -0
- package/server/features/notes/public/notes.js +1149 -0
- package/server/features/{drafts → notes}/registry_import.js +59 -74
- package/server/features/notes/routes.js +156 -0
- package/server/features/notes/service.js +326 -0
- package/server/features/{drafts → notes}/watcher.js +14 -16
- package/server/index.js +61 -30
- package/server/lib/content_validation.js +19 -8
- package/server/lib/workspace_catalog.js +18 -18
- package/server/public/task-launcher.css +11 -3
- package/server/public/tasker.css +336 -0
- package/server/public/tasker.js +407 -0
- package/server/views/d.ejs +33 -2
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/partials/workspace_row.ejs +11 -11
- package/server/views/pre.ejs +1 -1
- package/server/views/task_launch.ejs +10 -10
- package/server/views/tasker.ejs +40 -0
- package/server/views/terminal.ejs +15 -6
- package/server/views/terminals.ejs +0 -1
- package/server/views/workspaces.ejs +2 -1
- package/system/plugin/antigravity/pinokio.js +2 -4
- package/system/plugin/claude/pinokio.js +2 -4
- package/system/plugin/claude-auto/pinokio.js +2 -4
- package/system/plugin/claude-desktop/pinokio.js +2 -4
- package/system/plugin/codex/pinokio.js +2 -4
- package/system/plugin/codex-auto/pinokio.js +2 -4
- package/system/plugin/codex-desktop/pinokio.js +2 -4
- package/system/plugin/crush/pinokio.js +2 -4
- package/system/plugin/cursor/pinokio.js +2 -4
- package/system/plugin/gemini/pinokio.js +2 -4
- package/system/plugin/gemini-auto/pinokio.js +2 -4
- package/system/plugin/qwen/pinokio.js +2 -4
- package/system/plugin/vscode/pinokio.js +2 -4
- package/system/plugin/windsurf/pinokio.js +2 -4
- package/test/plugin-sources.test.js +45 -0
- package/server/features/drafts/public/drafts.js +0 -1569
- package/server/features/drafts/routes.js +0 -68
- package/server/features/drafts/service.js +0 -261
|
@@ -0,0 +1,1149 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
if (window.__PinokioNotesLoaded) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
window.__PinokioNotesLoaded = true;
|
|
6
|
+
|
|
7
|
+
const context = window.PinokioNoteContext || {};
|
|
8
|
+
const activeCwd = typeof context.cwd === "string" ? context.cwd.trim() : "";
|
|
9
|
+
if (!activeCwd) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const PUSH_ENDPOINT = "/push";
|
|
13
|
+
const SOUND_PREF_STORAGE_KEY = "pinokio:idle-sound";
|
|
14
|
+
const SOUND_SILENT_CHOICE = "__silent__";
|
|
15
|
+
const NOTE_NOTIFIED_PREFIX = "pinokio:note-notified:";
|
|
16
|
+
const state = {
|
|
17
|
+
initialRefreshComplete: false,
|
|
18
|
+
items: [],
|
|
19
|
+
lastSignature: "",
|
|
20
|
+
notifiedIds: new Set(),
|
|
21
|
+
drawerOpen: false,
|
|
22
|
+
drawerItemId: "",
|
|
23
|
+
drawerTab: "preview",
|
|
24
|
+
highlightItemId: "",
|
|
25
|
+
panelMode: "list",
|
|
26
|
+
unseen: new Map(),
|
|
27
|
+
edits: new Map()
|
|
28
|
+
};
|
|
29
|
+
function resolveNotificationSound() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = localStorage.getItem(SOUND_PREF_STORAGE_KEY);
|
|
32
|
+
const parsed = raw ? JSON.parse(raw) : null;
|
|
33
|
+
const choice = parsed && typeof parsed.choice === "string" ? parsed.choice.trim() : "";
|
|
34
|
+
if (choice === SOUND_SILENT_CHOICE) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const withLeading = choice.startsWith("/") ? choice : `/${choice}`;
|
|
38
|
+
const decoded = decodeURIComponent(withLeading);
|
|
39
|
+
if (decoded.startsWith("/sound/") && !decoded.includes("..")) {
|
|
40
|
+
return withLeading;
|
|
41
|
+
}
|
|
42
|
+
} catch (_) {}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function claimNoteNotification(id) {
|
|
47
|
+
const normalized = typeof id === "string" ? id.trim() : "";
|
|
48
|
+
if (!normalized) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (state.notifiedIds.has(normalized)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
state.notifiedIds.add(normalized);
|
|
55
|
+
try {
|
|
56
|
+
const key = `${NOTE_NOTIFIED_PREFIX}${normalized}`;
|
|
57
|
+
if (localStorage.getItem(key)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const token = `${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
61
|
+
localStorage.setItem(key, token);
|
|
62
|
+
return localStorage.getItem(key) === token;
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function notifyNoteReady(item) {
|
|
69
|
+
const notificationKey = item && item.id
|
|
70
|
+
? `${item.id}:${item.revision || item.updatedAt || ""}`
|
|
71
|
+
: "";
|
|
72
|
+
if (!item || !claimNoteNotification(notificationKey)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const title = typeof item.title === "string" && item.title.trim() ? item.title.trim() : "Note";
|
|
76
|
+
const workspaceName = typeof item.workspaceName === "string" && item.workspaceName.trim() ? item.workspaceName.trim() : "";
|
|
77
|
+
const message = workspaceName ? `${workspaceName}: ${title}` : `Note ready: ${title}`;
|
|
78
|
+
const sound = resolveNotificationSound();
|
|
79
|
+
const playedInline = typeof window.PinokioPlayNotificationSound === "function"
|
|
80
|
+
? window.PinokioPlayNotificationSound(sound)
|
|
81
|
+
: false;
|
|
82
|
+
const payload = {
|
|
83
|
+
title: "Pinokio",
|
|
84
|
+
message,
|
|
85
|
+
timeout: 60,
|
|
86
|
+
sound: playedInline ? false : sound,
|
|
87
|
+
audience: "device",
|
|
88
|
+
device_id: (typeof window.PinokioGetDeviceId === "function") ? window.PinokioGetDeviceId() : undefined
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
fetch(PUSH_ENDPOINT, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: {
|
|
94
|
+
"Content-Type": "application/json"
|
|
95
|
+
},
|
|
96
|
+
credentials: "include",
|
|
97
|
+
body: JSON.stringify(payload)
|
|
98
|
+
}).catch(() => {});
|
|
99
|
+
} catch (_) {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getRoot() {
|
|
104
|
+
let root = document.getElementById("pinokio-notes");
|
|
105
|
+
if (!root) {
|
|
106
|
+
const slot = document.getElementById("pinokio-notes-slot");
|
|
107
|
+
root = document.createElement("div");
|
|
108
|
+
root.id = "pinokio-notes";
|
|
109
|
+
root.className = `pinokio-notes ${slot ? "is-inline" : "is-floating"}`;
|
|
110
|
+
root.setAttribute("aria-live", "polite");
|
|
111
|
+
(slot || document.body).appendChild(root);
|
|
112
|
+
}
|
|
113
|
+
return root;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function removeRoot() {
|
|
117
|
+
const root = document.getElementById("pinokio-notes");
|
|
118
|
+
if (root) {
|
|
119
|
+
root.remove();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getSheetHost() {
|
|
124
|
+
const slot = document.getElementById("pinokio-notes-slot");
|
|
125
|
+
if (!slot) {
|
|
126
|
+
return document.body;
|
|
127
|
+
}
|
|
128
|
+
const scopedHost = slot.closest("[data-pinokio-notes-scope]");
|
|
129
|
+
if (scopedHost) {
|
|
130
|
+
return scopedHost;
|
|
131
|
+
}
|
|
132
|
+
return document.body;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createElement(tag, className, text) {
|
|
136
|
+
const element = document.createElement(tag);
|
|
137
|
+
if (className) {
|
|
138
|
+
element.className = className;
|
|
139
|
+
}
|
|
140
|
+
if (text !== undefined && text !== null) {
|
|
141
|
+
element.textContent = text;
|
|
142
|
+
}
|
|
143
|
+
return element;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function createIcon(name) {
|
|
147
|
+
const icon = createElement("i", name);
|
|
148
|
+
icon.setAttribute("aria-hidden", "true");
|
|
149
|
+
return icon;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createActionButton(className, label, iconName) {
|
|
153
|
+
const button = createElement("button", className);
|
|
154
|
+
if (iconName) {
|
|
155
|
+
button.appendChild(createIcon(iconName));
|
|
156
|
+
}
|
|
157
|
+
const text = createElement("span", "", label);
|
|
158
|
+
text.setAttribute("data-pinokio-note-label", "true");
|
|
159
|
+
button.appendChild(text);
|
|
160
|
+
return button;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setActionButtonLabel(button, label) {
|
|
164
|
+
if (!button) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const text = button.querySelector("[data-pinokio-note-label]");
|
|
168
|
+
if (text) {
|
|
169
|
+
text.textContent = label;
|
|
170
|
+
} else {
|
|
171
|
+
button.textContent = label;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function canPublishToRegistry(item) {
|
|
176
|
+
const publish = item && item.publish && typeof item.publish === "object" ? item.publish : null;
|
|
177
|
+
const target = publish && typeof publish.target === "string" ? publish.target.trim().toLowerCase() : "";
|
|
178
|
+
const type = publish && typeof publish.type === "string" ? publish.type.trim().toLowerCase() : "post";
|
|
179
|
+
return target === "registry" && type === "post";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function noteSignature(items) {
|
|
183
|
+
return (Array.isArray(items) ? items : [])
|
|
184
|
+
.map((item) => `${item.id}:${item.revision || item.updatedAt}`)
|
|
185
|
+
.join("|");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getNoteEdit(item) {
|
|
189
|
+
if (!item || !item.id) {
|
|
190
|
+
return {
|
|
191
|
+
markdown: String((item && item.markdown) || ""),
|
|
192
|
+
originalMarkdown: String((item && item.markdown) || ""),
|
|
193
|
+
revision: item && item.revision ? item.revision : "",
|
|
194
|
+
dirty: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
const existing = state.edits.get(item.id);
|
|
198
|
+
const markdown = String(item.markdown || "");
|
|
199
|
+
const revision = item.revision || item.updatedAt || "";
|
|
200
|
+
if (existing) {
|
|
201
|
+
if (existing.dirty) {
|
|
202
|
+
return existing;
|
|
203
|
+
}
|
|
204
|
+
if (existing.revision === revision) {
|
|
205
|
+
return existing;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const next = {
|
|
209
|
+
markdown,
|
|
210
|
+
originalMarkdown: markdown,
|
|
211
|
+
revision,
|
|
212
|
+
dirty: false
|
|
213
|
+
};
|
|
214
|
+
state.edits.set(item.id, next);
|
|
215
|
+
return next;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getEditedMarkdown(item) {
|
|
219
|
+
return getNoteEdit(item).markdown;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isNoteDirty(item) {
|
|
223
|
+
return !!getNoteEdit(item).dirty;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function setNoteMarkdown(item, value) {
|
|
227
|
+
if (!item || !item.id) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const edit = getNoteEdit(item);
|
|
231
|
+
edit.markdown = String(value || "");
|
|
232
|
+
edit.dirty = edit.markdown !== edit.originalMarkdown;
|
|
233
|
+
state.edits.set(item.id, edit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function resetNoteEdit(item) {
|
|
237
|
+
if (!item || !item.id) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
state.edits.set(item.id, {
|
|
241
|
+
markdown: String(item.markdown || ""),
|
|
242
|
+
originalMarkdown: String(item.markdown || ""),
|
|
243
|
+
revision: item.revision || item.updatedAt || "",
|
|
244
|
+
dirty: false
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function replaceNoteItem(nextItem) {
|
|
249
|
+
if (!nextItem || !nextItem.id) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const index = state.items.findIndex((item) => item && item.id === nextItem.id);
|
|
253
|
+
if (index >= 0) {
|
|
254
|
+
state.items.splice(index, 1, nextItem);
|
|
255
|
+
} else {
|
|
256
|
+
state.items.unshift(nextItem);
|
|
257
|
+
}
|
|
258
|
+
resetNoteEdit(nextItem);
|
|
259
|
+
state.drawerItemId = nextItem.id;
|
|
260
|
+
state.lastSignature = noteSignature(state.items);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function setButtonsForNote(item, dirty) {
|
|
264
|
+
const id = item && item.id ? item.id : "";
|
|
265
|
+
document.querySelectorAll("[data-pinokio-note-save]").forEach((button) => {
|
|
266
|
+
if (button.dataset.pinokioNoteSave === id) {
|
|
267
|
+
button.hidden = !dirty;
|
|
268
|
+
button.disabled = !dirty;
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
document.querySelectorAll("[data-pinokio-note-revert]").forEach((button) => {
|
|
272
|
+
if (button.dataset.pinokioNoteRevert === id) {
|
|
273
|
+
button.hidden = !dirty;
|
|
274
|
+
button.disabled = !dirty;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
document.querySelectorAll("[data-pinokio-note-publish]").forEach((button) => {
|
|
278
|
+
if (button.dataset.pinokioNotePublish === id) {
|
|
279
|
+
button.disabled = !!dirty;
|
|
280
|
+
button.title = dirty ? "Save changes before publishing" : "";
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
document.querySelectorAll("[data-pinokio-note-unsaved]").forEach((badge) => {
|
|
284
|
+
if (badge.dataset.pinokioNoteUnsaved === id) {
|
|
285
|
+
badge.hidden = !dirty;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function formatBytes(bytes) {
|
|
291
|
+
const value = Number(bytes);
|
|
292
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
293
|
+
return "";
|
|
294
|
+
}
|
|
295
|
+
if (value < 1024) {
|
|
296
|
+
return `${value} B`;
|
|
297
|
+
}
|
|
298
|
+
if (value < 1024 * 1024) {
|
|
299
|
+
return `${(value / 1024).toFixed(1)} KB`;
|
|
300
|
+
}
|
|
301
|
+
return `${(value / 1024 / 1024).toFixed(1)} MB`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function formatUpdatedAt(value) {
|
|
305
|
+
const date = new Date(value);
|
|
306
|
+
if (!Number.isFinite(date.getTime())) {
|
|
307
|
+
return "";
|
|
308
|
+
}
|
|
309
|
+
return date.toLocaleString([], {
|
|
310
|
+
month: "short",
|
|
311
|
+
day: "numeric",
|
|
312
|
+
hour: "numeric",
|
|
313
|
+
minute: "2-digit"
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function getItemById(id) {
|
|
318
|
+
return (Array.isArray(state.items) ? state.items : []).find((item) => item && item.id === id) || null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getNoteMetaParts(item) {
|
|
322
|
+
const parts = [];
|
|
323
|
+
const updatedAt = formatUpdatedAt(item && item.updatedAt);
|
|
324
|
+
const noteSize = formatBytes(item && item.noteBytes);
|
|
325
|
+
const mediaCount = Number(item && item.mediaCount || 0);
|
|
326
|
+
const missingMedia = Number(item && item.missingMediaCount || 0);
|
|
327
|
+
if (item && item.workspaceName) {
|
|
328
|
+
parts.push(item.workspaceName);
|
|
329
|
+
}
|
|
330
|
+
if (updatedAt) {
|
|
331
|
+
parts.push(updatedAt);
|
|
332
|
+
}
|
|
333
|
+
if (noteSize) {
|
|
334
|
+
parts.push(noteSize);
|
|
335
|
+
}
|
|
336
|
+
if (mediaCount > 0) {
|
|
337
|
+
parts.push(`${mediaCount} media ${mediaCount === 1 ? "file" : "files"}`);
|
|
338
|
+
}
|
|
339
|
+
if (missingMedia > 0) {
|
|
340
|
+
parts.push(`${missingMedia} missing`);
|
|
341
|
+
}
|
|
342
|
+
return parts;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function normalizeRef(value) {
|
|
346
|
+
const raw = String(value || "").trim().replace(/^<|>$/g, "").replace(/\\/g, "/");
|
|
347
|
+
if (!raw) {
|
|
348
|
+
return "";
|
|
349
|
+
}
|
|
350
|
+
const withoutHash = raw.split("#")[0];
|
|
351
|
+
return withoutHash.split("?")[0];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function getMediaItems(item) {
|
|
355
|
+
return Array.isArray(item && item.media) ? item.media : [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function findMediaByRef(item, ref) {
|
|
359
|
+
const normalized = normalizeRef(ref);
|
|
360
|
+
if (!normalized) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return getMediaItems(item).find((media) => normalizeRef(media && media.ref) === normalized) || null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function getMediaUrl(item, media) {
|
|
367
|
+
return `/notes/${encodeURIComponent(item.id)}/media/${encodeURIComponent(String(media.index))}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function mediaKind(media) {
|
|
371
|
+
const ext = String((media && media.ext) || "").toLowerCase();
|
|
372
|
+
if ([".apng", ".avif", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".webp"].includes(ext)) {
|
|
373
|
+
return "image";
|
|
374
|
+
}
|
|
375
|
+
if ([".mp4", ".webm", ".ogg"].includes(ext)) {
|
|
376
|
+
return "video";
|
|
377
|
+
}
|
|
378
|
+
if ([".m4a", ".mp3", ".wav"].includes(ext)) {
|
|
379
|
+
return "audio";
|
|
380
|
+
}
|
|
381
|
+
return "file";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function isSafeExternalHref(value) {
|
|
385
|
+
return /^(https?:|mailto:)/i.test(String(value || "").trim());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function appendInline(parent, text, item) {
|
|
389
|
+
const source = String(text || "");
|
|
390
|
+
const pattern = /(`[^`]+`|\*\*[^*]+\*\*|\[[^\]]+]\([^)]+\))/g;
|
|
391
|
+
let cursor = 0;
|
|
392
|
+
let match = null;
|
|
393
|
+
while ((match = pattern.exec(source))) {
|
|
394
|
+
if (match.index > cursor) {
|
|
395
|
+
parent.appendChild(document.createTextNode(source.slice(cursor, match.index)));
|
|
396
|
+
}
|
|
397
|
+
const token = match[0];
|
|
398
|
+
if (token.startsWith("`") && token.endsWith("`")) {
|
|
399
|
+
parent.appendChild(createElement("code", "", token.slice(1, -1)));
|
|
400
|
+
} else if (token.startsWith("**") && token.endsWith("**")) {
|
|
401
|
+
const strong = createElement("strong");
|
|
402
|
+
appendInline(strong, token.slice(2, -2), item);
|
|
403
|
+
parent.appendChild(strong);
|
|
404
|
+
} else {
|
|
405
|
+
const linkMatch = token.match(/^\[([^\]]+)]\(([^)]+)\)$/);
|
|
406
|
+
const label = linkMatch ? linkMatch[1] : token;
|
|
407
|
+
const href = linkMatch ? String(linkMatch[2] || "").trim() : "";
|
|
408
|
+
const media = findMediaByRef(item, href);
|
|
409
|
+
const safeHref = media && media.exists
|
|
410
|
+
? getMediaUrl(item, media)
|
|
411
|
+
: (isSafeExternalHref(href) ? href : "");
|
|
412
|
+
if (safeHref) {
|
|
413
|
+
const link = createElement("a");
|
|
414
|
+
link.href = safeHref;
|
|
415
|
+
if (isSafeExternalHref(safeHref)) {
|
|
416
|
+
link.target = "_blank";
|
|
417
|
+
link.rel = "noreferrer";
|
|
418
|
+
}
|
|
419
|
+
link.textContent = label;
|
|
420
|
+
parent.appendChild(link);
|
|
421
|
+
} else {
|
|
422
|
+
parent.appendChild(document.createTextNode(label));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
cursor = pattern.lastIndex;
|
|
426
|
+
}
|
|
427
|
+
if (cursor < source.length) {
|
|
428
|
+
parent.appendChild(document.createTextNode(source.slice(cursor)));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function createMissingMedia(ref) {
|
|
433
|
+
const figure = createElement("figure", "pinokio-note-media-figure");
|
|
434
|
+
const missing = createElement("div", "pinokio-note-media-item");
|
|
435
|
+
const icon = createElement("div", "pinokio-note-media-item-icon");
|
|
436
|
+
icon.appendChild(createIcon("fa-solid fa-triangle-exclamation"));
|
|
437
|
+
const copy = createElement("div", "pinokio-note-media-item-copy");
|
|
438
|
+
copy.appendChild(createElement("div", "pinokio-note-media-item-title", ref || "Missing media"));
|
|
439
|
+
copy.appendChild(createElement("div", "pinokio-note-media-item-meta", "Referenced file was not found in the note folder."));
|
|
440
|
+
missing.appendChild(icon);
|
|
441
|
+
missing.appendChild(copy);
|
|
442
|
+
figure.appendChild(missing);
|
|
443
|
+
return figure;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function createMediaFigure(item, ref, altText) {
|
|
447
|
+
const media = findMediaByRef(item, ref);
|
|
448
|
+
if (!media || !media.exists) {
|
|
449
|
+
return createMissingMedia(ref);
|
|
450
|
+
}
|
|
451
|
+
const figure = createElement("figure", "pinokio-note-media-figure");
|
|
452
|
+
const kind = mediaKind(media);
|
|
453
|
+
const url = getMediaUrl(item, media);
|
|
454
|
+
let element = null;
|
|
455
|
+
if (kind === "image") {
|
|
456
|
+
element = document.createElement("img");
|
|
457
|
+
element.alt = altText || media.ref || "";
|
|
458
|
+
element.loading = "lazy";
|
|
459
|
+
} else if (kind === "video") {
|
|
460
|
+
element = document.createElement("video");
|
|
461
|
+
element.controls = true;
|
|
462
|
+
element.preload = "metadata";
|
|
463
|
+
} else if (kind === "audio") {
|
|
464
|
+
element = document.createElement("audio");
|
|
465
|
+
element.controls = true;
|
|
466
|
+
element.preload = "metadata";
|
|
467
|
+
}
|
|
468
|
+
if (!element) {
|
|
469
|
+
const link = createElement("a", "", media.ref || "Open media");
|
|
470
|
+
link.href = url;
|
|
471
|
+
figure.appendChild(link);
|
|
472
|
+
} else {
|
|
473
|
+
element.src = url;
|
|
474
|
+
figure.appendChild(element);
|
|
475
|
+
}
|
|
476
|
+
const captionBits = [media.ref || ""];
|
|
477
|
+
const size = formatBytes(media.bytes);
|
|
478
|
+
if (size) {
|
|
479
|
+
captionBits.push(size);
|
|
480
|
+
}
|
|
481
|
+
figure.appendChild(createElement("figcaption", "pinokio-note-media-caption", captionBits.filter(Boolean).join(" / ")));
|
|
482
|
+
return figure;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function splitTableRow(line) {
|
|
486
|
+
let value = String(line || "").trim();
|
|
487
|
+
if (value.startsWith("|")) {
|
|
488
|
+
value = value.slice(1);
|
|
489
|
+
}
|
|
490
|
+
if (value.endsWith("|")) {
|
|
491
|
+
value = value.slice(0, -1);
|
|
492
|
+
}
|
|
493
|
+
return value.split("|").map((cell) => cell.trim());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isMarkdownTableSeparator(line) {
|
|
497
|
+
const cells = splitTableRow(line);
|
|
498
|
+
return cells.length > 1 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderMarkdownTable(root, item, headerLine, rowLines) {
|
|
502
|
+
const table = createElement("table");
|
|
503
|
+
const thead = createElement("thead");
|
|
504
|
+
const headerRow = createElement("tr");
|
|
505
|
+
splitTableRow(headerLine).forEach((cell) => {
|
|
506
|
+
const th = createElement("th");
|
|
507
|
+
appendInline(th, cell, item);
|
|
508
|
+
headerRow.appendChild(th);
|
|
509
|
+
});
|
|
510
|
+
thead.appendChild(headerRow);
|
|
511
|
+
table.appendChild(thead);
|
|
512
|
+
const tbody = createElement("tbody");
|
|
513
|
+
rowLines.forEach((line) => {
|
|
514
|
+
const row = createElement("tr");
|
|
515
|
+
splitTableRow(line).forEach((cell) => {
|
|
516
|
+
const td = createElement("td");
|
|
517
|
+
appendInline(td, cell, item);
|
|
518
|
+
row.appendChild(td);
|
|
519
|
+
});
|
|
520
|
+
tbody.appendChild(row);
|
|
521
|
+
});
|
|
522
|
+
table.appendChild(tbody);
|
|
523
|
+
root.appendChild(table);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function renderMarkdownPreview(container, item) {
|
|
527
|
+
const markdown = getEditedMarkdown(item);
|
|
528
|
+
const lines = markdown.split(/\r?\n/);
|
|
529
|
+
const root = createElement("div", "pinokio-note-markdown");
|
|
530
|
+
let paragraph = [];
|
|
531
|
+
let list = null;
|
|
532
|
+
let code = null;
|
|
533
|
+
|
|
534
|
+
const flushParagraph = () => {
|
|
535
|
+
if (!paragraph.length) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const p = createElement("p");
|
|
539
|
+
appendInline(p, paragraph.join(" "), item);
|
|
540
|
+
root.appendChild(p);
|
|
541
|
+
paragraph = [];
|
|
542
|
+
};
|
|
543
|
+
const flushList = () => {
|
|
544
|
+
if (list) {
|
|
545
|
+
root.appendChild(list);
|
|
546
|
+
list = null;
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
const flushCode = () => {
|
|
550
|
+
if (!code) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const pre = createElement("pre");
|
|
554
|
+
const codeEl = createElement("code", "", code.join("\n"));
|
|
555
|
+
pre.appendChild(codeEl);
|
|
556
|
+
root.appendChild(pre);
|
|
557
|
+
code = null;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
561
|
+
const line = lines[lineIndex];
|
|
562
|
+
const fenceMatch = line.match(/^```/);
|
|
563
|
+
if (fenceMatch) {
|
|
564
|
+
if (code) {
|
|
565
|
+
flushCode();
|
|
566
|
+
} else {
|
|
567
|
+
flushParagraph();
|
|
568
|
+
flushList();
|
|
569
|
+
code = [];
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (code) {
|
|
574
|
+
code.push(line);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (!line.trim()) {
|
|
578
|
+
flushParagraph();
|
|
579
|
+
flushList();
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (line.trim().startsWith("|") && isMarkdownTableSeparator(lines[lineIndex + 1])) {
|
|
583
|
+
flushParagraph();
|
|
584
|
+
flushList();
|
|
585
|
+
const rowLines = [];
|
|
586
|
+
lineIndex += 1;
|
|
587
|
+
while (lineIndex + 1 < lines.length && lines[lineIndex + 1].trim().startsWith("|")) {
|
|
588
|
+
lineIndex += 1;
|
|
589
|
+
rowLines.push(lines[lineIndex]);
|
|
590
|
+
}
|
|
591
|
+
renderMarkdownTable(root, item, line, rowLines);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const imageMatch = line.trim().match(/^!\[([^\]]*)]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)$/);
|
|
595
|
+
if (imageMatch) {
|
|
596
|
+
flushParagraph();
|
|
597
|
+
flushList();
|
|
598
|
+
root.appendChild(createMediaFigure(item, imageMatch[2], imageMatch[1]));
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const heading = line.match(/^(#{1,3})\s+(.+?)\s*#*\s*$/);
|
|
602
|
+
if (heading) {
|
|
603
|
+
flushParagraph();
|
|
604
|
+
flushList();
|
|
605
|
+
const h = createElement(`h${heading[1].length}`);
|
|
606
|
+
appendInline(h, heading[2], item);
|
|
607
|
+
root.appendChild(h);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const quote = line.match(/^>\s?(.*)$/);
|
|
611
|
+
if (quote) {
|
|
612
|
+
flushParagraph();
|
|
613
|
+
flushList();
|
|
614
|
+
const blockquote = createElement("blockquote");
|
|
615
|
+
appendInline(blockquote, quote[1], item);
|
|
616
|
+
root.appendChild(blockquote);
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
const unordered = line.match(/^\s*[-*]\s+(.+)$/);
|
|
620
|
+
const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
|
|
621
|
+
if (unordered || ordered) {
|
|
622
|
+
flushParagraph();
|
|
623
|
+
const tag = ordered ? "ol" : "ul";
|
|
624
|
+
if (!list || list.tagName.toLowerCase() !== tag) {
|
|
625
|
+
flushList();
|
|
626
|
+
list = createElement(tag);
|
|
627
|
+
}
|
|
628
|
+
const li = createElement("li");
|
|
629
|
+
appendInline(li, (unordered || ordered)[1], item);
|
|
630
|
+
list.appendChild(li);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
paragraph.push(line.trim());
|
|
634
|
+
}
|
|
635
|
+
flushCode();
|
|
636
|
+
flushParagraph();
|
|
637
|
+
flushList();
|
|
638
|
+
if (!root.childNodes.length) {
|
|
639
|
+
root.appendChild(createElement("p", "", "No preview available."));
|
|
640
|
+
}
|
|
641
|
+
container.appendChild(root);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function renderMarkdownEditor(container, item) {
|
|
645
|
+
const edit = getNoteEdit(item);
|
|
646
|
+
const shell = createElement("div", "pinokio-note-editor");
|
|
647
|
+
const textarea = document.createElement("textarea");
|
|
648
|
+
textarea.className = "pinokio-note-markdown-editor";
|
|
649
|
+
textarea.value = edit.markdown;
|
|
650
|
+
textarea.spellcheck = false;
|
|
651
|
+
textarea.setAttribute("aria-label", "Edit note markdown");
|
|
652
|
+
textarea.addEventListener("input", () => {
|
|
653
|
+
setNoteMarkdown(item, textarea.value);
|
|
654
|
+
setButtonsForNote(item, isNoteDirty(item));
|
|
655
|
+
});
|
|
656
|
+
shell.appendChild(textarea);
|
|
657
|
+
container.appendChild(shell);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function renderMediaList(container, item) {
|
|
661
|
+
const mediaItems = getMediaItems(item);
|
|
662
|
+
const list = createElement("div", "pinokio-note-media-list");
|
|
663
|
+
if (!mediaItems.length) {
|
|
664
|
+
list.appendChild(createElement("div", "pinokio-note-media-item", "No media files referenced from this note."));
|
|
665
|
+
container.appendChild(list);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
mediaItems.forEach((media) => {
|
|
669
|
+
const row = createElement("div", "pinokio-note-media-item");
|
|
670
|
+
const icon = createElement("div", "pinokio-note-media-item-icon");
|
|
671
|
+
const kind = mediaKind(media);
|
|
672
|
+
icon.appendChild(createIcon(kind === "video"
|
|
673
|
+
? "fa-solid fa-video"
|
|
674
|
+
: (kind === "audio" ? "fa-solid fa-volume-high" : (kind === "image" ? "fa-solid fa-image" : "fa-solid fa-file"))));
|
|
675
|
+
const copy = createElement("div", "pinokio-note-media-item-copy");
|
|
676
|
+
copy.appendChild(createElement("div", "pinokio-note-media-item-title", media.ref || "Media"));
|
|
677
|
+
const size = formatBytes(media.bytes);
|
|
678
|
+
copy.appendChild(createElement("div", "pinokio-note-media-item-meta", [media.exists ? "Ready" : "Missing", size].filter(Boolean).join(" / ")));
|
|
679
|
+
row.appendChild(icon);
|
|
680
|
+
row.appendChild(copy);
|
|
681
|
+
if (media.exists) {
|
|
682
|
+
const link = createActionButton("pinokio-note-button secondary", "Open", "fa-solid fa-up-right-from-square");
|
|
683
|
+
link.type = "button";
|
|
684
|
+
link.addEventListener("click", () => {
|
|
685
|
+
window.open(getMediaUrl(item, media), "_blank");
|
|
686
|
+
});
|
|
687
|
+
row.appendChild(link);
|
|
688
|
+
}
|
|
689
|
+
list.appendChild(row);
|
|
690
|
+
});
|
|
691
|
+
container.appendChild(list);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function getApiUrl() {
|
|
695
|
+
const url = new URL("/notes", window.location.origin);
|
|
696
|
+
if (activeCwd) {
|
|
697
|
+
url.searchParams.set("cwd", activeCwd);
|
|
698
|
+
}
|
|
699
|
+
if (context.publish && typeof context.publish === "object" && !Array.isArray(context.publish)) {
|
|
700
|
+
try {
|
|
701
|
+
url.searchParams.set("publish", JSON.stringify(context.publish));
|
|
702
|
+
} catch (_) {}
|
|
703
|
+
}
|
|
704
|
+
return url.toString();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function openNote(item, button) {
|
|
708
|
+
if (!item || !item.notePath) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const originalTextNode = button ? button.querySelector("[data-pinokio-note-label]") : null;
|
|
712
|
+
const originalText = originalTextNode ? originalTextNode.textContent : (button ? button.textContent : "");
|
|
713
|
+
if (button) {
|
|
714
|
+
button.disabled = true;
|
|
715
|
+
setActionButtonLabel(button, "Opening...");
|
|
716
|
+
}
|
|
717
|
+
try {
|
|
718
|
+
await fetch("/openfs", {
|
|
719
|
+
method: "POST",
|
|
720
|
+
headers: {
|
|
721
|
+
"Content-Type": "application/json"
|
|
722
|
+
},
|
|
723
|
+
body: JSON.stringify({
|
|
724
|
+
path: item.notePath
|
|
725
|
+
})
|
|
726
|
+
});
|
|
727
|
+
} finally {
|
|
728
|
+
if (button) {
|
|
729
|
+
button.disabled = false;
|
|
730
|
+
setActionButtonLabel(button, originalText || "File explorer");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function saveNote(item, button) {
|
|
736
|
+
if (!item || !item.id) {
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const edit = getNoteEdit(item);
|
|
740
|
+
if (!edit.dirty) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
const originalTextNode = button ? button.querySelector("[data-pinokio-note-label]") : null;
|
|
744
|
+
const originalText = originalTextNode ? originalTextNode.textContent : (button ? button.textContent : "");
|
|
745
|
+
if (button) {
|
|
746
|
+
button.disabled = true;
|
|
747
|
+
setActionButtonLabel(button, "Saving...");
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const response = await fetch(`/notes/${encodeURIComponent(item.id)}`, {
|
|
751
|
+
method: "PUT",
|
|
752
|
+
headers: {
|
|
753
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
754
|
+
Accept: "application/json",
|
|
755
|
+
"X-Pinokio-Note-Revision": edit.revision || ""
|
|
756
|
+
},
|
|
757
|
+
body: edit.markdown
|
|
758
|
+
});
|
|
759
|
+
const data = await response.json().catch(() => null);
|
|
760
|
+
if (response.status === 409) {
|
|
761
|
+
throw new Error((data && data.error) || "Note changed on disk. Reload it before saving.");
|
|
762
|
+
}
|
|
763
|
+
if (!response.ok || !data || !data.item) {
|
|
764
|
+
throw new Error((data && data.error) || "Unable to save note.");
|
|
765
|
+
}
|
|
766
|
+
replaceNoteItem(data.item);
|
|
767
|
+
render();
|
|
768
|
+
} catch (error) {
|
|
769
|
+
window.alert(error && error.message ? error.message : "Unable to save note.");
|
|
770
|
+
setButtonsForNote(item, true);
|
|
771
|
+
} finally {
|
|
772
|
+
if (button) {
|
|
773
|
+
button.disabled = false;
|
|
774
|
+
setActionButtonLabel(button, originalText || "Save");
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function revertNote(item) {
|
|
780
|
+
resetNoteEdit(item);
|
|
781
|
+
renderDrawer();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function closeNoteDrawer() {
|
|
785
|
+
state.drawerOpen = false;
|
|
786
|
+
state.drawerItemId = "";
|
|
787
|
+
state.panelMode = "list";
|
|
788
|
+
const existing = document.getElementById("pinokio-note-sheet-root");
|
|
789
|
+
if (existing) {
|
|
790
|
+
existing.remove();
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function openNoteDrawer(item, tab) {
|
|
795
|
+
state.drawerOpen = true;
|
|
796
|
+
if (item && item.id) {
|
|
797
|
+
state.drawerItemId = item.id;
|
|
798
|
+
state.panelMode = "detail";
|
|
799
|
+
} else if (!state.drawerItemId && state.items[0] && state.items[0].id) {
|
|
800
|
+
state.drawerItemId = state.items[0].id;
|
|
801
|
+
state.panelMode = "list";
|
|
802
|
+
} else {
|
|
803
|
+
state.panelMode = "list";
|
|
804
|
+
}
|
|
805
|
+
if (tab === "list") {
|
|
806
|
+
state.panelMode = "list";
|
|
807
|
+
} else {
|
|
808
|
+
state.drawerTab = tab || state.drawerTab || "preview";
|
|
809
|
+
}
|
|
810
|
+
renderDrawer();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function createDrawerTab(label, tab) {
|
|
814
|
+
const button = createElement("button", `pinokio-note-tab${state.drawerTab === tab ? " is-active" : ""}`, label);
|
|
815
|
+
button.type = "button";
|
|
816
|
+
button.setAttribute("aria-selected", state.drawerTab === tab ? "true" : "false");
|
|
817
|
+
button.addEventListener("click", () => {
|
|
818
|
+
state.drawerTab = tab;
|
|
819
|
+
renderDrawer();
|
|
820
|
+
});
|
|
821
|
+
return button;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function renderDrawer() {
|
|
825
|
+
const previous = document.getElementById("pinokio-note-sheet-root");
|
|
826
|
+
if (previous) {
|
|
827
|
+
previous.remove();
|
|
828
|
+
}
|
|
829
|
+
if (!state.drawerOpen) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const items = Array.isArray(state.items) ? state.items : [];
|
|
833
|
+
let item = getItemById(state.drawerItemId);
|
|
834
|
+
if (!item && items[0]) {
|
|
835
|
+
item = items[0];
|
|
836
|
+
state.drawerItemId = item.id;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const backdrop = createElement("div", "pinokio-note-sheet-backdrop");
|
|
840
|
+
backdrop.id = "pinokio-note-sheet-root";
|
|
841
|
+
backdrop.addEventListener("click", (event) => {
|
|
842
|
+
if (event.target === backdrop) {
|
|
843
|
+
closeNoteDrawer();
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
const sheet = createElement("section", "pinokio-note-sheet");
|
|
848
|
+
sheet.setAttribute("role", "dialog");
|
|
849
|
+
sheet.setAttribute("aria-modal", "true");
|
|
850
|
+
sheet.setAttribute("aria-label", "Notes");
|
|
851
|
+
|
|
852
|
+
const header = createElement("div", "pinokio-note-sheet-header");
|
|
853
|
+
const titleBlock = createElement("div", "pinokio-note-sheet-title-block");
|
|
854
|
+
const dirty = state.panelMode === "detail" && item ? isNoteDirty(item) : false;
|
|
855
|
+
titleBlock.appendChild(createElement("div", "pinokio-note-sheet-kicker", `${items.length} ${items.length === 1 ? "note" : "notes"}`));
|
|
856
|
+
const titleRow = createElement("div", "pinokio-note-sheet-title-row");
|
|
857
|
+
titleRow.appendChild(createElement("div", "pinokio-note-sheet-title", state.panelMode === "detail" && item ? item.title || "Note" : "Notes"));
|
|
858
|
+
if (state.panelMode === "detail" && item) {
|
|
859
|
+
const unsaved = createElement("span", "pinokio-note-unsaved-badge", "Unsaved");
|
|
860
|
+
unsaved.dataset.pinokioNoteUnsaved = item.id;
|
|
861
|
+
unsaved.hidden = !dirty;
|
|
862
|
+
titleRow.appendChild(unsaved);
|
|
863
|
+
}
|
|
864
|
+
titleBlock.appendChild(titleRow);
|
|
865
|
+
titleBlock.appendChild(createElement("div", "pinokio-note-sheet-meta", state.panelMode === "detail" && item ? getNoteMetaParts(item).join(" / ") : activeCwd));
|
|
866
|
+
const headerActions = createElement("div", "pinokio-note-sheet-header-actions");
|
|
867
|
+
const close = createElement("button", "pinokio-note-sheet-close", "x");
|
|
868
|
+
close.type = "button";
|
|
869
|
+
close.setAttribute("aria-label", "Close note preview");
|
|
870
|
+
close.addEventListener("click", closeNoteDrawer);
|
|
871
|
+
header.appendChild(titleBlock);
|
|
872
|
+
headerActions.appendChild(close);
|
|
873
|
+
header.appendChild(headerActions);
|
|
874
|
+
|
|
875
|
+
const bodyShell = createElement("div", "pinokio-note-sheet-body");
|
|
876
|
+
|
|
877
|
+
if (state.panelMode !== "detail" || !item) {
|
|
878
|
+
const list = createElement("div", "pinokio-note-list");
|
|
879
|
+
if (!items.length) {
|
|
880
|
+
const empty = createElement("div", "pinokio-note-list-empty");
|
|
881
|
+
empty.appendChild(createElement("div", "pinokio-note-list-empty-title", "No notes yet"));
|
|
882
|
+
empty.appendChild(createElement("div", "pinokio-note-list-empty-copy", "Ask the agent to save useful work as a local note. Saved notes stay private until you publish them."));
|
|
883
|
+
empty.appendChild(createElement("div", "pinokio-note-list-empty-prompt", "Try: Save this as a note."));
|
|
884
|
+
list.appendChild(empty);
|
|
885
|
+
} else {
|
|
886
|
+
items.forEach((note) => {
|
|
887
|
+
const marker = state.unseen && state.unseen.get(note.id);
|
|
888
|
+
const row = createElement("button", `pinokio-note-list-item${note.id === state.highlightItemId ? " is-highlighted" : ""}${marker ? " has-update" : ""}`);
|
|
889
|
+
row.type = "button";
|
|
890
|
+
const rowTop = createElement("div", "pinokio-note-list-top");
|
|
891
|
+
rowTop.appendChild(createElement("div", "pinokio-note-list-title", note.title || "Note"));
|
|
892
|
+
if (marker) {
|
|
893
|
+
rowTop.appendChild(createElement("span", "pinokio-note-list-badge", marker === "updated" ? "Updated" : "New"));
|
|
894
|
+
}
|
|
895
|
+
row.appendChild(rowTop);
|
|
896
|
+
row.appendChild(createElement("div", "pinokio-note-list-meta", getNoteMetaParts(note).join(" / ")));
|
|
897
|
+
row.addEventListener("click", () => {
|
|
898
|
+
state.drawerItemId = note.id;
|
|
899
|
+
state.panelMode = "detail";
|
|
900
|
+
state.unseen.delete(note.id);
|
|
901
|
+
if (state.highlightItemId === note.id) {
|
|
902
|
+
state.highlightItemId = "";
|
|
903
|
+
}
|
|
904
|
+
renderDrawer();
|
|
905
|
+
render();
|
|
906
|
+
});
|
|
907
|
+
list.appendChild(row);
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
bodyShell.appendChild(list);
|
|
911
|
+
sheet.appendChild(header);
|
|
912
|
+
sheet.appendChild(bodyShell);
|
|
913
|
+
backdrop.appendChild(sheet);
|
|
914
|
+
getSheetHost().appendChild(backdrop);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const detail = createElement("div", "pinokio-note-detail");
|
|
919
|
+
const toolbar = createElement("div", "pinokio-note-sheet-toolbar");
|
|
920
|
+
const back = createActionButton("pinokio-note-button secondary compact", "Notes", "fa-solid fa-chevron-left");
|
|
921
|
+
back.type = "button";
|
|
922
|
+
back.addEventListener("click", () => {
|
|
923
|
+
state.panelMode = "list";
|
|
924
|
+
renderDrawer();
|
|
925
|
+
});
|
|
926
|
+
const tabs = createElement("div", "pinokio-note-tabs");
|
|
927
|
+
tabs.setAttribute("role", "tablist");
|
|
928
|
+
tabs.appendChild(createDrawerTab("Preview", "preview"));
|
|
929
|
+
tabs.appendChild(createDrawerTab("Markdown", "markdown"));
|
|
930
|
+
tabs.appendChild(createDrawerTab("Media", "media"));
|
|
931
|
+
const actions = createElement("div", "pinokio-note-sheet-actions");
|
|
932
|
+
const saveButton = createActionButton("pinokio-note-button", "Save", "fa-solid fa-floppy-disk");
|
|
933
|
+
saveButton.type = "button";
|
|
934
|
+
saveButton.dataset.pinokioNoteSave = item.id;
|
|
935
|
+
saveButton.hidden = !isNoteDirty(item);
|
|
936
|
+
saveButton.disabled = !isNoteDirty(item);
|
|
937
|
+
saveButton.addEventListener("click", () => {
|
|
938
|
+
void saveNote(item, saveButton);
|
|
939
|
+
});
|
|
940
|
+
const revertButton = createActionButton("pinokio-note-button secondary", "Revert", "fa-solid fa-arrow-rotate-left");
|
|
941
|
+
revertButton.type = "button";
|
|
942
|
+
revertButton.dataset.pinokioNoteRevert = item.id;
|
|
943
|
+
revertButton.hidden = !isNoteDirty(item);
|
|
944
|
+
revertButton.disabled = !isNoteDirty(item);
|
|
945
|
+
revertButton.addEventListener("click", () => {
|
|
946
|
+
revertNote(item);
|
|
947
|
+
});
|
|
948
|
+
actions.appendChild(saveButton);
|
|
949
|
+
actions.appendChild(revertButton);
|
|
950
|
+
const openButton = createActionButton("pinokio-note-button secondary", "File explorer", "fa-solid fa-folder-open");
|
|
951
|
+
openButton.type = "button";
|
|
952
|
+
openButton.addEventListener("click", () => {
|
|
953
|
+
void openNote(item, openButton);
|
|
954
|
+
});
|
|
955
|
+
actions.appendChild(openButton);
|
|
956
|
+
if (canPublishToRegistry(item)) {
|
|
957
|
+
const publishButton = createActionButton("pinokio-note-button", "Publish", "fa-solid fa-arrow-up-from-bracket");
|
|
958
|
+
publishButton.type = "button";
|
|
959
|
+
publishButton.dataset.pinokioNotePublish = item.id;
|
|
960
|
+
publishButton.disabled = isNoteDirty(item);
|
|
961
|
+
publishButton.title = isNoteDirty(item) ? "Save changes before publishing" : "";
|
|
962
|
+
publishButton.addEventListener("click", () => {
|
|
963
|
+
if (isNoteDirty(item)) {
|
|
964
|
+
window.alert("Save changes before publishing.");
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
void openRegistryNoteImport(item);
|
|
968
|
+
});
|
|
969
|
+
actions.appendChild(publishButton);
|
|
970
|
+
}
|
|
971
|
+
toolbar.appendChild(back);
|
|
972
|
+
toolbar.appendChild(tabs);
|
|
973
|
+
toolbar.appendChild(actions);
|
|
974
|
+
|
|
975
|
+
const body = createElement("div", "pinokio-note-detail-body");
|
|
976
|
+
if (state.drawerTab === "markdown") {
|
|
977
|
+
renderMarkdownEditor(body, item);
|
|
978
|
+
} else if (state.drawerTab === "media") {
|
|
979
|
+
renderMediaList(body, item);
|
|
980
|
+
} else {
|
|
981
|
+
renderMarkdownPreview(body, item);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
detail.appendChild(toolbar);
|
|
985
|
+
detail.appendChild(body);
|
|
986
|
+
bodyShell.appendChild(detail);
|
|
987
|
+
sheet.appendChild(header);
|
|
988
|
+
sheet.appendChild(bodyShell);
|
|
989
|
+
backdrop.appendChild(sheet);
|
|
990
|
+
getSheetHost().appendChild(backdrop);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
async function openRegistryNoteImport(item) {
|
|
994
|
+
if (!item || !item.id || !canPublishToRegistry(item)) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
try {
|
|
998
|
+
const url = new URL("/registry/draft-import/authorize-url", window.location.origin);
|
|
999
|
+
url.searchParams.set("draft", item.id);
|
|
1000
|
+
url.searchParams.set("_", String(Date.now()));
|
|
1001
|
+
const response = await fetch(url.toString(), {
|
|
1002
|
+
headers: {
|
|
1003
|
+
Accept: "application/json"
|
|
1004
|
+
}
|
|
1005
|
+
});
|
|
1006
|
+
const data = await response.json().catch(() => null);
|
|
1007
|
+
if (!response.ok || !data || !data.authorizeUrl) {
|
|
1008
|
+
throw new Error((data && data.error) || "Unable to start registry import.");
|
|
1009
|
+
}
|
|
1010
|
+
const openResponse = await fetch("/pinokio/open", {
|
|
1011
|
+
method: "POST",
|
|
1012
|
+
headers: {
|
|
1013
|
+
"Content-Type": "application/json"
|
|
1014
|
+
},
|
|
1015
|
+
body: JSON.stringify({
|
|
1016
|
+
url: data.authorizeUrl,
|
|
1017
|
+
surface: "browser"
|
|
1018
|
+
})
|
|
1019
|
+
});
|
|
1020
|
+
if (!openResponse.ok) {
|
|
1021
|
+
throw new Error("Unable to open registry.");
|
|
1022
|
+
}
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
window.alert(error && error.message ? error.message : "Unable to start registry import.");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function render() {
|
|
1029
|
+
renderFooter();
|
|
1030
|
+
if (state.drawerOpen) {
|
|
1031
|
+
renderDrawer();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function renderFooter() {
|
|
1036
|
+
const items = Array.isArray(state.items) ? state.items : [];
|
|
1037
|
+
const root = getRoot();
|
|
1038
|
+
root.innerHTML = "";
|
|
1039
|
+
const highlighted = getItemById(state.highlightItemId);
|
|
1040
|
+
const item = highlighted || getItemById(state.drawerItemId) || items[0];
|
|
1041
|
+
const unseenValues = state.unseen ? Array.from(state.unseen.values()).filter(Boolean) : [];
|
|
1042
|
+
const hasUnseen = unseenValues.length > 0;
|
|
1043
|
+
const allNew = hasUnseen && unseenValues.every((value) => value === "new");
|
|
1044
|
+
const allUpdated = hasUnseen && unseenValues.every((value) => value === "updated");
|
|
1045
|
+
const hasNewNote = hasUnseen && unseenValues.includes("new");
|
|
1046
|
+
const countLabel = `${items.length} note${items.length === 1 ? "" : "s"}`;
|
|
1047
|
+
const updateLabel = !hasUnseen
|
|
1048
|
+
? ""
|
|
1049
|
+
: (unseenValues.length === 1
|
|
1050
|
+
? (unseenValues[0] === "updated" ? "Updated" : "New")
|
|
1051
|
+
: (allNew ? `${unseenValues.length} new` : allUpdated ? `${unseenValues.length} updated` : `${unseenValues.length} updates`));
|
|
1052
|
+
const footer = createElement("button", `pinokio-note-footer${hasUnseen ? " has-update" : ""}${items.length ? "" : " is-empty"}`);
|
|
1053
|
+
footer.type = "button";
|
|
1054
|
+
footer.setAttribute("aria-label", items.length ? `Open ${items.length} note${items.length === 1 ? "" : "s"}` : "Open notes");
|
|
1055
|
+
const accent = createElement("div", "pinokio-note-footer-accent");
|
|
1056
|
+
const icon = createElement("div", "pinokio-note-footer-icon");
|
|
1057
|
+
icon.appendChild(createIcon(hasUnseen ? "fa-solid fa-circle-check" : "fa-solid fa-file-lines"));
|
|
1058
|
+
const copy = createElement("div", "pinokio-note-footer-copy");
|
|
1059
|
+
const top = createElement("div", "pinokio-note-footer-top");
|
|
1060
|
+
top.appendChild(createElement("span", "pinokio-note-footer-count", countLabel));
|
|
1061
|
+
if (updateLabel) {
|
|
1062
|
+
top.appendChild(createElement("span", `pinokio-note-footer-badge${hasNewNote ? " is-new" : " is-updated"}`, updateLabel));
|
|
1063
|
+
}
|
|
1064
|
+
copy.appendChild(top);
|
|
1065
|
+
copy.appendChild(createElement("div", "pinokio-note-footer-title", item && item.title ? item.title : "Notes"));
|
|
1066
|
+
copy.appendChild(createElement("div", "pinokio-note-footer-meta", item ? getNoteMetaParts(item).join(" / ") : "Ask the agent: \"Save this as a note.\""));
|
|
1067
|
+
const chevron = createElement("div", "pinokio-note-footer-chevron");
|
|
1068
|
+
chevron.appendChild(createElement("span", "", "Open notes"));
|
|
1069
|
+
chevron.appendChild(createIcon("fa-solid fa-chevron-right"));
|
|
1070
|
+
footer.appendChild(accent);
|
|
1071
|
+
footer.appendChild(icon);
|
|
1072
|
+
footer.appendChild(copy);
|
|
1073
|
+
footer.appendChild(chevron);
|
|
1074
|
+
footer.addEventListener("click", () => {
|
|
1075
|
+
if (state.unseen) {
|
|
1076
|
+
state.unseen.clear();
|
|
1077
|
+
}
|
|
1078
|
+
openNoteDrawer(null, "list");
|
|
1079
|
+
render();
|
|
1080
|
+
});
|
|
1081
|
+
root.appendChild(footer);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
async function refresh() {
|
|
1085
|
+
try {
|
|
1086
|
+
const response = await fetch(getApiUrl(), {
|
|
1087
|
+
headers: {
|
|
1088
|
+
Accept: "application/json"
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
if (!response.ok) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
const payload = await response.json();
|
|
1095
|
+
const items = payload && Array.isArray(payload.items) ? payload.items : [];
|
|
1096
|
+
const signature = noteSignature(items);
|
|
1097
|
+
if (signature === state.lastSignature && state.initialRefreshComplete) {
|
|
1098
|
+
state.initialRefreshComplete = true;
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const previousVersions = new Map((Array.isArray(state.items) ? state.items : [])
|
|
1102
|
+
.filter((item) => item && item.id)
|
|
1103
|
+
.map((item) => [item.id, item.revision || item.updatedAt || ""]));
|
|
1104
|
+
const changedItems = state.initialRefreshComplete
|
|
1105
|
+
? items.filter((item) => {
|
|
1106
|
+
if (!item || !item.id) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
const nextVersion = item.revision || item.updatedAt || "";
|
|
1110
|
+
return !previousVersions.has(item.id) || previousVersions.get(item.id) !== nextVersion;
|
|
1111
|
+
})
|
|
1112
|
+
: [];
|
|
1113
|
+
state.lastSignature = signature;
|
|
1114
|
+
state.items = items;
|
|
1115
|
+
const liveIds = new Set(items.map((item) => item && item.id).filter(Boolean));
|
|
1116
|
+
Array.from(state.unseen.keys()).forEach((id) => {
|
|
1117
|
+
if (!liveIds.has(id)) {
|
|
1118
|
+
state.unseen.delete(id);
|
|
1119
|
+
}
|
|
1120
|
+
});
|
|
1121
|
+
changedItems.forEach((item) => {
|
|
1122
|
+
state.unseen.set(item.id, previousVersions.has(item.id) ? "updated" : "new");
|
|
1123
|
+
notifyNoteReady(item);
|
|
1124
|
+
});
|
|
1125
|
+
if (changedItems[0]) {
|
|
1126
|
+
state.highlightItemId = changedItems[0].id;
|
|
1127
|
+
}
|
|
1128
|
+
render();
|
|
1129
|
+
state.initialRefreshComplete = true;
|
|
1130
|
+
} catch (_) {
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function start() {
|
|
1135
|
+
if (!document.body) {
|
|
1136
|
+
window.addEventListener("DOMContentLoaded", start, { once: true });
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
void refresh();
|
|
1140
|
+
window.setInterval(refresh, 5000);
|
|
1141
|
+
document.addEventListener("visibilitychange", () => {
|
|
1142
|
+
if (!document.hidden) {
|
|
1143
|
+
void refresh();
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
start();
|
|
1149
|
+
})();
|