pinokiod 7.2.7 → 7.2.8

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.
@@ -0,0 +1,812 @@
1
+ <!doctype html>
2
+ <%
3
+ const catalog = workspaceCatalog || { root: "", sort: "most_used", running: [], offline: [], items: [], counts: {} };
4
+ const pageAgent = (typeof agent !== "undefined" && agent) ? agent : "browser";
5
+ const pageTheme = (typeof theme !== "undefined" && theme) ? theme : "";
6
+ const running = Array.isArray(catalog.running) ? catalog.running : [];
7
+ const offline = Array.isArray(catalog.offline) ? catalog.offline : [];
8
+ const items = Array.isArray(catalog.items) ? catalog.items : [];
9
+ const currentSort = typeof catalog.sort === "string" ? catalog.sort : "most_used";
10
+ const plural = (count, label) => `${count} ${label}${count === 1 ? "" : "s"}`;
11
+ const dateLabel = (value) => {
12
+ if (!value) return "";
13
+ const date = new Date(value);
14
+ if (Number.isNaN(date.getTime())) return "";
15
+ return date.toLocaleString([], { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
16
+ };
17
+ const bytes = (value) => {
18
+ const size = Number(value || 0);
19
+ if (!Number.isFinite(size) || size <= 0) return "";
20
+ if (size < 1024) return `${size} B`;
21
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
22
+ return `${(size / 1024 / 1024).toFixed(1)} MB`;
23
+ };
24
+ const safeJson = JSON.stringify(items).replace(/</g, "\\u003c");
25
+ %>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <meta name="viewport" content="width=device-width, initial-scale=1">
30
+ <title>Workspaces</title>
31
+ <link href="/css/fontawesome.min.css" rel="stylesheet">
32
+ <link href="/css/solid.min.css" rel="stylesheet">
33
+ <link href="/css/brands.min.css" rel="stylesheet">
34
+ <link href="/noty.css" rel="stylesheet">
35
+ <link href="/tom-select.css" rel="stylesheet">
36
+ <link href="/style.css" rel="stylesheet">
37
+ <% if (pageAgent === "electron") { %>
38
+ <link href="/electron.css" rel="stylesheet"/>
39
+ <% } %>
40
+ <style>
41
+ body.workspaces-page {
42
+ --home-page-accent: var(--pinokio-chrome-accent-fg-light);
43
+ --home-page-nav-bg: color-mix(in srgb, #f5f6f8 96%, white);
44
+ --home-page-nav-border: rgba(15, 23, 42, 0.12);
45
+ --task-panel: rgba(255, 255, 255, 0.98);
46
+ --task-border: rgba(15, 23, 42, 0.12);
47
+ --task-text: #101828;
48
+ --task-muted: #5f6b7a;
49
+ --task-soft: #eef2f7;
50
+ --task-accent: var(--pinokio-chrome-accent-fg-light);
51
+ background:
52
+ radial-gradient(circle at top left, color-mix(in srgb, var(--home-page-accent) 10%, transparent), transparent 24rem),
53
+ linear-gradient(180deg, #fafbfd 0%, #f3f5f8 100%);
54
+ background-attachment: fixed;
55
+ background-repeat: no-repeat;
56
+ isolation: isolate;
57
+ min-height: 100vh;
58
+ }
59
+ body.dark.workspaces-page {
60
+ --home-page-accent: var(--pinokio-chrome-accent-fg-dark);
61
+ --home-page-nav-bg: color-mix(in srgb, #090b10 94%, black);
62
+ --home-page-nav-border: rgba(255, 255, 255, 0.08);
63
+ --task-panel: rgba(13, 17, 24, 0.96);
64
+ --task-border: rgba(255, 255, 255, 0.08);
65
+ --task-text: #f5f7fb;
66
+ --task-muted: #9aa4b2;
67
+ --task-soft: rgba(255, 255, 255, 0.04);
68
+ --task-accent: var(--pinokio-chrome-accent-fg-dark);
69
+ background:
70
+ radial-gradient(circle at top left, color-mix(in srgb, var(--home-page-accent) 16%, transparent), transparent 24rem),
71
+ linear-gradient(180deg, #0a0c10 0%, #07090d 100%);
72
+ background-attachment: fixed;
73
+ background-repeat: no-repeat;
74
+ }
75
+ body.workspaces-page > header.navheader {
76
+ background: transparent;
77
+ backdrop-filter: none;
78
+ box-shadow: none;
79
+ isolation: isolate;
80
+ }
81
+ body.workspaces-page main {
82
+ display: flex;
83
+ align-items: flex-start;
84
+ }
85
+ .workspaces-page .container {
86
+ flex-grow: 1;
87
+ min-width: 0;
88
+ }
89
+ .workspaces-page .workspace-search-form {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 8px;
93
+ margin: 0;
94
+ padding: 10px;
95
+ border-bottom: 0;
96
+ }
97
+ .workspaces-page .workspace-search-form input[type='search'].flexible {
98
+ height: 30px;
99
+ min-height: 30px;
100
+ box-sizing: border-box;
101
+ padding: 0 10px;
102
+ border-radius: 5px;
103
+ font-size: 12px;
104
+ font-weight: 400;
105
+ line-height: 1;
106
+ }
107
+ .home-search-form .home-apps-sort {
108
+ --home-sort-bg: rgba(0, 0, 0, 0.05);
109
+ --home-sort-border: rgba(0, 0, 0, 0.08);
110
+ --home-sort-hover-border: rgba(0, 0, 0, 0.12);
111
+ --home-sort-hover-bg: rgba(0, 0, 0, 0.08);
112
+ --home-sort-focus-border: rgba(0, 0, 0, 0.14);
113
+ --home-sort-focus-bg: rgba(0, 0, 0, 0.08);
114
+ --home-sort-text: rgba(15, 23, 42, 0.9);
115
+ --home-sort-muted: rgba(15, 23, 42, 0.66);
116
+ --home-sort-dropdown-bg: rgba(255, 255, 255, 0.98);
117
+ --home-sort-dropdown-border: rgba(15, 23, 42, 0.12);
118
+ --home-sort-dropdown-shadow: 0 10px 24px rgba(15, 23, 42, 0.12);
119
+ --home-sort-option-hover-bg: rgba(15, 23, 42, 0.05);
120
+ --home-sort-option-hover-text: rgba(15, 23, 42, 0.95);
121
+ --home-sort-option-selected-bg: rgba(15, 23, 42, 0.08);
122
+ --home-sort-option-selected-text: rgba(15, 23, 42, 0.96);
123
+ display: inline-flex;
124
+ position: relative;
125
+ align-items: center;
126
+ gap: 5px;
127
+ height: 30px;
128
+ min-height: 30px;
129
+ padding: 0 10px;
130
+ border: 1px solid var(--home-sort-border);
131
+ border-radius: 6px;
132
+ background: var(--home-sort-bg);
133
+ color: var(--home-sort-text);
134
+ box-sizing: border-box;
135
+ margin-left: auto;
136
+ align-self: center;
137
+ flex: 0 0 auto;
138
+ cursor: pointer;
139
+ transition: border-color 0.12s ease, background-color 0.12s ease;
140
+ }
141
+ .home-search-form .home-apps-sort:hover {
142
+ border-color: var(--home-sort-hover-border);
143
+ background: var(--home-sort-hover-bg);
144
+ }
145
+ .home-search-form .home-apps-sort:focus-within {
146
+ border-color: var(--home-sort-focus-border);
147
+ background: var(--home-sort-focus-bg);
148
+ }
149
+ .home-search-form .home-apps-sort .home-apps-sort-icon,
150
+ .home-search-form .home-apps-sort .home-apps-sort-caret {
151
+ flex: 0 0 auto;
152
+ color: var(--home-sort-muted);
153
+ line-height: 1;
154
+ }
155
+ .home-search-form .home-apps-sort .home-apps-sort-icon {
156
+ font-size: 11px;
157
+ opacity: 0.85;
158
+ }
159
+ .home-search-form .home-apps-sort .home-apps-sort-caret {
160
+ font-size: 9px;
161
+ opacity: 0.7;
162
+ pointer-events: none;
163
+ }
164
+ .home-search-form .home-apps-sort label {
165
+ margin: 0;
166
+ color: var(--home-sort-muted);
167
+ font-size: 11px;
168
+ font-weight: 700;
169
+ line-height: 1;
170
+ white-space: nowrap;
171
+ }
172
+ .home-search-form .home-apps-sort select {
173
+ border: 0;
174
+ outline: none;
175
+ background: transparent;
176
+ color: inherit;
177
+ font-size: 11px;
178
+ font-weight: 700;
179
+ line-height: 1;
180
+ min-width: 88px;
181
+ height: 100%;
182
+ padding: 0;
183
+ margin: 0;
184
+ appearance: none;
185
+ -webkit-appearance: none;
186
+ cursor: pointer;
187
+ }
188
+ .home-search-form .home-apps-sort .ts-wrapper {
189
+ flex: 0 0 auto;
190
+ width: 132px;
191
+ min-width: 132px;
192
+ }
193
+ .home-search-form .home-apps-sort .ts-wrapper.single .ts-control {
194
+ border: 0;
195
+ outline: none;
196
+ background: transparent;
197
+ background-image: none;
198
+ box-shadow: none;
199
+ padding: 0;
200
+ margin: 0;
201
+ min-height: 0;
202
+ border-radius: 0;
203
+ cursor: pointer;
204
+ color: inherit;
205
+ }
206
+ .home-search-form .home-apps-sort .ts-control > .item {
207
+ color: var(--home-sort-text);
208
+ font-size: 11px;
209
+ font-weight: 700;
210
+ line-height: 1;
211
+ }
212
+ .home-search-form .home-apps-sort .ts-control > input {
213
+ width: 0 !important;
214
+ min-width: 0 !important;
215
+ margin: 0 !important;
216
+ padding: 0 !important;
217
+ color: var(--home-sort-text);
218
+ }
219
+ .home-search-form .home-apps-sort .ts-dropdown {
220
+ margin: 8px 0 0;
221
+ width: 100%;
222
+ min-width: 100%;
223
+ border: 1px solid var(--home-sort-dropdown-border);
224
+ border-radius: 8px;
225
+ background: var(--home-sort-dropdown-bg);
226
+ box-shadow: var(--home-sort-dropdown-shadow);
227
+ }
228
+ .home-search-form .home-apps-sort .ts-dropdown .ts-dropdown-content {
229
+ background: transparent;
230
+ }
231
+ .home-search-form .home-apps-sort .ts-dropdown .option {
232
+ color: var(--home-sort-text);
233
+ font-size: 12px;
234
+ font-weight: 700;
235
+ padding: 7px 10px;
236
+ white-space: nowrap;
237
+ transition: background-color 0.12s ease, color 0.12s ease;
238
+ }
239
+ .home-search-form .home-apps-sort .ts-dropdown .option.active,
240
+ .home-search-form .home-apps-sort .ts-dropdown [data-selectable].option:hover {
241
+ background: var(--home-sort-option-hover-bg);
242
+ color: var(--home-sort-option-hover-text);
243
+ }
244
+ .home-search-form .home-apps-sort .ts-dropdown .option.selected,
245
+ .home-search-form .home-apps-sort .ts-dropdown .option.selected.active,
246
+ .home-search-form .home-apps-sort .ts-dropdown [data-selectable].option.selected:hover {
247
+ background: var(--home-sort-option-selected-bg);
248
+ color: var(--home-sort-option-selected-text);
249
+ }
250
+ body.dark .home-search-form .home-apps-sort {
251
+ --home-sort-bg: rgba(255, 255, 255, 0.06);
252
+ --home-sort-border: rgba(255, 255, 255, 0.05);
253
+ --home-sort-hover-border: rgba(255, 255, 255, 0.1);
254
+ --home-sort-hover-bg: rgba(255, 255, 255, 0.09);
255
+ --home-sort-focus-border: rgba(255, 255, 255, 0.12);
256
+ --home-sort-focus-bg: rgba(255, 255, 255, 0.1);
257
+ --home-sort-text: rgba(226, 232, 240, 0.92);
258
+ --home-sort-muted: rgba(226, 232, 240, 0.78);
259
+ --home-sort-dropdown-bg: rgba(24, 24, 27, 0.98);
260
+ --home-sort-dropdown-border: rgba(255, 255, 255, 0.1);
261
+ --home-sort-dropdown-shadow: 0 14px 28px rgba(0, 0, 0, 0.42);
262
+ --home-sort-option-hover-bg: rgba(255, 255, 255, 0.06);
263
+ --home-sort-option-hover-text: rgba(248, 250, 252, 0.98);
264
+ --home-sort-option-selected-bg: rgba(255, 255, 255, 0.08);
265
+ --home-sort-option-selected-text: rgba(248, 250, 252, 0.98);
266
+ }
267
+ .workspaces-page .workspace-list {
268
+ display: block;
269
+ }
270
+ .workspaces-page .workspace-card {
271
+ cursor: default;
272
+ }
273
+ .workspaces-page .workspace-icon {
274
+ display: grid;
275
+ place-items: center;
276
+ flex: 0 0 70px;
277
+ width: 70px;
278
+ height: 70px;
279
+ border-radius: 4px;
280
+ color: var(--task-muted);
281
+ background: rgba(0, 0, 0, 0.05);
282
+ font-size: 24px;
283
+ }
284
+ body.dark.workspaces-page .workspace-icon {
285
+ background: rgba(255, 255, 255, 0.06);
286
+ color: var(--dark-fade);
287
+ }
288
+ .workspaces-page .workspace-draft-preview {
289
+ margin: 4px 0 8px;
290
+ padding: 8px 0 0;
291
+ color: inherit;
292
+ font-size: 12px;
293
+ line-height: 1.45;
294
+ opacity: 0.8;
295
+ white-space: pre-wrap;
296
+ overflow-wrap: anywhere;
297
+ cursor: text;
298
+ }
299
+ .workspaces-page .workspace-modal[hidden] {
300
+ display: none;
301
+ }
302
+ .workspaces-page .workspace-modal {
303
+ position: fixed;
304
+ inset: 0;
305
+ z-index: 1000;
306
+ display: grid;
307
+ place-items: center;
308
+ padding: 28px;
309
+ }
310
+ .workspaces-page .workspace-modal-backdrop {
311
+ position: absolute;
312
+ inset: 0;
313
+ border: 0;
314
+ background: rgba(0, 0, 0, 0.48);
315
+ }
316
+ .workspaces-page .workspace-modal-panel {
317
+ position: relative;
318
+ display: grid;
319
+ grid-template-rows: auto minmax(0, 1fr);
320
+ width: min(980px, calc(100vw - 56px));
321
+ height: min(760px, calc(100vh - 56px));
322
+ border: 1px solid var(--task-border);
323
+ border-radius: 8px;
324
+ background: var(--task-panel);
325
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.24);
326
+ overflow: hidden;
327
+ }
328
+ .workspaces-page .workspace-modal-head {
329
+ display: grid;
330
+ grid-template-columns: minmax(0, 1fr) auto;
331
+ gap: 16px;
332
+ align-items: start;
333
+ border-bottom: 1px solid var(--task-border);
334
+ padding: 16px;
335
+ }
336
+ .workspaces-page .workspace-modal-title {
337
+ margin: 0;
338
+ color: var(--task-text);
339
+ font-size: 18px;
340
+ line-height: 1.2;
341
+ }
342
+ .workspaces-page .workspace-modal-path {
343
+ margin-top: 5px;
344
+ color: var(--task-muted);
345
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
346
+ font-size: 11px;
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ white-space: nowrap;
350
+ }
351
+ .workspaces-page .workspace-modal-close {
352
+ display: inline-grid;
353
+ place-items: center;
354
+ width: 32px;
355
+ height: 32px;
356
+ border: 1px solid var(--task-border);
357
+ border-radius: 6px;
358
+ color: var(--task-muted);
359
+ background: transparent;
360
+ cursor: pointer;
361
+ }
362
+ .workspaces-page .workspace-modal-body {
363
+ display: flex;
364
+ flex-direction: column;
365
+ box-sizing: border-box;
366
+ height: 100%;
367
+ min-height: 0;
368
+ overflow: hidden;
369
+ padding: 16px;
370
+ gap: 14px;
371
+ }
372
+ .workspaces-page .workspace-terminal-list[hidden] {
373
+ display: none;
374
+ }
375
+ .workspaces-page .workspace-terminal-list {
376
+ display: grid;
377
+ flex: 0 0 auto;
378
+ gap: 8px;
379
+ align-content: start;
380
+ }
381
+ .workspaces-page .workspace-terminal-list h3,
382
+ .workspaces-page .workspace-launcher-title {
383
+ margin: 0;
384
+ color: var(--task-muted);
385
+ font-size: 12px;
386
+ font-weight: 800;
387
+ letter-spacing: 0;
388
+ text-transform: uppercase;
389
+ }
390
+ .workspaces-page .workspace-terminal-items {
391
+ display: flex;
392
+ flex-wrap: wrap;
393
+ gap: 8px;
394
+ }
395
+ .workspaces-page .workspace-terminal-items .btn.active {
396
+ color: var(--task-accent);
397
+ border-color: color-mix(in srgb, var(--task-accent) 40%, var(--task-border));
398
+ background: color-mix(in srgb, var(--task-accent) 10%, transparent);
399
+ }
400
+ .workspaces-page .workspace-launcher {
401
+ display: flex;
402
+ flex: 1 1 auto;
403
+ flex-direction: column;
404
+ min-height: 0;
405
+ overflow: hidden;
406
+ gap: 8px;
407
+ }
408
+ .workspaces-page .workspace-launch-frame {
409
+ display: block;
410
+ flex: 1 1 auto;
411
+ width: 100%;
412
+ height: auto;
413
+ min-height: 0;
414
+ border: 1px solid var(--task-border);
415
+ border-radius: 8px;
416
+ background: var(--task-panel);
417
+ }
418
+ @media (max-width: 760px) {
419
+ .workspaces-page .workspace-search-form {
420
+ flex-wrap: wrap;
421
+ }
422
+ .workspaces-page .home-search-form .home-apps-sort {
423
+ margin-left: 0;
424
+ }
425
+ .workspaces-page .workspace-modal {
426
+ padding: 10px;
427
+ }
428
+ .workspaces-page .workspace-modal-panel {
429
+ width: calc(100vw - 20px);
430
+ height: calc(100vh - 20px);
431
+ }
432
+ }
433
+ </style>
434
+ </head>
435
+ <body class="<%= [pageTheme, "is-home", "workspaces-page"].filter(Boolean).join(" ") %>" data-agent="<%= pageAgent %>">
436
+ <%- include('partials/app_navheader', { agent: pageAgent }) %>
437
+ <main>
438
+ <div id="terminal" class="hidden"></div>
439
+ <div class="container workspaces-container">
440
+ <form class="search home-search-form workspace-search-form" data-workspace-search-form>
441
+ <input type="search" class="flexible" placeholder="Search workspaces" aria-label="Search workspaces" data-workspace-search autofocus>
442
+ <div class="home-apps-sort">
443
+ <i class="fa-solid fa-sort home-apps-sort-icon" aria-hidden="true"></i>
444
+ <label for="workspace-sort-select">Sort</label>
445
+ <select id="workspace-sort-select" aria-label="Sort workspaces">
446
+ <option value="most_used" <%= currentSort === "most_used" ? "selected" : "" %>>Most used</option>
447
+ <option value="last_opened" <%= currentSort === "last_opened" ? "selected" : "" %>>Last opened</option>
448
+ <option value="az" <%= currentSort === "az" ? "selected" : "" %>>A-Z</option>
449
+ </select>
450
+ <i class="fa-solid fa-chevron-down home-apps-sort-caret" aria-hidden="true"></i>
451
+ </div>
452
+ </form>
453
+
454
+ <% if (!items.length) { %>
455
+ <div class="placeholder">
456
+ <h1>No workspaces.</h1>
457
+ <br>
458
+ <div>Create a workspace from a task or app page.</div>
459
+ </div>
460
+ <% } %>
461
+
462
+ <% if (running.length) { %>
463
+ <div class="workspace-list running-workspaces running-apps" data-workspace-section data-workspace-list>
464
+ <% running.forEach((workspace, index) => { %>
465
+ <%- include('partials/workspace_row', { workspace, index, plural, dateLabel, bytes }) %>
466
+ <% }) %>
467
+ </div>
468
+ <% } %>
469
+
470
+ <% if (offline.length) { %>
471
+ <div class="workspace-list offline-workspaces not-running-apps" data-workspace-section data-workspace-list>
472
+ <% offline.forEach((workspace, index) => { %>
473
+ <%- include('partials/workspace_row', { workspace, index: index + running.length, plural, dateLabel, bytes }) %>
474
+ <% }) %>
475
+ </div>
476
+ <% } %>
477
+ </div>
478
+ <%- include('partials/main_sidebar', { selected: 'workspaces' }) %>
479
+ </main>
480
+
481
+ <div class="workspace-modal" data-workspace-modal hidden>
482
+ <button type="button" class="workspace-modal-backdrop" data-workspace-modal-close aria-label="Close workspace launcher"></button>
483
+ <section class="workspace-modal-panel" role="dialog" aria-modal="true" aria-labelledby="workspace-modal-title">
484
+ <header class="workspace-modal-head">
485
+ <div>
486
+ <h2 class="workspace-modal-title" id="workspace-modal-title" data-workspace-modal-title>Workspace</h2>
487
+ <div class="workspace-modal-path" data-workspace-modal-path></div>
488
+ </div>
489
+ <button type="button" class="workspace-modal-close" data-workspace-modal-close aria-label="Close">
490
+ <i class="fa-solid fa-xmark" aria-hidden="true"></i>
491
+ </button>
492
+ </header>
493
+ <div class="workspace-modal-body">
494
+ <div class="workspace-terminal-list" data-workspace-terminal-list hidden>
495
+ <h3>Terminals</h3>
496
+ <div class="workspace-terminal-items" data-workspace-terminal-items></div>
497
+ </div>
498
+ <div class="workspace-launcher">
499
+ <h3 class="workspace-launcher-title" data-workspace-launcher-title>Start new session</h3>
500
+ <iframe class="workspace-launch-frame" data-workspace-launch-frame title="Workspace launcher" allow="clipboard-read *; clipboard-write *; accelerometer *; ambient-light-sensor *; autoplay *; battery *; camera *; display-capture *; fullscreen *; gamepad *; geolocation *; gyroscope *; hid *; identity-credentials-get *; microphone *; midi *; otp-credentials *; serial *;" allowfullscreen></iframe>
501
+ </div>
502
+ </div>
503
+ </section>
504
+ </div>
505
+
506
+ <script>
507
+ window.PinokioWorkspaceItems = <%- safeJson %>;
508
+ </script>
509
+ <%- include('partials/app_common_scripts') %>
510
+ <script src="/nav.js"></script>
511
+ <script src="/tom-select.complete.min.js"></script>
512
+ <script>
513
+ (() => {
514
+ const storageKey = "pinokio.workspaces.sort";
515
+ const sortModes = new Set(["most_used", "last_opened", "az"]);
516
+ const items = Array.isArray(window.PinokioWorkspaceItems) ? window.PinokioWorkspaceItems : [];
517
+ const itemByCwd = new Map(items.map((item) => [item.cwd, item]));
518
+ const params = new URLSearchParams(location.search);
519
+ const normalizeSortMode = (value) => sortModes.has(value) ? value : "most_used";
520
+ const updateSearchParam = (key, value, options = {}) => {
521
+ const next = new URLSearchParams(location.search);
522
+ if (value) {
523
+ next.set(key, value);
524
+ } else {
525
+ next.delete(key);
526
+ }
527
+ const query = next.toString();
528
+ const href = `${location.pathname}${query ? `?${query}` : ""}${location.hash || ""}`;
529
+ if (options.replace) {
530
+ history.replaceState(null, "", href);
531
+ } else {
532
+ history.pushState(null, "", href);
533
+ }
534
+ };
535
+ const readStoredSort = () => {
536
+ try {
537
+ return normalizeSortMode(localStorage.getItem(storageKey) || "");
538
+ } catch (_) {
539
+ return "most_used";
540
+ }
541
+ };
542
+ const persistSort = (value) => {
543
+ const next = normalizeSortMode(value);
544
+ try {
545
+ localStorage.setItem(storageKey, next);
546
+ } catch (_) {}
547
+ return next;
548
+ };
549
+ const rowMeta = (row) => {
550
+ const launchCount = Number.parseInt(row.getAttribute("data-launch-count-total") || "0", 10);
551
+ const lastLaunchRaw = row.getAttribute("data-last-launch-at") || "";
552
+ const lastLaunch = lastLaunchRaw ? (Date.parse(lastLaunchRaw) || 0) : 0;
553
+ const index = Number.parseInt(row.getAttribute("data-index") || String(Number.MAX_SAFE_INTEGER), 10);
554
+ return {
555
+ launchCount: Number.isFinite(launchCount) ? launchCount : 0,
556
+ lastLaunch,
557
+ index: Number.isFinite(index) ? index : Number.MAX_SAFE_INTEGER,
558
+ name: (row.getAttribute("data-name") || "").toLowerCase(),
559
+ };
560
+ };
561
+ const compareRows = (a, b, sortMode) => {
562
+ const aa = rowMeta(a);
563
+ const bb = rowMeta(b);
564
+ if (sortMode === "last_opened") {
565
+ if (aa.lastLaunch !== bb.lastLaunch) return bb.lastLaunch - aa.lastLaunch;
566
+ if (aa.launchCount !== bb.launchCount) return bb.launchCount - aa.launchCount;
567
+ const byName = aa.name.localeCompare(bb.name);
568
+ if (byName !== 0) return byName;
569
+ } else if (sortMode === "az") {
570
+ const byName = aa.name.localeCompare(bb.name);
571
+ if (byName !== 0) return byName;
572
+ } else {
573
+ if (aa.launchCount !== bb.launchCount) return bb.launchCount - aa.launchCount;
574
+ if (aa.lastLaunch !== bb.lastLaunch) return bb.lastLaunch - aa.lastLaunch;
575
+ const byName = aa.name.localeCompare(bb.name);
576
+ if (byName !== 0) return byName;
577
+ }
578
+ if (aa.index !== bb.index) return aa.index - bb.index;
579
+ return aa.name.localeCompare(bb.name);
580
+ };
581
+ const reorderSections = (sortMode) => {
582
+ for (const selector of [".running-workspaces", ".offline-workspaces"]) {
583
+ const section = document.querySelector(selector);
584
+ if (!section) continue;
585
+ const rows = Array.from(section.querySelectorAll(":scope > .workspace-card"));
586
+ rows.sort((x, y) => compareRows(x, y, sortMode));
587
+ for (const row of rows) section.appendChild(row);
588
+ }
589
+ };
590
+ const updateVisibleCounts = () => {
591
+ document.querySelectorAll("[data-workspace-section]").forEach((section) => {
592
+ const rows = Array.from(section.querySelectorAll(".workspace-card"));
593
+ const visible = rows.filter((row) => !row.classList.contains("hidden")).length;
594
+ const counter = section.querySelector("[data-section-count]");
595
+ if (counter) counter.textContent = `${visible} workspace${visible === 1 ? "" : "s"}`;
596
+ section.classList.toggle("hidden", visible === 0);
597
+ });
598
+ };
599
+ const applySearch = (value) => {
600
+ const query = String(value || "").trim().toLowerCase();
601
+ document.querySelectorAll(".workspace-card").forEach((row) => {
602
+ const haystack = (row.getAttribute("data-search") || "").toLowerCase();
603
+ row.classList.toggle("hidden", Boolean(query) && !haystack.includes(query));
604
+ });
605
+ updateVisibleCounts();
606
+ };
607
+ const openPath = async (filepath) => {
608
+ if (!filepath) return;
609
+ await fetch("/openfs", {
610
+ method: "POST",
611
+ headers: { "Content-Type": "application/json" },
612
+ body: JSON.stringify({ path: filepath })
613
+ }).catch(() => {});
614
+ };
615
+ const modal = document.querySelector("[data-workspace-modal]");
616
+ const modalTitle = document.querySelector("[data-workspace-modal-title]");
617
+ const modalPath = document.querySelector("[data-workspace-modal-path]");
618
+ const modalTerminals = document.querySelector("[data-workspace-terminal-list]");
619
+ const modalTerminalItems = document.querySelector("[data-workspace-terminal-items]");
620
+ const modalLauncherTitle = document.querySelector("[data-workspace-launcher-title]");
621
+ const modalFrame = document.querySelector("[data-workspace-launch-frame]");
622
+ const closeModal = () => {
623
+ if (!modal) return;
624
+ modal.hidden = true;
625
+ if (modalFrame) modalFrame.removeAttribute("src");
626
+ };
627
+ const normalizeLaunchHref = (href) => {
628
+ let next = typeof href === "string" ? href.trim() : "";
629
+ if (next.startsWith("@")) next = next.slice(1);
630
+ return next;
631
+ };
632
+ const prepareTopLaunchHref = (href) => {
633
+ const next = normalizeLaunchHref(href);
634
+ if (!next) return "";
635
+ try {
636
+ const url = new URL(next, location.origin);
637
+ if (url.origin === location.origin && url.pathname.startsWith("/shell/")) {
638
+ url.searchParams.delete("embed");
639
+ return `${url.pathname}${url.search}${url.hash}`;
640
+ }
641
+ if (
642
+ url.origin === location.origin
643
+ && (
644
+ url.pathname.startsWith("/run/plugin/")
645
+ || (url.pathname.startsWith("/run/api/") && /\/pinokio\.js$/i.test(url.pathname))
646
+ )
647
+ ) {
648
+ if (!url.searchParams.has("chrome")) {
649
+ url.searchParams.set("chrome", "full");
650
+ }
651
+ return `${url.pathname}${url.search}${url.hash}`;
652
+ }
653
+ } catch (_) {}
654
+ return next;
655
+ };
656
+ const setModalFrame = (href, title) => {
657
+ const next = normalizeLaunchHref(href);
658
+ if (!modalFrame || !next) return;
659
+ if (modalLauncherTitle) modalLauncherTitle.textContent = title || "Start new session";
660
+ modalFrame.src = next;
661
+ };
662
+ const markActiveTerminal = (button) => {
663
+ if (!modalTerminalItems) return;
664
+ modalTerminalItems.querySelectorAll(".btn.active").forEach((item) => item.classList.remove("active"));
665
+ if (button) button.classList.add("active");
666
+ };
667
+ const createTerminalLink = (shell, index) => {
668
+ const anchor = document.createElement("a");
669
+ anchor.className = "btn";
670
+ anchor.href = prepareTopLaunchHref(shell.url || "#");
671
+ anchor.innerHTML = '<i class="fa-solid fa-terminal" aria-hidden="true"></i><span></span>';
672
+ const span = anchor.querySelector("span");
673
+ if (span) span.textContent = shell.title || `Terminal ${index + 1}`;
674
+ return anchor;
675
+ };
676
+ const openWorkspaceModal = (workspace, options = {}) => {
677
+ if (!workspace || !modal || !modalFrame) return;
678
+ const mode = options.mode || "session";
679
+ if (modalTitle) modalTitle.textContent = workspace.name || "Workspace";
680
+ if (modalPath) modalPath.textContent = workspace.cwd || "";
681
+ if (modalTerminalItems) modalTerminalItems.innerHTML = "";
682
+ const shells = Array.isArray(workspace.shells) ? workspace.shells.filter((shell) => shell && shell.url) : [];
683
+ if (modalTerminals) modalTerminals.hidden = mode !== "terminals" || shells.length === 0;
684
+ if (modalTerminalItems) {
685
+ shells.forEach((shell, index) => {
686
+ modalTerminalItems.appendChild(createTerminalLink(shell, index));
687
+ });
688
+ }
689
+ modal.hidden = false;
690
+ setModalFrame(workspace.launchUrl, "Start new session");
691
+ markActiveTerminal(null);
692
+ };
693
+
694
+ const form = document.querySelector("[data-workspace-search-form]");
695
+ const search = document.querySelector("[data-workspace-search]");
696
+ if (form) {
697
+ form.addEventListener("submit", (event) => event.preventDefault());
698
+ }
699
+ if (search) {
700
+ const selected = params.get("selected") || "";
701
+ if (selected) {
702
+ search.value = selected;
703
+ applySearch(selected);
704
+ } else {
705
+ updateVisibleCounts();
706
+ }
707
+ search.addEventListener("input", (event) => {
708
+ const value = event.target.value || "";
709
+ applySearch(value);
710
+ updateSearchParam("selected", value, { replace: false });
711
+ });
712
+ requestAnimationFrame(() => {
713
+ search.focus();
714
+ });
715
+ }
716
+
717
+ const sortSelect = document.querySelector("#workspace-sort-select");
718
+ if (sortSelect) {
719
+ const hasSortQuery = typeof params.get("sort") === "string" && params.get("sort").trim().length > 0;
720
+ const initialSort = hasSortQuery ? normalizeSortMode(params.get("sort")) : readStoredSort();
721
+ if (sortSelect.value !== initialSort) sortSelect.value = initialSort;
722
+ persistSort(initialSort);
723
+ if (!hasSortQuery) updateSearchParam("sort", initialSort, { replace: true });
724
+ sortSelect.addEventListener("change", (event) => {
725
+ const next = persistSort(event.target.value);
726
+ event.target.value = next;
727
+ reorderSections(next);
728
+ updateSearchParam("sort", next, { replace: true });
729
+ });
730
+ reorderSections(initialSort);
731
+ if (typeof window.TomSelect === "function" && !sortSelect.tomselect) {
732
+ const parent = sortSelect.closest(".home-apps-sort");
733
+ new TomSelect(sortSelect, {
734
+ create: false,
735
+ maxItems: 1,
736
+ allowEmptyOption: false,
737
+ searchField: [],
738
+ controlInput: null,
739
+ dropdownParent: parent || undefined
740
+ });
741
+ }
742
+ const sortContainer = sortSelect.closest(".home-apps-sort");
743
+ if (sortContainer) {
744
+ sortContainer.addEventListener("click", (event) => {
745
+ if (!(event.target instanceof Element) || event.target.closest(".ts-dropdown")) return;
746
+ const control = sortSelect.tomselect;
747
+ if (control && !event.target.closest(".ts-control")) {
748
+ control.focus();
749
+ control.open();
750
+ return;
751
+ }
752
+ if (!event.target.closest("select")) {
753
+ sortSelect.focus();
754
+ sortSelect.click();
755
+ }
756
+ });
757
+ }
758
+ }
759
+
760
+ document.addEventListener("click", (event) => {
761
+ const target = event.target instanceof Element ? event.target : null;
762
+ if (!target) return;
763
+ if (target.closest("[data-workspace-modal-close]")) {
764
+ closeModal();
765
+ return;
766
+ }
767
+ const openWorkspaceButton = target.closest("[data-open-workspace]");
768
+ if (openWorkspaceButton) {
769
+ event.preventDefault();
770
+ event.stopPropagation();
771
+ const cwd = openWorkspaceButton.getAttribute("data-open-workspace") || "";
772
+ const mode = openWorkspaceButton.getAttribute("data-workspace-modal-mode") || "session";
773
+ const workspace = itemByCwd.get(cwd);
774
+ openWorkspaceModal(workspace, { mode });
775
+ return;
776
+ }
777
+ const openButton = target.closest("[data-open-path]");
778
+ if (openButton) {
779
+ event.preventDefault();
780
+ event.stopPropagation();
781
+ openPath(openButton.getAttribute("data-open-path") || "");
782
+ return;
783
+ }
784
+ const previewButton = target.closest("[data-preview-toggle]");
785
+ if (previewButton) {
786
+ event.preventDefault();
787
+ event.stopPropagation();
788
+ const preview = document.querySelector(`[data-preview-panel="${previewButton.getAttribute("data-preview-toggle")}"]`);
789
+ if (preview) preview.hidden = !preview.hidden;
790
+ return;
791
+ }
792
+ });
793
+
794
+ document.addEventListener("keydown", (event) => {
795
+ if (event.key === "Escape" && modal && !modal.hidden) {
796
+ closeModal();
797
+ return;
798
+ }
799
+ });
800
+
801
+ window.addEventListener("message", (event) => {
802
+ if (event.origin !== location.origin) return;
803
+ const launch = event.data && event.data.launch;
804
+ if (!launch) return;
805
+ let href = launch.href || launch.name || "";
806
+ if (!href) return;
807
+ location.href = prepareTopLaunchHref(href);
808
+ });
809
+ })();
810
+ </script>
811
+ </body>
812
+ </html>