pinokiod 3.86.0 → 3.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/Dockerfile +61 -0
  2. package/docker-entrypoint.sh +75 -0
  3. package/kernel/api/hf/index.js +1 -1
  4. package/kernel/api/index.js +1 -1
  5. package/kernel/api/shell/index.js +6 -0
  6. package/kernel/api/terminal/index.js +166 -0
  7. package/kernel/bin/conda.js +3 -2
  8. package/kernel/bin/index.js +53 -2
  9. package/kernel/bin/setup.js +32 -0
  10. package/kernel/bin/vs.js +11 -2
  11. package/kernel/index.js +42 -2
  12. package/kernel/info.js +36 -0
  13. package/kernel/peer.js +42 -15
  14. package/kernel/router/index.js +23 -15
  15. package/kernel/router/localhost_static_router.js +0 -3
  16. package/kernel/router/pinokio_domain_router.js +333 -0
  17. package/kernel/shells.js +21 -1
  18. package/kernel/util.js +2 -2
  19. package/package.json +2 -1
  20. package/script/install-mode.js +33 -0
  21. package/script/pinokio.json +7 -0
  22. package/server/index.js +513 -173
  23. package/server/public/Socket.js +48 -0
  24. package/server/public/common.js +1441 -276
  25. package/server/public/fseditor.js +71 -12
  26. package/server/public/install.js +1 -1
  27. package/server/public/layout.js +740 -0
  28. package/server/public/modalinput.js +0 -1
  29. package/server/public/style.css +97 -105
  30. package/server/public/tab-idle-notifier.js +629 -0
  31. package/server/public/terminal_input_tracker.js +63 -0
  32. package/server/public/urldropdown.css +319 -53
  33. package/server/public/urldropdown.js +615 -159
  34. package/server/public/window_storage.js +97 -28
  35. package/server/socket.js +40 -9
  36. package/server/views/500.ejs +2 -2
  37. package/server/views/app.ejs +3136 -1367
  38. package/server/views/bookmarklet.ejs +1 -1
  39. package/server/views/bootstrap.ejs +1 -1
  40. package/server/views/columns.ejs +2 -13
  41. package/server/views/connect.ejs +3 -4
  42. package/server/views/container.ejs +1 -2
  43. package/server/views/d.ejs +223 -53
  44. package/server/views/editor.ejs +1 -1
  45. package/server/views/file_explorer.ejs +1 -1
  46. package/server/views/index.ejs +12 -11
  47. package/server/views/index2.ejs +4 -4
  48. package/server/views/init/index.ejs +4 -5
  49. package/server/views/install.ejs +1 -1
  50. package/server/views/layout.ejs +105 -0
  51. package/server/views/net.ejs +39 -7
  52. package/server/views/network.ejs +20 -6
  53. package/server/views/network2.ejs +1 -1
  54. package/server/views/old_network.ejs +2 -2
  55. package/server/views/partials/dynamic.ejs +3 -5
  56. package/server/views/partials/menu.ejs +3 -5
  57. package/server/views/partials/running.ejs +1 -1
  58. package/server/views/pro.ejs +1 -1
  59. package/server/views/prototype/index.ejs +1 -1
  60. package/server/views/review.ejs +11 -23
  61. package/server/views/rows.ejs +2 -13
  62. package/server/views/screenshots.ejs +293 -138
  63. package/server/views/settings.ejs +3 -4
  64. package/server/views/setup.ejs +1 -2
  65. package/server/views/shell.ejs +277 -26
  66. package/server/views/terminal.ejs +322 -49
  67. package/server/views/tools.ejs +448 -4
@@ -0,0 +1,629 @@
1
+ (() => {
2
+ if (window.PinokioIdleNotifierInitialized) {
3
+ return;
4
+ }
5
+ window.PinokioIdleNotifierInitialized = true;
6
+
7
+ const PUSH_ENDPOINT = '/push';
8
+ const TAB_UPDATED_SELECTOR = '.tab-updated';
9
+ const CAN_NOTIFY_ATTR = 'data-can-notify';
10
+ const FRAME_LINK_SELECTOR = '.frame-link';
11
+ const LIVE_CLASS = 'is-live';
12
+ const MAX_MESSAGE_PREVIEW = 140;
13
+
14
+ const tabStates = new Map();
15
+ const observedIndicators = new WeakSet();
16
+ const containerObservers = new WeakMap();
17
+ const TAB_MAIN_CLASS = 'tab-main';
18
+ const TAB_DETAILS_CLASS = 'tab-details';
19
+ const PREF_STORAGE_KEY = 'pinokio:idle-prefs';
20
+ const notifyPreferences = new Map();
21
+
22
+ const hydratePreferences = () => {
23
+ try {
24
+ const raw = localStorage.getItem(PREF_STORAGE_KEY);
25
+ if (!raw) {
26
+ return;
27
+ }
28
+ const parsed = JSON.parse(raw);
29
+ if (!parsed || typeof parsed !== 'object') {
30
+ return;
31
+ }
32
+ Object.entries(parsed).forEach(([key, value]) => {
33
+ notifyPreferences.set(key, Boolean(value));
34
+ });
35
+ } catch (error) {
36
+ log('Failed to hydrate notification preferences', error);
37
+ }
38
+ };
39
+
40
+ const persistPreferences = () => {
41
+ try {
42
+ const serialisable = {};
43
+ notifyPreferences.forEach((value, key) => {
44
+ serialisable[key] = value;
45
+ });
46
+ if (Object.keys(serialisable).length === 0) {
47
+ localStorage.removeItem(PREF_STORAGE_KEY);
48
+ } else {
49
+ localStorage.setItem(PREF_STORAGE_KEY, JSON.stringify(serialisable));
50
+ }
51
+ } catch (error) {
52
+ log('Failed to persist notification preferences', error);
53
+ }
54
+ };
55
+
56
+ const getPreference = (frameName) => {
57
+ if (!frameName) {
58
+ return true;
59
+ }
60
+ if (notifyPreferences.has(frameName)) {
61
+ return Boolean(notifyPreferences.get(frameName));
62
+ }
63
+ return true;
64
+ };
65
+
66
+ const setPreference = (frameName, enabled) => {
67
+ if (!frameName) {
68
+ return;
69
+ }
70
+ if (enabled) {
71
+ notifyPreferences.delete(frameName);
72
+ } else {
73
+ notifyPreferences.set(frameName, false);
74
+ }
75
+ persistPreferences();
76
+ };
77
+
78
+ const DEBUG_STORAGE_KEY = 'pinokio:idle-debug';
79
+ const readDebugFlag = () => {
80
+ try {
81
+ return localStorage.getItem(DEBUG_STORAGE_KEY) === '1';
82
+ } catch (_) {
83
+ return false;
84
+ }
85
+ };
86
+
87
+ let DEBUG = readDebugFlag();
88
+ const log = (...args) => {
89
+ if (DEBUG) {
90
+ console.debug('[PinokioIdleNotifier]', ...args);
91
+ }
92
+ };
93
+
94
+ const updateDebugFlag = () => {
95
+ DEBUG = readDebugFlag();
96
+ };
97
+
98
+ const aggregateDebounce = (fn, delay = 100) => {
99
+ let timer = null;
100
+ return () => {
101
+ if (timer) {
102
+ clearTimeout(timer);
103
+ }
104
+ timer = setTimeout(() => {
105
+ timer = null;
106
+ fn();
107
+ }, delay);
108
+ };
109
+ };
110
+
111
+ hydratePreferences();
112
+
113
+ let ensureIndicatorObservers;
114
+
115
+ const escapeForSelector = (value) => {
116
+ if (typeof value !== 'string') {
117
+ return '';
118
+ }
119
+ if (window.CSS && typeof window.CSS.escape === 'function') {
120
+ return window.CSS.escape(value);
121
+ }
122
+ return value.replace(/([\0-\x1F\x7F"\\#.:;?+*~\[\]\s])/g, '\\$1');
123
+ };
124
+
125
+ const getOrCreateState = (frameName) => {
126
+ if (!frameName) {
127
+ return null;
128
+ }
129
+ let state = tabStates.get(frameName);
130
+ if (!state) {
131
+ state = {
132
+ hasRecentInput: false,
133
+ awaitingLive: false,
134
+ awaitingIdle: false,
135
+ isLive: false,
136
+ notified: false,
137
+ lastInput: '',
138
+ lastLiveTimestamp: 0,
139
+ notifyEnabled: getPreference(frameName),
140
+ };
141
+ tabStates.set(frameName, state);
142
+ log('Created state for frame', frameName);
143
+ } else if (typeof state.notifyEnabled === 'undefined') {
144
+ state.notifyEnabled = getPreference(frameName);
145
+ }
146
+ return state;
147
+ };
148
+
149
+ const sanitisePreview = (value) => {
150
+ if (typeof value !== 'string') {
151
+ return '';
152
+ }
153
+ const trimmed = value.trim();
154
+ if (!trimmed) {
155
+ return '';
156
+ }
157
+ if (trimmed.length <= MAX_MESSAGE_PREVIEW) {
158
+ return trimmed;
159
+ }
160
+ return `${trimmed.slice(0, MAX_MESSAGE_PREVIEW)}…`;
161
+ };
162
+
163
+ const extractFrameNameFromLink = (link) => {
164
+ if (!link) {
165
+ return null;
166
+ }
167
+ const attrCandidates = [
168
+ 'target',
169
+ 'data-target-full',
170
+ 'data-shell',
171
+ 'data-script',
172
+ ];
173
+ for (const attr of attrCandidates) {
174
+ const value = link.getAttribute(attr);
175
+ if (typeof value === 'string' && value.length > 0) {
176
+ return value;
177
+ }
178
+ }
179
+ return null;
180
+ };
181
+
182
+ const findLinkByFrameName = (frameName) => {
183
+ if (!frameName) {
184
+ return null;
185
+ }
186
+ const escaped = escapeForSelector(frameName);
187
+ if (!escaped) {
188
+ return null;
189
+ }
190
+ let link = document.querySelector(`${FRAME_LINK_SELECTOR}[target="${escaped}"]`);
191
+ if (link) {
192
+ return link;
193
+ }
194
+ link = document.querySelector(`${FRAME_LINK_SELECTOR}[data-target-full="${escaped}"]`);
195
+ if (link) {
196
+ return link;
197
+ }
198
+ return null;
199
+ };
200
+
201
+ const findIndicatorForFrame = (frameName) => {
202
+ const link = findLinkByFrameName(frameName);
203
+ if (!link) {
204
+ return null;
205
+ }
206
+ return link.querySelector(TAB_UPDATED_SELECTOR);
207
+ };
208
+
209
+ const TOGGLE_CLASS = 'tab-notify-toggle';
210
+ let toggleStylesInjected = false;
211
+
212
+ const injectToggleStyles = () => {
213
+ if (toggleStylesInjected) {
214
+ return;
215
+ }
216
+ const style = document.createElement('style');
217
+ style.textContent = `
218
+ .${TOGGLE_CLASS} {
219
+ display: inline-flex;
220
+ align-items: center;
221
+ justify-content: center;
222
+ cursor: pointer;
223
+ font-size: 0.85em;
224
+ color: inherit;
225
+ user-select: none;
226
+ }
227
+ .${TOGGLE_CLASS}[data-enabled="false"] {
228
+ opacity: 0.45;
229
+ }
230
+ .frame-link .${TOGGLE_CLASS} i {
231
+ font-size: 12px !important;
232
+ pointer-events: none;
233
+ }
234
+ .frame-link .${TOGGLE_CLASS}:focus-visible {
235
+ outline: 2px solid var(--pinokio-focus-color, #4c9afe);
236
+ outline-offset: 2px;
237
+ }
238
+ `; // style injection for notify toggle
239
+ document.head.appendChild(style);
240
+ toggleStylesInjected = true;
241
+ };
242
+
243
+ const syncToggleAppearance = (toggle, enabled) => {
244
+ if (!toggle) {
245
+ return;
246
+ }
247
+ const icon = toggle.querySelector('i') || toggle;
248
+ icon.classList.add('fa-solid');
249
+ icon.classList.toggle('fa-bell', enabled);
250
+ icon.classList.toggle('fa-bell-slash', !enabled);
251
+ toggle.dataset.enabled = enabled ? 'true' : 'false';
252
+ toggle.setAttribute('aria-pressed', enabled ? 'true' : 'false');
253
+ toggle.setAttribute('title', enabled ? 'Desktop notifications enabled' : 'Desktop notifications disabled');
254
+ toggle.setAttribute('aria-label', enabled ? 'Disable desktop notifications for this tab' : 'Enable desktop notifications for this tab');
255
+ };
256
+
257
+ const positionToggleWithinTab = (tab, toggle) => {
258
+ if (!tab || !toggle) {
259
+ return;
260
+ }
261
+ const container = tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab;
262
+
263
+ if (toggle.parentNode !== container) {
264
+ container.insertBefore(toggle, container.firstChild);
265
+ }
266
+
267
+ const iconHost = Array.from(container.children).find((node) => {
268
+ const tag = node.tagName?.toLowerCase();
269
+ if (tag === 'img') {
270
+ return true;
271
+ }
272
+ if (tag === 'i' && node !== toggle && !node.classList.contains(TOGGLE_CLASS)) {
273
+ return true;
274
+ }
275
+ return false;
276
+ });
277
+
278
+ if (iconHost) {
279
+ if (iconHost.nextSibling !== toggle) {
280
+ iconHost.parentNode.insertBefore(toggle, iconHost.nextSibling);
281
+ }
282
+ } else {
283
+ const details = container.querySelector(`.${TAB_DETAILS_CLASS}`);
284
+ if (details && details !== toggle) {
285
+ if (details.previousSibling !== toggle) {
286
+ container.insertBefore(toggle, details);
287
+ }
288
+ } else if (container.firstChild !== toggle) {
289
+ container.insertBefore(toggle, container.firstChild);
290
+ }
291
+ }
292
+ };
293
+
294
+ const installToggleForLink = (link, frameName, state) => {
295
+ if (!(link instanceof HTMLElement)) {
296
+ return;
297
+ }
298
+ const tab = link.querySelector('.tab');
299
+ if (!tab) {
300
+ return;
301
+ }
302
+ let toggle = tab.querySelector(`.${TOGGLE_CLASS}`);
303
+ if (!toggle) {
304
+ injectToggleStyles();
305
+ toggle = document.createElement('span');
306
+ toggle.className = TOGGLE_CLASS;
307
+ toggle.setAttribute('role', 'button');
308
+ toggle.setAttribute('tabindex', '0');
309
+ const icon = document.createElement('i');
310
+ toggle.appendChild(icon);
311
+ (tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab).appendChild(toggle);
312
+
313
+ const activate = () => {
314
+ const current = getOrCreateState(frameName);
315
+ if (!current) {
316
+ return;
317
+ }
318
+ const next = !current.notifyEnabled;
319
+ current.notifyEnabled = next;
320
+ setPreference(frameName, next);
321
+ syncToggleAppearance(toggle, next);
322
+ log('Notification preference changed', { frameName, enabled: next });
323
+ };
324
+
325
+ toggle.addEventListener('click', (event) => {
326
+ event.preventDefault();
327
+ event.stopPropagation();
328
+ activate();
329
+ });
330
+
331
+ toggle.addEventListener('keydown', (event) => {
332
+ if (event.key === 'Enter' || event.key === ' ') {
333
+ event.preventDefault();
334
+ activate();
335
+ }
336
+ });
337
+ }
338
+ positionToggleWithinTab(tab, toggle);
339
+ const container = tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab;
340
+ if (!containerObservers.has(container)) {
341
+ const observer = new MutationObserver(() => {
342
+ positionToggleWithinTab(tab, toggle);
343
+ });
344
+ observer.observe(container, { childList: true });
345
+ containerObservers.set(container, observer);
346
+ }
347
+ syncToggleAppearance(toggle, state.notifyEnabled);
348
+ };
349
+
350
+ const detachToggleForLink = (link) => {
351
+ if (!(link instanceof HTMLElement)) {
352
+ return;
353
+ }
354
+ const tab = link.querySelector('.tab');
355
+ if (!tab) {
356
+ return;
357
+ }
358
+ const toggle = tab.querySelector(`.${TOGGLE_CLASS}`);
359
+ if (toggle && toggle.parentNode) {
360
+ toggle.parentNode.removeChild(toggle);
361
+ }
362
+ const container = tab.querySelector(`.${TAB_MAIN_CLASS}`) || tab;
363
+ const observer = containerObservers.get(container);
364
+ if (observer) {
365
+ observer.disconnect();
366
+ containerObservers.delete(container);
367
+ }
368
+ };
369
+
370
+ const ensureTabAccessories = aggregateDebounce(() => {
371
+ document.querySelectorAll(FRAME_LINK_SELECTOR).forEach((link) => {
372
+ if (!(link instanceof HTMLElement)) {
373
+ return;
374
+ }
375
+ const frameName = extractFrameNameFromLink(link);
376
+ if (!frameName) {
377
+ detachToggleForLink(link);
378
+ return;
379
+ }
380
+ const canNotify = link.getAttribute(CAN_NOTIFY_ATTR);
381
+ if (canNotify !== 'true') {
382
+ detachToggleForLink(link);
383
+ return;
384
+ }
385
+ const state = getOrCreateState(frameName);
386
+ if (!state) {
387
+ return;
388
+ }
389
+ installToggleForLink(link, frameName, state);
390
+ });
391
+ });
392
+
393
+ ensureIndicatorObservers = aggregateDebounce(() => {
394
+ document.querySelectorAll(TAB_UPDATED_SELECTOR).forEach((node) => {
395
+ if (!(node instanceof HTMLElement)) {
396
+ return;
397
+ }
398
+ if (observedIndicators.has(node)) {
399
+ return;
400
+ }
401
+ indicatorObserver.observe(node, { attributes: true, attributeFilter: ['class'] });
402
+ observedIndicators.add(node);
403
+ log('Observing indicator', node);
404
+ });
405
+ ensureTabAccessories();
406
+ });
407
+
408
+ const resolveFrameName = (frameHint, sourceWindow) => {
409
+ if (typeof frameHint === 'string' && frameHint.length > 0) {
410
+ return frameHint;
411
+ }
412
+ if (!sourceWindow) {
413
+ return null;
414
+ }
415
+ const frames = document.querySelectorAll('iframe');
416
+ for (const frame of frames) {
417
+ if (frame.contentWindow === sourceWindow) {
418
+ return frame.name || frame.dataset?.src || null;
419
+ }
420
+ }
421
+ return null;
422
+ };
423
+
424
+ const sendNotification = (link, state) => {
425
+ if (!link || !state) {
426
+ return;
427
+ }
428
+ const tab = link.querySelector('.tab');
429
+ const title = tab ? tab.textContent.trim() : 'Tab activity';
430
+ const subtitle = title || 'Pinokio';
431
+ const message = state.lastInput ? `Last input: ${state.lastInput}` : 'Tab is now idle.';
432
+
433
+ const payload = {
434
+ title: 'Pinokio',
435
+ subtitle,
436
+ message,
437
+ sound: true,
438
+ };
439
+
440
+ try {
441
+ log('Sending notification payload', payload);
442
+ fetch(PUSH_ENDPOINT, {
443
+ method: 'POST',
444
+ headers: {
445
+ 'Content-Type': 'application/json'
446
+ },
447
+ credentials: 'include',
448
+ body: JSON.stringify(payload),
449
+ }).catch(() => {});
450
+ } catch (_) {
451
+ // Ignore failures – desktop notifications are best-effort.
452
+ }
453
+ };
454
+
455
+ const shouldNotify = (link) => {
456
+ if (!link) {
457
+ return false;
458
+ }
459
+ return true;
460
+ };
461
+
462
+ const handleIndicatorChange = (indicator) => {
463
+ const link = indicator.closest(FRAME_LINK_SELECTOR);
464
+ if (!link) {
465
+ return;
466
+ }
467
+ const frameName = extractFrameNameFromLink(link);
468
+ if (!frameName) {
469
+ return;
470
+ }
471
+ const state = getOrCreateState(frameName);
472
+ if (!state) {
473
+ return;
474
+ }
475
+ const isLive = indicator.classList.contains(LIVE_CLASS);
476
+ state.isLive = isLive;
477
+ log('Indicator change', { frameName, isLive, awaitingLive: state.awaitingLive, awaitingIdle: state.awaitingIdle });
478
+
479
+ if (isLive) {
480
+ state.lastLiveTimestamp = Date.now();
481
+ if (state.awaitingLive && state.hasRecentInput) {
482
+ state.awaitingLive = false;
483
+ state.awaitingIdle = true;
484
+ }
485
+ return;
486
+ }
487
+
488
+ if (state.awaitingIdle && state.hasRecentInput && !state.notified) {
489
+ if (!state.notifyEnabled) {
490
+ log('Notifications disabled for frame', frameName);
491
+ } else if (shouldNotify(link)) {
492
+ sendNotification(link, state);
493
+ state.notified = true;
494
+ log('Idle notification dispatched', frameName);
495
+ }
496
+ }
497
+
498
+ state.hasRecentInput = false;
499
+ state.awaitingIdle = false;
500
+ state.awaitingLive = false;
501
+ };
502
+
503
+ const indicatorObserver = new MutationObserver((mutations) => {
504
+ for (const mutation of mutations) {
505
+ if (mutation.type === 'attributes' && mutation.target instanceof HTMLElement) {
506
+ handleIndicatorChange(mutation.target);
507
+ }
508
+ }
509
+ });
510
+
511
+ const treeObserver = new MutationObserver((mutations) => {
512
+ let shouldRescan = false;
513
+ for (const mutation of mutations) {
514
+ for (const node of mutation.addedNodes) {
515
+ if (!(node instanceof HTMLElement)) {
516
+ continue;
517
+ }
518
+ if (node.matches(TAB_UPDATED_SELECTOR)) {
519
+ indicatorObserver.observe(node, { attributes: true, attributeFilter: ['class'] });
520
+ observedIndicators.add(node);
521
+ log('Observed newly added indicator', node);
522
+ shouldRescan = true;
523
+ } else if (node.classList && node.classList.contains('frame-link')) {
524
+ shouldRescan = true;
525
+ } else if (node.querySelector) {
526
+ if (node.querySelector(TAB_UPDATED_SELECTOR) || node.querySelector(FRAME_LINK_SELECTOR)) {
527
+ shouldRescan = true;
528
+ }
529
+ }
530
+ }
531
+ }
532
+ if (shouldRescan) {
533
+ ensureIndicatorObservers();
534
+ }
535
+ });
536
+
537
+ const handleTerminalInput = (event) => {
538
+ const data = event.data;
539
+ if (!data || typeof data !== 'object') {
540
+ return;
541
+ }
542
+ const hasContent = typeof data.hasContent === 'boolean'
543
+ ? data.hasContent
544
+ : Boolean(data.line && data.line.length > 0);
545
+ if (!hasContent) {
546
+ return;
547
+ }
548
+ const frameName = resolveFrameName(data.frame, event.source);
549
+ if (!frameName) {
550
+ return;
551
+ }
552
+ const state = getOrCreateState(frameName);
553
+ if (!state) {
554
+ return;
555
+ }
556
+ state.hasRecentInput = true;
557
+ state.awaitingLive = true;
558
+ state.awaitingIdle = false;
559
+ state.notified = false;
560
+ state.lastInput = sanitisePreview(data.line || '');
561
+ log('Terminal input captured', { frameName, line: data.line, state: { ...state } });
562
+
563
+ const indicator = findIndicatorForFrame(frameName);
564
+ if (indicator && indicator.classList.contains(LIVE_CLASS)) {
565
+ state.awaitingLive = false;
566
+ state.awaitingIdle = true;
567
+ log('Indicator already live when input arrived', frameName);
568
+ }
569
+ };
570
+
571
+ const handleMessageEvent = (event) => {
572
+ if (!event || typeof event.data !== 'object' || event.data === null) {
573
+ return;
574
+ }
575
+ if (event.data.type === 'terminal-input') {
576
+ handleTerminalInput(event);
577
+ }
578
+ };
579
+
580
+ const initialise = () => {
581
+ log('Initialising idle notifier');
582
+ ensureIndicatorObservers();
583
+ ensureTabAccessories();
584
+ treeObserver.observe(document.body, { childList: true, subtree: true });
585
+ window.addEventListener('message', handleMessageEvent, true);
586
+ window.addEventListener('storage', (event) => {
587
+ if (event.key === DEBUG_STORAGE_KEY) {
588
+ updateDebugFlag();
589
+ log('Debug flag updated via storage event');
590
+ } else if (event.key === PREF_STORAGE_KEY) {
591
+ notifyPreferences.clear();
592
+ hydratePreferences();
593
+ tabStates.forEach((state, frame) => {
594
+ state.notifyEnabled = getPreference(frame);
595
+ });
596
+ ensureTabAccessories();
597
+ log('Notification preferences refreshed from storage event');
598
+ }
599
+ });
600
+ };
601
+
602
+ if (document.readyState === 'loading') {
603
+ document.addEventListener('DOMContentLoaded', initialise, { once: true });
604
+ } else {
605
+ initialise();
606
+ }
607
+ window.PinokioIdleNotifier = {
608
+ enableDebug() {
609
+ try {
610
+ localStorage.setItem(DEBUG_STORAGE_KEY, '1');
611
+ } catch (_) {}
612
+ updateDebugFlag();
613
+ log('Debug enabled');
614
+ },
615
+ disableDebug() {
616
+ try {
617
+ localStorage.removeItem(DEBUG_STORAGE_KEY);
618
+ } catch (_) {}
619
+ updateDebugFlag();
620
+ log('Debug disabled');
621
+ },
622
+ refreshDebug: updateDebugFlag,
623
+ forceScan() {
624
+ ensureIndicatorObservers();
625
+ ensureTabAccessories();
626
+ log('Force scan triggered');
627
+ }
628
+ };
629
+ })();
@@ -0,0 +1,63 @@
1
+ (function (global) {
2
+ const DEFAULT_LIMIT = 200
3
+
4
+ class TerminalInputTracker {
5
+ constructor(options) {
6
+ this.limit = (options && Number.isFinite(options.limit)) ? options.limit : DEFAULT_LIMIT
7
+ this.getFrameName = options && typeof options.getFrameName === "function"
8
+ ? options.getFrameName
9
+ : () => (global.name || null)
10
+ this.getWindow = options && typeof options.getWindow === "function"
11
+ ? options.getWindow
12
+ : () => global
13
+ this.buffer = ""
14
+ }
15
+
16
+ reset() {
17
+ this.buffer = ""
18
+ }
19
+
20
+ handleBackspace() {
21
+ if (this.buffer.length > 0) {
22
+ this.buffer = this.buffer.slice(0, -1)
23
+ }
24
+ }
25
+
26
+ capture(text) {
27
+ if (typeof text !== "string" || text.length === 0) {
28
+ return
29
+ }
30
+ const normalized = text.replace(/\r/g, "\n")
31
+ const segments = normalized.split("\n")
32
+ for (let i = 0; i < segments.length; i++) {
33
+ const segment = segments[i]
34
+ const isLast = (i === segments.length - 1)
35
+ if (!isLast) {
36
+ const line = this.buffer + segment
37
+ this.buffer = ""
38
+ this.submit(line)
39
+ } else {
40
+ this.buffer += segment
41
+ }
42
+ }
43
+ }
44
+
45
+ submit(line) {
46
+ const win = this.getWindow()
47
+ if (!win || !win.parent || typeof win.parent.postMessage !== "function") {
48
+ return
49
+ }
50
+ const safeLine = (line || "").replace(/[\x00-\x1F\x7F]/g, "")
51
+ const preview = safeLine.trim()
52
+ const truncated = preview.length > this.limit ? `${preview.slice(0, this.limit)}...` : preview
53
+ win.parent.postMessage({
54
+ type: "terminal-input",
55
+ frame: this.getFrameName(),
56
+ line: truncated,
57
+ hasContent: truncated.length > 0
58
+ }, "*")
59
+ }
60
+ }
61
+
62
+ global.TerminalInputTracker = TerminalInputTracker
63
+ })(typeof window !== "undefined" ? window : this)