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.
- package/dist/index.ts +198 -160
- 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 {
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
2444
|
-
const tabs = `${
|
|
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
|
|
2452
|
-
const fixedWidth =
|
|
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", `
|
|
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
|
|
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
|
|
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} ${
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2565
|
+
const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
|
|
2566
|
+
lines2.push(`${cursor} ${value} ${label}${dirty}`);
|
|
2536
2567
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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
|
|
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}
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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",
|
|
3859
|
-
{ label: "Allow for this 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: "
|
|
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: "
|
|
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",
|
|
3878
|
-
{ label: "Allow for this session
|
|
3879
|
-
{ label: "
|
|
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
|
-
|
|
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(
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
5663
|
-
|
|
5664
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 -
|
|
5735
|
+
return text(truncateToWidth3(value, Math.max(10, innerWidth - 20)));
|
|
5694
5736
|
};
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
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
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
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"
|