pi-landstrip 0.18.6 → 0.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.ts +262 -165
  2. package/package.json +2 -2
package/dist/index.ts CHANGED
@@ -88,7 +88,13 @@ import {
88
88
  keyHint,
89
89
  SessionManager
90
90
  } from "@earendil-works/pi-coding-agent";
91
- import { matchesKey, Text, truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@earendil-works/pi-tui";
91
+ import {
92
+ matchesKey,
93
+ Text,
94
+ truncateToWidth as truncateToWidth2,
95
+ visibleWidth as visibleWidth2,
96
+ wrapTextWithAnsi
97
+ } from "@earendil-works/pi-tui";
92
98
  import { Type } from "typebox";
93
99
 
94
100
  // agents.ts
@@ -1273,6 +1279,13 @@ function prepareProjectAgentEditor(cwd, agent, agentDir = getAgentDir3()) {
1273
1279
 
1274
1280
  // box.ts
1275
1281
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
1282
+ function dialogTabs(theme, tabs, active) {
1283
+ return tabs.map((tab) => tab === active ? theme.fg("accent", theme.bold(`[${tab}]`)) : theme.fg("muted", tab)).join(" ");
1284
+ }
1285
+ function dialogKeys(theme, hints) {
1286
+ const separator = theme.fg("dim", " · ");
1287
+ return hints.map(([key, description]) => `${theme.fg("accent", key)} ${theme.fg("muted", description)}`).join(separator);
1288
+ }
1276
1289
  function boxTop(theme, width, title) {
1277
1290
  if (width < 5)
1278
1291
  return truncateToWidth(theme.fg("accent", title), Math.max(1, width));
@@ -1720,22 +1733,27 @@ var MAX_DEPTH = 3;
1720
1733
  var packageDir = dirname4(fileURLToPath(import.meta.url));
1721
1734
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
1722
1735
  var INSPECTOR_BODY_LINES = 16;
1736
+ var TASK_PREVIEW_MIN_WIDTH = 92;
1737
+ var TASK_PREVIEW_LINES = 7;
1723
1738
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
1724
1739
  var AGENTS_HELP_ROWS = [
1725
1740
  ["Tab", "Next tab"],
1726
- ["Shift+Tab", "Change scope"],
1727
- ["↑ / ↓", "Select or scroll"],
1728
- ["← / →", "Sibling task"],
1729
- ["Enter", "Open / activate / save"],
1730
- ["Esc", "Back / close"],
1741
+ ["Shift+Tab / S", "Switch scope in Agents or Settings"],
1742
+ ["↑ / ↓", "Select item or scroll when follow is off"],
1743
+ ["F", "Toggle task log follow"],
1744
+ ["Page Up / Down", "Scroll task output by page"],
1745
+ ["Home / End", "Jump to task output boundary"],
1746
+ ["Enter", "Inspect task, set primary, or save"],
1747
+ ["Esc", "Back or close"],
1731
1748
  ["Ctrl+C", "Close"],
1732
- ["D / Space", "Toggle agent"],
1733
- ["Space", "Toggle sandbox"],
1749
+ ["Space", "Enable agent or toggle sandbox"],
1750
+ ["E", "Edit project agent"],
1751
+ ["X", "Delete project agent"],
1752
+ ["Y / N", "Confirm or cancel deletion"],
1734
1753
  ["I", "Use global value"],
1754
+ ["R", "Discard setting changes"],
1735
1755
  ["0-9 / + / -", "Set subagent limit"],
1736
- ["P / Backspace", "Parent task"],
1737
- ["Ctrl+G", "Edit agent"],
1738
- ["Ctrl+D", "Delete agent"]
1756
+ ["Backspace", "Open parent task"]
1739
1757
  ];
1740
1758
  var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
1741
1759
  var cachedPiPackage;
@@ -1848,6 +1866,40 @@ function taskDuration(details) {
1848
1866
  const seconds = Math.max(0, end - details.startedAt) / 1000;
1849
1867
  return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
1850
1868
  }
1869
+ function activeTaskRecords(tasks) {
1870
+ const byId = new Map(tasks.map((task) => [task.id, task]));
1871
+ const visibleIds = new Set;
1872
+ for (const task of tasks) {
1873
+ if (task.state !== "queued" && task.state !== "running")
1874
+ continue;
1875
+ const seen = new Set;
1876
+ let current = task;
1877
+ while (current && !seen.has(current.id)) {
1878
+ seen.add(current.id);
1879
+ visibleIds.add(current.id);
1880
+ current = current.parentTaskId ? byId.get(current.parentTaskId) : undefined;
1881
+ }
1882
+ }
1883
+ return tasks.filter((task) => visibleIds.has(task.id));
1884
+ }
1885
+ function taskProgress(task) {
1886
+ if (task.state === "queued")
1887
+ return ["waiting for slot"];
1888
+ if (task.state !== "running")
1889
+ return [];
1890
+ const progress = [];
1891
+ if (task.currentTool)
1892
+ progress.push(`→ ${task.currentTool}`);
1893
+ if (task.toolCalls !== undefined) {
1894
+ progress.push(`${task.toolCalls} call${task.toolCalls === 1 ? "" : "s"}`);
1895
+ }
1896
+ const duration = taskDuration(taskDetails(task));
1897
+ if (duration)
1898
+ progress.push(duration);
1899
+ if (task.retryAttempt)
1900
+ progress.push(`retry ${task.retryAttempt}`);
1901
+ return progress;
1902
+ }
1851
1903
  function taskOutput(details, fallback) {
1852
1904
  return details.error ?? details.output ?? fallback;
1853
1905
  }
@@ -1993,7 +2045,7 @@ class PermissionBroker {
1993
2045
  if (signal?.aborted)
1994
2046
  throw new Error("Permission request cancelled");
1995
2047
  const choice = await ctx.ui.select(`${task}: permission required
1996
- ${permission}: ${resource}`, ["Allow once", "Allow for this session", "Reject"], { signal });
2048
+ ${permission}: ${resource}`, ["Allow once", "Allow for this session", "Keep blocked"], { signal });
1997
2049
  if (choice === "Allow for this session")
1998
2050
  this.grants.add(key);
1999
2051
  if (choice !== "Allow once" && choice !== "Allow for this session") {
@@ -2370,7 +2422,7 @@ ${this.primaryAgent.prompt}` };
2370
2422
  return;
2371
2423
  }
2372
2424
  const projectTrusted = isProjectTrusted(ctx);
2373
- const tasks = [...this.tasks.values()];
2425
+ let tasks = [...this.tasks.values()];
2374
2426
  const requested = args.trim();
2375
2427
  let scope = projectTrusted ? "project" : "global";
2376
2428
  let catalog = this.getAgentCatalog(ctx);
@@ -2395,6 +2447,7 @@ ${this.primaryAgent.prompt}` };
2395
2447
  let editing = false;
2396
2448
  let confirmingDeleteAgent;
2397
2449
  let detail = requested.length > 0;
2450
+ let follow = detail;
2398
2451
  let scroll = 0;
2399
2452
  const reloadAgents = () => {
2400
2453
  const selectedName = agents[selectedAgent]?.name;
@@ -2409,6 +2462,12 @@ ${this.primaryAgent.prompt}` };
2409
2462
  maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2410
2463
  sandboxSettings = sandboxCallbacks?.load(ctx.cwd, projectTrusted) ?? { global: true };
2411
2464
  };
2465
+ const refreshTasks = () => {
2466
+ const selectedId = tasks[selectedTask]?.id;
2467
+ tasks = [...this.tasks.values()];
2468
+ const nextSelected = selectedId ? tasks.findIndex((task) => task.id === selectedId) : selectedTask;
2469
+ selectedTask = Math.max(0, Math.min(nextSelected < 0 ? 0 : nextSelected, tasks.length - 1));
2470
+ };
2412
2471
  const refreshAgentRuntime = async (name) => {
2413
2472
  reloadAgents();
2414
2473
  if (this.primaryAgent?.name === name) {
@@ -2440,38 +2499,39 @@ ${this.primaryAgent.prompt}` };
2440
2499
  const clipped = truncateToWidth2(value, cellWidth);
2441
2500
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
2442
2501
  };
2443
- const tabLabel = (label, active) => active ? theme.fg("accent", theme.bold(`[${label}]`)) : theme.fg("muted", label);
2444
- const tabs = `${tabLabel("Agents", tab === "agents")} ${tabLabel("Tasks", tab === "tasks")} ${tabLabel("Settings", tab === "settings")} ${tabLabel("Help", tab === "help")} ${theme.fg("dim", `· ${scope === "project" ? "Project" : "Global"} scope`)}`;
2502
+ refreshTasks();
2503
+ const tabs = `${dialogTabs(theme, ["Agents", "Tasks", "Settings", "Help"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`)}${tab === "tasks" ? "" : theme.fg("dim", ` · Scope: ${scope === "project" ? "Project" : "Global"}`)}`;
2445
2504
  if (tab === "agents") {
2446
2505
  const start = Math.max(0, Math.min(selectedAgent - 3, agents.length - 7));
2447
2506
  const shown2 = agents.slice(start, start + 7);
2448
2507
  const nameWidth = Math.min(20, Math.max(5, ...agents.map((agent2) => visibleWidth2(`@${agent2.name}`))));
2508
+ const primaryWidth = 7;
2449
2509
  const modeWidth = 8;
2450
2510
  const sourceWidth = 8;
2451
- const disabledWidth = 8;
2452
- const fixedWidth = 4 + nameWidth + 1 + modeWidth + 1 + sourceWidth + 1 + disabledWidth;
2511
+ const stateWidth = 13;
2512
+ const fixedWidth = 2 + primaryWidth + 1 + nameWidth + 1 + modeWidth + 1 + sourceWidth + 1 + stateWidth;
2453
2513
  const modelWidth = Math.max(10, Math.min(30, contentWidth - fixedWidth - 1));
2454
2514
  const lines2 = [
2455
2515
  tabs,
2456
2516
  "",
2457
- theme.fg("dim", ` ${pad("Agent", nameWidth)} ${pad("Mode", modeWidth)} ${pad("Source", sourceWidth)} ${pad("Model", modelWidth)} ${pad("Disabled", disabledWidth)}`)
2517
+ theme.fg("dim", ` ${pad("Primary", primaryWidth)} ${pad("Agent", nameWidth)} ${pad("Mode", modeWidth)} ${pad("Source", sourceWidth)} ${pad("Model", modelWidth)} ${pad("State", stateWidth)}`)
2458
2518
  ];
2459
2519
  for (const [offset, agent2] of shown2.entries()) {
2460
2520
  const index = start + offset;
2461
2521
  const selected = index === selectedAgent;
2462
2522
  const cursor = selected ? theme.fg("accent", "›") : " ";
2463
- const active = agent2.name === this.primaryAgent?.name ? theme.fg("success", "●") : theme.fg("dim", "○");
2523
+ const primary = agent2.name === this.primaryAgent?.name ? "yes" : "";
2464
2524
  const current = scopedAgent(agent2);
2465
2525
  const unavailable = current === undefined;
2466
2526
  const scopedDisabled = current?.disabled ?? false;
2467
2527
  const disabled = agent2.disabled;
2468
2528
  const deleting = confirmingDeleteAgent === agent2.name;
2469
2529
  const inherited = scope === "project" && !disabledOverrides.project.has(agent2.name);
2470
- const toggle = unavailable ? "n/a" : inherited ? "[-]" : scopedDisabled ? "[x]" : "[ ]";
2530
+ const state = unavailable ? "unavailable" : inherited ? `inherited ${scopedDisabled ? "off" : "on"}` : scopedDisabled ? "disabled" : "enabled";
2471
2531
  const color = deleting ? "error" : disabled ? "muted" : "text";
2472
2532
  const plainName = pad(`@${agent2.name}`, nameWidth);
2473
2533
  const name = deleting ? theme.fg("error", plainName) : disabled ? theme.fg("muted", plainName) : colorizeAgentText(agent2.color, plainName, (agentColor, text) => theme.fg(agentColor, text));
2474
- lines2.push(`${cursor} ${active} ${name} ${theme.fg(color, pad(agent2.mode, modeWidth))} ${theme.fg(color, pad(agent2.source, sourceWidth))} ${theme.fg(color, pad(agent2.model ?? "current model", modelWidth))} ${theme.fg(deleting ? "error" : unavailable ? "muted" : scopedDisabled ? "warning" : color, pad(toggle, disabledWidth))}`);
2534
+ lines2.push(`${cursor} ${theme.fg(primary ? "success" : "dim", pad(primary, primaryWidth))} ${name} ${theme.fg(color, pad(agent2.mode, modeWidth))} ${theme.fg(color, pad(agent2.source, sourceWidth))} ${theme.fg(color, pad(agent2.model ?? "current model", modelWidth))} ${theme.fg(deleting ? "error" : unavailable ? "muted" : scopedDisabled ? "warning" : color, pad(state, stateWidth))}`);
2475
2535
  }
2476
2536
  if (agents.length === 0)
2477
2537
  lines2.push(theme.fg("muted", " No agents configured."));
@@ -2499,7 +2559,9 @@ ${this.primaryAgent.prompt}` };
2499
2559
  if (agent.variant && !PI_THINKING_LEVELS.has(agent.variant)) {
2500
2560
  unsupported.push(`variant=${agent.variant}`);
2501
2561
  }
2502
- lines2.push(`${theme.fg("dim", "Unsupported RPC options:")} ${theme.fg("text", unsupported.join(", ") || "none")}`);
2562
+ if (unsupported.length > 0) {
2563
+ lines2.push(`${theme.fg("dim", "Unsupported RPC options:")} ${theme.fg("text", unsupported.join(", "))}`);
2564
+ }
2503
2565
  }
2504
2566
  if (catalog.diagnostics.length > 0) {
2505
2567
  lines2.push(theme.fg("error", "Catalog diagnostics"));
@@ -2508,7 +2570,10 @@ ${this.primaryAgent.prompt}` };
2508
2570
  }
2509
2571
  }
2510
2572
  if (confirmingDeleteAgent) {
2511
- lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? enter confirm esc cancel`));
2573
+ lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2574
+ ["Y", "delete"],
2575
+ ["N / Esc", "cancel"]
2576
+ ]));
2512
2577
  } else if (saving) {
2513
2578
  lines2.push("", theme.fg("dim", "Saving…"));
2514
2579
  }
@@ -2532,11 +2597,10 @@ ${this.primaryAgent.prompt}` };
2532
2597
  const text = `[ ${row.value} ]`;
2533
2598
  const value = selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
2534
2599
  const label = theme.fg(row.unavailable ? "muted" : "text", row.label);
2535
- lines2.push(`${cursor} ${value} ${label}`);
2536
- }
2537
- if (selectedMaxSubagents() === 0) {
2538
- lines2.push(` ${theme.fg("dim", "Subagents disabled")}`);
2600
+ const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
2601
+ lines2.push(`${cursor} ${value} ${label}${dirty}`);
2539
2602
  }
2603
+ lines2.push("", theme.fg("dim", selectedSetting === 0 ? "Sets concurrent subagents. Zero disables the task tool." : "Controls OS sandboxing. Use /sandbox to inspect its policy."));
2540
2604
  if (saving)
2541
2605
  lines2.push("", theme.fg("dim", "Saving…"));
2542
2606
  return box(lines2);
@@ -2553,35 +2617,61 @@ ${this.primaryAgent.prompt}` };
2553
2617
  return box(lines2);
2554
2618
  }
2555
2619
  if (!detail) {
2556
- const lines2 = [tabs, ""];
2620
+ const listLines = [];
2557
2621
  if (tasks.length === 0) {
2558
- lines2.push("No task sessions in this session.");
2622
+ listLines.push("No task sessions in this session.");
2559
2623
  } else {
2560
2624
  const start = Math.max(0, Math.min(selectedTask - 5, tasks.length - 11));
2561
2625
  const shown2 = tasks.slice(start, start + 11);
2562
- for (const [offset, task2] of shown2.entries()) {
2626
+ for (const [offset, task3] of shown2.entries()) {
2563
2627
  const index = start + offset;
2564
2628
  const cursor = index === selectedTask ? theme.fg("accent", "›") : " ";
2565
- const indent = " ".repeat(Math.max(0, task2.depth - 1));
2566
- lines2.push(`${cursor} ${indent}${taskState(theme, task2)} ${theme.fg("accent", `@${task2.agent}`)} ${theme.fg("text", task2.description)} ${theme.fg("dim", task2.id.slice(0, 8))}`);
2629
+ const indent = " ".repeat(Math.max(0, task3.depth - 1));
2630
+ listLines.push(`${cursor} ${indent}${taskState(theme, task3)} ${theme.fg("accent", `@${task3.agent}`)} ${theme.fg("text", task3.description)} ${theme.fg("dim", task3.id.slice(0, 8))}`);
2567
2631
  }
2568
2632
  }
2569
- return box(lines2);
2633
+ if (width < TASK_PREVIEW_MIN_WIDTH || tasks.length === 0) {
2634
+ return box([tabs, "", ...listLines]);
2635
+ }
2636
+ const task2 = tasks[selectedTask];
2637
+ if (!task2)
2638
+ return box([tabs, "", ...listLines]);
2639
+ const leftWidth = Math.max(44, Math.min(64, Math.floor(contentWidth * 0.52)));
2640
+ const rightWidth = Math.max(1, contentWidth - leftWidth - 3);
2641
+ const transcript2 = taskTranscript(task2).flatMap((line) => wrapTextWithAnsi(line, rightWidth));
2642
+ const duration2 = taskDuration(taskDetails(task2));
2643
+ const metrics2 = [
2644
+ task2.currentTool ? `→ ${task2.currentTool}` : undefined,
2645
+ task2.toolCalls === undefined ? undefined : `${task2.toolCalls} tool call${task2.toolCalls === 1 ? "" : "s"}`,
2646
+ duration2,
2647
+ task2.retryAttempt ? `retry ${task2.retryAttempt}` : undefined
2648
+ ].filter(Boolean);
2649
+ const previewLines = [
2650
+ `${theme.fg("accent", theme.bold(`@${task2.agent}`))} ${theme.fg("text", task2.description)}`,
2651
+ `${taskState(theme, task2)} ${theme.fg("dim", task2.id)}`,
2652
+ metrics2.length > 0 ? theme.fg("dim", metrics2.join(" · ")) : "",
2653
+ theme.fg("dim", "Latest activity"),
2654
+ ...transcript2.slice(-TASK_PREVIEW_LINES).map((line) => theme.fg("toolOutput", line))
2655
+ ];
2656
+ const divider = theme.fg("border", "│");
2657
+ const rowCount = Math.max(listLines.length, previewLines.length);
2658
+ const lines2 = Array.from({ length: rowCount }, (_, index) => {
2659
+ const left = pad(listLines[index] ?? "", leftWidth);
2660
+ const right = truncateToWidth2(previewLines[index] ?? "", rightWidth);
2661
+ return `${left} ${divider} ${right}`;
2662
+ });
2663
+ return box([tabs, "", ...lines2]);
2570
2664
  }
2571
2665
  const task = tasks[selectedTask];
2572
2666
  if (!task)
2573
2667
  return box([tabs, "", "No task sessions in this session."]);
2574
- const siblings = tasks.filter((candidate) => candidate.parentTaskId === task.parentTaskId);
2575
- const siblingIndex = siblings.findIndex((candidate) => candidate.id === task.id);
2576
- const transcript = taskTranscript(task).flatMap((line) => line.split(`
2577
- `));
2668
+ const transcript = taskTranscript(task).flatMap((line) => wrapTextWithAnsi(line, contentWidth));
2578
2669
  const maxScroll = Math.max(0, transcript.length - INSPECTOR_BODY_LINES);
2579
- scroll = Math.min(scroll, maxScroll);
2670
+ scroll = follow ? maxScroll : Math.min(scroll, maxScroll);
2580
2671
  const shown = transcript.slice(scroll, scroll + INSPECTOR_BODY_LINES);
2581
2672
  const duration = taskDuration(taskDetails(task));
2582
2673
  const metrics = [
2583
2674
  task.id,
2584
- `${siblingIndex + 1} of ${siblings.length}`,
2585
2675
  task.toolCalls === undefined ? undefined : `${task.toolCalls} tool call${task.toolCalls === 1 ? "" : "s"}`,
2586
2676
  duration
2587
2677
  ].filter(Boolean);
@@ -2589,25 +2679,25 @@ ${this.primaryAgent.prompt}` };
2589
2679
  tabs,
2590
2680
  "",
2591
2681
  `${theme.fg("accent", theme.bold(`@${task.agent}`))} ${theme.fg("text", task.description)}`,
2592
- `${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))}`,
2682
+ `${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))} ${theme.fg("dim", "·")} ${theme.fg(follow ? "accent" : "muted", `Follow: ${follow ? "on" : "off"}`)}`,
2593
2683
  "",
2594
2684
  ...shown.map((line) => theme.fg("toolOutput", line))
2595
2685
  ];
2596
2686
  while (lines.length < INSPECTOR_BODY_LINES + 5)
2597
2687
  lines.push("");
2598
2688
  if (transcript.length > INSPECTOR_BODY_LINES) {
2599
- lines.push(theme.fg("dim", `${scroll + 1}-${scroll + shown.length} of ${transcript.length}`));
2689
+ lines.push(theme.fg("dim", `${scroll + 1}–${scroll + shown.length} of ${transcript.length}`));
2600
2690
  }
2601
2691
  return box(lines.map((line) => truncateToWidth2(line, contentWidth)));
2602
2692
  },
2603
2693
  handleInput: (data) => {
2604
2694
  if (confirmingDeleteAgent !== undefined) {
2605
- if (matchesKey(data, "escape")) {
2695
+ if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
2606
2696
  confirmingDeleteAgent = undefined;
2607
2697
  tui.requestRender();
2608
2698
  return;
2609
2699
  }
2610
- if (matchesKey(data, "return")) {
2700
+ if (data.toLowerCase() === "y") {
2611
2701
  const name = confirmingDeleteAgent;
2612
2702
  const agent = agents.find((candidate) => candidate.name === name);
2613
2703
  confirmingDeleteAgent = undefined;
@@ -2629,15 +2719,18 @@ ${this.primaryAgent.prompt}` };
2629
2719
  }
2630
2720
  return;
2631
2721
  }
2632
- if (matchesKey(data, "shift+tab")) {
2722
+ if ((matchesKey(data, "shift+tab") || data.toLowerCase() === "s") && (tab === "agents" || tab === "settings")) {
2633
2723
  if (saving)
2634
2724
  return;
2725
+ if (dirtySettings.size > 0) {
2726
+ ctx.ui.notify("Save or discard settings changes before changing scope", "warning");
2727
+ return;
2728
+ }
2635
2729
  if (!projectTrusted && scope === "global") {
2636
2730
  ctx.ui.notify("Project settings require a trusted project", "warning");
2637
2731
  return;
2638
2732
  }
2639
2733
  scope = scope === "project" ? "global" : "project";
2640
- dirtySettings.clear();
2641
2734
  editing = false;
2642
2735
  reloadAgents();
2643
2736
  reloadSettings();
@@ -2645,9 +2738,14 @@ ${this.primaryAgent.prompt}` };
2645
2738
  return;
2646
2739
  }
2647
2740
  if (matchesKey(data, "tab")) {
2741
+ if (tab === "settings" && dirtySettings.size > 0) {
2742
+ ctx.ui.notify("Save changes with Enter or discard them with R before leaving", "warning");
2743
+ return;
2744
+ }
2648
2745
  const tabs = ["agents", "tasks", "settings", "help"];
2649
2746
  tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "agents";
2650
2747
  detail = false;
2748
+ follow = false;
2651
2749
  editing = false;
2652
2750
  scroll = 0;
2653
2751
  tui.requestRender();
@@ -2663,15 +2761,19 @@ ${this.primaryAgent.prompt}` };
2663
2761
  selectedAgent = Math.max(0, selectedAgent - 1);
2664
2762
  else if (matchesKey(data, "down") && agents.length > 0) {
2665
2763
  selectedAgent = Math.min(agents.length - 1, selectedAgent + 1);
2666
- } else if (matchesKey(data, "ctrl+d") && agent && !saving) {
2667
- if (!projectTrusted || !canDeleteProjectAgent(ctx.cwd, agent))
2764
+ } else if (data.toLowerCase() === "x" && agent && !saving) {
2765
+ if (!projectTrusted || !canDeleteProjectAgent(ctx.cwd, agent)) {
2766
+ ctx.ui.notify("Only project agent files can be deleted here", "warning");
2668
2767
  return;
2768
+ }
2669
2769
  confirmingDeleteAgent = agent.name;
2670
2770
  tui.requestRender();
2671
2771
  return;
2672
- } else if (matchesKey(data, "ctrl+g") && agent && !saving) {
2673
- if (!projectTrusted)
2772
+ } else if (data.toLowerCase() === "e" && agent && !saving) {
2773
+ if (!projectTrusted) {
2774
+ ctx.ui.notify("Agent editing requires a trusted project", "warning");
2674
2775
  return;
2776
+ }
2675
2777
  saving = true;
2676
2778
  (async () => {
2677
2779
  const document = prepareProjectAgentEditor(ctx.cwd, agent);
@@ -2686,7 +2788,7 @@ ${this.primaryAgent.prompt}` };
2686
2788
  tui.requestRender();
2687
2789
  });
2688
2790
  return;
2689
- } else if ((data === "d" || data === " ") && agent && !saving) {
2791
+ } else if (data === " " && agent && !saving) {
2690
2792
  const current = scopedAgent(agent);
2691
2793
  if (!current) {
2692
2794
  ctx.ui.notify("Local-only agents are unavailable in Global scope", "warning");
@@ -2702,7 +2804,7 @@ ${this.primaryAgent.prompt}` };
2702
2804
  tui.requestRender();
2703
2805
  });
2704
2806
  return;
2705
- } else if (data === "i" && scope === "project" && agent && !saving) {
2807
+ } else if (data.toLowerCase() === "i" && scope === "project" && agent && !saving) {
2706
2808
  if (!disabledOverrides.project.has(agent.name))
2707
2809
  return;
2708
2810
  saving = true;
@@ -2717,6 +2819,9 @@ ${this.primaryAgent.prompt}` };
2717
2819
  } else if (matchesKey(data, "return") && agent && !agent.disabled && agentSupportsMode(agent, "primary")) {
2718
2820
  this.selectPrimaryAgent(agent.name, ctx).then(() => tui.requestRender()).catch((error) => ctx.ui.notify(`Could not select primary agent: ${formatError(error)}`, "error"));
2719
2821
  return;
2822
+ } else if (matchesKey(data, "return") && agent) {
2823
+ ctx.ui.notify(agent.disabled ? "Enable this agent before setting it as primary" : "This agent cannot be used as the primary agent", "warning");
2824
+ return;
2720
2825
  } else
2721
2826
  return;
2722
2827
  tui.requestRender();
@@ -2726,9 +2831,20 @@ ${this.primaryAgent.prompt}` };
2726
2831
  if (saving)
2727
2832
  return;
2728
2833
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
2834
+ if (dirtySettings.size > 0) {
2835
+ ctx.ui.notify("Save changes with Enter or discard them with R before closing", "warning");
2836
+ return;
2837
+ }
2729
2838
  done();
2730
2839
  return;
2731
2840
  }
2841
+ if (data.toLowerCase() === "r") {
2842
+ reloadSettings();
2843
+ dirtySettings.clear();
2844
+ editing = false;
2845
+ tui.requestRender();
2846
+ return;
2847
+ }
2732
2848
  if (matchesKey(data, "up")) {
2733
2849
  selectedSetting = Math.max(0, selectedSetting - 1);
2734
2850
  editing = false;
@@ -2803,16 +2919,13 @@ ${this.primaryAgent.prompt}` };
2803
2919
  if (!detail) {
2804
2920
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2805
2921
  done();
2806
- else if (matchesKey(data, "left"))
2807
- tab = "agents";
2808
- else if (matchesKey(data, "right"))
2809
- tab = "settings";
2810
2922
  else if (matchesKey(data, "up"))
2811
2923
  selectedTask = Math.max(0, selectedTask - 1);
2812
2924
  else if (matchesKey(data, "down") && tasks.length > 0) {
2813
2925
  selectedTask = Math.min(tasks.length - 1, selectedTask + 1);
2814
2926
  } else if (matchesKey(data, "return") && tasks.length > 0) {
2815
2927
  detail = true;
2928
+ follow = true;
2816
2929
  scroll = 0;
2817
2930
  } else
2818
2931
  return;
@@ -2824,27 +2937,33 @@ ${this.primaryAgent.prompt}` };
2824
2937
  return;
2825
2938
  if (matchesKey(data, "escape")) {
2826
2939
  detail = false;
2940
+ follow = false;
2827
2941
  scroll = 0;
2828
2942
  } else if (matchesKey(data, "ctrl+c")) {
2829
2943
  done();
2830
- } else if (matchesKey(data, "up")) {
2944
+ } else if (data.toLowerCase() === "f") {
2945
+ follow = !follow;
2946
+ } else if (!follow && matchesKey(data, "up")) {
2831
2947
  scroll = Math.max(0, scroll - 1);
2832
- } else if (matchesKey(data, "down")) {
2948
+ } else if (!follow && matchesKey(data, "down")) {
2833
2949
  scroll += 1;
2834
- } else if (data === "p" || matchesKey(data, "backspace")) {
2950
+ } else if (!follow && matchesKey(data, "pageUp")) {
2951
+ scroll = Math.max(0, scroll - INSPECTOR_BODY_LINES);
2952
+ } else if (!follow && matchesKey(data, "pageDown")) {
2953
+ scroll += INSPECTOR_BODY_LINES;
2954
+ } else if (!follow && matchesKey(data, "home")) {
2955
+ scroll = 0;
2956
+ } else if (!follow && matchesKey(data, "end")) {
2957
+ scroll = Number.MAX_SAFE_INTEGER;
2958
+ } else if (matchesKey(data, "backspace")) {
2835
2959
  const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
2836
- if (parentIndex >= 0)
2960
+ if (parentIndex >= 0) {
2837
2961
  selectedTask = parentIndex;
2838
- else
2962
+ follow = true;
2963
+ } else {
2839
2964
  detail = false;
2840
- scroll = 0;
2841
- } else if (matchesKey(data, "left") || matchesKey(data, "right")) {
2842
- const siblings = tasks.filter((candidate) => candidate.parentTaskId === task.parentTaskId);
2843
- const siblingIndex = siblings.findIndex((candidate) => candidate.id === task.id);
2844
- const offset = matchesKey(data, "left") ? -1 : 1;
2845
- const sibling = siblings[siblingIndex + offset];
2846
- if (sibling)
2847
- selectedTask = tasks.indexOf(sibling);
2965
+ follow = false;
2966
+ }
2848
2967
  scroll = 0;
2849
2968
  } else
2850
2969
  return;
@@ -2917,7 +3036,7 @@ ${descriptions}` : ""),
2917
3036
  const background = input.background ? theme.fg("muted", " (background)") : "";
2918
3037
  const description = input.description?.trim() || "...";
2919
3038
  const agent = input.subagent_type?.trim() || "...";
2920
- const title = theme.fg("toolTitle", theme.bold("Agent Task"));
3039
+ const title = theme.fg("toolTitle", theme.bold("Agent"));
2921
3040
  return new Text(`${title}${background}${theme.fg("muted", " — ")}${theme.fg("text", description)}
2922
3041
  ` + theme.fg("dim", ` @${agent}`), 0, 0);
2923
3042
  },
@@ -3608,33 +3727,26 @@ ${renderTaskResult(task.id, "error", message)}`);
3608
3727
  if (!ctx.hasUI || ctx.mode !== undefined && ctx.mode !== "tui")
3609
3728
  return;
3610
3729
  const tasks = [...this.tasks.values()];
3611
- const byId = new Map(tasks.map((task) => [task.id, task]));
3612
- const rootId = (task) => {
3613
- const seen = new Set;
3614
- let current = task;
3615
- while (current.parentTaskId && !seen.has(current.id)) {
3616
- seen.add(current.id);
3617
- const parent = byId.get(current.parentTaskId);
3618
- if (!parent)
3619
- break;
3620
- current = parent;
3621
- }
3622
- return current.id;
3623
- };
3624
3730
  const active = tasks.filter((task) => task.state === "queued" || task.state === "running");
3625
- const activeRoots = new Set(active.map(rootId));
3626
- if (activeRoots.size === 0) {
3731
+ if (active.length === 0) {
3627
3732
  ctx.ui.setWidget(TASK_WIDGET, undefined);
3628
3733
  return;
3629
3734
  }
3630
- const visible = tasks.filter((task) => activeRoots.has(rootId(task)));
3735
+ const visible = activeTaskRecords(tasks);
3736
+ const byId = new Map(tasks.map((task) => [task.id, task]));
3737
+ const running = active.filter((task) => task.state === "running").length;
3738
+ const queued = active.length - running;
3739
+ const runningSummary = this.maxSubagents > 0 ? `${running}/${this.maxSubagents} running` : `${running} running`;
3740
+ const summary = queued > 0 ? `${runningSummary} · ${queued} queued` : runningSummary;
3631
3741
  ctx.ui.setWidget(TASK_WIDGET, (_tui, theme) => ({
3632
3742
  render: (width) => {
3633
- const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${active.length} active`);
3743
+ const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${summary}`);
3634
3744
  const tree = taskTreeLines(visible, (task) => {
3635
3745
  const agent = theme.fg("accent", `@${task.agent}`);
3636
3746
  const description = theme.fg("text", task.description);
3637
- return `${taskState(theme, task)} ${agent} ${description}`;
3747
+ const progress = taskProgress(byId.get(task.id));
3748
+ const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
3749
+ return `${taskState(theme, task)} ${agent} ${description}${suffix}`;
3638
3750
  });
3639
3751
  const shown = tree.slice(0, 8);
3640
3752
  if (tree.length > shown.length) {
@@ -3855,28 +3967,26 @@ var PROXY_ENVIRONMENT_VARIABLES = [
3855
3967
  ];
3856
3968
  var packageDir2 = dirname5(fileURLToPath2(import.meta.url));
3857
3969
  var PERMISSION_OPTIONS = [
3858
- { label: "Allow once", key: "o", action: "once" },
3859
- { label: "Allow for this session only", key: "s", action: "session" },
3860
- { label: "Abort (keep blocked)", key: "esc", action: "abort" },
3970
+ { label: "Allow once", action: "once" },
3971
+ { label: "Allow for this session", action: "session" },
3861
3972
  {
3862
3973
  label: "Allow for this project",
3863
- key: "P",
3864
3974
  action: "project",
3865
3975
  confirm: true,
3866
- hint: "-> .pi/sandbox.json"
3976
+ hint: "save to .pi/sandbox.json"
3867
3977
  },
3868
3978
  {
3869
3979
  label: "Allow for all projects",
3870
- key: "A",
3871
3980
  action: "global",
3872
3981
  confirm: true,
3873
- hint: "-> ~/.pi/agent/sandbox.json"
3874
- }
3982
+ hint: "save to ~/.pi/agent/sandbox.json"
3983
+ },
3984
+ { label: "Keep blocked", action: "abort" }
3875
3985
  ];
3876
3986
  var NETWORK_PERMISSION_OPTIONS = [
3877
- { label: "Allow once", key: "o", action: "once" },
3878
- { label: "Allow for this session only", key: "s", action: "session" },
3879
- { label: "Abort (keep blocked)", key: "esc", action: "abort" }
3987
+ { label: "Allow once", action: "once" },
3988
+ { label: "Allow for this session", action: "session" },
3989
+ { label: "Keep blocked", action: "abort" }
3880
3990
  ];
3881
3991
  function loadSandboxConfig(cwd, includeProject) {
3882
3992
  const projectConfigPath = join7(cwd, ".pi", "sandbox.json");
@@ -3990,10 +4100,7 @@ function writeConfigFile(configPath, config) {
3990
4100
  }
3991
4101
  function getSandboxConfigWriteTarget(cwd, includeProject = true) {
3992
4102
  const { globalPath, projectPath } = getConfigPaths(cwd);
3993
- if (includeProject && existsSync5(projectPath)) {
3994
- return { scope: "project", path: projectPath };
3995
- }
3996
- return { scope: "global", path: globalPath };
4103
+ return includeProject ? { scope: "project", path: projectPath } : { scope: "global", path: globalPath };
3997
4104
  }
3998
4105
  async function writeSandboxConfigEnabled(cwd, enabled, scope) {
3999
4106
  const paths = getConfigPaths(cwd);
@@ -4350,21 +4457,23 @@ async function stopPreparedChild(child) {
4350
4457
  async function showPermissionPrompt(ctx, title, options, signal) {
4351
4458
  if (!ctx.hasUI)
4352
4459
  return "abort";
4353
- const labels = options.map((option2) => option2.hint ? `${option2.label} ${option2.hint}` : option2.label);
4460
+ const labels = options.map((option2) => option2.hint ? `${option2.label} — ${option2.hint}` : option2.label);
4354
4461
  const selected = await ctx.ui.select(title, labels, { signal });
4355
4462
  const index = selected === undefined ? -1 : labels.indexOf(selected);
4356
4463
  const option = options[index];
4357
4464
  if (!option)
4358
4465
  return "abort";
4359
4466
  if (option.confirm) {
4360
- const confirmed = await ctx.ui.confirm(`Confirm ${option.label.toLowerCase()}`, option.hint ?? "This changes persisted sandbox policy.", { signal });
4467
+ const confirmed = await ctx.ui.confirm("Save this permission?", `${title}
4468
+
4469
+ ${option.hint ?? "This changes persisted sandbox policy."}`, { signal });
4361
4470
  if (!confirmed)
4362
4471
  return "abort";
4363
4472
  }
4364
4473
  return option.action;
4365
4474
  }
4366
4475
  function promptDomainBlock(ctx, domain, signal) {
4367
- return showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in allowedDomains`, PERMISSION_OPTIONS, signal);
4476
+ return showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, signal);
4368
4477
  }
4369
4478
  function promptReadBlock(ctx, filePath, reason, signal) {
4370
4479
  const title = reason ? `Read blocked: "${filePath}" is in denyRead (${reason})` : `Read blocked: "${filePath}" is not in allowRead`;
@@ -4374,7 +4483,7 @@ function promptWriteBlock(ctx, filePath, signal) {
4374
4483
  return showPermissionPrompt(ctx, `Write blocked: "${filePath}" is not in allowWrite`, PERMISSION_OPTIONS, signal);
4375
4484
  }
4376
4485
  function promptNetworkBlock(ctx, operation, target, signal) {
4377
- return showPermissionPrompt(ctx, `Network blocked: ${operation} to "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
4486
+ return showPermissionPrompt(ctx, `Network blocked: ${operation} "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
4378
4487
  }
4379
4488
  function landstripAvailable() {
4380
4489
  try {
@@ -5656,82 +5765,62 @@ ${message}
5656
5765
  handler: async (_args, ctx) => {
5657
5766
  if (!ctx.hasUI)
5658
5767
  return;
5659
- const config = loadConfig(ctx.cwd);
5768
+ const trustContext = ctx;
5769
+ const projectTrusted = trustContext.isProjectTrusted?.() ?? projectConfigTrusted;
5770
+ const config = loadSandboxConfig(ctx.cwd, projectTrusted);
5660
5771
  const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
5661
5772
  const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
5662
- const shouldToggle = await ctx.ui.custom((_tui, theme, _kb, done) => {
5663
- const { dim, muted, accent, text } = themeColors(theme);
5664
- function sandboxStatus() {
5773
+ const target = getSandboxConfigWriteTarget(ctx.cwd, projectTrusted);
5774
+ let tab = "Overview";
5775
+ const shouldToggle = await ctx.ui.custom((tui, theme, _kb, done) => {
5776
+ const { muted, accent, text } = themeColors(theme);
5777
+ const sandboxStatus = () => {
5665
5778
  if (noSandboxFlag)
5666
- return { color: "warning", label: "Disabled (--no-sandbox)" };
5779
+ return { color: "warning", label: "Disabled by --no-sandbox" };
5667
5780
  if (!config.enabled)
5668
- return { color: "warning", label: "Disabled" };
5781
+ return { color: "warning", label: "Disabled by configuration" };
5669
5782
  if (!sandboxEnabled || !sandboxReady)
5670
5783
  return { color: "warning", label: "Inactive" };
5671
5784
  return { color: "success", label: "Active" };
5672
- }
5673
- function boolValue(value) {
5674
- return value ? theme.fg("warning", "yes") : theme.fg("success", "no");
5675
- }
5785
+ };
5786
+ const networkMode = config.network.allowNetwork ? "Unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "Blocked" : "Proxied";
5676
5787
  return {
5677
5788
  render(width) {
5678
5789
  const innerWidth = Math.max(1, width - 4);
5679
- const row = (content = "") => boxRow(theme, width, content);
5680
- const lines = [boxTop(theme, width, "Sandbox")];
5681
5790
  const status = sandboxStatus();
5682
- const statusDot = theme.fg(status.color, "●");
5683
- const pathSnippet = text(truncateToWidth3(landstripDisplayPath(), Math.max(20, innerWidth - 28)));
5684
- const section = (title, detail) => {
5685
- lines.push(row(""));
5686
- lines.push(row(`${accent(title)}${detail ? dim(` · ${detail}`) : ""}`));
5687
- };
5688
- const item = (label, value) => {
5689
- lines.push(row(` ${dim("•")} ${muted(label.padEnd(13))} ${value}`));
5690
- };
5791
+ const tabs = dialogTabs(theme, ["Overview", "Policy"], tab);
5691
5792
  const listValue = (values) => {
5692
5793
  const value = values.join(", ") || "none";
5693
- return text(truncateToWidth3(value, Math.max(10, innerWidth - 17)));
5794
+ return text(truncateToWidth3(value, Math.max(10, innerWidth - 20)));
5694
5795
  };
5695
- lines.push(row(`${statusDot} ${text(status.label)} ${dim("·")} ${muted("landstrip")} ${pathSnippet}`));
5696
- section("Config");
5697
- item("project", text(projectPath));
5698
- item("global", text(globalPath));
5699
- const networkMode = config.network.allowNetwork ? "unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "blocked" : "proxied";
5700
- section("Network", networkMode);
5701
- item("allow network", boolValue(config.network.allowNetwork));
5702
- item("allowed", listValue(config.network.allowedDomains));
5703
- item("denied", listValue(config.network.deniedDomains));
5704
- item("unix sockets", config.network.allowAllUnixSockets ? text("all") : listValue(config.network.allowUnixSockets));
5705
- if (sessionAllowedDomains.length > 0) {
5706
- item("session", theme.fg("accent", sessionAllowedDomains.join(", ")));
5707
- }
5708
- section("Filesystem");
5709
- item("deny read", listValue(config.filesystem.denyRead));
5710
- item("allow read", listValue(config.filesystem.allowRead));
5711
- item("allow write", listValue(config.filesystem.allowWrite));
5712
- item("deny write", listValue(config.filesystem.denyWrite));
5713
- if (windowsImplementation !== undefined) {
5714
- section("Windows");
5715
- item("implementation", text(windowsImplementation === "restrictedUser" ? "restricted user" : "AppContainer"));
5716
- if (windowsImplementation === "appContainer") {
5717
- const mode = config.windows.appContainerMode;
5718
- item("container", mode === "standard" ? theme.fg("warning", `${mode} (weaker)`) : text(mode));
5719
- item("loopback", boolValue(config.windows.allowLoopback));
5720
- }
5721
- }
5722
- if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
5723
- section("Session grants");
5724
- if (sessionAllowedReadPaths.length > 0) {
5725
- item("read", theme.fg("accent", sessionAllowedReadPaths.join(", ")));
5726
- }
5727
- if (sessionAllowedWritePaths.length > 0) {
5728
- item("write", theme.fg("accent", sessionAllowedWritePaths.join(", ")));
5796
+ const item = (label, value) => ` ${muted(label.padEnd(16))} ${value}`;
5797
+ const footer = dialogKeys(theme, [
5798
+ ["Tab", "next tab"],
5799
+ ["Enter", `${config.enabled ? "disable" : "enable"} in ${target.scope}`],
5800
+ ["Esc", "close"]
5801
+ ]);
5802
+ const content = [tabs, ""];
5803
+ if (tab === "Overview") {
5804
+ content.push(`${theme.fg(status.color, "●")} ${text(status.label)}`, item("Configured", config.enabled ? text("Enabled") : theme.fg("warning", "Disabled")), item("Runtime", sandboxEnabled && sandboxReady ? theme.fg("success", "Running") : muted("Not running")), "", accent("Protection"), item("Network", text(networkMode)), item("Filesystem", text(`${config.filesystem.denyRead.length + config.filesystem.allowRead.length} read rules, ${config.filesystem.allowWrite.length + config.filesystem.denyWrite.length} write rules`)), item("Session grants", text(String(sessionAllowedDomains.length + sessionAllowedReadPaths.length + sessionAllowedWritePaths.length))), "", accent("Configuration"), item("Change scope", text(target.scope === "project" ? "Project" : "Global")), item("Change file", text(truncateToWidth3(target.path, Math.max(10, innerWidth - 20)))), item("Project file", text(truncateToWidth3(projectPath, Math.max(10, innerWidth - 20)))), item("Global file", text(truncateToWidth3(globalPath, Math.max(10, innerWidth - 20)))), item("Landstrip binary", text(truncateToWidth3(landstripDisplayPath(), Math.max(10, innerWidth - 20)))));
5805
+ if (windowsImplementation !== undefined) {
5806
+ content.push(item("Windows mode", text(windowsImplementation === "restrictedUser" ? "Restricted user" : `AppContainer (${config.windows.appContainerMode})`)));
5729
5807
  }
5808
+ } else {
5809
+ content.push(accent(`Network · ${networkMode}`), item("Allowed domains", listValue(config.network.allowedDomains)), item("Denied domains", listValue(config.network.deniedDomains)), item("Unix sockets", config.network.allowAllUnixSockets ? text("all") : listValue(config.network.allowUnixSockets)), "", accent("Filesystem"), item("Denied reads", listValue(config.filesystem.denyRead)), item("Allowed reads", listValue(config.filesystem.allowRead)), item("Allowed writes", listValue(config.filesystem.allowWrite)), item("Denied writes", listValue(config.filesystem.denyWrite)), "", accent("Session grants"), item("Domains", listValue(sessionAllowedDomains)), item("Read paths", listValue(sessionAllowedReadPaths)), item("Write paths", listValue(sessionAllowedWritePaths)));
5730
5810
  }
5731
- lines.push(row(""), row(`${dim("enter")} ${muted(config.enabled ? "disable" : "enable")} ${dim("esc")} ${muted("close")}`), boxBottom(theme, width));
5732
- return lines;
5811
+ content.push("", footer);
5812
+ return [
5813
+ boxTop(theme, width, "Sandbox"),
5814
+ ...content.map((line) => boxRow(theme, width, line)),
5815
+ boxBottom(theme, width)
5816
+ ];
5733
5817
  },
5734
5818
  handleInput(data) {
5819
+ if (matchesKey2(data, "tab")) {
5820
+ tab = tab === "Overview" ? "Policy" : "Overview";
5821
+ tui.requestRender();
5822
+ return;
5823
+ }
5735
5824
  if (matchesKey2(data, "return")) {
5736
5825
  done(true);
5737
5826
  return;
@@ -5748,8 +5837,16 @@ ${message}
5748
5837
  if (!shouldToggle)
5749
5838
  return;
5750
5839
  const enabled = !config.enabled;
5840
+ if (!enabled) {
5841
+ const confirmed = await ctx.ui.confirm("Disable sandbox?", `Commands will run without OS isolation.
5842
+
5843
+ Change: ${target.scope} configuration
5844
+ ${target.path}`);
5845
+ if (!confirmed)
5846
+ return;
5847
+ }
5751
5848
  try {
5752
- const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectConfigTrusted);
5849
+ const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectTrusted);
5753
5850
  if (!enabled) {
5754
5851
  disableSandbox(ctx);
5755
5852
  } else if (!noSandboxFlag) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.6",
3
+ "version": "0.18.8",
4
4
  "description": "Sandboxed Bash and process-backed agents for Pi",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -39,7 +39,7 @@
39
39
  "ci:test": "npm run test"
40
40
  },
41
41
  "dependencies": {
42
- "@landstrip/landstrip": "^0.18.6",
42
+ "@landstrip/landstrip": "^0.18.8",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"