pinokiod 7.1.27 → 7.1.29

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.
@@ -4,7 +4,7 @@ const semver = require('semver')
4
4
 
5
5
  class Bluefairy {
6
6
  description = "Installs Bluefairy, a standalone package freshness guard."
7
- version = ">=0.0.7"
7
+ version = ">=0.0.14"
8
8
 
9
9
  packageName() {
10
10
  return "bluefairy"
@@ -84,12 +84,12 @@ class Bluefairy {
84
84
  return []
85
85
  }
86
86
  if (shell.isCmdShell()) {
87
- return [`bluefairy-activate.cmd`, `bluefairy doctor --short`]
87
+ return [`bluefairy-activate.cmd`]
88
88
  }
89
89
  if (shell.isPowerShell()) {
90
- return [`& bluefairy-activate.ps1`, `bluefairy doctor --short`]
90
+ return [`& bluefairy-activate.ps1`]
91
91
  }
92
- return [`. bluefairy-activate`, `bluefairy doctor --short`]
92
+ return [`. bluefairy-activate`]
93
93
  }
94
94
  }
95
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.1.27",
3
+ "version": "7.1.29",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -2782,6 +2782,7 @@ class Server {
2782
2782
  execUrl: "~" + req.originalUrl.replace(/^\/_api/, "\/api"),
2783
2783
  proxies: this.kernel.api.proxies[filepath],
2784
2784
  cwd: req.query.cwd,
2785
+ taskSaveWorkspacesRoot: this.kernel.path("workspaces"),
2785
2786
  script_id: (req.base ? `${full_filepath}?cwd=${req.query.cwd}` : null),
2786
2787
  script_path: (req.base ? full_filepath : null),
2787
2788
  }
@@ -6890,10 +6891,13 @@ class Server {
6890
6891
  try {
6891
6892
  if (!this.kernel.plugin.config) {
6892
6893
  await this.kernel.plugin.init()
6894
+ } else {
6895
+ await this.kernel.plugin.setConfig()
6893
6896
  }
6894
6897
  const pluginMenu = this.kernel.plugin && this.kernel.plugin.config && Array.isArray(this.kernel.plugin.config.menu)
6895
6898
  ? this.kernel.plugin.config.menu
6896
6899
  : []
6900
+ res.set("Cache-Control", "no-store")
6897
6901
  res.json({ menu: pluginMenu })
6898
6902
  } catch (error) {
6899
6903
  console.warn('Failed to load plugin menu for create launcher modal', error)
@@ -7596,6 +7600,7 @@ class Server {
7596
7600
  }
7597
7601
  return copied
7598
7602
  }
7603
+ const shellQuote = (value) => JSON.stringify(String(value))
7599
7604
  const createLauncherTargetFolder = async (rootDir, folderName, options = {}) => {
7600
7605
  if (!isValidTerminalWorkspaceName(folderName)) {
7601
7606
  const error = new Error("Invalid folder name.")
@@ -7616,8 +7621,11 @@ class Server {
7616
7621
  error.status = 409
7617
7622
  throw error
7618
7623
  }
7619
- await fs.promises.mkdir(targetPath, { recursive: false })
7620
- if (options.initializeGit !== false) {
7624
+ const shouldCreateDirectory = options.createDirectory !== false
7625
+ if (shouldCreateDirectory) {
7626
+ await fs.promises.mkdir(targetPath, { recursive: false })
7627
+ }
7628
+ if (shouldCreateDirectory && options.initializeGit !== false) {
7621
7629
  try {
7622
7630
  await initializeLauncherTargetGitRepository(targetPath)
7623
7631
  } catch (error) {
@@ -7655,9 +7663,6 @@ class Server {
7655
7663
  if (!rawValue) {
7656
7664
  return ""
7657
7665
  }
7658
- if (this.kernel && this.kernel.git && typeof this.kernel.git.canonicalRepoUrl === "function") {
7659
- return this.kernel.git.canonicalRepoUrl(rawValue) || rawValue
7660
- }
7661
7666
  return rawValue
7662
7667
  }
7663
7668
  const cloneLauncherRemoteRepo = async ({ rootDir, folderName, ref }) => {
@@ -7668,17 +7673,14 @@ class Server {
7668
7673
  throw error
7669
7674
  }
7670
7675
  const targetPath = await createLauncherTargetFolder(rootDir, folderName, {
7676
+ createDirectory: false,
7671
7677
  initializeGit: false
7672
7678
  })
7673
7679
  try {
7674
- await git.clone({
7675
- fs,
7676
- http,
7677
- dir: targetPath,
7678
- url: normalizedRef,
7679
- singleBranch: true,
7680
- depth: 1
7681
- })
7680
+ await this.kernel.exec({
7681
+ message: [`git clone --depth 1 --single-branch ${shellQuote(normalizedRef)} ${shellQuote(targetPath)}`],
7682
+ path: path.resolve(rootDir)
7683
+ }, () => {})
7682
7684
  return targetPath
7683
7685
  } catch (error) {
7684
7686
  await fs.promises.rm(targetPath, { recursive: true, force: true }).catch(() => {})
@@ -7832,15 +7834,46 @@ class Server {
7832
7834
  const nextInputs = JSON.parse(value)
7833
7835
  return Array.isArray(nextInputs) ? nextInputs : []
7834
7836
  }
7835
- const buildTaskBuilderDefaults = (body, inputs) => ({
7836
- title: typeof body?.title === "string" ? body.title : "",
7837
- description: typeof body?.description === "string" ? body.description : "",
7838
- path: typeof body?.path === "string" && body.path.trim()
7839
- ? body.path.trim()
7840
- : "workspaces",
7841
- template: typeof body?.template === "string" ? body.template : "",
7842
- inputs: Array.isArray(inputs) ? inputs : []
7843
- })
7837
+ const normalizeTaskBuilderSourceWorkspace = (value) => {
7838
+ const raw = typeof value === "string" ? value.trim() : ""
7839
+ if (!raw) {
7840
+ return {
7841
+ cwd: "",
7842
+ label: ""
7843
+ }
7844
+ }
7845
+ const workspacesRoot = path.resolve(this.kernel.path("workspaces"))
7846
+ const normalizedPath = path.resolve(raw)
7847
+ const relative = path.relative(workspacesRoot, normalizedPath)
7848
+ if (!relative || relative === "." || relative.startsWith("..") || path.isAbsolute(relative)) {
7849
+ return {
7850
+ cwd: "",
7851
+ label: ""
7852
+ }
7853
+ }
7854
+ return {
7855
+ cwd: normalizedPath,
7856
+ label: path.basename(normalizedPath) || relative.split(path.sep).pop() || "workspace"
7857
+ }
7858
+ }
7859
+ const buildTaskBuilderDefaults = (body, inputs, options = {}) => {
7860
+ const sourceWorkspace = options.sourceWorkspace && typeof options.sourceWorkspace === "object"
7861
+ ? options.sourceWorkspace
7862
+ : normalizeTaskBuilderSourceWorkspace(body?.sourceWorkspaceCwd)
7863
+ return {
7864
+ title: typeof body?.title === "string" ? body.title : "",
7865
+ description: typeof body?.description === "string" ? body.description : "",
7866
+ path: typeof body?.path === "string" && body.path.trim()
7867
+ ? body.path.trim()
7868
+ : "workspaces",
7869
+ template: typeof body?.template === "string" ? body.template : "",
7870
+ inputs: Array.isArray(inputs) ? inputs : [],
7871
+ sourceWorkspaceCwd: sourceWorkspace.cwd || "",
7872
+ sourceWorkspaceLabel: sourceWorkspace.label || "",
7873
+ rememberCurrentWorkspace: body?.rememberCurrentWorkspace === "1" || body?.rememberCurrentWorkspace === "on" || body?.rememberCurrentWorkspace === true,
7874
+ lockPathSelection: body?.lockPathSelection === "1" || body?.lockPathSelection === "on" || body?.lockPathSelection === true
7875
+ }
7876
+ }
7844
7877
  const summarizeTaskRemoteLabel = (value) => {
7845
7878
  const raw = typeof value === "string" ? value.trim() : ""
7846
7879
  if (!raw) {
@@ -8914,7 +8947,10 @@ class Server {
8914
8947
  }))
8915
8948
 
8916
8949
  this.app.get("/tasks/new", ex(async (req, res) => {
8950
+ const sourceWorkspace = normalizeTaskBuilderSourceWorkspace(req.query.sourceWorkspaceCwd)
8951
+ const lockPathSelection = req.query.lockPath === "1" || req.query.lockPath === "true"
8917
8952
  await renderTaskBuilderPage(req, res, {
8953
+ allowPathSelection: !lockPathSelection,
8918
8954
  defaults: {
8919
8955
  path: typeof req.query.path === "string" && req.query.path.trim()
8920
8956
  ? req.query.path.trim()
@@ -8922,7 +8958,11 @@ class Server {
8922
8958
  title: typeof req.query.title === "string" ? req.query.title : "",
8923
8959
  description: typeof req.query.description === "string" ? req.query.description : "",
8924
8960
  template: typeof req.query.template === "string" ? req.query.template : "",
8925
- inputs: []
8961
+ inputs: [],
8962
+ sourceWorkspaceCwd: sourceWorkspace.cwd,
8963
+ sourceWorkspaceLabel: sourceWorkspace.label,
8964
+ rememberCurrentWorkspace: false,
8965
+ lockPathSelection
8926
8966
  }
8927
8967
  })
8928
8968
  }))
@@ -8960,14 +9000,45 @@ class Server {
8960
9000
 
8961
9001
  this.app.post("/tasks", ex(async (req, res) => {
8962
9002
  const body = req.body && typeof req.body === "object" ? req.body : {}
9003
+ const sourceWorkspace = normalizeTaskBuilderSourceWorkspace(body.sourceWorkspaceCwd)
8963
9004
  let parsedInputs = []
8964
9005
  if (typeof body.inputsJson === "string" && body.inputsJson.trim()) {
8965
9006
  try {
8966
9007
  parsedInputs = parseTaskBuilderInputs(body.inputsJson)
8967
9008
  } catch (error) {
9009
+ const defaults = buildTaskBuilderDefaults(body, [], { sourceWorkspace })
8968
9010
  await renderTaskBuilderPage(req, res, {
8969
9011
  error: "Inputs JSON is invalid.",
8970
- defaults: buildTaskBuilderDefaults(body, [])
9012
+ allowPathSelection: !defaults.lockPathSelection,
9013
+ defaults
9014
+ })
9015
+ return
9016
+ }
9017
+ }
9018
+ const defaults = buildTaskBuilderDefaults(body, parsedInputs, { sourceWorkspace })
9019
+ if (defaults.rememberCurrentWorkspace) {
9020
+ if (defaults.path !== "workspaces") {
9021
+ await renderTaskBuilderPage(req, res, {
9022
+ error: "Current workspace can only be remembered for workspace tasks.",
9023
+ allowPathSelection: !defaults.lockPathSelection,
9024
+ defaults
9025
+ })
9026
+ return
9027
+ }
9028
+ if (!sourceWorkspace.cwd) {
9029
+ await renderTaskBuilderPage(req, res, {
9030
+ error: "Current workspace is no longer available to remember.",
9031
+ allowPathSelection: !defaults.lockPathSelection,
9032
+ defaults
9033
+ })
9034
+ return
9035
+ }
9036
+ const sourceStats = await fs.promises.stat(sourceWorkspace.cwd).catch(() => null)
9037
+ if (!sourceStats || !sourceStats.isDirectory()) {
9038
+ await renderTaskBuilderPage(req, res, {
9039
+ error: "Current workspace could not be found.",
9040
+ allowPathSelection: !defaults.lockPathSelection,
9041
+ defaults
8971
9042
  })
8972
9043
  return
8973
9044
  }
@@ -8983,11 +9054,25 @@ class Server {
8983
9054
  },
8984
9055
  template: typeof body.template === "string" ? body.template : ""
8985
9056
  })
9057
+ if (defaults.rememberCurrentWorkspace) {
9058
+ const workspaceRef = taskWorkspaceLinks.createWorkspaceRef("workspaces", sourceWorkspace.cwd)
9059
+ if (!workspaceRef) {
9060
+ await taskPackages.deleteTaskPackage(task.id).catch(() => {})
9061
+ throw new Error("Failed to remember current workspace.")
9062
+ }
9063
+ try {
9064
+ await taskWorkspaceLinks.touchTaskWorkspace(task.id, workspaceRef)
9065
+ } catch (error) {
9066
+ await taskPackages.deleteTaskPackage(task.id).catch(() => {})
9067
+ throw error
9068
+ }
9069
+ }
8986
9070
  res.redirect(buildTaskPath({ id: task.id }))
8987
9071
  } catch (error) {
8988
9072
  await renderTaskBuilderPage(req, res, {
8989
9073
  error: error && error.message ? error.message : "Failed to create task.",
8990
- defaults: buildTaskBuilderDefaults(body, parsedInputs)
9074
+ allowPathSelection: !defaults.lockPathSelection,
9075
+ defaults
8991
9076
  })
8992
9077
  }
8993
9078
  }))
@@ -11683,7 +11768,8 @@ class Server {
11683
11768
  done_message,
11684
11769
  callback,
11685
11770
  callback_target,
11686
- running: (shell ? true : false)
11771
+ running: (shell ? true : false),
11772
+ taskSaveWorkspacesRoot: this.kernel.path("workspaces")
11687
11773
  })
11688
11774
  }))
11689
11775
  this.app.get("/pro", ex(async (req, res) => {
@@ -1,7 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const git = require("isomorphic-git");
4
- const http = require("isomorphic-git/http/node");
5
4
 
6
5
  const TASK_INDEX_VERSION = 1;
7
6
  const TASK_INDEX_FILENAME = "index.json";
@@ -10,6 +9,10 @@ const TASK_TEMPLATE_FILENAME = "task.md";
10
9
  const TASK_ID_PATTERN = /^t[1-9][0-9]*$/;
11
10
  const TASK_INPUT_NAME_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
12
11
 
12
+ function shellQuote(value) {
13
+ return JSON.stringify(String(value));
14
+ }
15
+
13
16
  function slugify(value, fallback = "task") {
14
17
  const normalized = typeof value === "string" ? value : "";
15
18
  const slug = normalized
@@ -562,14 +565,10 @@ function createTaskPackageService({ kernel }) {
562
565
  const taskDir = taskDirForId(allocation.id);
563
566
  await fs.promises.mkdir(tasksRoot(), { recursive: true });
564
567
  try {
565
- await git.clone({
566
- fs,
567
- http,
568
- dir: taskDir,
569
- url: rawRef,
570
- singleBranch: true,
571
- depth: 1
572
- });
568
+ await kernel.exec({
569
+ message: [`git clone --depth 1 --single-branch ${shellQuote(rawRef)} ${shellQuote(taskDir)}`],
570
+ path: tasksRoot()
571
+ }, () => {});
573
572
  await readTaskPackageById(allocation.id);
574
573
  allocation.index.items.push({
575
574
  id: allocation.id,
@@ -2688,7 +2688,16 @@ if (typeof hotkeys === 'function') {
2688
2688
  try { overlay.remove(); } catch (_) {}
2689
2689
  try { window.__pinokioConnectCurtainInstalled = true; window.__pinokioConnectCurtainInstalling = false; } catch (_) {}
2690
2690
  };
2691
- overlay.addEventListener('pointerdown', onTap, { once: true, capture: true });
2691
+ overlay.addEventListener('click', onTap, { once: true, capture: true });
2692
+ overlay.addEventListener('keydown', (e) => {
2693
+ if (!e) {
2694
+ return;
2695
+ }
2696
+ const key = e.key || '';
2697
+ if (key === 'Enter' || key === ' ' || key === 'Spacebar') {
2698
+ onTap(e);
2699
+ }
2700
+ }, { capture: true });
2692
2701
  document.body.appendChild(overlay);
2693
2702
  };
2694
2703
 
@@ -3701,18 +3710,6 @@ document.addEventListener("DOMContentLoaded", () => {
3701
3710
  } else {
3702
3711
  api.showModal();
3703
3712
  }
3704
- wait_ready(null, { showLoader: false, requireStartup: true }).then(({ ready }) => {
3705
- createLauncherDebugLog('guardUniversalLauncher wait_ready resolved', { ready });
3706
- if (ready) {
3707
- return;
3708
- }
3709
- if (api && typeof api.hideModal === 'function') {
3710
- api.hideModal();
3711
- }
3712
- const callback = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash);
3713
- createLauncherDebugLog('guardUniversalLauncher redirecting to /setup/dev', { callback });
3714
- window.location.href = `/setup/dev?callback=${callback}`;
3715
- });
3716
3713
  }
3717
3714
 
3718
3715
  function initializeCreateLauncherIntegration() {
@@ -0,0 +1,109 @@
1
+ (function(window, document) {
2
+ "use strict";
3
+
4
+ function normalizePath(value) {
5
+ return typeof value === "string" && value.trim()
6
+ ? value.trim().replace(/\\/g, "/").replace(/\/+$/, "")
7
+ : "";
8
+ }
9
+
10
+ function isPathWithin(candidate, parent) {
11
+ const normalizedCandidate = normalizePath(candidate);
12
+ const normalizedParent = normalizePath(parent);
13
+ if (!normalizedCandidate || !normalizedParent) {
14
+ return false;
15
+ }
16
+ if (normalizedCandidate === normalizedParent) {
17
+ return true;
18
+ }
19
+ return normalizedCandidate.startsWith(`${normalizedParent}/`);
20
+ }
21
+
22
+ function getFirstPromptLine(prompt) {
23
+ const lines = String(prompt || "").split(/\r?\n/);
24
+ for (let index = 0; index < lines.length; index += 1) {
25
+ const line = lines[index].trim();
26
+ if (line) {
27
+ return line;
28
+ }
29
+ }
30
+ return "";
31
+ }
32
+
33
+ function getWorkspaceName(cwd) {
34
+ const normalized = normalizePath(cwd);
35
+ if (!normalized) {
36
+ return "";
37
+ }
38
+ const segments = normalized.split("/").filter(Boolean);
39
+ return segments.length > 0 ? segments[segments.length - 1] : "";
40
+ }
41
+
42
+ function suggestTitle(prompt, cwd) {
43
+ const firstLine = getFirstPromptLine(prompt);
44
+ if (firstLine) {
45
+ return firstLine.slice(0, 120);
46
+ }
47
+ const workspaceName = getWorkspaceName(cwd);
48
+ return workspaceName ? `Task for ${workspaceName}` : "New task";
49
+ }
50
+
51
+ function getPrompt() {
52
+ try {
53
+ const params = new URLSearchParams(window.location.search || "");
54
+ return typeof params.get("prompt") === "string"
55
+ ? params.get("prompt").trim()
56
+ : "";
57
+ } catch (_) {
58
+ return "";
59
+ }
60
+ }
61
+
62
+ function openTaskBuilder(url) {
63
+ const popup = window.open(url.toString(), "_blank", "noopener");
64
+ if (!popup) {
65
+ window.location.href = url.toString();
66
+ }
67
+ }
68
+
69
+ function initRunTaskSave() {
70
+ const button = document.querySelector("[data-save-task-button]");
71
+ const body = document.body;
72
+ if (!button || !body) {
73
+ return;
74
+ }
75
+
76
+ const cwd = body.dataset.taskSaveCwd || "";
77
+ const workspacesRoot = body.dataset.taskSaveWorkspacesRoot || "";
78
+ const prompt = getPrompt();
79
+ if (!cwd || !workspacesRoot || !prompt) {
80
+ return;
81
+ }
82
+
83
+ const normalizedCwd = normalizePath(cwd);
84
+ const normalizedRoot = normalizePath(workspacesRoot);
85
+ if (!isPathWithin(normalizedCwd, normalizedRoot) || normalizedCwd === normalizedRoot) {
86
+ return;
87
+ }
88
+
89
+ const url = new URL("/tasks/new", window.location.origin);
90
+ url.searchParams.set("path", "workspaces");
91
+ url.searchParams.set("lockPath", "1");
92
+ url.searchParams.set("title", suggestTitle(prompt, normalizedCwd));
93
+ url.searchParams.set("template", prompt);
94
+ url.searchParams.set("sourceWorkspaceCwd", cwd);
95
+
96
+ button.hidden = false;
97
+ button.classList.remove("hidden");
98
+ button.addEventListener("click", (event) => {
99
+ event.preventDefault();
100
+ openTaskBuilder(url);
101
+ });
102
+ }
103
+
104
+ if (document.readyState === "loading") {
105
+ document.addEventListener("DOMContentLoaded", initRunTaskSave, { once: true });
106
+ } else {
107
+ initRunTaskSave();
108
+ }
109
+ })(window, document);
@@ -2,6 +2,12 @@
2
2
  "use strict";
3
3
 
4
4
  const CATEGORY_ORDER = ["CLI", "IDE"];
5
+ const TOOL_PREFERENCE_KEY = "pinokio.universalLauncher.tool";
6
+ const TOOL_VALUE_ALIASES = {
7
+ claude: "code/claude",
8
+ codex: "code/codex",
9
+ gemini: "code/gemini"
10
+ };
5
11
  const FALLBACK_TOOLS = [
6
12
  {
7
13
  value: "code/claude",
@@ -71,6 +77,35 @@
71
77
  return category === "IDE" ? "Desktop app" : category;
72
78
  }
73
79
 
80
+ function normalizeToolValue(value) {
81
+ const trimmed = typeof value === "string"
82
+ ? value.trim().replace(/^\/+|\/+$/g, "")
83
+ : "";
84
+ if (!trimmed) {
85
+ return "";
86
+ }
87
+ return TOOL_VALUE_ALIASES[trimmed] || trimmed;
88
+ }
89
+
90
+ function getStoredToolPreference() {
91
+ try {
92
+ return normalizeToolValue(window.localStorage.getItem(TOOL_PREFERENCE_KEY));
93
+ } catch (_) {
94
+ return "";
95
+ }
96
+ }
97
+
98
+ function setStoredToolPreference(value) {
99
+ try {
100
+ const normalizedValue = normalizeToolValue(value);
101
+ if (normalizedValue) {
102
+ window.localStorage.setItem(TOOL_PREFERENCE_KEY, normalizedValue);
103
+ } else {
104
+ window.localStorage.removeItem(TOOL_PREFERENCE_KEY);
105
+ }
106
+ } catch (_) {}
107
+ }
108
+
74
109
  function buildTaskToolPicker(tools, host, hiddenInput) {
75
110
  if (!host || !hiddenInput) {
76
111
  return null;
@@ -291,11 +326,15 @@
291
326
  }
292
327
 
293
328
  const api = {
294
- setValue(value) {
295
- const nextValue = typeof value === "string" ? value.trim() : "";
329
+ setValue(value, options) {
330
+ const opts = options && typeof options === "object" ? options : {};
331
+ const nextValue = normalizeToolValue(value);
296
332
  const entry = nextValue ? getEntryByValue(nextValue) : null;
297
333
  selectedValue = entry ? entry.meta.value : "";
298
334
  hiddenInput.value = selectedValue;
335
+ if (opts.persist !== false) {
336
+ setStoredToolPreference(selectedValue);
337
+ }
299
338
  syncTrigger();
300
339
  },
301
340
  openMenu() {
@@ -352,13 +391,20 @@
352
391
  api.closeMenu();
353
392
  }, true);
354
393
 
355
- const initialValue = hiddenInput.value.trim();
356
- const defaultTool = tools.find((tool) => tool.value === initialValue)
394
+ const initialValue = normalizeToolValue(hiddenInput.value);
395
+ const storedValue = getStoredToolPreference();
396
+ const explicitEntry = initialValue ? getEntryByValue(initialValue) : null;
397
+ const storedEntry = explicitEntry ? null : (storedValue ? getEntryByValue(storedValue) : null);
398
+ if (!explicitEntry && storedValue && !storedEntry) {
399
+ setStoredToolPreference("");
400
+ }
401
+ const defaultTool = (explicitEntry && explicitEntry.meta)
402
+ || (storedEntry && storedEntry.meta)
357
403
  || tools.find((tool) => tool.isDefault)
358
404
  || tools[0]
359
405
  || null;
360
406
  if (defaultTool) {
361
- api.setValue(defaultTool.value);
407
+ api.setValue(defaultTool.value, { persist: false });
362
408
  } else {
363
409
  syncTrigger();
364
410
  }
@@ -368,7 +414,9 @@
368
414
 
369
415
  async function getTools() {
370
416
  try {
371
- const response = await fetch("/api/plugin/menu");
417
+ const response = await fetch("/api/plugin/menu", {
418
+ cache: "no-store"
419
+ });
372
420
  if (!response.ok) {
373
421
  throw new Error(String(response.status));
374
422
  }
@@ -120,33 +120,38 @@
120
120
  event.preventDefault();
121
121
  this.closeModal();
122
122
  };
123
+ this.prefersModalInput = this.shouldPreferModalInput();
123
124
  const stored = this.loadDirectTypingPreference();
124
- if (stored === null) {
125
- this.directTypingEnabled = !this.shouldPreferModalInput();
125
+ if (!this.prefersModalInput) {
126
+ this.directTypingEnabled = true;
127
+ } else if (stored === null) {
128
+ this.directTypingEnabled = false;
126
129
  } else {
127
130
  this.directTypingEnabled = stored;
128
131
  }
129
132
  }
130
133
 
131
134
  shouldPreferModalInput() {
132
- if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
133
- try {
134
- if (window.matchMedia('(pointer: coarse)').matches) {
135
- return true;
136
- }
137
- } catch (_) {}
135
+ if (typeof navigator !== 'undefined') {
138
136
  try {
139
- if (window.matchMedia('(max-width: 768px)').matches) {
140
- return true;
137
+ if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
138
+ if (navigator.userAgentData.mobile) {
139
+ return true;
140
+ }
141
141
  }
142
142
  } catch (_) {}
143
- }
144
- if (typeof navigator !== 'undefined') {
145
143
  const ua = navigator.userAgent || '';
146
144
  if (/Mobi|Android|iPhone|iPad|Tablet/i.test(ua)) {
147
145
  return true;
148
146
  }
149
147
  }
148
+ if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
149
+ try {
150
+ if (window.matchMedia('(pointer: coarse)').matches) {
151
+ return true;
152
+ }
153
+ } catch (_) {}
154
+ }
150
155
  return false;
151
156
  }
152
157
 
@@ -294,7 +299,7 @@
294
299
  }
295
300
 
296
301
  setDirectTypingEnabled(enabled) {
297
- const next = Boolean(enabled);
302
+ const next = this.prefersModalInput ? Boolean(enabled) : true;
298
303
  if (next === this.directTypingEnabled) {
299
304
  return;
300
305
  }
@@ -1358,6 +1363,23 @@
1358
1363
  });
1359
1364
  }
1360
1365
 
1366
+ applyMobileRunnerLayout(runner) {
1367
+ if (!runner || !this.mobileInput || !this.mobileInput.prefersModalInput) {
1368
+ return;
1369
+ }
1370
+ const nodes = runner.querySelectorAll('#open-fs');
1371
+ if (!nodes || !nodes.length) {
1372
+ return;
1373
+ }
1374
+ nodes.forEach((node) => {
1375
+ if (!node) {
1376
+ return;
1377
+ }
1378
+ node.hidden = true;
1379
+ node.setAttribute('aria-hidden', 'true');
1380
+ });
1381
+ }
1382
+
1361
1383
  initRunnerMenus() {
1362
1384
  if (typeof document === 'undefined') {
1363
1385
  return;
@@ -1383,12 +1405,13 @@
1383
1405
  if (menu) {
1384
1406
  this.menus.add(menu);
1385
1407
  }
1386
- if (this.mobileInput && !this.minimalRunnerMode) {
1408
+ if (this.mobileInput && this.mobileInput.prefersModalInput && !this.minimalRunnerMode) {
1387
1409
  this.mobileInput.attachKeyboardButton(runner, utilities);
1388
1410
  }
1389
1411
  if (!this.minimalRunnerMode) {
1390
1412
  this.attachForceResizeButton(runner, utilities);
1391
1413
  }
1414
+ this.applyMobileRunnerLayout(runner);
1392
1415
  this.applyMinimalRunnerLayout(runner);
1393
1416
  });
1394
1417
  }
@@ -250,6 +250,10 @@ body.universal-launcher-open {
250
250
  display: none !important;
251
251
  }
252
252
 
253
+ .universal-launcher-overlay [hidden] {
254
+ display: none !important;
255
+ }
256
+
253
257
  .universal-launcher-panel {
254
258
  position: relative;
255
259
  width: min(620px, calc(100vw - 28px));
@@ -21,6 +21,7 @@
21
21
  <script src="/terminal-settings.js"></script>
22
22
  <script src="/pinokio-touch.js"></script>
23
23
  <script src="/common.js"></script>
24
+ <script src="/run-task-save.js"></script>
24
25
  <script src="/he.js"></script>
25
26
  <script src="/opener.js"></script>
26
27
  <!--
@@ -1848,7 +1849,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1848
1849
 
1849
1850
  </script>
1850
1851
  </head>
1851
- <body class='<%=theme%>' data-agent="<%=agent%>">
1852
+ <body class='<%=theme%>' data-agent="<%=agent%>" data-task-save-cwd="<%= cwd || '' %>" data-task-save-workspaces-root="<%= taskSaveWorkspacesRoot || '' %>">
1852
1853
  <% if (target === "_top") { %>
1853
1854
  <header class='navheader grabbable'>
1854
1855
  <% } else { %>
@@ -1867,6 +1868,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1867
1868
  <div class='hidden btn run stop-btn'>
1868
1869
  <span class='stop'><i class="fa-solid fa-stop"></i> Stop</span>
1869
1870
  </div>
1871
+ <button class='hidden btn' type='button' data-save-task-button hidden><i class="fa-solid fa-bookmark"></i> Save task</button>
1870
1872
  <div class='hidden btn stopped-btn'>
1871
1873
  <span class='stopped'><i class="fa-solid fa-hand"></i> Stopped</span>
1872
1874
  </div>
@@ -103,6 +103,24 @@
103
103
  </section>
104
104
 
105
105
  <input type="hidden" name="inputsJson" value="[]" data-task-inputs-json>
106
+ <input type="hidden" name="sourceWorkspaceCwd" value="<%= defaults.sourceWorkspaceCwd || '' %>">
107
+ <input type="hidden" name="lockPathSelection" value="<%= defaults.lockPathSelection ? '1' : '0' %>">
108
+
109
+ <% if (defaults.sourceWorkspaceCwd && (defaults.path || 'workspaces') === 'workspaces') { %>
110
+ <section class="task-section task-builder-workspace-section">
111
+ <div class="task-section-head">
112
+ <div>
113
+ <h2 class="task-section-title">Workspace</h2>
114
+ <p class="task-section-subcopy">Optional. Keep the task generic unless you want it to remember this workspace.</p>
115
+ </div>
116
+ </div>
117
+ <label class="task-toggle">
118
+ <input type="checkbox" name="rememberCurrentWorkspace" value="1" <%= defaults.rememberCurrentWorkspace ? 'checked' : '' %>>
119
+ <span>Remember current workspace</span>
120
+ </label>
121
+ <p class="task-inline-help">This lets the task offer "Continue in <%= defaults.sourceWorkspaceLabel || 'this workspace' %>" later. No files are copied or saved.</p>
122
+ </section>
123
+ <% } %>
106
124
 
107
125
  <div class="task-actions task-builder-actions">
108
126
  <button class="task-button primary" type="submit">
@@ -21,6 +21,7 @@
21
21
  <script src="/terminal-settings.js"></script>
22
22
  <script src="/pinokio-touch.js"></script>
23
23
  <script src="/common.js"></script>
24
+ <script src="/run-task-save.js"></script>
24
25
  <script src="/he.js"></script>
25
26
  <script src="/opener.js"></script>
26
27
  <script src="/loading.js"></script>
@@ -2631,9 +2632,9 @@ const reloadMemory = async () => {
2631
2632
  </script>
2632
2633
  </head>
2633
2634
  <% if (install_required) { %>
2634
- <body class='terminal-page frozen <%=theme%> <%= locals.full_navbar ? "has-full-navbar" : "" %>'>
2635
+ <body class='terminal-page frozen <%=theme%> <%= locals.full_navbar ? "has-full-navbar" : "" %>' data-task-save-cwd="<%= cwd || '' %>" data-task-save-workspaces-root="<%= taskSaveWorkspacesRoot || '' %>">
2635
2636
  <% } else { %>
2636
- <body class='terminal-page <%=theme%> <%= locals.full_navbar ? "has-full-navbar" : "" %>'>
2637
+ <body class='terminal-page <%=theme%> <%= locals.full_navbar ? "has-full-navbar" : "" %>' data-task-save-cwd="<%= cwd || '' %>" data-task-save-workspaces-root="<%= taskSaveWorkspacesRoot || '' %>">
2637
2638
  <% } %>
2638
2639
  <% if (locals.full_navbar) { %>
2639
2640
  <header class="navheader grabbable">
@@ -2675,8 +2676,9 @@ const reloadMemory = async () => {
2675
2676
  <span class='stop hidden'><i class="fa-solid fa-stop"></i> Stop</span>
2676
2677
  </div>
2677
2678
  <% if (locals.filepath) { %>
2678
- <button class='btn' id='open-fs' data-filepath="<%=filepath%>"><i class="fa-solid fa-eye"></i> Script</button>
2679
+ <button class='btn' id='open-fs' data-filepath="<%=filepath%>" data-command="view"><i class="fa-solid fa-folder-open"></i> File Explorer</button>
2679
2680
  <% } %>
2681
+ <button class='btn hidden' type='button' data-save-task-button hidden><i class="fa-solid fa-bookmark"></i> Save task</button>
2680
2682
  <div id='status-window'></div>
2681
2683
  <div id='progress-window' class='hidden'><div id='progress-bar'></div></div>
2682
2684
  </div>