pinokiod 7.1.13 → 7.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/kernel/index.js CHANGED
@@ -906,6 +906,11 @@ class Kernel {
906
906
  /// })
907
907
  /// }
908
908
  async init(options) {
909
+ // Re-arm startup readiness on every init cycle so restart waits for the
910
+ // newly rebuilt template/sysinfo state instead of a stale resolved promise.
911
+ this.sysReady = new Promise((resolve) => {
912
+ this._resolveSysReady = resolve
913
+ })
909
914
 
910
915
  let home = this.store.get("home") || process.env.PINOKIO_HOME
911
916
  this.homedir = home
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.1.13",
3
+ "version": "7.1.15",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -970,12 +970,11 @@ class Server {
970
970
  console.log("Chrome")
971
971
 
972
972
  let name = req.params.name
973
- const binCheckLabel = `bin check ${name} ${process.hrtime.bigint()}`
974
- console.time(binCheckLabel)
973
+ console.time("bin check")
975
974
  let { requirements, install_required, requirements_pending, error } = await this.kernel.bin.check({
976
975
  bin: this.kernel.bin.preset("dev"),
977
976
  })
978
- console.timeEnd(binCheckLabel)
977
+ console.timeEnd("bin check")
979
978
  if (!requirements_pending && install_required) {
980
979
  res.redirect(`/setup/dev?callback=${req.originalUrl}`)
981
980
  return
@@ -4765,8 +4764,12 @@ class Server {
4765
4764
  // }
4766
4765
  }
4767
4766
  }
4768
- async terminals(filepath) {
4769
- let venvs = await Util.find_venv(filepath)
4767
+ async terminals(filepath, options = {}) {
4768
+ const includeVenvs = options.includeVenvs !== false
4769
+ let venvs = []
4770
+ if (includeVenvs) {
4771
+ venvs = await Util.find_venv(filepath)
4772
+ }
4770
4773
  let terminal
4771
4774
  const windowsBashPath = await this.resolveWindowsBashPath()
4772
4775
  const hasWindowsBashOption = this.kernel.platform === "win32" && typeof windowsBashPath === "string" && windowsBashPath.length > 0
@@ -4845,6 +4848,107 @@ class Server {
4845
4848
  }
4846
4849
  return terminal
4847
4850
  }
4851
+ async resolveDevTerminalShell(shellKey) {
4852
+ if (this.kernel.platform === "win32") {
4853
+ if (shellKey === "cmd") {
4854
+ return {
4855
+ key: "cmd",
4856
+ title: "Cmd",
4857
+ icon: "fa-brands fa-windows",
4858
+ groupIndex: 0,
4859
+ shellPath: null,
4860
+ }
4861
+ }
4862
+ if (shellKey === "bash") {
4863
+ const windowsBashPath = await this.resolveWindowsBashPath()
4864
+ if (typeof windowsBashPath === "string" && windowsBashPath.length > 0) {
4865
+ return {
4866
+ key: "bash",
4867
+ title: "Bash",
4868
+ icon: "fa-solid fa-terminal",
4869
+ groupIndex: 1,
4870
+ shellPath: windowsBashPath,
4871
+ }
4872
+ }
4873
+ }
4874
+ return null
4875
+ }
4876
+ if (shellKey === "bash") {
4877
+ return {
4878
+ key: "bash",
4879
+ title: "Bash",
4880
+ icon: "fa-solid fa-terminal",
4881
+ groupIndex: 0,
4882
+ shellPath: null,
4883
+ }
4884
+ }
4885
+ return null
4886
+ }
4887
+ async devTerminals(filepath, refPath) {
4888
+ const shellKeys = this.kernel.platform === "win32" ? ["cmd", "bash"] : ["bash"]
4889
+ const menu = []
4890
+ for (const shellKey of shellKeys) {
4891
+ const shell = await this.resolveDevTerminalShell(shellKey)
4892
+ if (!shell) {
4893
+ continue
4894
+ }
4895
+ menu.push({
4896
+ icon: shell.icon,
4897
+ title: shell.title,
4898
+ subtitle: "Open shell options",
4899
+ shell_key: shell.key,
4900
+ options_url: `/pinokio/d-terminal-options/${refPath}?shell=${encodeURIComponent(shell.key)}`,
4901
+ })
4902
+ }
4903
+ return {
4904
+ icon: "fa-solid fa-terminal",
4905
+ title: "Terminals",
4906
+ subtitle: "Choose a shell, then open it with or without Python activated.",
4907
+ skip_sort: true,
4908
+ menu,
4909
+ }
4910
+ }
4911
+ async devTerminalOptions(filepath, shellKey) {
4912
+ const shell = await this.resolveDevTerminalShell(shellKey)
4913
+ if (!shell) {
4914
+ return []
4915
+ }
4916
+ const shellOptions = [
4917
+ this.renderShell(filepath, shell.groupIndex, 0, {
4918
+ icon: shell.icon,
4919
+ title: "Shell",
4920
+ subtitle: `Open a plain ${shell.title} shell`,
4921
+ text: "Shell",
4922
+ type: "Start",
4923
+ shell: {
4924
+ ...(shell.shellPath ? { shell: shell.shellPath } : {}),
4925
+ input: true,
4926
+ }
4927
+ })
4928
+ ]
4929
+ const venvs = await Util.find_venv(filepath)
4930
+ for (let i = 0; i < venvs.length; i++) {
4931
+ const venv = venvs[i]
4932
+ const parsed = path.parse(venv)
4933
+ let relativeVenv = path.relative(filepath, venv)
4934
+ if (!relativeVenv || relativeVenv.startsWith("..")) {
4935
+ relativeVenv = parsed.base || path.basename(venv)
4936
+ }
4937
+ shellOptions.push(this.renderShell(filepath, shell.groupIndex, i + 1, {
4938
+ icon: "fa-brands fa-python",
4939
+ title: "Python Shell",
4940
+ subtitle: `Activates ${relativeVenv}`,
4941
+ text: `Python Shell: ${relativeVenv}`,
4942
+ type: "Start",
4943
+ shell: {
4944
+ ...(shell.shellPath ? { shell: shell.shellPath } : {}),
4945
+ venv,
4946
+ input: true,
4947
+ }
4948
+ }))
4949
+ }
4950
+ return shellOptions
4951
+ }
4848
4952
  async getPluginGlobal(req, config, terminal, filepath) {
4849
4953
  // if (!this.kernel.plugin.config) {
4850
4954
  // await this.kernel.plugin.init()
@@ -10766,8 +10870,8 @@ class Server {
10766
10870
  }))
10767
10871
  this.app.get("/d/*", ex(async (req, res) => {
10768
10872
  let filepath = Util.u2p(req.params[0])
10769
- let terminal = await this.terminals(filepath)
10770
- let plugin = await this.getPluginGlobal(req, this.kernel.plugin.config, terminal, filepath)
10873
+ let terminal = await this.devTerminals(filepath, req.params[0])
10874
+ let plugin = await this.getPluginGlobal(req, this.kernel.plugin.config, { menu: [] }, filepath)
10771
10875
  let html = ""
10772
10876
  let plugin_menu
10773
10877
  try {
@@ -10779,7 +10883,7 @@ class Server {
10779
10883
  let current_urls = await this.current_urls(req.originalUrl.slice(1))
10780
10884
  let retry = false
10781
10885
  // if plugin_menu is empty, try again in 1 sec
10782
- if (plugin_menu.length === 0) {
10886
+ if (plugin_menu.length === 0 && (!terminal.menu || terminal.menu.length === 0)) {
10783
10887
  retry = true
10784
10888
  }
10785
10889
 
@@ -10867,7 +10971,6 @@ class Server {
10867
10971
  // let online_terminal = await this.getPluginGlobal(req, terminal, filepath)
10868
10972
  // console.log("online_terminal", online_terminal)
10869
10973
  terminal.menus = href_menus
10870
- sortNestedMenus(terminal.menu)
10871
10974
  sortNestedMenus(terminal.menus)
10872
10975
  let dynamic = [
10873
10976
  terminal,
@@ -10889,7 +10992,7 @@ class Server {
10889
10992
  },
10890
10993
  ]
10891
10994
  for (const item of dynamic) {
10892
- if (item && Array.isArray(item.menu)) {
10995
+ if (item && Array.isArray(item.menu) && !item.skip_sort) {
10893
10996
  sortNestedMenus(item.menu)
10894
10997
  }
10895
10998
  }
@@ -11051,6 +11154,23 @@ class Server {
11051
11154
  res.send("")
11052
11155
  }
11053
11156
  }))
11157
+ this.app.get("/pinokio/d-terminal-options/*", ex(async (req, res) => {
11158
+ let filepath = Util.u2p(req.params[0])
11159
+ const shellKey = typeof req.query.shell === "string" ? req.query.shell.trim().toLowerCase() : ""
11160
+ let options = await this.devTerminalOptions(filepath, shellKey)
11161
+ const html = await new Promise((resolve, reject) => {
11162
+ ejs.renderFile(path.resolve(__dirname, "views/partials/d_terminal_options.ejs"), {
11163
+ options,
11164
+ }, (err, html) => {
11165
+ if (err) {
11166
+ reject(err)
11167
+ return
11168
+ }
11169
+ resolve(html)
11170
+ })
11171
+ })
11172
+ res.send(html)
11173
+ }))
11054
11174
  this.app.get("/pinokio/dynamic/:name", ex(async (req, res) => {
11055
11175
  // await this.kernel.plugin.init()
11056
11176
 
@@ -3928,9 +3928,6 @@ document.addEventListener("DOMContentLoaded", () => {
3928
3928
  }
3929
3929
  const fallbackUrl = buildAskAiLaunchUrl(tool.href, workspaceCwd);
3930
3930
  if (fallbackUrl) {
3931
- refreshTerminalSessions(fallbackUrl, workspaceCwd, {
3932
- retryDelays: []
3933
- });
3934
3931
  window.location.href = fallbackUrl;
3935
3932
  }
3936
3933
  }
@@ -5,42 +5,56 @@ const installname = async (url, name, options) => {
5
5
  if (!defaultName.endsWith(".git")) {
6
6
  defaultName = defaultName + ".git"
7
7
  }
8
- // defaultName = defaultName.split(".")[0]
8
+ const normalizedPath = options && options.path ? normalizeInstallPath(options.path) : null
9
+ const relativePath = normalizedPath || DEFAULT_INSTALL_RELATIVE_PATH
10
+ const inputValue = name || defaultName
9
11
  let result = await Swal.fire({
10
12
  title: 'Save as',
11
- html: '<input id="swal-input1" class="swal2-input" placeholder="Name">',
13
+ html: `<p class="pinokio-download-note">Saved in <code>~/${relativePath}</code></p>`,
14
+ input: 'text',
15
+ inputLabel: 'Folder name',
16
+ inputValue,
17
+ inputPlaceholder: defaultName,
18
+ inputAttributes: {
19
+ autocapitalize: 'off',
20
+ autocorrect: 'off',
21
+ autocomplete: 'off',
22
+ spellcheck: 'false'
23
+ },
12
24
  focusConfirm: false,
13
25
  focusCancel: false,
14
26
  showCancelButton: true,
15
27
  showCloseButton: true,
16
28
  cancelButtonText: 'Cancel',
17
29
  confirmButtonText: 'Download',
30
+ buttonsStyling: false,
31
+ backdrop: 'rgba(9, 11, 15, 0.65)',
32
+ width: 'min(460px, 92vw)',
18
33
  allowOutsideClick: () => !Swal.isLoading(),
19
34
  allowEscapeKey: true,
20
35
  showLoaderOnConfirm: true,
36
+ loaderHtml: '<span class="pinokio-download-loader-spinner" aria-hidden="true"></span><span class="pinokio-download-loader-text">Downloading...</span>',
21
37
  customClass: {
22
- popup: 'pinokio-download-modal'
38
+ popup: 'pinokio-download-modal',
39
+ htmlContainer: 'pinokio-download-html',
40
+ inputLabel: 'pinokio-download-label',
41
+ input: 'pinokio-download-input',
42
+ validationMessage: 'pinokio-download-validation',
43
+ actions: 'pinokio-download-actions',
44
+ loader: 'pinokio-download-loader',
45
+ closeButton: 'pinokio-download-close',
46
+ confirmButton: 'pinokio-download-confirm',
47
+ cancelButton: 'pinokio-download-cancel'
23
48
  },
24
49
  didOpen: () => {
25
- let input = Swal.getPopup().querySelector('#swal-input1')
26
- if (name) {
27
- input.value = name
28
- } else {
29
- input.value = defaultName;
30
- }
31
- input.addEventListener("keypress", (e) => {
32
- if (e.key === "Enter") {
33
- e.preventDefault()
34
- e.stopPropagation()
35
- Swal.clickConfirm()
36
- }
37
- })
38
- setTimeout(() => {
50
+ const input = Swal.getInput()
51
+ if (input) {
39
52
  input.focus()
40
- }, 0)
53
+ input.select()
54
+ }
41
55
  },
42
- preConfirm: async () => {
43
- const folderName = (Swal.getPopup().querySelector("#swal-input1").value || "").trim()
56
+ preConfirm: async (value) => {
57
+ const folderName = String(value || "").trim()
44
58
  const validationError = validateInstallFolderName(folderName)
45
59
  if (validationError) {
46
60
  Swal.showValidationMessage(validationError)
@@ -2248,23 +2248,221 @@ body.dark .swal2-title {
2248
2248
  background: rgba(0,0,0,0.8) !important;
2249
2249
  */
2250
2250
  }
2251
- .pinokio-download-modal .swal2-actions {
2252
- justify-content: flex-start !important;
2253
- padding: 6px 15px 16px !important;
2251
+ .swal2-popup.pinokio-download-modal {
2252
+ border-radius: 14px !important;
2253
+ padding: 18px !important;
2254
+ background: #ffffff !important;
2255
+ color: #0f172a !important;
2256
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
2257
+ box-shadow: 0 24px 60px rgba(15, 23, 42, 0.18) !important;
2258
+ }
2259
+ body.dark .swal2-popup.pinokio-download-modal {
2260
+ background: #0f172a !important;
2261
+ color: #e2e8f0 !important;
2262
+ border: 1px solid rgba(148, 163, 184, 0.24) !important;
2263
+ box-shadow: 0 32px 90px rgba(2, 8, 23, 0.72) !important;
2264
+ }
2265
+ .swal2-popup.pinokio-download-modal .swal2-title {
2266
+ margin: 0 40px 6px 0 !important;
2267
+ padding: 0 !important;
2268
+ text-align: left !important;
2269
+ font-size: 20px !important;
2270
+ font-weight: 700 !important;
2271
+ line-height: 1.2 !important;
2272
+ letter-spacing: -0.02em !important;
2273
+ color: #0f172a !important;
2254
2274
  }
2255
- .pinokio-download-modal .swal2-close {
2256
- position: absolute;
2257
- top: 12px;
2258
- right: 12px;
2259
- width: 34px;
2260
- height: 34px;
2261
- line-height: 34px;
2275
+ body.dark .swal2-popup.pinokio-download-modal .swal2-title {
2276
+ color: #f8fafc !important;
2277
+ }
2278
+ .pinokio-download-html.swal2-html-container {
2279
+ margin: 0 0 12px 0 !important;
2280
+ padding: 0 !important;
2281
+ text-align: left !important;
2282
+ font-size: 13px !important;
2283
+ line-height: 1.45 !important;
2284
+ color: rgba(71, 85, 105, 0.84) !important;
2285
+ }
2286
+ body.dark .pinokio-download-html.swal2-html-container {
2287
+ color: rgba(148, 163, 184, 0.88) !important;
2288
+ }
2289
+ .pinokio-download-note {
2262
2290
  margin: 0;
2263
- font-size: 28px;
2264
- color: rgba(0,0,0,0.55);
2265
2291
  }
2266
- body.dark .pinokio-download-modal .swal2-close {
2267
- color: rgba(255,255,255,0.6);
2292
+ .pinokio-download-note code {
2293
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
2294
+ font-size: 12px;
2295
+ padding: 2px 6px;
2296
+ border-radius: 999px;
2297
+ background: rgba(15, 23, 42, 0.06);
2298
+ color: inherit;
2299
+ }
2300
+ body.dark .pinokio-download-note code {
2301
+ background: rgba(148, 163, 184, 0.16);
2302
+ }
2303
+ .pinokio-download-label.swal2-input-label {
2304
+ display: block !important;
2305
+ width: 100% !important;
2306
+ margin: 0 0 6px 0 !important;
2307
+ text-align: left !important;
2308
+ font-size: 12px !important;
2309
+ font-weight: 600 !important;
2310
+ line-height: 1.3 !important;
2311
+ color: rgba(15, 23, 42, 0.78) !important;
2312
+ }
2313
+ body.dark .pinokio-download-label.swal2-input-label {
2314
+ color: rgba(241, 245, 249, 0.84) !important;
2315
+ }
2316
+ .pinokio-download-input.swal2-input {
2317
+ width: 100% !important;
2318
+ margin: 0 !important;
2319
+ height: 40px !important;
2320
+ padding: 0 12px !important;
2321
+ border-radius: 10px !important;
2322
+ border: 1px solid rgba(148, 163, 184, 0.58) !important;
2323
+ background: #ffffff !important;
2324
+ color: #0f172a !important;
2325
+ box-shadow: none !important;
2326
+ font-size: 14px !important;
2327
+ line-height: 1.2 !important;
2328
+ }
2329
+ .pinokio-download-input.swal2-input:focus {
2330
+ border-color: rgba(59, 130, 246, 0.78) !important;
2331
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.16) !important;
2332
+ }
2333
+ body.dark .pinokio-download-input.swal2-input {
2334
+ border-color: rgba(148, 163, 184, 0.45) !important;
2335
+ background: rgba(15, 23, 42, 0.65) !important;
2336
+ color: rgba(248, 250, 252, 0.96) !important;
2337
+ }
2338
+ .pinokio-download-actions.swal2-actions {
2339
+ width: 100% !important;
2340
+ justify-content: flex-end !important;
2341
+ gap: 8px !important;
2342
+ margin-top: 14px !important;
2343
+ padding: 0 !important;
2344
+ }
2345
+ .pinokio-download-actions.swal2-actions.swal2-loading {
2346
+ justify-content: flex-end !important;
2347
+ gap: 8px !important;
2348
+ }
2349
+ .pinokio-download-confirm.swal2-confirm,
2350
+ .pinokio-download-cancel.swal2-cancel {
2351
+ min-width: 92px !important;
2352
+ height: 36px !important;
2353
+ margin: 0 !important;
2354
+ padding: 0 14px !important;
2355
+ border-radius: 10px !important;
2356
+ font-size: 13px !important;
2357
+ font-weight: 650 !important;
2358
+ }
2359
+ .swal2-popup.pinokio-download-modal .pinokio-download-confirm.swal2-confirm {
2360
+ border: none !important;
2361
+ background: royalblue !important;
2362
+ color: #ffffff !important;
2363
+ }
2364
+ .swal2-popup.pinokio-download-modal .pinokio-download-confirm.swal2-confirm:hover {
2365
+ background: #315cd8 !important;
2366
+ }
2367
+ .swal2-popup.pinokio-download-modal .pinokio-download-cancel.swal2-cancel {
2368
+ border: 1px solid rgba(148, 163, 184, 0.6) !important;
2369
+ background: #ffffff !important;
2370
+ color: #0f172a !important;
2371
+ }
2372
+ body.dark .swal2-popup.pinokio-download-modal .pinokio-download-cancel.swal2-cancel {
2373
+ border-color: rgba(148, 163, 184, 0.52) !important;
2374
+ background: rgba(15, 23, 42, 0.64) !important;
2375
+ color: rgba(248, 250, 252, 0.96) !important;
2376
+ }
2377
+ .swal2-popup.pinokio-download-modal .pinokio-download-cancel.swal2-cancel:hover {
2378
+ background: rgba(148, 163, 184, 0.12) !important;
2379
+ }
2380
+ body.dark .swal2-popup.pinokio-download-modal .pinokio-download-cancel.swal2-cancel:hover {
2381
+ background: rgba(148, 163, 184, 0.18) !important;
2382
+ }
2383
+ .pinokio-download-close.swal2-close {
2384
+ position: absolute !important;
2385
+ top: 14px !important;
2386
+ right: 14px !important;
2387
+ width: 28px !important;
2388
+ height: 28px !important;
2389
+ line-height: 28px !important;
2390
+ margin: 0 !important;
2391
+ font-size: 20px !important;
2392
+ color: rgba(71, 85, 105, 0.7) !important;
2393
+ border-radius: 999px !important;
2394
+ display: flex !important;
2395
+ align-items: center !important;
2396
+ justify-content: center !important;
2397
+ transition: background 0.2s ease, color 0.2s ease !important;
2398
+ }
2399
+ .pinokio-download-close.swal2-close:hover {
2400
+ background: rgba(148, 163, 184, 0.14) !important;
2401
+ color: #0f172a !important;
2402
+ }
2403
+ body.dark .pinokio-download-close.swal2-close {
2404
+ color: rgba(226, 232, 240, 0.72) !important;
2405
+ }
2406
+ body.dark .pinokio-download-close.swal2-close:hover {
2407
+ background: rgba(148, 163, 184, 0.16) !important;
2408
+ color: #f8fafc !important;
2409
+ }
2410
+ .pinokio-download-validation.swal2-validation-message {
2411
+ margin: 10px 0 0 0 !important;
2412
+ padding: 10px 12px !important;
2413
+ border-radius: 10px !important;
2414
+ background: rgba(255, 66, 66, 0.1) !important;
2415
+ border: 1px solid rgba(255, 66, 66, 0.18) !important;
2416
+ border-left: 4px solid rgba(255, 66, 66, 0.5) !important;
2417
+ color: rgba(15, 23, 42, 0.92) !important;
2418
+ font-size: 13px !important;
2419
+ line-height: 1.4 !important;
2420
+ justify-content: flex-start !important;
2421
+ align-items: flex-start !important;
2422
+ }
2423
+ .pinokio-download-validation.swal2-validation-message::before {
2424
+ display: none !important;
2425
+ }
2426
+ body.dark .pinokio-download-validation.swal2-validation-message {
2427
+ background: rgba(255, 66, 66, 0.16) !important;
2428
+ border-color: rgba(255, 66, 66, 0.24) !important;
2429
+ border-left-color: rgba(255, 66, 66, 0.68) !important;
2430
+ color: rgba(248, 250, 252, 0.94) !important;
2431
+ }
2432
+ .pinokio-download-loader.swal2-loader {
2433
+ align-items: center !important;
2434
+ justify-content: center !important;
2435
+ gap: 8px !important;
2436
+ width: auto !important;
2437
+ height: 36px !important;
2438
+ margin: 0 !important;
2439
+ padding: 0 14px !important;
2440
+ animation: none !important;
2441
+ border: none !important;
2442
+ border-radius: 10px !important;
2443
+ background: royalblue !important;
2444
+ color: #ffffff !important;
2445
+ }
2446
+ .pinokio-download-actions.swal2-actions.swal2-loading .pinokio-download-loader.swal2-loader {
2447
+ display: inline-flex !important;
2448
+ }
2449
+ .pinokio-download-loader-spinner {
2450
+ width: 14px;
2451
+ height: 14px;
2452
+ border: 2px solid rgba(255, 255, 255, 0.34);
2453
+ border-top-color: #ffffff;
2454
+ border-radius: 50%;
2455
+ animation: pinokio-download-spin 0.8s linear infinite;
2456
+ }
2457
+ .pinokio-download-loader-text {
2458
+ font-size: 13px;
2459
+ font-weight: 700;
2460
+ line-height: 1;
2461
+ }
2462
+ @keyframes pinokio-download-spin {
2463
+ to {
2464
+ transform: rotate(360deg);
2465
+ }
2268
2466
  }
2269
2467
  .swal2-validation-message {
2270
2468
  margin: 12px 15px 0;
@@ -5302,6 +5302,7 @@ header.navheader .mode-selector .community-mode-toggle {
5302
5302
  let interacted = false
5303
5303
  let global_selector
5304
5304
  let pendingSelectionRetry = null
5305
+ let initialWorkspaceDiskUsageRequested = false
5305
5306
  const scheduleSelectionRetry = (delay = 100) => {
5306
5307
  if (pendingSelectionRetry !== null) {
5307
5308
  return
@@ -5470,10 +5471,12 @@ header.navheader .mode-selector .community-mode-toggle {
5470
5471
  })
5471
5472
  let frame = document.createElement("iframe")
5472
5473
  frame.name = name
5473
- refreshTerminalSessions(href)
5474
5474
  frame.src = href
5475
5475
  frame.classList.add("selected")
5476
5476
  iframe_onerror(frame)
5477
+ frame.addEventListener("load", () => {
5478
+ requestInitialWorkspaceDiskUsage()
5479
+ }, { once: true })
5477
5480
  frame.setAttribute(
5478
5481
  "allow",
5479
5482
  "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 *;"
@@ -5484,17 +5487,6 @@ header.navheader .mode-selector .community-mode-toggle {
5484
5487
  document.querySelector("main").appendChild(frame)
5485
5488
  loaded[name] = true
5486
5489
  }
5487
- function refreshTerminalSessions(rawUrl, workspaceCwd = "", options = {}) {
5488
- try {
5489
- const discovery = window.PinokioTerminalsDiscovery
5490
- if (!discovery || typeof discovery.refreshTerminalSessions !== "function") {
5491
- return false
5492
- }
5493
- return discovery.refreshTerminalSessions(rawUrl, workspaceCwd, options)
5494
- } catch (_) {
5495
- return false
5496
- }
5497
- }
5498
5490
  installBrowserviewInjectTargetObserver()
5499
5491
  document.addEventListener("click", (e) => {
5500
5492
  interacted = true
@@ -6579,14 +6571,10 @@ const rerenderMenuSection = (container, html) => {
6579
6571
 
6580
6572
 
6581
6573
  // Instantiate a frame with the selected target's href
6582
- let url = target.href
6583
6574
  const targetInjectDescriptors = readLinkInjectDescriptors(target)
6584
- if (!url) {
6575
+ if (!target.href) {
6585
6576
  return
6586
6577
  }
6587
- // document.querySelector("#open-browser").href = url
6588
- // document.querySelector("#clone-tab").setAttribute("data-href", url)
6589
-
6590
6578
  //document.querySelector("#location").value = url
6591
6579
 
6592
6580
  // document.querySelector("#open-location").setAttribute('href', target.href)
@@ -6634,6 +6622,7 @@ const rerenderMenuSection = (container, html) => {
6634
6622
  eventParam.preventDefault()
6635
6623
  eventParam.stopPropagation()
6636
6624
  }
6625
+ requestInitialWorkspaceDiskUsage()
6637
6626
  browserPopoutSurface.show(target)
6638
6627
  return
6639
6628
  }
@@ -6709,7 +6698,6 @@ const rerenderMenuSection = (container, html) => {
6709
6698
  let mode = target.getAttribute("data-mode")
6710
6699
  if (mode === "refresh") {
6711
6700
  if (targetFrame.src !== target.href) {
6712
- refreshTerminalSessions(target.href)
6713
6701
  targetFrame.src = target.href
6714
6702
  }
6715
6703
  } else {
@@ -6720,7 +6708,6 @@ const rerenderMenuSection = (container, html) => {
6720
6708
  let sub = subUrlOf(target.href, targetFrame.src)
6721
6709
  if (!sub) {
6722
6710
  if (targetFrame.src !== target.href) {
6723
- refreshTerminalSessions(target.href)
6724
6711
  targetFrame.src = target.href
6725
6712
  }
6726
6713
  }
@@ -6736,7 +6723,6 @@ const rerenderMenuSection = (container, html) => {
6736
6723
  } else {
6737
6724
  if (force) {
6738
6725
  if (targetFrame.src !== target.href) {
6739
- refreshTerminalSessions(target.href)
6740
6726
  targetFrame.src = target.href
6741
6727
  }
6742
6728
  }
@@ -6758,15 +6744,17 @@ const rerenderMenuSection = (container, html) => {
6758
6744
  } else {
6759
6745
  let frame = document.createElement("iframe")
6760
6746
  frame.name = target.target
6761
- refreshTerminalSessions(target.href)
6762
6747
  frame.src = target.href
6763
- iframe_onerror(frame)
6764
- frame.setAttribute(
6765
- "allow",
6766
- "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 *;"
6748
+ iframe_onerror(frame)
6749
+ frame.addEventListener("load", () => {
6750
+ requestInitialWorkspaceDiskUsage()
6751
+ }, { once: true })
6752
+ frame.setAttribute(
6753
+ "allow",
6754
+ "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 *;"
6767
6755
  )
6768
6756
  frame.setAttribute("allowfullscreen", "")
6769
- writeFrameInjectDescriptors(frame, targetInjectDescriptors)
6757
+ writeFrameInjectDescriptors(frame, targetInjectDescriptors)
6770
6758
  publishBrowserviewInjectTargets([frame], { sync: true })
6771
6759
  document.querySelector("main").appendChild(frame)
6772
6760
  loaded[target.target] = true
@@ -6922,14 +6910,17 @@ const rerenderMenuSection = (container, html) => {
6922
6910
 
6923
6911
  document.querySelector(".temp-menu").appendChild(item)
6924
6912
 
6925
- let frame = document.createElement("iframe")
6926
- frame.name = id
6927
- frame.src = item.href
6928
- iframe_onerror(frame)
6929
- frame.setAttribute(
6930
- "allow",
6931
- "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 *;"
6932
- )
6913
+ let frame = document.createElement("iframe")
6914
+ frame.name = id
6915
+ frame.src = item.href
6916
+ iframe_onerror(frame)
6917
+ frame.addEventListener("load", () => {
6918
+ requestInitialWorkspaceDiskUsage()
6919
+ }, { once: true })
6920
+ frame.setAttribute(
6921
+ "allow",
6922
+ "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 *;"
6923
+ )
6933
6924
  frame.setAttribute("allowfullscreen", "")
6934
6925
  document.querySelector("main").appendChild(frame)
6935
6926
 
@@ -7998,6 +7989,14 @@ const rerenderMenuSection = (container, html) => {
7998
7989
  })
7999
7990
  }
8000
7991
  }
7992
+ const requestInitialWorkspaceDiskUsage = () => {
7993
+ if (initialWorkspaceDiskUsageRequested) {
7994
+ return
7995
+ }
7996
+ initialWorkspaceDiskUsageRequested = true
7997
+ refresh_du()
7998
+ refresh_du("logs")
7999
+ }
8001
8000
 
8002
8001
  const try_dynamic = async (options = {}) => {
8003
8002
  let rendered
@@ -8032,7 +8031,8 @@ const rerenderMenuSection = (container, html) => {
8032
8031
  } else {
8033
8032
  rendered = false
8034
8033
  }
8035
- if (!rendered) {
8034
+ if (rendered) {
8035
+ } else {
8036
8036
  if (options.retryOnEmpty === false) {
8037
8037
  return
8038
8038
  }
@@ -8089,13 +8089,9 @@ const rerenderMenuSection = (container, html) => {
8089
8089
  renderSelection()
8090
8090
  }
8091
8091
  }
8092
- let init = document.querySelector("[data-init]")
8093
- if (init) {
8094
- init.click()
8095
- }
8096
8092
  <% if (type === 'browse') { %>
8097
8093
  setTimeout(() => {
8098
- try_dynamic({ retryOnEmpty: false })
8094
+ try_dynamic()
8099
8095
  }, 0)
8100
8096
  <% } %>
8101
8097
  window.addEventListener('message', (event) => {
@@ -8155,6 +8151,8 @@ const rerenderMenuSection = (container, html) => {
8155
8151
  if (event.data.type === 'stream') {
8156
8152
  const frameName = resolveFrameName(null, event.source)
8157
8153
  updateTabTimestamp(frameName, Date.now())
8154
+ } else if (event.data.type === 'idle') {
8155
+ return
8158
8156
  } else if (event.data.type === 'restart') {
8159
8157
  <% if (type === 'run') { %>
8160
8158
  clearPersistedFrameLinkSelection()
@@ -8183,8 +8181,6 @@ const rerenderMenuSection = (container, html) => {
8183
8181
 
8184
8182
 
8185
8183
  });
8186
- refresh_du()
8187
- refresh_du("logs")
8188
8184
  renderSelection({ force: true })
8189
8185
  <% if (type === "browse" || type === "files") { %>
8190
8186
  const repoStatusCache = new Map()
@@ -12405,7 +12401,6 @@ document.addEventListener("DOMContentLoaded", () => {
12405
12401
  if (!normalized) {
12406
12402
  return false
12407
12403
  }
12408
- refreshTerminalSessions(normalized, workspaceCwd)
12409
12404
  pickerRequestId += 1
12410
12405
  currentUrl = normalized
12411
12406
  setLocation(normalized)
@@ -292,6 +292,42 @@ body.dark .menu-column .tab-header {
292
292
  .menu-column .tab-content .tab {
293
293
  flex: 0 0 auto;
294
294
  }
295
+ .terminal-shell-group {
296
+ display: flex;
297
+ flex-direction: column;
298
+ gap: 4px;
299
+ }
300
+ .terminal-shell-group.is-expanded > .terminal-shell-tab .disclosure-indicator {
301
+ transform: rotate(90deg);
302
+ }
303
+ .terminal-shell-tab.is-loading {
304
+ cursor: progress;
305
+ }
306
+ .terminal-shell-tab.is-loading .disclosure-indicator {
307
+ transform: none;
308
+ }
309
+ .terminal-shell-options {
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 4px;
313
+ margin-left: 20px;
314
+ padding-left: 12px;
315
+ border-left: 1px solid rgba(0, 0, 0, 0.08);
316
+ }
317
+ body.dark .terminal-shell-options {
318
+ border-left-color: rgba(255, 255, 255, 0.12);
319
+ }
320
+ .terminal-option-tab {
321
+ padding-left: 12px;
322
+ }
323
+ .terminal-shell-status {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 8px;
327
+ font-size: 12px;
328
+ opacity: 0.72;
329
+ padding: 8px 10px 10px 12px;
330
+ }
295
331
  @media (max-width: 920px) {
296
332
  .menu-grid.menu-grid--triptych {
297
333
  grid-template-columns: 1fr;
@@ -524,51 +560,7 @@ body.dark #update-spec {
524
560
 
525
561
  <div class='menu-grid <%= primaryColumnCount === 3 ? "menu-grid--triptych" : "" %>'>
526
562
  <% if (userTerminal && userTerminal.menu && userTerminal.menu.length) { %>
527
- <div class='menu-container menu-column user-terminal'>
528
- <div class='tab-header'>
529
- <h3><i class='<%= userTerminal.icon %>'></i> <%= userTerminal.title %></h3>
530
- </div>
531
- <% if (userTerminal.subtitle || userTerminal.subtitle_link_href) { %>
532
- <div class='column-subtitle'>
533
- <% if (userTerminal.subtitle) { %>
534
- <span><%= userTerminal.subtitle %></span><% if (userTerminal.subtitle_link_href && userTerminal.subtitle_link_label) { %> <% } %>
535
- <% } %>
536
- <% if (userTerminal.subtitle_link_href && userTerminal.subtitle_link_label) { %>
537
- <a class='column-subtitle-link' href="<%= userTerminal.subtitle_link_href %>" target="_parent"><%= userTerminal.subtitle_link_label %></a>
538
- <% } %>
539
- </div>
540
- <% } %>
541
- <div class='tab-content'>
542
- <% userTerminal.menu.forEach((i) => { %>
543
- <div class='tab' role="button" tabindex="0" data-index="<%= index++ %>" data-target="@<%= i.href %>" data-href="<%= i.href %>">
544
- <% if (i.image) { %>
545
- <img src="<%= i.image %>">
546
- <% } else if (i.icon) { %>
547
- <i class="img <%= i.icon %>"></i>
548
- <% } %>
549
- <div class='tab-copy'>
550
- <h2 class='tab-title'><%= i.title %></h2>
551
- <% if (i.subtitle) { %>
552
- <div class='tab-subtitle subtitle'><%= i.subtitle %></div>
553
- <% } %>
554
- </div>
555
- <div class='tab-actions'>
556
- <% if (i.link) { %>
557
- <button class="tab-action-link" type="button" data-doc-link="true" data-href="<%= i.link %>" title="Open docs" data-tippy-content="Open docs" aria-label="Open docs">
558
- <i class="fa-solid fa-circle-info"></i>
559
- </button>
560
- <% } %>
561
- <button class="ai-perm-link" type="button" data-ai-consent="<%= i.href %>" title="AI permissions" data-tippy-content="AI permissions" aria-label="AI permissions">
562
- <i class="fa-solid fa-shield-halved"></i>
563
- </button>
564
- <div class='disclosure-indicator' aria-hidden="true">
565
- <i class="fa-solid fa-chevron-right"></i>
566
- </div>
567
- </div>
568
- </div>
569
- <% }) %>
570
- </div>
571
- </div>
563
+ <%- include('./partials/d_terminal_column', { userTerminal }) %>
572
564
  <% } %>
573
565
 
574
566
  <% if (cliMenu && cliMenu.menu && cliMenu.menu.length) { %>
@@ -797,12 +789,17 @@ document.querySelector("#update-spec").addEventListener("click", (e) => {
797
789
  })
798
790
  */
799
791
  let list = []
800
- document.querySelectorAll(".tab").forEach((el, index) => {
801
- list.push({
802
- index: parseInt(el.getAttribute("data-index")),
803
- text: el.textContent,
792
+ const rebuildTabList = () => {
793
+ list = []
794
+ document.querySelectorAll(".tab").forEach((el, index) => {
795
+ el.setAttribute("data-index", String(index))
796
+ list.push({
797
+ index,
798
+ text: el.textContent,
799
+ isTerminalOption: el.hasAttribute("data-terminal-option-tab"),
800
+ })
804
801
  })
805
- })
802
+ }
806
803
  const search = (items, value) => {
807
804
  let filtered = []
808
805
  for(let i=0; i<items.length; i++) {
@@ -822,8 +819,9 @@ let filteredBtnsCount = 0
822
819
 
823
820
  const renderSearch = () => {
824
821
  let target = document.querySelector("form input[type=search]")
822
+ const searching = target.value.trim().length > 0
825
823
  let result
826
- if (target.value.trim().length === 0) {
824
+ if (!searching) {
827
825
  result = list.map((i, index) => {
828
826
  return { item: i }
829
827
  })
@@ -835,12 +833,36 @@ const renderSearch = () => {
835
833
  el.classList.add("hidden")
836
834
  el.classList.remove("selected")
837
835
  }
836
+ document.querySelectorAll(".terminal-shell-group").forEach((group) => {
837
+ const panel = group.querySelector("[data-terminal-options-panel]")
838
+ if (!panel) {
839
+ return
840
+ }
841
+ if (searching) {
842
+ panel.classList.add("hidden")
843
+ } else {
844
+ panel.classList.toggle("hidden", !group.classList.contains("is-expanded"))
845
+ }
846
+ })
838
847
 
839
848
  filteredCount = result.length
840
849
  for(let i=0; i<result.length; i++) {
841
850
  let selector = result[i]
842
851
  let el = document.querySelector(".tab[data-index='" + selector.item.index + "']")
843
852
  el.classList.remove("hidden")
853
+ if (selector.item.isTerminalOption) {
854
+ const group = el.closest(".terminal-shell-group")
855
+ if (group) {
856
+ const shellTab = group.querySelector(".tab[data-terminal-group]")
857
+ if (shellTab) {
858
+ shellTab.classList.remove("hidden")
859
+ }
860
+ const panel = group.querySelector("[data-terminal-options-panel]")
861
+ if (panel) {
862
+ panel.classList.remove("hidden")
863
+ }
864
+ }
865
+ }
844
866
  if (i === selectedIndex) {
845
867
  el.classList.add("selected")
846
868
  } else {
@@ -848,6 +870,7 @@ const renderSearch = () => {
848
870
  }
849
871
  }
850
872
  }
873
+ rebuildTabList()
851
874
  renderSearch()
852
875
  const workspaceCwd = (() => {
853
876
  const node = document.querySelector(".file-open")
@@ -915,6 +938,8 @@ const revokeConsentForHref = (href) => {
915
938
  }
916
939
  const START_ICON_HTML = '<i class="fa-solid fa-chevron-right"></i>'
917
940
  const STARTING_ICON_HTML = '<i class="fa-solid fa-circle-notch fa-spin"></i>'
941
+ const TERMINAL_GROUP_ICON_HTML = '<i class="fa-solid fa-chevron-right"></i>'
942
+ const TERMINAL_GROUP_LOADING_HTML = '<i class="fa-solid fa-circle-notch fa-spin"></i>'
918
943
  const setTabLaunchState = (tab, state) => {
919
944
  if (!tab) return
920
945
  const indicator = tab.querySelector('.disclosure-indicator')
@@ -927,6 +952,76 @@ const setTabLaunchState = (tab, state) => {
927
952
  tab.classList.remove('is-starting')
928
953
  }
929
954
  }
955
+ const setTerminalGroupState = (tab, state) => {
956
+ if (!tab) {
957
+ return
958
+ }
959
+ const indicator = tab.querySelector('.disclosure-indicator')
960
+ if (indicator) {
961
+ indicator.innerHTML = state === 'loading' ? TERMINAL_GROUP_LOADING_HTML : TERMINAL_GROUP_ICON_HTML
962
+ }
963
+ if (state === 'loading') {
964
+ tab.classList.add('is-loading')
965
+ } else {
966
+ tab.classList.remove('is-loading')
967
+ }
968
+ }
969
+ const setTerminalGroupExpanded = (group, expanded) => {
970
+ if (!group) {
971
+ return
972
+ }
973
+ group.classList.toggle('is-expanded', expanded)
974
+ const panel = group.querySelector('[data-terminal-options-panel]')
975
+ if (panel) {
976
+ panel.classList.toggle('hidden', !expanded)
977
+ }
978
+ }
979
+ const toggleTerminalGroup = async (tab) => {
980
+ if (!tab) {
981
+ return
982
+ }
983
+ const group = tab.closest('.terminal-shell-group')
984
+ if (!group) {
985
+ return
986
+ }
987
+ const panel = group.querySelector('[data-terminal-options-panel]')
988
+ if (!panel) {
989
+ return
990
+ }
991
+ if (group.dataset.loaded === '1') {
992
+ setTerminalGroupExpanded(group, !group.classList.contains('is-expanded'))
993
+ rebuildTabList()
994
+ renderSearch()
995
+ return
996
+ }
997
+ if (group.dataset.loading === '1') {
998
+ return
999
+ }
1000
+ const url = tab.dataset.optionsUrl
1001
+ if (!url) {
1002
+ return
1003
+ }
1004
+ group.dataset.loading = '1'
1005
+ setTerminalGroupExpanded(group, true)
1006
+ setTerminalGroupState(tab, 'loading')
1007
+ panel.innerHTML = '<div class="terminal-shell-status"><i class="fa-solid fa-circle-notch fa-spin"></i><span>Loading shell options...</span></div>'
1008
+ try {
1009
+ const html = await fetch(url).then((res) => res.text())
1010
+ if (!html || html.trim().length === 0) {
1011
+ panel.innerHTML = '<div class="terminal-shell-status"><span>No shell options available.</span></div>'
1012
+ } else {
1013
+ panel.innerHTML = html
1014
+ group.dataset.loaded = '1'
1015
+ }
1016
+ } catch (_) {
1017
+ panel.innerHTML = '<div class="terminal-shell-status"><span>Unable to load shell options.</span></div>'
1018
+ } finally {
1019
+ delete group.dataset.loading
1020
+ setTerminalGroupState(tab, 'idle')
1021
+ rebuildTabList()
1022
+ renderSearch()
1023
+ }
1024
+ }
930
1025
  const pendingLaunchTabs = new Map()
931
1026
  const launchTab = (tab) => {
932
1027
  if (!tab) {
@@ -997,6 +1092,14 @@ document.addEventListener("click", (e) => {
997
1092
  return
998
1093
  }
999
1094
 
1095
+ const groupTab = e.target.closest('.tab[data-terminal-group]')
1096
+ if (groupTab) {
1097
+ e.preventDefault()
1098
+ e.stopPropagation()
1099
+ toggleTerminalGroup(groupTab)
1100
+ return
1101
+ }
1102
+
1000
1103
  const tab = e.target.closest('.tab[data-href]')
1001
1104
  if (!tab) {
1002
1105
  return
@@ -1018,6 +1121,14 @@ document.addEventListener('keydown', (e) => {
1018
1121
  return
1019
1122
  }
1020
1123
 
1124
+ const groupTab = target.closest('.tab[data-terminal-group]')
1125
+ if (groupTab && groupTab === target) {
1126
+ e.preventDefault()
1127
+ e.stopPropagation()
1128
+ toggleTerminalGroup(groupTab)
1129
+ return
1130
+ }
1131
+
1021
1132
  const tab = target.closest('.tab[data-href]')
1022
1133
  if (!tab || tab !== target) {
1023
1134
  return
@@ -0,0 +1,40 @@
1
+ <div class='menu-container menu-column user-terminal'>
2
+ <div class='tab-header'>
3
+ <h3><i class='<%= userTerminal.icon %>'></i> <%= userTerminal.title %></h3>
4
+ </div>
5
+ <% if (userTerminal.subtitle || userTerminal.subtitle_link_href) { %>
6
+ <div class='column-subtitle'>
7
+ <% if (userTerminal.subtitle) { %>
8
+ <span><%= userTerminal.subtitle %></span><% if (userTerminal.subtitle_link_href && userTerminal.subtitle_link_label) { %> <% } %>
9
+ <% } %>
10
+ <% if (userTerminal.subtitle_link_href && userTerminal.subtitle_link_label) { %>
11
+ <a class='column-subtitle-link' href="<%= userTerminal.subtitle_link_href %>" target="_parent"><%= userTerminal.subtitle_link_label %></a>
12
+ <% } %>
13
+ </div>
14
+ <% } %>
15
+ <div class='tab-content'>
16
+ <% userTerminal.menu.forEach((i) => { %>
17
+ <div class='terminal-shell-group' data-terminal-shell-group="<%= i.shell_key %>">
18
+ <div class='tab terminal-shell-tab' role="button" tabindex="0" data-terminal-group="<%= i.shell_key %>" data-options-url="<%= i.options_url %>">
19
+ <% if (i.image) { %>
20
+ <img src="<%= i.image %>">
21
+ <% } else if (i.icon) { %>
22
+ <i class="img <%= i.icon %>"></i>
23
+ <% } %>
24
+ <div class='tab-copy'>
25
+ <h2 class='tab-title'><%= i.title %></h2>
26
+ <% if (i.subtitle) { %>
27
+ <div class='tab-subtitle subtitle'><%= i.subtitle %></div>
28
+ <% } %>
29
+ </div>
30
+ <div class='tab-actions'>
31
+ <div class='disclosure-indicator' aria-hidden="true">
32
+ <i class="fa-solid fa-chevron-right"></i>
33
+ </div>
34
+ </div>
35
+ </div>
36
+ <div class='terminal-shell-options hidden' data-terminal-options-panel="<%= i.shell_key %>"></div>
37
+ </div>
38
+ <% }) %>
39
+ </div>
40
+ </div>
@@ -0,0 +1,28 @@
1
+ <% options.forEach((i) => { %>
2
+ <div class='tab terminal-option-tab' role="button" tabindex="0" data-terminal-option-tab="true" data-target="@<%= i.href %>" data-href="<%= i.href %>">
3
+ <% if (i.image) { %>
4
+ <img src="<%= i.image %>">
5
+ <% } else if (i.icon) { %>
6
+ <i class="img <%= i.icon %>"></i>
7
+ <% } %>
8
+ <div class='tab-copy'>
9
+ <h2 class='tab-title'><%= i.title %></h2>
10
+ <% if (i.subtitle) { %>
11
+ <div class='tab-subtitle subtitle'><%= i.subtitle %></div>
12
+ <% } %>
13
+ </div>
14
+ <div class='tab-actions'>
15
+ <% if (i.link) { %>
16
+ <button class="tab-action-link" type="button" data-doc-link="true" data-href="<%= i.link %>" title="Open docs" data-tippy-content="Open docs" aria-label="Open docs">
17
+ <i class="fa-solid fa-circle-info"></i>
18
+ </button>
19
+ <% } %>
20
+ <button class="ai-perm-link" type="button" data-ai-consent="<%= i.href %>" title="AI permissions" data-tippy-content="AI permissions" aria-label="AI permissions">
21
+ <i class="fa-solid fa-shield-halved"></i>
22
+ </button>
23
+ <div class='disclosure-indicator' aria-hidden="true">
24
+ <i class="fa-solid fa-chevron-right"></i>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ <% }) %>
@@ -552,6 +552,67 @@ const createRunControls = () => {
552
552
  }
553
553
  return { set }
554
554
  }
555
+ const PLUGIN_TERMINAL_IDLE_WINDOW_MS = 1200
556
+ const createPluginTerminalDiscoveryRefresher = (context = {}) => {
557
+ const enabled = (() => {
558
+ try {
559
+ return window.location.pathname.startsWith("/run/plugin/")
560
+ } catch (_) {
561
+ return false
562
+ }
563
+ })()
564
+ let idleTimer = null
565
+ let armed = false
566
+ let sawStream = false
567
+ const clearIdleTimer = () => {
568
+ if (idleTimer) {
569
+ clearTimeout(idleTimer)
570
+ idleTimer = null
571
+ }
572
+ }
573
+ const refresh = () => {
574
+ if (!enabled || !armed || !sawStream) {
575
+ return
576
+ }
577
+ armed = false
578
+ sawStream = false
579
+ try {
580
+ const discovery = window.PinokioTerminalsDiscovery
581
+ if (!discovery || typeof discovery.refreshTerminalSessions !== "function") {
582
+ return
583
+ }
584
+ discovery.refreshTerminalSessions(window.location.href, context.cwd || "", {
585
+ retryDelays: []
586
+ })
587
+ } catch (_) {}
588
+ }
589
+ return {
590
+ arm(meaningful) {
591
+ if (!enabled || !meaningful) {
592
+ return
593
+ }
594
+ armed = true
595
+ sawStream = false
596
+ clearIdleTimer()
597
+ },
598
+ markActivity() {
599
+ if (!enabled || !armed) {
600
+ return
601
+ }
602
+ sawStream = true
603
+ clearIdleTimer()
604
+ idleTimer = setTimeout(() => {
605
+ idleTimer = null
606
+ refresh()
607
+ }, PLUGIN_TERMINAL_IDLE_WINDOW_MS)
608
+ },
609
+ clear() {
610
+ armed = false
611
+ sawStream = false
612
+ clearIdleTimer()
613
+ }
614
+ }
615
+ }
555
616
  const createAiConsentManager = (context = {}) => {
556
617
  const storage = getSafeLocalStorage()
557
618
  const storageSupported = !!storage
@@ -848,6 +909,9 @@ document.addEventListener("DOMContentLoaded", async () => {
848
909
  uri: scriptUri || ("~" + location.pathname)
849
910
  })
850
911
  const runControls = createRunControls()
912
+ const pluginTerminalDiscoveryRefresher = createPluginTerminalDiscoveryRefresher({
913
+ cwd: scriptCwd
914
+ })
851
915
  consentManager.showChipIfRemembered()
852
916
  class RPC {
853
917
  constructor() {
@@ -923,16 +987,17 @@ document.addEventListener("DOMContentLoaded", async () => {
923
987
  }
924
988
  }
925
989
  notifyLineSubmitted(line, meta = {}) {
926
- if (this.inputTracker) {
927
- this.inputTracker.submit(line, meta)
928
- return
929
- }
930
990
  const safeLine = sanitizePreviewLine(line || "")
931
991
  const preview = safeLine.trim()
932
992
  const limit = 200
933
993
  const truncated = preview.length > limit ? preview.slice(0, limit) + "..." : preview
934
994
  const hadLineBreak = Boolean(meta && meta.hadLineBreak)
935
995
  const meaningful = truncated.length > 0 || hadLineBreak
996
+ pluginTerminalDiscoveryRefresher.arm(meaningful)
997
+ if (this.inputTracker) {
998
+ this.inputTracker.submit(line, meta)
999
+ return
1000
+ }
936
1001
  postMessageToAncestors({
937
1002
  type: "terminal-input",
938
1003
  frame: window.name || null,
@@ -1096,6 +1161,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1096
1161
  }
1097
1162
  } else if (packet.type === "stream") {
1098
1163
  refreshParent(packet)
1164
+ pluginTerminalDiscoveryRefresher.markActivity()
1099
1165
  // set the current shell id
1100
1166
  const previousShellId = shell_id
1101
1167
  if (packet.data.id) {
@@ -1128,6 +1194,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1128
1194
  runControls.set("running")
1129
1195
  } else if (packet.type === 'disconnect') {
1130
1196
  refreshParent(packet)
1197
+ pluginTerminalDiscoveryRefresher.clear()
1131
1198
  reloadMemory()
1132
1199
  this.term.write("\r\nDisconnected...\r\n")
1133
1200
  document.querySelector("#status-window").innerHTML = "<b>Ready</b>"
@@ -1423,6 +1490,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1423
1490
  n.Noty(payload)
1424
1491
  }
1425
1492
  } else if (packet.type === "restart") {
1493
+ pluginTerminalDiscoveryRefresher.clear()
1426
1494
  try {
1427
1495
  if (window.parent && window.parent !== window && typeof window.parent.postMessage === "function") {
1428
1496
  window.parent.postMessage(packet, "*")
@@ -1469,6 +1537,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1469
1537
  text: `${packet.data}`,
1470
1538
  })
1471
1539
  } else if (packet.type === "error") {
1540
+ pluginTerminalDiscoveryRefresher.clear()
1472
1541
  runControls.set("idle")
1473
1542
 
1474
1543
 
@@ -1547,6 +1616,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1547
1616
  type: 'success'
1548
1617
  })
1549
1618
  */
1619
+ pluginTerminalDiscoveryRefresher.clear()
1550
1620
  runControls.set("idle")
1551
1621
  }, 0)
1552
1622
  //this.socket.close()
@@ -1575,6 +1645,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1575
1645
  this.mode = (mode ? mode : "run")
1576
1646
  const allowed = aiConsentRequired ? await consentManager.ensureAllowed() : true
1577
1647
  if (!allowed) {
1648
+ pluginTerminalDiscoveryRefresher.clear()
1578
1649
  runControls.set("idle")
1579
1650
  n.Noty({
1580
1651
  text: "Run canceled; AI agents remain blocked for this folder.",
@@ -1601,6 +1672,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1601
1672
  await this.start(mode)
1602
1673
  return true
1603
1674
  } catch (error) {
1675
+ pluginTerminalDiscoveryRefresher.clear()
1604
1676
  runControls.set("idle")
1605
1677
  const message = error && error.message ? error.message : "Failed to start"
1606
1678
  n.Noty({