pinokiod 7.3.1 → 7.3.3

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 (122) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/git.js +9 -10
  7. package/kernel/bin/huggingface.js +1 -1
  8. package/kernel/bin/zip.js +9 -1
  9. package/kernel/connect/providers/github/README.md +5 -4
  10. package/kernel/environment.js +195 -92
  11. package/kernel/git.js +98 -19
  12. package/kernel/gitconfig_template +7 -0
  13. package/kernel/gpu/amd.js +72 -0
  14. package/kernel/gpu/apple.js +8 -0
  15. package/kernel/gpu/common.js +12 -0
  16. package/kernel/gpu/intel.js +47 -0
  17. package/kernel/gpu/nvidia.js +8 -0
  18. package/kernel/index.js +11 -1
  19. package/kernel/managed_skills.js +871 -0
  20. package/kernel/plugin.js +6 -58
  21. package/kernel/plugin_sources.js +316 -0
  22. package/kernel/resource_usage/gpu.js +349 -0
  23. package/kernel/resource_usage/index.js +322 -0
  24. package/kernel/resource_usage/macos_footprint.js +197 -0
  25. package/kernel/resource_usage/preferences.js +92 -0
  26. package/kernel/resource_usage/process_tree.js +303 -0
  27. package/kernel/scripts/git/create +4 -4
  28. package/kernel/scripts/git/fork +7 -8
  29. package/kernel/shell.js +23 -2
  30. package/kernel/shells.js +41 -0
  31. package/kernel/sysinfo.js +62 -9
  32. package/kernel/util.js +60 -0
  33. package/package.json +1 -1
  34. package/server/index.js +984 -156
  35. package/server/lib/app_log_report.js +543 -0
  36. package/server/lib/content_validation.js +55 -33
  37. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  38. package/server/lib/terminal_session_helpers.js +0 -3
  39. package/server/public/common.js +77 -31
  40. package/server/public/create-launcher.js +4 -32
  41. package/server/public/logs.js +1428 -0
  42. package/server/public/nav.js +7 -0
  43. package/server/public/plugin-detail.js +93 -10
  44. package/server/public/privacy_filter_worker.js +391 -0
  45. package/server/public/style.css +1104 -154
  46. package/server/public/task-launcher.js +8 -29
  47. package/server/public/universal-launcher.css +8 -6
  48. package/server/public/universal-launcher.js +3 -27
  49. package/server/routes/apps.js +195 -1
  50. package/server/views/app.ejs +3041 -717
  51. package/server/views/autolaunch.ejs +917 -0
  52. package/server/views/bootstrap.ejs +7 -1
  53. package/server/views/d.ejs +408 -65
  54. package/server/views/editor.ejs +85 -19
  55. package/server/views/index.ejs +661 -111
  56. package/server/views/init/index.ejs +1 -1
  57. package/server/views/install.ejs +1 -1
  58. package/server/views/logs.ejs +164 -86
  59. package/server/views/net.ejs +7 -1
  60. package/server/views/partials/d_terminal_column.ejs +2 -2
  61. package/server/views/partials/d_terminal_options.ejs +0 -8
  62. package/server/views/partials/fs_status.ejs +47 -0
  63. package/server/views/partials/home_action_modal.ejs +86 -0
  64. package/server/views/partials/home_run_menu.ejs +87 -0
  65. package/server/views/partials/main_sidebar.ejs +2 -0
  66. package/server/views/partials/menu.ejs +1 -1
  67. package/server/views/plugin_detail.ejs +19 -4
  68. package/server/views/plugins.ejs +201 -3
  69. package/server/views/pre.ejs +1 -1
  70. package/server/views/pro.ejs +1 -1
  71. package/server/views/shell.ejs +40 -18
  72. package/server/views/skills.ejs +506 -0
  73. package/server/views/terminal.ejs +45 -19
  74. package/spec/INSTRUCTION_SYNC.md +20 -10
  75. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  76. package/system/plugin/antigravity-cli/common.js +155 -0
  77. package/system/plugin/antigravity-cli/install.js +272 -0
  78. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  79. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  80. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  81. package/system/plugin/claude/claude.png +0 -0
  82. package/system/plugin/claude/pinokio.js +47 -0
  83. package/system/plugin/claude-auto/claude.png +0 -0
  84. package/system/plugin/claude-auto/pinokio.js +58 -0
  85. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  86. package/system/plugin/claude-desktop/pinokio.js +23 -0
  87. package/system/plugin/codex/openai.webp +0 -0
  88. package/system/plugin/codex/pinokio.js +42 -0
  89. package/system/plugin/codex-auto/openai.webp +0 -0
  90. package/system/plugin/codex-auto/pinokio.js +49 -0
  91. package/system/plugin/codex-desktop/icon.png +0 -0
  92. package/system/plugin/codex-desktop/pinokio.js +23 -0
  93. package/system/plugin/crush/crush.png +0 -0
  94. package/system/plugin/crush/pinokio.js +15 -0
  95. package/system/plugin/cursor/cursor.jpeg +0 -0
  96. package/system/plugin/cursor/pinokio.js +23 -0
  97. package/system/plugin/qwen/pinokio.js +34 -0
  98. package/system/plugin/qwen/qwen.png +0 -0
  99. package/system/plugin/vscode/pinokio.js +20 -0
  100. package/system/plugin/vscode/vscode.png +0 -0
  101. package/system/plugin/windsurf/pinokio.js +23 -0
  102. package/system/plugin/windsurf/windsurf.png +0 -0
  103. package/test/antigravity-cli-plugin.test.js +185 -0
  104. package/test/app-api.test.js +239 -0
  105. package/test/app-log-report.test.js +67 -0
  106. package/test/environment-cache-preflight.test.js +98 -0
  107. package/test/git-bin.test.js +59 -0
  108. package/test/git-defaults.test.js +97 -0
  109. package/test/github-api.test.js +158 -0
  110. package/test/github-connection.test.js +117 -0
  111. package/test/huggingface-bin.test.js +25 -0
  112. package/test/managed-skills.test.js +351 -0
  113. package/test/plugin-action-functions.test.js +337 -0
  114. package/test/plugin-dev-iframe.test.js +17 -0
  115. package/test/plugin-sources.test.js +203 -0
  116. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  117. package/test/process-wait.test.js +169 -0
  118. package/test/script-api.test.js +97 -0
  119. package/test/shell-api.test.js +134 -0
  120. package/test/shell-run-template.test.js +209 -0
  121. package/test/storage-api.test.js +137 -0
  122. package/test/uri-api.test.js +100 -0
@@ -0,0 +1,917 @@
1
+ <html>
2
+ <head>
3
+ <meta charset="UTF-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
5
+ <link href="/css/fontawesome.min.css" rel="stylesheet">
6
+ <link href="/css/solid.min.css" rel="stylesheet">
7
+ <link href="/css/regular.min.css" rel="stylesheet">
8
+ <link href="/css/brands.min.css" rel="stylesheet">
9
+ <link href="/noty.css" rel="stylesheet"/>
10
+ <link href="/style.css" rel="stylesheet"/>
11
+ <link href="/task-launcher.css" rel="stylesheet"/>
12
+ <% if (agent === "electron") { %>
13
+ <link href="/electron.css" rel="stylesheet"/>
14
+ <% } %>
15
+ <style>
16
+ body.autolaunch-page .task-container {
17
+ padding: 20px 28px 32px;
18
+ }
19
+ body.autolaunch-page .task-shell {
20
+ width: min(1140px, 100%);
21
+ margin: 0;
22
+ background: transparent;
23
+ border: 0;
24
+ border-radius: 0;
25
+ box-shadow: none;
26
+ overflow: visible;
27
+ backdrop-filter: none;
28
+ }
29
+ body.autolaunch-page .task-shell-header {
30
+ padding-bottom: 18px;
31
+ }
32
+ body.autolaunch-page .task-shell-body {
33
+ padding: 0 28px 28px;
34
+ }
35
+ body.autolaunch-page .task-shell-header-main {
36
+ max-width: 720px;
37
+ }
38
+ body.autolaunch-page .task-description {
39
+ max-width: 70ch;
40
+ }
41
+ body.autolaunch-page .autolaunch-summary {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 8px;
45
+ flex-wrap: wrap;
46
+ justify-content: flex-end;
47
+ }
48
+ body.autolaunch-page .autolaunch-summary-item {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: 4px;
52
+ min-height: 26px;
53
+ box-sizing: border-box;
54
+ padding: 0 9px;
55
+ border: 1px solid var(--task-border);
56
+ border-radius: 999px;
57
+ color: var(--task-muted);
58
+ font-size: 11px;
59
+ line-height: 1;
60
+ }
61
+ body.autolaunch-page .autolaunch-summary-item strong {
62
+ color: var(--task-text);
63
+ font-weight: 700;
64
+ }
65
+ body.autolaunch-page .task-button:disabled {
66
+ cursor: not-allowed;
67
+ opacity: 0.45;
68
+ }
69
+ body.autolaunch-page .autolaunch-root {
70
+ padding-top: 18px;
71
+ }
72
+ body.autolaunch-page .autolaunch-toolbar {
73
+ margin: 0 0 14px;
74
+ }
75
+ body.autolaunch-page .autolaunch-search-field {
76
+ width: min(420px, 100%);
77
+ min-height: 32px;
78
+ box-sizing: border-box;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 8px;
82
+ padding: 0 10px;
83
+ border: 1px solid var(--task-border-strong);
84
+ border-radius: 7px;
85
+ background: color-mix(in srgb, var(--task-panel) 98%, white);
86
+ color: var(--task-muted);
87
+ }
88
+ body.dark.autolaunch-page .autolaunch-search-field {
89
+ background: color-mix(in srgb, var(--task-panel) 95%, black);
90
+ }
91
+ body.autolaunch-page .autolaunch-search-field input,
92
+ body.autolaunch-page .autolaunch-manual input {
93
+ width: 100%;
94
+ min-width: 0;
95
+ box-sizing: border-box;
96
+ border: 0;
97
+ outline: none;
98
+ background: transparent;
99
+ color: var(--task-text);
100
+ font: inherit;
101
+ font-size: 13px;
102
+ }
103
+ body.autolaunch-page .autolaunch-layout {
104
+ display: grid;
105
+ grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
106
+ gap: 18px;
107
+ align-items: start;
108
+ }
109
+ body.autolaunch-page .autolaunch-apps,
110
+ body.autolaunch-page .autolaunch-detail {
111
+ border: 1px solid var(--task-border);
112
+ border-radius: 8px;
113
+ background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
114
+ overflow: hidden;
115
+ }
116
+ body.autolaunch-page .autolaunch-apps {
117
+ position: sticky;
118
+ top: 18px;
119
+ max-height: calc(100vh - 160px);
120
+ display: flex;
121
+ flex-direction: column;
122
+ }
123
+ body.autolaunch-page .autolaunch-app-list {
124
+ overflow: auto;
125
+ padding: 0;
126
+ }
127
+ body.autolaunch-page .autolaunch-app-row {
128
+ width: 100%;
129
+ border: 0;
130
+ border-bottom: 1px solid var(--task-border);
131
+ border-radius: 0;
132
+ background: transparent;
133
+ color: var(--task-text);
134
+ padding: 9px 12px;
135
+ display: grid;
136
+ grid-template-columns: 28px minmax(0, 1fr) auto;
137
+ gap: 10px;
138
+ align-items: center;
139
+ text-align: left;
140
+ cursor: pointer;
141
+ transition: background 140ms ease, box-shadow 140ms ease;
142
+ }
143
+ body.autolaunch-page .autolaunch-app-row:last-child {
144
+ border-bottom: 0;
145
+ }
146
+ body.autolaunch-page .autolaunch-app-row:hover,
147
+ body.autolaunch-page .autolaunch-app-row.selected {
148
+ background: color-mix(in srgb, var(--task-soft) 68%, transparent);
149
+ }
150
+ body.autolaunch-page .autolaunch-app-row.selected {
151
+ box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--task-accent) 34%, transparent);
152
+ }
153
+ body.autolaunch-page .autolaunch-app-row img,
154
+ body.autolaunch-page .autolaunch-detail-icon {
155
+ width: 28px;
156
+ height: 28px;
157
+ border-radius: 6px;
158
+ object-fit: cover;
159
+ background: color-mix(in srgb, var(--task-soft) 70%, transparent);
160
+ }
161
+ body.autolaunch-page .autolaunch-detail-icon {
162
+ width: 34px;
163
+ height: 34px;
164
+ border-radius: 8px;
165
+ }
166
+ body.autolaunch-page .autolaunch-app-name {
167
+ font-size: 13px;
168
+ font-weight: 500;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
172
+ }
173
+ body.autolaunch-page .autolaunch-app-meta {
174
+ min-width: 0;
175
+ display: grid;
176
+ gap: 2px;
177
+ }
178
+ body.autolaunch-page .autolaunch-app-path,
179
+ body.autolaunch-page .autolaunch-app-folder,
180
+ body.autolaunch-page .autolaunch-app-script,
181
+ body.autolaunch-page .autolaunch-current,
182
+ body.autolaunch-page .autolaunch-script-path {
183
+ color: var(--task-muted);
184
+ font-size: 11px;
185
+ line-height: 1.25;
186
+ overflow: hidden;
187
+ text-overflow: ellipsis;
188
+ white-space: nowrap;
189
+ }
190
+ body.autolaunch-page .autolaunch-app-folder {
191
+ direction: rtl;
192
+ text-align: left;
193
+ unicode-bidi: plaintext;
194
+ }
195
+ body.autolaunch-page .autolaunch-app-script {
196
+ font-size: 10px;
197
+ }
198
+ body.autolaunch-page .autolaunch-pill {
199
+ display: inline-flex;
200
+ align-items: center;
201
+ justify-content: center;
202
+ min-width: 34px;
203
+ min-height: 20px;
204
+ box-sizing: border-box;
205
+ border: 1px solid var(--task-border);
206
+ border-radius: 999px;
207
+ padding: 0 7px;
208
+ color: var(--task-muted);
209
+ font-size: 10px;
210
+ font-weight: 600;
211
+ line-height: 1;
212
+ }
213
+ body.autolaunch-page .autolaunch-pill.on {
214
+ border-color: color-mix(in srgb, #22c55e 28%, transparent);
215
+ background: color-mix(in srgb, #22c55e 12%, var(--task-panel));
216
+ color: #166534;
217
+ }
218
+ body.dark.autolaunch-page .autolaunch-pill.on {
219
+ color: #86efac;
220
+ }
221
+ body.autolaunch-page .autolaunch-switch {
222
+ display: inline-flex;
223
+ align-items: center;
224
+ justify-content: center;
225
+ gap: 7px;
226
+ min-width: 78px;
227
+ min-height: 28px;
228
+ box-sizing: border-box;
229
+ padding: 0 8px;
230
+ border: 1px solid var(--task-border);
231
+ border-radius: 999px;
232
+ background: color-mix(in srgb, var(--task-panel) 96%, var(--task-soft));
233
+ color: var(--task-muted);
234
+ cursor: pointer;
235
+ font-size: 11px;
236
+ font-weight: 650;
237
+ line-height: 1;
238
+ transition: background 140ms ease, border-color 140ms ease, color 140ms ease;
239
+ }
240
+ body.autolaunch-page .autolaunch-switch:hover {
241
+ background: color-mix(in srgb, var(--task-soft) 76%, var(--task-panel));
242
+ color: var(--task-text);
243
+ }
244
+ body.autolaunch-page .autolaunch-switch[aria-checked="true"] {
245
+ border-color: color-mix(in srgb, #22c55e 30%, transparent);
246
+ background: color-mix(in srgb, #22c55e 12%, var(--task-panel));
247
+ color: #166534;
248
+ }
249
+ body.dark.autolaunch-page .autolaunch-switch[aria-checked="true"] {
250
+ color: #86efac;
251
+ }
252
+ body.autolaunch-page .autolaunch-switch:disabled {
253
+ cursor: wait;
254
+ opacity: 0.62;
255
+ }
256
+ body.autolaunch-page .autolaunch-switch-track {
257
+ position: relative;
258
+ width: 30px;
259
+ height: 16px;
260
+ border-radius: 999px;
261
+ background: color-mix(in srgb, var(--task-muted) 24%, transparent);
262
+ transition: background 140ms ease;
263
+ }
264
+ body.autolaunch-page .autolaunch-switch[aria-checked="true"] .autolaunch-switch-track {
265
+ background: color-mix(in srgb, #22c55e 52%, transparent);
266
+ }
267
+ body.autolaunch-page .autolaunch-switch-thumb {
268
+ position: absolute;
269
+ top: 2px;
270
+ left: 2px;
271
+ width: 12px;
272
+ height: 12px;
273
+ border-radius: 999px;
274
+ background: var(--task-panel);
275
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.18);
276
+ transition: transform 140ms ease;
277
+ }
278
+ body.autolaunch-page .autolaunch-switch[aria-checked="true"] .autolaunch-switch-thumb {
279
+ transform: translateX(14px);
280
+ }
281
+ body.autolaunch-page .autolaunch-switch-label {
282
+ min-width: 18px;
283
+ text-align: left;
284
+ }
285
+ body.autolaunch-page .autolaunch-detail {
286
+ min-height: 480px;
287
+ }
288
+ body.autolaunch-page .autolaunch-detail-header {
289
+ display: grid;
290
+ grid-template-columns: 34px minmax(0, 1fr) auto;
291
+ gap: 12px;
292
+ align-items: center;
293
+ padding: 13px 16px;
294
+ border-bottom: 1px solid var(--task-border);
295
+ }
296
+ body.autolaunch-page .autolaunch-detail-title {
297
+ margin: 0;
298
+ color: var(--task-text);
299
+ font-size: 15px;
300
+ line-height: 1.2;
301
+ font-weight: 700;
302
+ }
303
+ body.autolaunch-page .autolaunch-section {
304
+ padding: 16px;
305
+ border-top: 1px solid var(--task-border);
306
+ }
307
+ body.autolaunch-page .autolaunch-detail-header + .autolaunch-section {
308
+ border-top: 0;
309
+ }
310
+ body.autolaunch-page .autolaunch-section-header {
311
+ display: flex;
312
+ align-items: flex-start;
313
+ justify-content: space-between;
314
+ gap: 12px;
315
+ margin-bottom: 10px;
316
+ }
317
+ body.autolaunch-page .autolaunch-section-title {
318
+ margin: 0;
319
+ font-size: 13px;
320
+ font-weight: 700;
321
+ color: var(--task-text);
322
+ }
323
+ body.autolaunch-page .autolaunch-section-note {
324
+ color: var(--task-muted);
325
+ font-size: 11px;
326
+ line-height: 1.35;
327
+ text-align: right;
328
+ }
329
+ body.autolaunch-page .autolaunch-script-list {
330
+ display: grid;
331
+ gap: 0;
332
+ overflow: hidden;
333
+ border: 1px solid var(--task-border);
334
+ border-radius: 8px;
335
+ background: color-mix(in srgb, var(--task-panel) 98%, var(--task-soft));
336
+ }
337
+ body.autolaunch-page .autolaunch-script-option {
338
+ border: 0;
339
+ border-bottom: 1px solid var(--task-border);
340
+ border-radius: 0;
341
+ padding: 9px 10px;
342
+ display: grid;
343
+ grid-template-columns: 16px 26px minmax(0, 1fr);
344
+ gap: 10px;
345
+ align-items: center;
346
+ cursor: pointer;
347
+ background: transparent;
348
+ transition: background 140ms ease;
349
+ }
350
+ body.autolaunch-page .autolaunch-script-option:last-child {
351
+ border-bottom: 0;
352
+ }
353
+ body.autolaunch-page .autolaunch-script-option:hover,
354
+ body.autolaunch-page .autolaunch-script-option.selected {
355
+ background: color-mix(in srgb, var(--task-accent) 8%, transparent);
356
+ }
357
+ body.autolaunch-page .autolaunch-script-option input {
358
+ margin: 0;
359
+ }
360
+ body.autolaunch-page .autolaunch-script-option i {
361
+ width: 26px;
362
+ height: 26px;
363
+ display: inline-flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ border-radius: 6px;
367
+ background: color-mix(in srgb, var(--task-accent) 10%, transparent);
368
+ color: var(--task-accent);
369
+ font-size: 12px;
370
+ }
371
+ body.autolaunch-page .autolaunch-script-title {
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 8px;
375
+ min-width: 0;
376
+ font-size: 13px;
377
+ font-weight: 500;
378
+ color: var(--task-text);
379
+ }
380
+ body.autolaunch-page .autolaunch-script-title span:first-child {
381
+ overflow: hidden;
382
+ text-overflow: ellipsis;
383
+ white-space: nowrap;
384
+ }
385
+ body.autolaunch-page .autolaunch-tag {
386
+ border-radius: 999px;
387
+ padding: 2px 6px 3px;
388
+ background: color-mix(in srgb, var(--task-accent) 12%, transparent);
389
+ color: var(--task-accent);
390
+ font-size: 10px;
391
+ font-weight: 650;
392
+ line-height: 1;
393
+ }
394
+ body.autolaunch-page .autolaunch-manual {
395
+ padding: 16px;
396
+ border-top: 1px solid var(--task-border);
397
+ }
398
+ body.autolaunch-page .autolaunch-manual-row {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 8px;
402
+ }
403
+ body.autolaunch-page .autolaunch-manual input {
404
+ flex: 1 1 auto;
405
+ min-height: 32px;
406
+ padding: 0 10px;
407
+ border: 1px solid var(--task-border-strong);
408
+ border-radius: 7px;
409
+ background: color-mix(in srgb, var(--task-panel) 98%, white);
410
+ }
411
+ body.dark.autolaunch-page .autolaunch-manual input {
412
+ background: color-mix(in srgb, var(--task-panel) 95%, black);
413
+ }
414
+ body.autolaunch-page .autolaunch-feedback {
415
+ min-height: 16px;
416
+ margin-top: 7px;
417
+ color: var(--task-muted);
418
+ font-size: 12px;
419
+ line-height: 1.35;
420
+ }
421
+ body.autolaunch-page .autolaunch-empty,
422
+ body.autolaunch-page .autolaunch-loading {
423
+ padding: 16px;
424
+ border: 1px dashed var(--task-border);
425
+ border-radius: 8px;
426
+ color: var(--task-muted);
427
+ font-size: 13px;
428
+ }
429
+ body.autolaunch-page .autolaunch-detail > .autolaunch-empty {
430
+ margin: 16px;
431
+ }
432
+ body.autolaunch-page .autolaunch-loading i {
433
+ margin-right: 6px;
434
+ }
435
+ @media only screen and (max-width: 820px) {
436
+ body.autolaunch-page .task-shell-header,
437
+ body.autolaunch-page .task-shell-body {
438
+ padding-left: 18px;
439
+ padding-right: 18px;
440
+ }
441
+ body.autolaunch-page .autolaunch-summary {
442
+ flex-direction: column;
443
+ align-items: stretch;
444
+ }
445
+ body.autolaunch-page .autolaunch-manual-row {
446
+ align-items: stretch;
447
+ flex-direction: column;
448
+ }
449
+ body.autolaunch-page .autolaunch-layout {
450
+ grid-template-columns: 1fr;
451
+ }
452
+ body.autolaunch-page .autolaunch-apps {
453
+ position: relative;
454
+ top: auto;
455
+ max-height: 360px;
456
+ }
457
+ body.autolaunch-page .autolaunch-detail-header {
458
+ grid-template-columns: 46px minmax(0, 1fr);
459
+ }
460
+ body.autolaunch-page .autolaunch-detail-header .autolaunch-switch {
461
+ grid-column: 1 / -1;
462
+ justify-self: start;
463
+ }
464
+ }
465
+ @media only screen and (max-width: 560px) {
466
+ body.autolaunch-page .task-container {
467
+ padding: 16px 14px 24px;
468
+ }
469
+ body.autolaunch-page .task-shell-body {
470
+ padding-bottom: 22px;
471
+ }
472
+ }
473
+ </style>
474
+ </head>
475
+ <body class='<%=theme%> task-launcher-page task-page autolaunch-page' data-agent="<%=agent%>">
476
+ <%- include('partials/app_navheader', { agent }) %>
477
+ <main>
478
+ <div class="task-container">
479
+ <section class="task-shell autolaunch-shell">
480
+ <header class="task-shell-header">
481
+ <div class="task-shell-header-main">
482
+ <p class="task-eyebrow">Autolaunch</p>
483
+ <h1 class="task-title">Startup scripts</h1>
484
+ <p class="task-description">Choose which installed app script runs when Pinokio starts. Script choices load when you select an app.</p>
485
+ </div>
486
+ <div class="task-header-actions autolaunch-summary">
487
+ <span class="autolaunch-summary-item"><strong data-enabled-count><%= enabledCount %></strong> enabled</span>
488
+ <span class="autolaunch-summary-item"><strong data-app-count><%= apps.length %></strong> apps</span>
489
+ <button type="button" class="task-button danger" data-disable-all <%= enabledCount ? '' : 'disabled' %>>
490
+ <i class="fa-solid fa-power-off"></i>
491
+ Disable all
492
+ </button>
493
+ </div>
494
+ </header>
495
+
496
+ <div class="task-shell-body autolaunch-shell-body">
497
+ <section class="task-section autolaunch-root">
498
+ <div class="task-section-head">
499
+ <div>
500
+ <h2 class="task-section-title">Installed apps</h2>
501
+ <p class="task-section-subcopy">Apps with autolaunch enabled appear first. Changes take effect the next time Pinokio starts.</p>
502
+ </div>
503
+ </div>
504
+ <div class="autolaunch-toolbar">
505
+ <label class="autolaunch-search-field">
506
+ <i class="fa-solid fa-magnifying-glass" aria-hidden="true"></i>
507
+ <input type="search" placeholder="Search apps" data-app-search autocomplete="off">
508
+ </label>
509
+ </div>
510
+ <div class="autolaunch-layout">
511
+ <aside class="autolaunch-apps" aria-label="Installed apps">
512
+ <div class="autolaunch-app-list" data-app-list></div>
513
+ </aside>
514
+
515
+ <section class="autolaunch-detail" data-detail>
516
+ <div class="autolaunch-empty">Select an app to configure its startup script.</div>
517
+ </section>
518
+ </div>
519
+ </section>
520
+ </div>
521
+ </section>
522
+ </div>
523
+ <%- include('partials/main_sidebar', { selected: 'autolaunch' }) %>
524
+ </main>
525
+ <%- include('partials/app_common_scripts') %>
526
+ <script>
527
+ let autolaunchApps = <%- appsJson %>;
528
+ let selectedAppId = "";
529
+ let selectedScript = "";
530
+ let candidateState = null;
531
+ let savingAutolaunch = false;
532
+
533
+ const appListEl = document.querySelector("[data-app-list]");
534
+ const searchEl = document.querySelector("[data-app-search]");
535
+ const detailEl = document.querySelector("[data-detail]");
536
+ const enabledCountEl = document.querySelector("[data-enabled-count]");
537
+ const appCountEl = document.querySelector("[data-app-count]");
538
+ const disableAllButton = document.querySelector("[data-disable-all]");
539
+
540
+ const escapeHtml = (value) => String(value || "")
541
+ .replace(/&/g, "&amp;")
542
+ .replace(/</g, "&lt;")
543
+ .replace(/>/g, "&gt;")
544
+ .replace(/"/g, "&quot;")
545
+ .replace(/'/g, "&#39;");
546
+
547
+ const formatAppFolderPath = (app) => {
548
+ const raw = String((app && (app.workspace_path || app.launcher_path || app.id)) || "").replace(/\\/g, "/");
549
+ if (!raw) return "";
550
+ const pinokioApiMatch = raw.match(/\/pinokio\/api\/(.+)$/i);
551
+ if (pinokioApiMatch && pinokioApiMatch[1]) {
552
+ return `~/pinokio/api/${pinokioApiMatch[1]}`;
553
+ }
554
+ return raw.replace(/^\/Users\/[^/]+/, "~");
555
+ };
556
+
557
+ const currentApp = () => autolaunchApps.find((app) => app.id === selectedAppId) || null;
558
+ const enabledCount = () => autolaunchApps.filter((app) => app.autolaunch_enabled).length;
559
+ const compareApps = (a, b) => {
560
+ const enabledDelta = Number(!!b.autolaunch_enabled) - Number(!!a.autolaunch_enabled);
561
+ if (enabledDelta !== 0) return enabledDelta;
562
+ const aTitle = a.title || a.name || a.id || "";
563
+ const bTitle = b.title || b.name || b.id || "";
564
+ return aTitle.localeCompare(bTitle, undefined, { sensitivity: "base", numeric: true }) ||
565
+ String(a.id || "").localeCompare(String(b.id || ""), undefined, { sensitivity: "base", numeric: true });
566
+ };
567
+ const orderedApps = () => autolaunchApps.slice().sort(compareApps);
568
+
569
+ const updateStats = () => {
570
+ enabledCountEl.textContent = String(enabledCount());
571
+ appCountEl.textContent = String(autolaunchApps.length);
572
+ disableAllButton.disabled = enabledCount() === 0;
573
+ };
574
+
575
+ const setFeedback = (message, isError = false) => {
576
+ const feedback = detailEl.querySelector("[data-feedback]");
577
+ if (!feedback) return;
578
+ feedback.textContent = message || "";
579
+ feedback.style.color = isError ? "#b42318" : "";
580
+ };
581
+
582
+ const syncAppState = (updatedApp) => {
583
+ if (!updatedApp || !updatedApp.id) return;
584
+ autolaunchApps = autolaunchApps.map((app) => app.id === updatedApp.id ? updatedApp : app);
585
+ updateStats();
586
+ renderAppList();
587
+ };
588
+
589
+ const setDetailBusy = (busy) => {
590
+ detailEl.querySelectorAll("[data-autolaunch-switch], [data-apply-manual]").forEach((control) => {
591
+ control.disabled = !!busy;
592
+ });
593
+ };
594
+
595
+ const renderAppList = () => {
596
+ const query = (searchEl.value || "").trim().toLowerCase();
597
+ const visible = orderedApps().filter((app) => {
598
+ const haystack = `${app.title || ""} ${app.id || ""} ${app.description || ""} ${app.workspace_path || ""} ${app.launcher_path || ""}`.toLowerCase();
599
+ return !query || haystack.includes(query);
600
+ });
601
+ appListEl.innerHTML = "";
602
+ if (visible.length === 0) {
603
+ const empty = document.createElement("div");
604
+ empty.className = "autolaunch-empty";
605
+ empty.textContent = "No apps match that search.";
606
+ appListEl.appendChild(empty);
607
+ return;
608
+ }
609
+ visible.forEach((app) => {
610
+ const folderPath = formatAppFolderPath(app);
611
+ const fullFolderPath = app.workspace_path || app.launcher_path || folderPath;
612
+ const scriptLine = app.autolaunch_enabled && app.autolaunch
613
+ ? `<div class="autolaunch-app-script">Runs ${escapeHtml(app.autolaunch)}</div>`
614
+ : "";
615
+ const button = document.createElement("button");
616
+ button.type = "button";
617
+ button.className = `autolaunch-app-row${app.id === selectedAppId ? " selected" : ""}`;
618
+ button.dataset.appId = app.id;
619
+ button.innerHTML = `
620
+ <img src="${escapeHtml(app.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
621
+ <div class="autolaunch-app-meta">
622
+ <div class="autolaunch-app-name">${escapeHtml(app.title || app.id)}</div>
623
+ <div class="autolaunch-app-folder" title="${escapeHtml(fullFolderPath)}">${escapeHtml(folderPath)}</div>
624
+ ${scriptLine}
625
+ </div>
626
+ <span class="autolaunch-pill ${app.autolaunch_enabled ? "on" : ""}">${app.autolaunch_enabled ? "ON" : "OFF"}</span>
627
+ `;
628
+ button.addEventListener("click", () => selectApp(app.id));
629
+ appListEl.appendChild(button);
630
+ });
631
+ };
632
+
633
+ const renderDetailShell = (app, bodyHtml) => {
634
+ const checked = app.autolaunch_enabled ? "true" : "false";
635
+ const label = app.autolaunch_enabled ? "ON" : "OFF";
636
+ detailEl.innerHTML = `
637
+ <div class="autolaunch-detail-header">
638
+ <img class="autolaunch-detail-icon" src="${escapeHtml(app.icon || "/pinokio-black.png")}" alt="" onerror="this.onerror=null; this.src='/pinokio-black.png'">
639
+ <div>
640
+ <h2 class="autolaunch-detail-title">${escapeHtml(app.title || app.id)}</h2>
641
+ <div class="autolaunch-current">${escapeHtml(app.workspace_path || app.id)}</div>
642
+ </div>
643
+ <button type="button" class="autolaunch-switch" role="switch" aria-checked="${checked}" data-autolaunch-switch ${savingAutolaunch ? "disabled" : ""}>
644
+ <span class="autolaunch-switch-track" aria-hidden="true"><span class="autolaunch-switch-thumb"></span></span>
645
+ <span class="autolaunch-switch-label">${label}</span>
646
+ </button>
647
+ </div>
648
+ ${bodyHtml}
649
+ `;
650
+ };
651
+
652
+ const renderLoading = (app) => {
653
+ renderDetailShell(app, `
654
+ <div class="autolaunch-section">
655
+ <div class="autolaunch-loading"><i class="fa-solid fa-circle-notch fa-spin"></i> Loading script choices...</div>
656
+ </div>
657
+ `);
658
+ };
659
+
660
+ const renderScriptSection = (title, note, candidates) => {
661
+ if (!Array.isArray(candidates) || candidates.length === 0) {
662
+ return "";
663
+ }
664
+ const rows = candidates.map((candidate) => {
665
+ const checked = candidate.script === selectedScript ? "checked" : "";
666
+ const selected = candidate.script === selectedScript ? " selected" : "";
667
+ const icon = candidate.icon || "fa-regular fa-file-code";
668
+ const tags = [
669
+ candidate.menu_default ? '<span class="autolaunch-tag">Default</span>' : "",
670
+ candidate.has_params ? '<span class="autolaunch-tag">Params ignored</span>' : ""
671
+ ].filter(Boolean).join("");
672
+ const group = candidate.group ? `<div class="autolaunch-script-path">${escapeHtml(candidate.group)}</div>` : "";
673
+ return `
674
+ <label class="autolaunch-script-option${selected}">
675
+ <input type="radio" name="autolaunch-script" value="${escapeHtml(candidate.script)}" ${checked}>
676
+ <i class="${escapeHtml(icon)}"></i>
677
+ <div>
678
+ <div class="autolaunch-script-title"><span>${escapeHtml(candidate.label)}</span>${tags}</div>
679
+ <div class="autolaunch-script-path">${escapeHtml(candidate.script)}</div>
680
+ ${group}
681
+ </div>
682
+ </label>
683
+ `;
684
+ }).join("");
685
+ return `
686
+ <div class="autolaunch-section">
687
+ <div class="autolaunch-section-header">
688
+ <h3 class="autolaunch-section-title">${escapeHtml(title)}</h3>
689
+ <span class="autolaunch-section-note">${escapeHtml(note)}</span>
690
+ </div>
691
+ <div class="autolaunch-script-list">${rows}</div>
692
+ </div>
693
+ `;
694
+ };
695
+
696
+ const chooseInitialScript = (data) => {
697
+ const all = [].concat(data.menu || [], data.other || []);
698
+ if (data.current) {
699
+ return data.current;
700
+ }
701
+ const defaultCandidate = all.find((candidate) => candidate.menu_default);
702
+ if (defaultCandidate) {
703
+ return defaultCandidate.script;
704
+ }
705
+ return all.length > 0 ? all[0].script : (data.current || "");
706
+ };
707
+
708
+ const bindDetailControls = () => {
709
+ detailEl.querySelectorAll("input[name='autolaunch-script']").forEach((input) => {
710
+ input.addEventListener("change", () => {
711
+ selectedScript = input.value;
712
+ const manual = detailEl.querySelector("[data-manual-script]");
713
+ if (manual) manual.value = selectedScript;
714
+ renderCandidates();
715
+ const app = currentApp();
716
+ if (app && app.autolaunch_enabled) {
717
+ saveScript(selectedScript);
718
+ }
719
+ });
720
+ });
721
+ const toggle = detailEl.querySelector("[data-autolaunch-switch]");
722
+ if (toggle) toggle.addEventListener("click", toggleAutolaunch);
723
+ const manual = detailEl.querySelector("[data-manual-script]");
724
+ if (manual) {
725
+ manual.addEventListener("input", () => {
726
+ selectedScript = manual.value.trim();
727
+ setFeedback("");
728
+ const applyManual = detailEl.querySelector("[data-apply-manual]");
729
+ if (applyManual) applyManual.disabled = !selectedScript || savingAutolaunch;
730
+ detailEl.querySelectorAll("input[name='autolaunch-script']").forEach((input) => {
731
+ input.checked = input.value === selectedScript;
732
+ });
733
+ detailEl.querySelectorAll(".autolaunch-script-option").forEach((option) => {
734
+ const input = option.querySelector("input");
735
+ option.classList.toggle("selected", !!input && input.checked);
736
+ });
737
+ });
738
+ manual.addEventListener("keydown", (event) => {
739
+ if (event.key === "Enter") {
740
+ event.preventDefault();
741
+ saveSelectedScript();
742
+ }
743
+ });
744
+ }
745
+ const applyManual = detailEl.querySelector("[data-apply-manual]");
746
+ if (applyManual) applyManual.addEventListener("click", saveSelectedScript);
747
+ };
748
+
749
+ const renderCandidates = () => {
750
+ const app = currentApp();
751
+ if (!app || !candidateState) return;
752
+ const hasCandidates = (candidateState.menu && candidateState.menu.length) || (candidateState.other && candidateState.other.length);
753
+ const currentValue = candidateState.current || "";
754
+ renderDetailShell(app, `
755
+ <div class="autolaunch-section">
756
+ <div class="autolaunch-section-header">
757
+ <h3 class="autolaunch-section-title">Current</h3>
758
+ <span class="autolaunch-section-note">Takes effect next time Pinokio starts</span>
759
+ </div>
760
+ <div class="autolaunch-current">${escapeHtml(currentValue || "OFF")}</div>
761
+ <div class="autolaunch-feedback" data-feedback></div>
762
+ </div>
763
+ ${hasCandidates ? "" : '<div class="autolaunch-section"><div class="autolaunch-empty">No local scripts were found for this app.</div></div>'}
764
+ ${renderScriptSection("Menu scripts", "From pinokio.js", candidateState.menu || [])}
765
+ ${renderScriptSection("Other local scripts", "Advanced", candidateState.other || [])}
766
+ <div class="autolaunch-manual">
767
+ <div class="autolaunch-section-header">
768
+ <h3 class="autolaunch-section-title">Manual relative path</h3>
769
+ <span class="autolaunch-section-note">Advanced</span>
770
+ </div>
771
+ <div class="autolaunch-manual-row">
772
+ <input type="text" data-manual-script value="${escapeHtml(selectedScript)}" placeholder="start.js">
773
+ <button type="button" class="task-button" data-apply-manual ${selectedScript ? "" : "disabled"}>
774
+ <i class="fa-solid fa-check"></i>
775
+ Apply
776
+ </button>
777
+ </div>
778
+ </div>
779
+ `);
780
+ bindDetailControls();
781
+ };
782
+
783
+ const toggleAutolaunch = () => {
784
+ const app = currentApp();
785
+ if (!app || savingAutolaunch) return;
786
+ if (app.autolaunch_enabled) {
787
+ saveScript("");
788
+ return;
789
+ }
790
+ const manual = detailEl.querySelector("[data-manual-script]");
791
+ const script = manual && manual.value.trim() ? manual.value.trim() : selectedScript;
792
+ if (!script) {
793
+ setFeedback("Choose a script first.", true);
794
+ return;
795
+ }
796
+ selectedScript = script;
797
+ saveScript(script);
798
+ };
799
+
800
+ const loadCandidates = async (appId) => {
801
+ const app = currentApp();
802
+ if (!app) return;
803
+ candidateState = null;
804
+ selectedScript = "";
805
+ renderLoading(app);
806
+ try {
807
+ const response = await fetch(`/autolaunch/candidates?app=${encodeURIComponent(appId)}`, {
808
+ headers: { "Accept": "application/json" }
809
+ });
810
+ const data = await response.json();
811
+ if (!response.ok || !data.ok) {
812
+ throw new Error(data.error || "Failed to load script choices.");
813
+ }
814
+ candidateState = data;
815
+ syncAppState(data.app);
816
+ selectedScript = chooseInitialScript(data);
817
+ renderCandidates();
818
+ } catch (error) {
819
+ renderDetailShell(app, `
820
+ <div class="autolaunch-section">
821
+ <div class="autolaunch-empty">${escapeHtml(error && error.message ? error.message : "Failed to load script choices.")}</div>
822
+ </div>
823
+ `);
824
+ }
825
+ };
826
+
827
+ const selectApp = (appId) => {
828
+ selectedAppId = appId;
829
+ renderAppList();
830
+ loadCandidates(appId);
831
+ };
832
+
833
+ const saveScript = async (script) => {
834
+ const app = currentApp();
835
+ if (!app || savingAutolaunch) return;
836
+ savingAutolaunch = true;
837
+ setDetailBusy(true);
838
+ setFeedback(script ? "Saving..." : "Disabling...");
839
+ try {
840
+ const response = await fetch("/autolaunch", {
841
+ method: "POST",
842
+ headers: {
843
+ "Content-Type": "application/json",
844
+ "Accept": "application/json"
845
+ },
846
+ body: JSON.stringify({
847
+ app: app.id,
848
+ script
849
+ })
850
+ });
851
+ const data = await response.json();
852
+ if (!response.ok || !data.ok) {
853
+ throw new Error(data.error || "Failed to save autolaunch.");
854
+ }
855
+ syncAppState(data.app);
856
+ if (candidateState) {
857
+ candidateState.current = data.app.autolaunch || "";
858
+ }
859
+ selectedScript = data.app.autolaunch || selectedScript;
860
+ renderCandidates();
861
+ setFeedback(data.app.autolaunch_enabled ? "Saved." : "Disabled.");
862
+ } catch (error) {
863
+ setFeedback(error && error.message ? error.message : "Failed to save autolaunch.", true);
864
+ } finally {
865
+ savingAutolaunch = false;
866
+ setDetailBusy(false);
867
+ }
868
+ };
869
+
870
+ const saveSelectedScript = () => {
871
+ const manual = detailEl.querySelector("[data-manual-script]");
872
+ const script = manual ? manual.value.trim() : selectedScript;
873
+ if (!script) {
874
+ setFeedback("Choose a script first.", true);
875
+ return;
876
+ }
877
+ saveScript(script);
878
+ };
879
+
880
+ const disableAll = async () => {
881
+ if (!window.confirm("Disable autolaunch for every installed app?")) {
882
+ return;
883
+ }
884
+ disableAllButton.disabled = true;
885
+ try {
886
+ const response = await fetch("/autolaunch/disable-all", {
887
+ method: "POST",
888
+ headers: { "Accept": "application/json" }
889
+ });
890
+ const data = await response.json();
891
+ if (!response.ok || !data.ok) {
892
+ throw new Error(data.error || "Failed to disable autolaunch.");
893
+ }
894
+ autolaunchApps = data.apps || autolaunchApps.map((app) => ({ ...app, autolaunch: "", autolaunch_enabled: false }));
895
+ updateStats();
896
+ renderAppList();
897
+ if (selectedAppId) {
898
+ loadCandidates(selectedAppId);
899
+ }
900
+ } catch (error) {
901
+ window.alert(error && error.message ? error.message : "Failed to disable autolaunch.");
902
+ updateStats();
903
+ }
904
+ };
905
+
906
+ searchEl.addEventListener("input", renderAppList);
907
+ disableAllButton.addEventListener("click", disableAll);
908
+
909
+ updateStats();
910
+ renderAppList();
911
+ const firstApp = orderedApps()[0];
912
+ if (firstApp) {
913
+ selectApp(firstApp.id);
914
+ }
915
+ </script>
916
+ </body>
917
+ </html>