pi-landstrip 0.18.6 → 0.18.7

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 +198 -160
  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));
@@ -1721,22 +1734,6 @@ var packageDir = dirname4(fileURLToPath(import.meta.url));
1721
1734
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
1722
1735
  var INSPECTOR_BODY_LINES = 16;
1723
1736
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
1724
- var AGENTS_HELP_ROWS = [
1725
- ["Tab", "Next tab"],
1726
- ["Shift+Tab", "Change scope"],
1727
- ["↑ / ↓", "Select or scroll"],
1728
- ["← / →", "Sibling task"],
1729
- ["Enter", "Open / activate / save"],
1730
- ["Esc", "Back / close"],
1731
- ["Ctrl+C", "Close"],
1732
- ["D / Space", "Toggle agent"],
1733
- ["Space", "Toggle sandbox"],
1734
- ["I", "Use global value"],
1735
- ["0-9 / + / -", "Set subagent limit"],
1736
- ["P / Backspace", "Parent task"],
1737
- ["Ctrl+G", "Edit agent"],
1738
- ["Ctrl+D", "Delete agent"]
1739
- ];
1740
1737
  var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
1741
1738
  var cachedPiPackage;
1742
1739
  var piPackageResolved = false;
@@ -1993,7 +1990,7 @@ class PermissionBroker {
1993
1990
  if (signal?.aborted)
1994
1991
  throw new Error("Permission request cancelled");
1995
1992
  const choice = await ctx.ui.select(`${task}: permission required
1996
- ${permission}: ${resource}`, ["Allow once", "Allow for this session", "Reject"], { signal });
1993
+ ${permission}: ${resource}`, ["Allow once", "Allow for this session", "Keep blocked"], { signal });
1997
1994
  if (choice === "Allow for this session")
1998
1995
  this.grants.add(key);
1999
1996
  if (choice !== "Allow once" && choice !== "Allow for this session") {
@@ -2370,7 +2367,7 @@ ${this.primaryAgent.prompt}` };
2370
2367
  return;
2371
2368
  }
2372
2369
  const projectTrusted = isProjectTrusted(ctx);
2373
- const tasks = [...this.tasks.values()];
2370
+ let tasks = [...this.tasks.values()];
2374
2371
  const requested = args.trim();
2375
2372
  let scope = projectTrusted ? "project" : "global";
2376
2373
  let catalog = this.getAgentCatalog(ctx);
@@ -2409,6 +2406,12 @@ ${this.primaryAgent.prompt}` };
2409
2406
  maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2410
2407
  sandboxSettings = sandboxCallbacks?.load(ctx.cwd, projectTrusted) ?? { global: true };
2411
2408
  };
2409
+ const refreshTasks = () => {
2410
+ const selectedId = tasks[selectedTask]?.id;
2411
+ tasks = [...this.tasks.values()];
2412
+ const nextSelected = selectedId ? tasks.findIndex((task) => task.id === selectedId) : selectedTask;
2413
+ selectedTask = Math.max(0, Math.min(nextSelected < 0 ? 0 : nextSelected, tasks.length - 1));
2414
+ };
2412
2415
  const refreshAgentRuntime = async (name) => {
2413
2416
  reloadAgents();
2414
2417
  if (this.primaryAgent?.name === name) {
@@ -2440,38 +2443,39 @@ ${this.primaryAgent.prompt}` };
2440
2443
  const clipped = truncateToWidth2(value, cellWidth);
2441
2444
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
2442
2445
  };
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`)}`;
2446
+ refreshTasks();
2447
+ const tabs = `${dialogTabs(theme, ["Agents", "Tasks", "Settings"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`)}${tab === "tasks" ? "" : theme.fg("dim", ` · Scope: ${scope === "project" ? "Project" : "Global"}`)}`;
2445
2448
  if (tab === "agents") {
2446
2449
  const start = Math.max(0, Math.min(selectedAgent - 3, agents.length - 7));
2447
2450
  const shown2 = agents.slice(start, start + 7);
2448
2451
  const nameWidth = Math.min(20, Math.max(5, ...agents.map((agent2) => visibleWidth2(`@${agent2.name}`))));
2452
+ const primaryWidth = 7;
2449
2453
  const modeWidth = 8;
2450
2454
  const sourceWidth = 8;
2451
- const disabledWidth = 8;
2452
- const fixedWidth = 4 + nameWidth + 1 + modeWidth + 1 + sourceWidth + 1 + disabledWidth;
2455
+ const stateWidth = 13;
2456
+ const fixedWidth = 2 + primaryWidth + 1 + nameWidth + 1 + modeWidth + 1 + sourceWidth + 1 + stateWidth;
2453
2457
  const modelWidth = Math.max(10, Math.min(30, contentWidth - fixedWidth - 1));
2454
2458
  const lines2 = [
2455
2459
  tabs,
2456
2460
  "",
2457
- theme.fg("dim", ` ${pad("Agent", nameWidth)} ${pad("Mode", modeWidth)} ${pad("Source", sourceWidth)} ${pad("Model", modelWidth)} ${pad("Disabled", disabledWidth)}`)
2461
+ theme.fg("dim", ` ${pad("Primary", primaryWidth)} ${pad("Agent", nameWidth)} ${pad("Mode", modeWidth)} ${pad("Source", sourceWidth)} ${pad("Model", modelWidth)} ${pad("State", stateWidth)}`)
2458
2462
  ];
2459
2463
  for (const [offset, agent2] of shown2.entries()) {
2460
2464
  const index = start + offset;
2461
2465
  const selected = index === selectedAgent;
2462
2466
  const cursor = selected ? theme.fg("accent", "›") : " ";
2463
- const active = agent2.name === this.primaryAgent?.name ? theme.fg("success", "●") : theme.fg("dim", "○");
2467
+ const primary = agent2.name === this.primaryAgent?.name ? "yes" : "";
2464
2468
  const current = scopedAgent(agent2);
2465
2469
  const unavailable = current === undefined;
2466
2470
  const scopedDisabled = current?.disabled ?? false;
2467
2471
  const disabled = agent2.disabled;
2468
2472
  const deleting = confirmingDeleteAgent === agent2.name;
2469
2473
  const inherited = scope === "project" && !disabledOverrides.project.has(agent2.name);
2470
- const toggle = unavailable ? "n/a" : inherited ? "[-]" : scopedDisabled ? "[x]" : "[ ]";
2474
+ const state = unavailable ? "unavailable" : inherited ? `inherited ${scopedDisabled ? "off" : "on"}` : scopedDisabled ? "disabled" : "enabled";
2471
2475
  const color = deleting ? "error" : disabled ? "muted" : "text";
2472
2476
  const plainName = pad(`@${agent2.name}`, nameWidth);
2473
2477
  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))}`);
2478
+ 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
2479
  }
2476
2480
  if (agents.length === 0)
2477
2481
  lines2.push(theme.fg("muted", " No agents configured."));
@@ -2499,7 +2503,9 @@ ${this.primaryAgent.prompt}` };
2499
2503
  if (agent.variant && !PI_THINKING_LEVELS.has(agent.variant)) {
2500
2504
  unsupported.push(`variant=${agent.variant}`);
2501
2505
  }
2502
- lines2.push(`${theme.fg("dim", "Unsupported RPC options:")} ${theme.fg("text", unsupported.join(", ") || "none")}`);
2506
+ if (unsupported.length > 0) {
2507
+ lines2.push(`${theme.fg("dim", "Unsupported RPC options:")} ${theme.fg("text", unsupported.join(", "))}`);
2508
+ }
2503
2509
  }
2504
2510
  if (catalog.diagnostics.length > 0) {
2505
2511
  lines2.push(theme.fg("error", "Catalog diagnostics"));
@@ -2507,10 +2513,34 @@ ${this.primaryAgent.prompt}` };
2507
2513
  lines2.push(` ${theme.fg("error", diagnostic)}`);
2508
2514
  }
2509
2515
  }
2516
+ lines2.push("");
2510
2517
  if (confirmingDeleteAgent) {
2511
- lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? enter confirm esc cancel`));
2518
+ lines2.push(theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2519
+ ["Y", "delete"],
2520
+ ["N / Esc", "cancel"]
2521
+ ]));
2512
2522
  } else if (saving) {
2513
- lines2.push("", theme.fg("dim", "Saving…"));
2523
+ lines2.push(theme.fg("dim", "Saving…"));
2524
+ } else {
2525
+ const mainHints = [["↑↓", "select"]];
2526
+ if (agent && !agent.disabled && agentSupportsMode(agent, "primary")) {
2527
+ mainHints.push(["Enter", "set primary"]);
2528
+ }
2529
+ if (agent && scopedAgent(agent))
2530
+ mainHints.push(["Space", "enable / disable"]);
2531
+ const actionHints = [];
2532
+ if (agent && projectTrusted)
2533
+ actionHints.push(["E", "edit"]);
2534
+ if (agent && projectTrusted && canDeleteProjectAgent(ctx.cwd, agent)) {
2535
+ actionHints.push(["X", "delete"]);
2536
+ }
2537
+ if (agent && scope === "project" && disabledOverrides.project.has(agent.name)) {
2538
+ actionHints.push(["I", "inherit"]);
2539
+ }
2540
+ if (projectTrusted)
2541
+ actionHints.push(["S", "scope"]);
2542
+ actionHints.push(["Tab", "next tab"], ["Esc", "close"]);
2543
+ lines2.push(dialogKeys(theme, mainHints), dialogKeys(theme, actionHints));
2514
2544
  }
2515
2545
  return box(lines2);
2516
2546
  }
@@ -2532,24 +2562,31 @@ ${this.primaryAgent.prompt}` };
2532
2562
  const text = `[ ${row.value} ]`;
2533
2563
  const value = selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
2534
2564
  const label = theme.fg(row.unavailable ? "muted" : "text", row.label);
2535
- lines2.push(`${cursor} ${value} ${label}`);
2565
+ const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
2566
+ lines2.push(`${cursor} ${value} ${label}${dirty}`);
2536
2567
  }
2537
- if (selectedMaxSubagents() === 0) {
2538
- lines2.push(` ${theme.fg("dim", "Subagents disabled")}`);
2539
- }
2540
- if (saving)
2568
+ lines2.push("", theme.fg("dim", selectedSetting === 0 ? "Sets concurrent subagents. Zero disables the task tool." : "Controls OS sandboxing. Use /sandbox to inspect its policy."));
2569
+ if (saving) {
2541
2570
  lines2.push("", theme.fg("dim", "Saving…"));
2542
- return box(lines2);
2543
- }
2544
- if (tab === "help") {
2545
- const shortcutWidth = Math.max(visibleWidth2("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth2(shortcut)));
2546
- const descriptionWidth = Math.max(1, contentWidth - shortcutWidth - 4);
2547
- const lines2 = [
2548
- tabs,
2549
- "",
2550
- theme.fg("dim", ` ${pad("Shortcut", shortcutWidth)} Description`),
2551
- ...AGENTS_HELP_ROWS.map(([shortcut, description]) => ` ${theme.fg("accent", pad(shortcut, shortcutWidth))} ${theme.fg("text", truncateToWidth2(description, descriptionWidth))}`)
2552
- ];
2571
+ } else {
2572
+ const editHints = [["↑↓", "select"]];
2573
+ if (selectedSetting === 0)
2574
+ editHints.push(["0–9 / +−", "change limit"]);
2575
+ else if (sandboxCallbacks)
2576
+ editHints.push(["Space", "toggle"]);
2577
+ const canInherit = scope === "project" && (selectedSetting === 0 ? maxSubagentsSettings.project !== undefined : sandboxSettings.project !== undefined);
2578
+ if (canInherit)
2579
+ editHints.push(["I", "inherit"]);
2580
+ const actionHints = [];
2581
+ if (dirtySettings.size > 0) {
2582
+ if (dirtySettings.has(selectedSetting))
2583
+ actionHints.push(["Enter", "save"]);
2584
+ actionHints.push(["R", "discard"]);
2585
+ } else {
2586
+ actionHints.push(["S", "scope"], ["Tab", "next tab"], ["Esc", "close"]);
2587
+ }
2588
+ lines2.push("", dialogKeys(theme, editHints), dialogKeys(theme, actionHints));
2589
+ }
2553
2590
  return box(lines2);
2554
2591
  }
2555
2592
  if (!detail) {
@@ -2566,22 +2603,24 @@ ${this.primaryAgent.prompt}` };
2566
2603
  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))}`);
2567
2604
  }
2568
2605
  }
2606
+ lines2.push("", dialogKeys(theme, [
2607
+ ["↑↓", "select"],
2608
+ ["Enter", "inspect"],
2609
+ ["Tab", "next tab"],
2610
+ ["Esc", "close"]
2611
+ ]));
2569
2612
  return box(lines2);
2570
2613
  }
2571
2614
  const task = tasks[selectedTask];
2572
2615
  if (!task)
2573
2616
  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
- `));
2617
+ const transcript = taskTranscript(task).flatMap((line) => wrapTextWithAnsi(line, contentWidth));
2578
2618
  const maxScroll = Math.max(0, transcript.length - INSPECTOR_BODY_LINES);
2579
2619
  scroll = Math.min(scroll, maxScroll);
2580
2620
  const shown = transcript.slice(scroll, scroll + INSPECTOR_BODY_LINES);
2581
2621
  const duration = taskDuration(taskDetails(task));
2582
2622
  const metrics = [
2583
2623
  task.id,
2584
- `${siblingIndex + 1} of ${siblings.length}`,
2585
2624
  task.toolCalls === undefined ? undefined : `${task.toolCalls} tool call${task.toolCalls === 1 ? "" : "s"}`,
2586
2625
  duration
2587
2626
  ].filter(Boolean);
@@ -2596,18 +2635,24 @@ ${this.primaryAgent.prompt}` };
2596
2635
  while (lines.length < INSPECTOR_BODY_LINES + 5)
2597
2636
  lines.push("");
2598
2637
  if (transcript.length > INSPECTOR_BODY_LINES) {
2599
- lines.push(theme.fg("dim", `${scroll + 1}-${scroll + shown.length} of ${transcript.length}`));
2638
+ lines.push(theme.fg("dim", `${scroll + 1}–${scroll + shown.length} of ${transcript.length}`));
2600
2639
  }
2640
+ lines.push("", dialogKeys(theme, [
2641
+ ["↑↓", "scroll"],
2642
+ ...task.parentTaskId ? [["Backspace", "parent"]] : [],
2643
+ ["Esc", "task list"],
2644
+ ["Tab", "next tab"]
2645
+ ]));
2601
2646
  return box(lines.map((line) => truncateToWidth2(line, contentWidth)));
2602
2647
  },
2603
2648
  handleInput: (data) => {
2604
2649
  if (confirmingDeleteAgent !== undefined) {
2605
- if (matchesKey(data, "escape")) {
2650
+ if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
2606
2651
  confirmingDeleteAgent = undefined;
2607
2652
  tui.requestRender();
2608
2653
  return;
2609
2654
  }
2610
- if (matchesKey(data, "return")) {
2655
+ if (data.toLowerCase() === "y") {
2611
2656
  const name = confirmingDeleteAgent;
2612
2657
  const agent = agents.find((candidate) => candidate.name === name);
2613
2658
  confirmingDeleteAgent = undefined;
@@ -2629,15 +2674,18 @@ ${this.primaryAgent.prompt}` };
2629
2674
  }
2630
2675
  return;
2631
2676
  }
2632
- if (matchesKey(data, "shift+tab")) {
2677
+ if (data.toLowerCase() === "s" && tab !== "tasks") {
2633
2678
  if (saving)
2634
2679
  return;
2680
+ if (dirtySettings.size > 0) {
2681
+ ctx.ui.notify("Save or discard settings changes before changing scope", "warning");
2682
+ return;
2683
+ }
2635
2684
  if (!projectTrusted && scope === "global") {
2636
2685
  ctx.ui.notify("Project settings require a trusted project", "warning");
2637
2686
  return;
2638
2687
  }
2639
2688
  scope = scope === "project" ? "global" : "project";
2640
- dirtySettings.clear();
2641
2689
  editing = false;
2642
2690
  reloadAgents();
2643
2691
  reloadSettings();
@@ -2645,7 +2693,11 @@ ${this.primaryAgent.prompt}` };
2645
2693
  return;
2646
2694
  }
2647
2695
  if (matchesKey(data, "tab")) {
2648
- const tabs = ["agents", "tasks", "settings", "help"];
2696
+ if (tab === "settings" && dirtySettings.size > 0) {
2697
+ ctx.ui.notify("Save changes with Enter or discard them with R before leaving", "warning");
2698
+ return;
2699
+ }
2700
+ const tabs = ["agents", "tasks", "settings"];
2649
2701
  tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "agents";
2650
2702
  detail = false;
2651
2703
  editing = false;
@@ -2663,15 +2715,19 @@ ${this.primaryAgent.prompt}` };
2663
2715
  selectedAgent = Math.max(0, selectedAgent - 1);
2664
2716
  else if (matchesKey(data, "down") && agents.length > 0) {
2665
2717
  selectedAgent = Math.min(agents.length - 1, selectedAgent + 1);
2666
- } else if (matchesKey(data, "ctrl+d") && agent && !saving) {
2667
- if (!projectTrusted || !canDeleteProjectAgent(ctx.cwd, agent))
2718
+ } else if (data.toLowerCase() === "x" && agent && !saving) {
2719
+ if (!projectTrusted || !canDeleteProjectAgent(ctx.cwd, agent)) {
2720
+ ctx.ui.notify("Only project agent files can be deleted here", "warning");
2668
2721
  return;
2722
+ }
2669
2723
  confirmingDeleteAgent = agent.name;
2670
2724
  tui.requestRender();
2671
2725
  return;
2672
- } else if (matchesKey(data, "ctrl+g") && agent && !saving) {
2673
- if (!projectTrusted)
2726
+ } else if (data.toLowerCase() === "e" && agent && !saving) {
2727
+ if (!projectTrusted) {
2728
+ ctx.ui.notify("Agent editing requires a trusted project", "warning");
2674
2729
  return;
2730
+ }
2675
2731
  saving = true;
2676
2732
  (async () => {
2677
2733
  const document = prepareProjectAgentEditor(ctx.cwd, agent);
@@ -2686,7 +2742,7 @@ ${this.primaryAgent.prompt}` };
2686
2742
  tui.requestRender();
2687
2743
  });
2688
2744
  return;
2689
- } else if ((data === "d" || data === " ") && agent && !saving) {
2745
+ } else if (data === " " && agent && !saving) {
2690
2746
  const current = scopedAgent(agent);
2691
2747
  if (!current) {
2692
2748
  ctx.ui.notify("Local-only agents are unavailable in Global scope", "warning");
@@ -2702,7 +2758,7 @@ ${this.primaryAgent.prompt}` };
2702
2758
  tui.requestRender();
2703
2759
  });
2704
2760
  return;
2705
- } else if (data === "i" && scope === "project" && agent && !saving) {
2761
+ } else if (data.toLowerCase() === "i" && scope === "project" && agent && !saving) {
2706
2762
  if (!disabledOverrides.project.has(agent.name))
2707
2763
  return;
2708
2764
  saving = true;
@@ -2717,6 +2773,9 @@ ${this.primaryAgent.prompt}` };
2717
2773
  } else if (matchesKey(data, "return") && agent && !agent.disabled && agentSupportsMode(agent, "primary")) {
2718
2774
  this.selectPrimaryAgent(agent.name, ctx).then(() => tui.requestRender()).catch((error) => ctx.ui.notify(`Could not select primary agent: ${formatError(error)}`, "error"));
2719
2775
  return;
2776
+ } else if (matchesKey(data, "return") && agent) {
2777
+ ctx.ui.notify(agent.disabled ? "Enable this agent before setting it as primary" : "This agent cannot be used as the primary agent", "warning");
2778
+ return;
2720
2779
  } else
2721
2780
  return;
2722
2781
  tui.requestRender();
@@ -2726,9 +2785,20 @@ ${this.primaryAgent.prompt}` };
2726
2785
  if (saving)
2727
2786
  return;
2728
2787
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
2788
+ if (dirtySettings.size > 0) {
2789
+ ctx.ui.notify("Save changes with Enter or discard them with R before closing", "warning");
2790
+ return;
2791
+ }
2729
2792
  done();
2730
2793
  return;
2731
2794
  }
2795
+ if (data.toLowerCase() === "r") {
2796
+ reloadSettings();
2797
+ dirtySettings.clear();
2798
+ editing = false;
2799
+ tui.requestRender();
2800
+ return;
2801
+ }
2732
2802
  if (matchesKey(data, "up")) {
2733
2803
  selectedSetting = Math.max(0, selectedSetting - 1);
2734
2804
  editing = false;
@@ -2795,18 +2865,9 @@ ${this.primaryAgent.prompt}` };
2795
2865
  tui.requestRender();
2796
2866
  return;
2797
2867
  }
2798
- if (tab === "help") {
2799
- if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2800
- done();
2801
- return;
2802
- }
2803
2868
  if (!detail) {
2804
2869
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2805
2870
  done();
2806
- else if (matchesKey(data, "left"))
2807
- tab = "agents";
2808
- else if (matchesKey(data, "right"))
2809
- tab = "settings";
2810
2871
  else if (matchesKey(data, "up"))
2811
2872
  selectedTask = Math.max(0, selectedTask - 1);
2812
2873
  else if (matchesKey(data, "down") && tasks.length > 0) {
@@ -2831,21 +2892,13 @@ ${this.primaryAgent.prompt}` };
2831
2892
  scroll = Math.max(0, scroll - 1);
2832
2893
  } else if (matchesKey(data, "down")) {
2833
2894
  scroll += 1;
2834
- } else if (data === "p" || matchesKey(data, "backspace")) {
2895
+ } else if (matchesKey(data, "backspace")) {
2835
2896
  const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
2836
2897
  if (parentIndex >= 0)
2837
2898
  selectedTask = parentIndex;
2838
2899
  else
2839
2900
  detail = false;
2840
2901
  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);
2848
- scroll = 0;
2849
2902
  } else
2850
2903
  return;
2851
2904
  tui.requestRender();
@@ -2917,7 +2970,7 @@ ${descriptions}` : ""),
2917
2970
  const background = input.background ? theme.fg("muted", " (background)") : "";
2918
2971
  const description = input.description?.trim() || "...";
2919
2972
  const agent = input.subagent_type?.trim() || "...";
2920
- const title = theme.fg("toolTitle", theme.bold("Agent Task"));
2973
+ const title = theme.fg("toolTitle", theme.bold("Agent"));
2921
2974
  return new Text(`${title}${background}${theme.fg("muted", " — ")}${theme.fg("text", description)}
2922
2975
  ` + theme.fg("dim", ` @${agent}`), 0, 0);
2923
2976
  },
@@ -3855,28 +3908,26 @@ var PROXY_ENVIRONMENT_VARIABLES = [
3855
3908
  ];
3856
3909
  var packageDir2 = dirname5(fileURLToPath2(import.meta.url));
3857
3910
  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" },
3911
+ { label: "Allow once", action: "once" },
3912
+ { label: "Allow for this session", action: "session" },
3861
3913
  {
3862
3914
  label: "Allow for this project",
3863
- key: "P",
3864
3915
  action: "project",
3865
3916
  confirm: true,
3866
- hint: "-> .pi/sandbox.json"
3917
+ hint: "save to .pi/sandbox.json"
3867
3918
  },
3868
3919
  {
3869
3920
  label: "Allow for all projects",
3870
- key: "A",
3871
3921
  action: "global",
3872
3922
  confirm: true,
3873
- hint: "-> ~/.pi/agent/sandbox.json"
3874
- }
3923
+ hint: "save to ~/.pi/agent/sandbox.json"
3924
+ },
3925
+ { label: "Keep blocked", action: "abort" }
3875
3926
  ];
3876
3927
  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" }
3928
+ { label: "Allow once", action: "once" },
3929
+ { label: "Allow for this session", action: "session" },
3930
+ { label: "Keep blocked", action: "abort" }
3880
3931
  ];
3881
3932
  function loadSandboxConfig(cwd, includeProject) {
3882
3933
  const projectConfigPath = join7(cwd, ".pi", "sandbox.json");
@@ -3990,10 +4041,7 @@ function writeConfigFile(configPath, config) {
3990
4041
  }
3991
4042
  function getSandboxConfigWriteTarget(cwd, includeProject = true) {
3992
4043
  const { globalPath, projectPath } = getConfigPaths(cwd);
3993
- if (includeProject && existsSync5(projectPath)) {
3994
- return { scope: "project", path: projectPath };
3995
- }
3996
- return { scope: "global", path: globalPath };
4044
+ return includeProject ? { scope: "project", path: projectPath } : { scope: "global", path: globalPath };
3997
4045
  }
3998
4046
  async function writeSandboxConfigEnabled(cwd, enabled, scope) {
3999
4047
  const paths = getConfigPaths(cwd);
@@ -4350,21 +4398,23 @@ async function stopPreparedChild(child) {
4350
4398
  async function showPermissionPrompt(ctx, title, options, signal) {
4351
4399
  if (!ctx.hasUI)
4352
4400
  return "abort";
4353
- const labels = options.map((option2) => option2.hint ? `${option2.label} ${option2.hint}` : option2.label);
4401
+ const labels = options.map((option2) => option2.hint ? `${option2.label} — ${option2.hint}` : option2.label);
4354
4402
  const selected = await ctx.ui.select(title, labels, { signal });
4355
4403
  const index = selected === undefined ? -1 : labels.indexOf(selected);
4356
4404
  const option = options[index];
4357
4405
  if (!option)
4358
4406
  return "abort";
4359
4407
  if (option.confirm) {
4360
- const confirmed = await ctx.ui.confirm(`Confirm ${option.label.toLowerCase()}`, option.hint ?? "This changes persisted sandbox policy.", { signal });
4408
+ const confirmed = await ctx.ui.confirm("Save this permission?", `${title}
4409
+
4410
+ ${option.hint ?? "This changes persisted sandbox policy."}`, { signal });
4361
4411
  if (!confirmed)
4362
4412
  return "abort";
4363
4413
  }
4364
4414
  return option.action;
4365
4415
  }
4366
4416
  function promptDomainBlock(ctx, domain, signal) {
4367
- return showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in allowedDomains`, PERMISSION_OPTIONS, signal);
4417
+ return showPermissionPrompt(ctx, `Network blocked: "${domain}" is not in the allowed domains list`, PERMISSION_OPTIONS, signal);
4368
4418
  }
4369
4419
  function promptReadBlock(ctx, filePath, reason, signal) {
4370
4420
  const title = reason ? `Read blocked: "${filePath}" is in denyRead (${reason})` : `Read blocked: "${filePath}" is not in allowRead`;
@@ -4374,7 +4424,7 @@ function promptWriteBlock(ctx, filePath, signal) {
4374
4424
  return showPermissionPrompt(ctx, `Write blocked: "${filePath}" is not in allowWrite`, PERMISSION_OPTIONS, signal);
4375
4425
  }
4376
4426
  function promptNetworkBlock(ctx, operation, target, signal) {
4377
- return showPermissionPrompt(ctx, `Network blocked: ${operation} to "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
4427
+ return showPermissionPrompt(ctx, `Network blocked: ${operation} "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
4378
4428
  }
4379
4429
  function landstripAvailable() {
4380
4430
  try {
@@ -5656,82 +5706,62 @@ ${message}
5656
5706
  handler: async (_args, ctx) => {
5657
5707
  if (!ctx.hasUI)
5658
5708
  return;
5659
- const config = loadConfig(ctx.cwd);
5709
+ const trustContext = ctx;
5710
+ const projectTrusted = trustContext.isProjectTrusted?.() ?? projectConfigTrusted;
5711
+ const config = loadSandboxConfig(ctx.cwd, projectTrusted);
5660
5712
  const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
5661
5713
  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() {
5714
+ const target = getSandboxConfigWriteTarget(ctx.cwd, projectTrusted);
5715
+ let tab = "Overview";
5716
+ const shouldToggle = await ctx.ui.custom((tui, theme, _kb, done) => {
5717
+ const { muted, accent, text } = themeColors(theme);
5718
+ const sandboxStatus = () => {
5665
5719
  if (noSandboxFlag)
5666
- return { color: "warning", label: "Disabled (--no-sandbox)" };
5720
+ return { color: "warning", label: "Disabled by --no-sandbox" };
5667
5721
  if (!config.enabled)
5668
- return { color: "warning", label: "Disabled" };
5722
+ return { color: "warning", label: "Disabled by configuration" };
5669
5723
  if (!sandboxEnabled || !sandboxReady)
5670
5724
  return { color: "warning", label: "Inactive" };
5671
5725
  return { color: "success", label: "Active" };
5672
- }
5673
- function boolValue(value) {
5674
- return value ? theme.fg("warning", "yes") : theme.fg("success", "no");
5675
- }
5726
+ };
5727
+ const networkMode = config.network.allowNetwork ? "Unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "Blocked" : "Proxied";
5676
5728
  return {
5677
5729
  render(width) {
5678
5730
  const innerWidth = Math.max(1, width - 4);
5679
- const row = (content = "") => boxRow(theme, width, content);
5680
- const lines = [boxTop(theme, width, "Sandbox")];
5681
5731
  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
- };
5732
+ const tabs = dialogTabs(theme, ["Overview", "Policy"], tab);
5691
5733
  const listValue = (values) => {
5692
5734
  const value = values.join(", ") || "none";
5693
- return text(truncateToWidth3(value, Math.max(10, innerWidth - 17)));
5735
+ return text(truncateToWidth3(value, Math.max(10, innerWidth - 20)));
5694
5736
  };
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));
5737
+ const item = (label, value) => ` ${muted(label.padEnd(16))} ${value}`;
5738
+ const footer = dialogKeys(theme, [
5739
+ ["Tab", "next tab"],
5740
+ ["Enter", `${config.enabled ? "disable" : "enable"} in ${target.scope}`],
5741
+ ["Esc", "close"]
5742
+ ]);
5743
+ const content = [tabs, ""];
5744
+ if (tab === "Overview") {
5745
+ 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)))));
5746
+ if (windowsImplementation !== undefined) {
5747
+ content.push(item("Windows mode", text(windowsImplementation === "restrictedUser" ? "Restricted user" : `AppContainer (${config.windows.appContainerMode})`)));
5720
5748
  }
5749
+ } else {
5750
+ 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)));
5721
5751
  }
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(", ")));
5729
- }
5730
- }
5731
- lines.push(row(""), row(`${dim("enter")} ${muted(config.enabled ? "disable" : "enable")} ${dim("esc")} ${muted("close")}`), boxBottom(theme, width));
5732
- return lines;
5752
+ content.push("", footer);
5753
+ return [
5754
+ boxTop(theme, width, "Sandbox"),
5755
+ ...content.map((line) => boxRow(theme, width, line)),
5756
+ boxBottom(theme, width)
5757
+ ];
5733
5758
  },
5734
5759
  handleInput(data) {
5760
+ if (matchesKey2(data, "tab")) {
5761
+ tab = tab === "Overview" ? "Policy" : "Overview";
5762
+ tui.requestRender();
5763
+ return;
5764
+ }
5735
5765
  if (matchesKey2(data, "return")) {
5736
5766
  done(true);
5737
5767
  return;
@@ -5748,8 +5778,16 @@ ${message}
5748
5778
  if (!shouldToggle)
5749
5779
  return;
5750
5780
  const enabled = !config.enabled;
5781
+ if (!enabled) {
5782
+ const confirmed = await ctx.ui.confirm("Disable sandbox?", `Commands will run without OS isolation.
5783
+
5784
+ Change: ${target.scope} configuration
5785
+ ${target.path}`);
5786
+ if (!confirmed)
5787
+ return;
5788
+ }
5751
5789
  try {
5752
- const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectConfigTrusted);
5790
+ const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectTrusted);
5753
5791
  if (!enabled) {
5754
5792
  disableSandbox(ctx);
5755
5793
  } 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.7",
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.7",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"