pi-landstrip 0.18.5 → 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 +204 -132
- 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));
|
|
@@ -1977,7 +1990,7 @@ class PermissionBroker {
|
|
|
1977
1990
|
if (signal?.aborted)
|
|
1978
1991
|
throw new Error("Permission request cancelled");
|
|
1979
1992
|
const choice = await ctx.ui.select(`${task}: permission required
|
|
1980
|
-
${permission}: ${resource}`, ["Allow once", "Allow for this session", "
|
|
1993
|
+
${permission}: ${resource}`, ["Allow once", "Allow for this session", "Keep blocked"], { signal });
|
|
1981
1994
|
if (choice === "Allow for this session")
|
|
1982
1995
|
this.grants.add(key);
|
|
1983
1996
|
if (choice !== "Allow once" && choice !== "Allow for this session") {
|
|
@@ -2354,7 +2367,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
2354
2367
|
return;
|
|
2355
2368
|
}
|
|
2356
2369
|
const projectTrusted = isProjectTrusted(ctx);
|
|
2357
|
-
|
|
2370
|
+
let tasks = [...this.tasks.values()];
|
|
2358
2371
|
const requested = args.trim();
|
|
2359
2372
|
let scope = projectTrusted ? "project" : "global";
|
|
2360
2373
|
let catalog = this.getAgentCatalog(ctx);
|
|
@@ -2393,6 +2406,12 @@ ${this.primaryAgent.prompt}` };
|
|
|
2393
2406
|
maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
|
|
2394
2407
|
sandboxSettings = sandboxCallbacks?.load(ctx.cwd, projectTrusted) ?? { global: true };
|
|
2395
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
|
+
};
|
|
2396
2415
|
const refreshAgentRuntime = async (name) => {
|
|
2397
2416
|
reloadAgents();
|
|
2398
2417
|
if (this.primaryAgent?.name === name) {
|
|
@@ -2420,42 +2439,43 @@ ${this.primaryAgent.prompt}` };
|
|
|
2420
2439
|
...lines2.map((line) => boxRow(theme, width, line)),
|
|
2421
2440
|
boxBottom(theme, width)
|
|
2422
2441
|
];
|
|
2423
|
-
const
|
|
2424
|
-
|
|
2442
|
+
const pad = (value, cellWidth) => {
|
|
2443
|
+
const clipped = truncateToWidth2(value, cellWidth);
|
|
2444
|
+
return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
|
|
2445
|
+
};
|
|
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"}`)}`;
|
|
2425
2448
|
if (tab === "agents") {
|
|
2426
2449
|
const start = Math.max(0, Math.min(selectedAgent - 3, agents.length - 7));
|
|
2427
2450
|
const shown2 = agents.slice(start, start + 7);
|
|
2428
2451
|
const nameWidth = Math.min(20, Math.max(5, ...agents.map((agent2) => visibleWidth2(`@${agent2.name}`))));
|
|
2452
|
+
const primaryWidth = 7;
|
|
2429
2453
|
const modeWidth = 8;
|
|
2430
2454
|
const sourceWidth = 8;
|
|
2431
|
-
const
|
|
2432
|
-
const fixedWidth =
|
|
2455
|
+
const stateWidth = 13;
|
|
2456
|
+
const fixedWidth = 2 + primaryWidth + 1 + nameWidth + 1 + modeWidth + 1 + sourceWidth + 1 + stateWidth;
|
|
2433
2457
|
const modelWidth = Math.max(10, Math.min(30, contentWidth - fixedWidth - 1));
|
|
2434
|
-
const pad = (value, cellWidth) => {
|
|
2435
|
-
const clipped = truncateToWidth2(value, cellWidth);
|
|
2436
|
-
return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
|
|
2437
|
-
};
|
|
2438
2458
|
const lines2 = [
|
|
2439
2459
|
tabs,
|
|
2440
2460
|
"",
|
|
2441
|
-
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)}`)
|
|
2442
2462
|
];
|
|
2443
2463
|
for (const [offset, agent2] of shown2.entries()) {
|
|
2444
2464
|
const index = start + offset;
|
|
2445
2465
|
const selected = index === selectedAgent;
|
|
2446
2466
|
const cursor = selected ? theme.fg("accent", "›") : " ";
|
|
2447
|
-
const
|
|
2467
|
+
const primary = agent2.name === this.primaryAgent?.name ? "yes" : "";
|
|
2448
2468
|
const current = scopedAgent(agent2);
|
|
2449
2469
|
const unavailable = current === undefined;
|
|
2450
2470
|
const scopedDisabled = current?.disabled ?? false;
|
|
2451
2471
|
const disabled = agent2.disabled;
|
|
2452
2472
|
const deleting = confirmingDeleteAgent === agent2.name;
|
|
2453
2473
|
const inherited = scope === "project" && !disabledOverrides.project.has(agent2.name);
|
|
2454
|
-
const
|
|
2474
|
+
const state = unavailable ? "unavailable" : inherited ? `inherited ${scopedDisabled ? "off" : "on"}` : scopedDisabled ? "disabled" : "enabled";
|
|
2455
2475
|
const color = deleting ? "error" : disabled ? "muted" : "text";
|
|
2456
2476
|
const plainName = pad(`@${agent2.name}`, nameWidth);
|
|
2457
2477
|
const name = deleting ? theme.fg("error", plainName) : disabled ? theme.fg("muted", plainName) : colorizeAgentText(agent2.color, plainName, (agentColor, text) => theme.fg(agentColor, text));
|
|
2458
|
-
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))}`);
|
|
2459
2479
|
}
|
|
2460
2480
|
if (agents.length === 0)
|
|
2461
2481
|
lines2.push(theme.fg("muted", " No agents configured."));
|
|
@@ -2483,7 +2503,9 @@ ${this.primaryAgent.prompt}` };
|
|
|
2483
2503
|
if (agent.variant && !PI_THINKING_LEVELS.has(agent.variant)) {
|
|
2484
2504
|
unsupported.push(`variant=${agent.variant}`);
|
|
2485
2505
|
}
|
|
2486
|
-
|
|
2506
|
+
if (unsupported.length > 0) {
|
|
2507
|
+
lines2.push(`${theme.fg("dim", "Unsupported RPC options:")} ${theme.fg("text", unsupported.join(", "))}`);
|
|
2508
|
+
}
|
|
2487
2509
|
}
|
|
2488
2510
|
if (catalog.diagnostics.length > 0) {
|
|
2489
2511
|
lines2.push(theme.fg("error", "Catalog diagnostics"));
|
|
@@ -2491,8 +2513,35 @@ ${this.primaryAgent.prompt}` };
|
|
|
2491
2513
|
lines2.push(` ${theme.fg("error", diagnostic)}`);
|
|
2492
2514
|
}
|
|
2493
2515
|
}
|
|
2494
|
-
|
|
2495
|
-
|
|
2516
|
+
lines2.push("");
|
|
2517
|
+
if (confirmingDeleteAgent) {
|
|
2518
|
+
lines2.push(theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
|
|
2519
|
+
["Y", "delete"],
|
|
2520
|
+
["N / Esc", "cancel"]
|
|
2521
|
+
]));
|
|
2522
|
+
} else if (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));
|
|
2544
|
+
}
|
|
2496
2545
|
return box(lines2);
|
|
2497
2546
|
}
|
|
2498
2547
|
if (tab === "settings") {
|
|
@@ -2513,12 +2562,31 @@ ${this.primaryAgent.prompt}` };
|
|
|
2513
2562
|
const text = `[ ${row.value} ]`;
|
|
2514
2563
|
const value = selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
|
|
2515
2564
|
const label = theme.fg(row.unavailable ? "muted" : "text", row.label);
|
|
2516
|
-
|
|
2565
|
+
const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
|
|
2566
|
+
lines2.push(`${cursor} ${value} ${label}${dirty}`);
|
|
2517
2567
|
}
|
|
2518
|
-
|
|
2519
|
-
|
|
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) {
|
|
2570
|
+
lines2.push("", theme.fg("dim", "Saving…"));
|
|
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));
|
|
2520
2589
|
}
|
|
2521
|
-
lines2.push("", theme.fg("dim", saving ? "Saving…" : "↑↓ select 0-9/+/- max space toggle i inherit enter save tab next ⇧tab scope esc close"));
|
|
2522
2590
|
return box(lines2);
|
|
2523
2591
|
}
|
|
2524
2592
|
if (!detail) {
|
|
@@ -2535,23 +2603,24 @@ ${this.primaryAgent.prompt}` };
|
|
|
2535
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))}`);
|
|
2536
2604
|
}
|
|
2537
2605
|
}
|
|
2538
|
-
lines2.push("", theme
|
|
2606
|
+
lines2.push("", dialogKeys(theme, [
|
|
2607
|
+
["↑↓", "select"],
|
|
2608
|
+
["Enter", "inspect"],
|
|
2609
|
+
["Tab", "next tab"],
|
|
2610
|
+
["Esc", "close"]
|
|
2611
|
+
]));
|
|
2539
2612
|
return box(lines2);
|
|
2540
2613
|
}
|
|
2541
2614
|
const task = tasks[selectedTask];
|
|
2542
2615
|
if (!task)
|
|
2543
2616
|
return box([tabs, "", "No task sessions in this session."]);
|
|
2544
|
-
const
|
|
2545
|
-
const siblingIndex = siblings.findIndex((candidate) => candidate.id === task.id);
|
|
2546
|
-
const transcript = taskTranscript(task).flatMap((line) => line.split(`
|
|
2547
|
-
`));
|
|
2617
|
+
const transcript = taskTranscript(task).flatMap((line) => wrapTextWithAnsi(line, contentWidth));
|
|
2548
2618
|
const maxScroll = Math.max(0, transcript.length - INSPECTOR_BODY_LINES);
|
|
2549
2619
|
scroll = Math.min(scroll, maxScroll);
|
|
2550
2620
|
const shown = transcript.slice(scroll, scroll + INSPECTOR_BODY_LINES);
|
|
2551
2621
|
const duration = taskDuration(taskDetails(task));
|
|
2552
2622
|
const metrics = [
|
|
2553
2623
|
task.id,
|
|
2554
|
-
`${siblingIndex + 1} of ${siblings.length}`,
|
|
2555
2624
|
task.toolCalls === undefined ? undefined : `${task.toolCalls} tool call${task.toolCalls === 1 ? "" : "s"}`,
|
|
2556
2625
|
duration
|
|
2557
2626
|
].filter(Boolean);
|
|
@@ -2566,19 +2635,24 @@ ${this.primaryAgent.prompt}` };
|
|
|
2566
2635
|
while (lines.length < INSPECTOR_BODY_LINES + 5)
|
|
2567
2636
|
lines.push("");
|
|
2568
2637
|
if (transcript.length > INSPECTOR_BODY_LINES) {
|
|
2569
|
-
lines.push(theme.fg("dim", `${scroll + 1}
|
|
2638
|
+
lines.push(theme.fg("dim", `${scroll + 1}–${scroll + shown.length} of ${transcript.length}`));
|
|
2570
2639
|
}
|
|
2571
|
-
lines.push(
|
|
2640
|
+
lines.push("", dialogKeys(theme, [
|
|
2641
|
+
["↑↓", "scroll"],
|
|
2642
|
+
...task.parentTaskId ? [["Backspace", "parent"]] : [],
|
|
2643
|
+
["Esc", "task list"],
|
|
2644
|
+
["Tab", "next tab"]
|
|
2645
|
+
]));
|
|
2572
2646
|
return box(lines.map((line) => truncateToWidth2(line, contentWidth)));
|
|
2573
2647
|
},
|
|
2574
2648
|
handleInput: (data) => {
|
|
2575
2649
|
if (confirmingDeleteAgent !== undefined) {
|
|
2576
|
-
if (matchesKey(data, "escape")) {
|
|
2650
|
+
if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
|
|
2577
2651
|
confirmingDeleteAgent = undefined;
|
|
2578
2652
|
tui.requestRender();
|
|
2579
2653
|
return;
|
|
2580
2654
|
}
|
|
2581
|
-
if (
|
|
2655
|
+
if (data.toLowerCase() === "y") {
|
|
2582
2656
|
const name = confirmingDeleteAgent;
|
|
2583
2657
|
const agent = agents.find((candidate) => candidate.name === name);
|
|
2584
2658
|
confirmingDeleteAgent = undefined;
|
|
@@ -2600,15 +2674,18 @@ ${this.primaryAgent.prompt}` };
|
|
|
2600
2674
|
}
|
|
2601
2675
|
return;
|
|
2602
2676
|
}
|
|
2603
|
-
if (
|
|
2677
|
+
if (data.toLowerCase() === "s" && tab !== "tasks") {
|
|
2604
2678
|
if (saving)
|
|
2605
2679
|
return;
|
|
2680
|
+
if (dirtySettings.size > 0) {
|
|
2681
|
+
ctx.ui.notify("Save or discard settings changes before changing scope", "warning");
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2606
2684
|
if (!projectTrusted && scope === "global") {
|
|
2607
2685
|
ctx.ui.notify("Project settings require a trusted project", "warning");
|
|
2608
2686
|
return;
|
|
2609
2687
|
}
|
|
2610
2688
|
scope = scope === "project" ? "global" : "project";
|
|
2611
|
-
dirtySettings.clear();
|
|
2612
2689
|
editing = false;
|
|
2613
2690
|
reloadAgents();
|
|
2614
2691
|
reloadSettings();
|
|
@@ -2616,6 +2693,10 @@ ${this.primaryAgent.prompt}` };
|
|
|
2616
2693
|
return;
|
|
2617
2694
|
}
|
|
2618
2695
|
if (matchesKey(data, "tab")) {
|
|
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
|
+
}
|
|
2619
2700
|
const tabs = ["agents", "tasks", "settings"];
|
|
2620
2701
|
tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "agents";
|
|
2621
2702
|
detail = false;
|
|
@@ -2634,15 +2715,19 @@ ${this.primaryAgent.prompt}` };
|
|
|
2634
2715
|
selectedAgent = Math.max(0, selectedAgent - 1);
|
|
2635
2716
|
else if (matchesKey(data, "down") && agents.length > 0) {
|
|
2636
2717
|
selectedAgent = Math.min(agents.length - 1, selectedAgent + 1);
|
|
2637
|
-
} else if (
|
|
2638
|
-
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");
|
|
2639
2721
|
return;
|
|
2722
|
+
}
|
|
2640
2723
|
confirmingDeleteAgent = agent.name;
|
|
2641
2724
|
tui.requestRender();
|
|
2642
2725
|
return;
|
|
2643
|
-
} else if (
|
|
2644
|
-
if (!projectTrusted)
|
|
2726
|
+
} else if (data.toLowerCase() === "e" && agent && !saving) {
|
|
2727
|
+
if (!projectTrusted) {
|
|
2728
|
+
ctx.ui.notify("Agent editing requires a trusted project", "warning");
|
|
2645
2729
|
return;
|
|
2730
|
+
}
|
|
2646
2731
|
saving = true;
|
|
2647
2732
|
(async () => {
|
|
2648
2733
|
const document = prepareProjectAgentEditor(ctx.cwd, agent);
|
|
@@ -2657,7 +2742,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
2657
2742
|
tui.requestRender();
|
|
2658
2743
|
});
|
|
2659
2744
|
return;
|
|
2660
|
-
} else if (
|
|
2745
|
+
} else if (data === " " && agent && !saving) {
|
|
2661
2746
|
const current = scopedAgent(agent);
|
|
2662
2747
|
if (!current) {
|
|
2663
2748
|
ctx.ui.notify("Local-only agents are unavailable in Global scope", "warning");
|
|
@@ -2673,7 +2758,7 @@ ${this.primaryAgent.prompt}` };
|
|
|
2673
2758
|
tui.requestRender();
|
|
2674
2759
|
});
|
|
2675
2760
|
return;
|
|
2676
|
-
} else if (data === "i" && scope === "project" && agent && !saving) {
|
|
2761
|
+
} else if (data.toLowerCase() === "i" && scope === "project" && agent && !saving) {
|
|
2677
2762
|
if (!disabledOverrides.project.has(agent.name))
|
|
2678
2763
|
return;
|
|
2679
2764
|
saving = true;
|
|
@@ -2688,6 +2773,9 @@ ${this.primaryAgent.prompt}` };
|
|
|
2688
2773
|
} else if (matchesKey(data, "return") && agent && !agent.disabled && agentSupportsMode(agent, "primary")) {
|
|
2689
2774
|
this.selectPrimaryAgent(agent.name, ctx).then(() => tui.requestRender()).catch((error) => ctx.ui.notify(`Could not select primary agent: ${formatError(error)}`, "error"));
|
|
2690
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;
|
|
2691
2779
|
} else
|
|
2692
2780
|
return;
|
|
2693
2781
|
tui.requestRender();
|
|
@@ -2697,9 +2785,20 @@ ${this.primaryAgent.prompt}` };
|
|
|
2697
2785
|
if (saving)
|
|
2698
2786
|
return;
|
|
2699
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
|
+
}
|
|
2700
2792
|
done();
|
|
2701
2793
|
return;
|
|
2702
2794
|
}
|
|
2795
|
+
if (data.toLowerCase() === "r") {
|
|
2796
|
+
reloadSettings();
|
|
2797
|
+
dirtySettings.clear();
|
|
2798
|
+
editing = false;
|
|
2799
|
+
tui.requestRender();
|
|
2800
|
+
return;
|
|
2801
|
+
}
|
|
2703
2802
|
if (matchesKey(data, "up")) {
|
|
2704
2803
|
selectedSetting = Math.max(0, selectedSetting - 1);
|
|
2705
2804
|
editing = false;
|
|
@@ -2769,10 +2868,6 @@ ${this.primaryAgent.prompt}` };
|
|
|
2769
2868
|
if (!detail) {
|
|
2770
2869
|
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
|
|
2771
2870
|
done();
|
|
2772
|
-
else if (matchesKey(data, "left"))
|
|
2773
|
-
tab = "agents";
|
|
2774
|
-
else if (matchesKey(data, "right"))
|
|
2775
|
-
tab = "settings";
|
|
2776
2871
|
else if (matchesKey(data, "up"))
|
|
2777
2872
|
selectedTask = Math.max(0, selectedTask - 1);
|
|
2778
2873
|
else if (matchesKey(data, "down") && tasks.length > 0) {
|
|
@@ -2797,21 +2892,13 @@ ${this.primaryAgent.prompt}` };
|
|
|
2797
2892
|
scroll = Math.max(0, scroll - 1);
|
|
2798
2893
|
} else if (matchesKey(data, "down")) {
|
|
2799
2894
|
scroll += 1;
|
|
2800
|
-
} else if (
|
|
2895
|
+
} else if (matchesKey(data, "backspace")) {
|
|
2801
2896
|
const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
|
|
2802
2897
|
if (parentIndex >= 0)
|
|
2803
2898
|
selectedTask = parentIndex;
|
|
2804
2899
|
else
|
|
2805
2900
|
detail = false;
|
|
2806
2901
|
scroll = 0;
|
|
2807
|
-
} else if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
2808
|
-
const siblings = tasks.filter((candidate) => candidate.parentTaskId === task.parentTaskId);
|
|
2809
|
-
const siblingIndex = siblings.findIndex((candidate) => candidate.id === task.id);
|
|
2810
|
-
const offset = matchesKey(data, "left") ? -1 : 1;
|
|
2811
|
-
const sibling = siblings[siblingIndex + offset];
|
|
2812
|
-
if (sibling)
|
|
2813
|
-
selectedTask = tasks.indexOf(sibling);
|
|
2814
|
-
scroll = 0;
|
|
2815
2902
|
} else
|
|
2816
2903
|
return;
|
|
2817
2904
|
tui.requestRender();
|
|
@@ -2883,7 +2970,7 @@ ${descriptions}` : ""),
|
|
|
2883
2970
|
const background = input.background ? theme.fg("muted", " (background)") : "";
|
|
2884
2971
|
const description = input.description?.trim() || "...";
|
|
2885
2972
|
const agent = input.subagent_type?.trim() || "...";
|
|
2886
|
-
const title = theme.fg("toolTitle", theme.bold("Agent
|
|
2973
|
+
const title = theme.fg("toolTitle", theme.bold("Agent"));
|
|
2887
2974
|
return new Text(`${title}${background}${theme.fg("muted", " — ")}${theme.fg("text", description)}
|
|
2888
2975
|
` + theme.fg("dim", ` @${agent}`), 0, 0);
|
|
2889
2976
|
},
|
|
@@ -3821,28 +3908,26 @@ var PROXY_ENVIRONMENT_VARIABLES = [
|
|
|
3821
3908
|
];
|
|
3822
3909
|
var packageDir2 = dirname5(fileURLToPath2(import.meta.url));
|
|
3823
3910
|
var PERMISSION_OPTIONS = [
|
|
3824
|
-
{ label: "Allow once",
|
|
3825
|
-
{ label: "Allow for this session
|
|
3826
|
-
{ label: "Abort (keep blocked)", key: "esc", action: "abort" },
|
|
3911
|
+
{ label: "Allow once", action: "once" },
|
|
3912
|
+
{ label: "Allow for this session", action: "session" },
|
|
3827
3913
|
{
|
|
3828
3914
|
label: "Allow for this project",
|
|
3829
|
-
key: "P",
|
|
3830
3915
|
action: "project",
|
|
3831
3916
|
confirm: true,
|
|
3832
|
-
hint: "
|
|
3917
|
+
hint: "save to .pi/sandbox.json"
|
|
3833
3918
|
},
|
|
3834
3919
|
{
|
|
3835
3920
|
label: "Allow for all projects",
|
|
3836
|
-
key: "A",
|
|
3837
3921
|
action: "global",
|
|
3838
3922
|
confirm: true,
|
|
3839
|
-
hint: "
|
|
3840
|
-
}
|
|
3923
|
+
hint: "save to ~/.pi/agent/sandbox.json"
|
|
3924
|
+
},
|
|
3925
|
+
{ label: "Keep blocked", action: "abort" }
|
|
3841
3926
|
];
|
|
3842
3927
|
var NETWORK_PERMISSION_OPTIONS = [
|
|
3843
|
-
{ label: "Allow once",
|
|
3844
|
-
{ label: "Allow for this session
|
|
3845
|
-
{ label: "
|
|
3928
|
+
{ label: "Allow once", action: "once" },
|
|
3929
|
+
{ label: "Allow for this session", action: "session" },
|
|
3930
|
+
{ label: "Keep blocked", action: "abort" }
|
|
3846
3931
|
];
|
|
3847
3932
|
function loadSandboxConfig(cwd, includeProject) {
|
|
3848
3933
|
const projectConfigPath = join7(cwd, ".pi", "sandbox.json");
|
|
@@ -3956,10 +4041,7 @@ function writeConfigFile(configPath, config) {
|
|
|
3956
4041
|
}
|
|
3957
4042
|
function getSandboxConfigWriteTarget(cwd, includeProject = true) {
|
|
3958
4043
|
const { globalPath, projectPath } = getConfigPaths(cwd);
|
|
3959
|
-
|
|
3960
|
-
return { scope: "project", path: projectPath };
|
|
3961
|
-
}
|
|
3962
|
-
return { scope: "global", path: globalPath };
|
|
4044
|
+
return includeProject ? { scope: "project", path: projectPath } : { scope: "global", path: globalPath };
|
|
3963
4045
|
}
|
|
3964
4046
|
async function writeSandboxConfigEnabled(cwd, enabled, scope) {
|
|
3965
4047
|
const paths = getConfigPaths(cwd);
|
|
@@ -4316,21 +4398,23 @@ async function stopPreparedChild(child) {
|
|
|
4316
4398
|
async function showPermissionPrompt(ctx, title, options, signal) {
|
|
4317
4399
|
if (!ctx.hasUI)
|
|
4318
4400
|
return "abort";
|
|
4319
|
-
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);
|
|
4320
4402
|
const selected = await ctx.ui.select(title, labels, { signal });
|
|
4321
4403
|
const index = selected === undefined ? -1 : labels.indexOf(selected);
|
|
4322
4404
|
const option = options[index];
|
|
4323
4405
|
if (!option)
|
|
4324
4406
|
return "abort";
|
|
4325
4407
|
if (option.confirm) {
|
|
4326
|
-
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 });
|
|
4327
4411
|
if (!confirmed)
|
|
4328
4412
|
return "abort";
|
|
4329
4413
|
}
|
|
4330
4414
|
return option.action;
|
|
4331
4415
|
}
|
|
4332
4416
|
function promptDomainBlock(ctx, domain, signal) {
|
|
4333
|
-
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);
|
|
4334
4418
|
}
|
|
4335
4419
|
function promptReadBlock(ctx, filePath, reason, signal) {
|
|
4336
4420
|
const title = reason ? `Read blocked: "${filePath}" is in denyRead (${reason})` : `Read blocked: "${filePath}" is not in allowRead`;
|
|
@@ -4340,7 +4424,7 @@ function promptWriteBlock(ctx, filePath, signal) {
|
|
|
4340
4424
|
return showPermissionPrompt(ctx, `Write blocked: "${filePath}" is not in allowWrite`, PERMISSION_OPTIONS, signal);
|
|
4341
4425
|
}
|
|
4342
4426
|
function promptNetworkBlock(ctx, operation, target, signal) {
|
|
4343
|
-
return showPermissionPrompt(ctx, `Network blocked: ${operation}
|
|
4427
|
+
return showPermissionPrompt(ctx, `Network blocked: ${operation} "${target}"`, NETWORK_PERMISSION_OPTIONS, signal);
|
|
4344
4428
|
}
|
|
4345
4429
|
function landstripAvailable() {
|
|
4346
4430
|
try {
|
|
@@ -5622,82 +5706,62 @@ ${message}
|
|
|
5622
5706
|
handler: async (_args, ctx) => {
|
|
5623
5707
|
if (!ctx.hasUI)
|
|
5624
5708
|
return;
|
|
5625
|
-
const
|
|
5709
|
+
const trustContext = ctx;
|
|
5710
|
+
const projectTrusted = trustContext.isProjectTrusted?.() ?? projectConfigTrusted;
|
|
5711
|
+
const config = loadSandboxConfig(ctx.cwd, projectTrusted);
|
|
5626
5712
|
const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
|
|
5627
5713
|
const windowsImplementation = process.platform === "win32" ? activeWindowsImplementation() : undefined;
|
|
5628
|
-
const
|
|
5629
|
-
|
|
5630
|
-
|
|
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 = () => {
|
|
5631
5719
|
if (noSandboxFlag)
|
|
5632
|
-
return { color: "warning", label: "Disabled
|
|
5720
|
+
return { color: "warning", label: "Disabled by --no-sandbox" };
|
|
5633
5721
|
if (!config.enabled)
|
|
5634
|
-
return { color: "warning", label: "Disabled" };
|
|
5722
|
+
return { color: "warning", label: "Disabled by configuration" };
|
|
5635
5723
|
if (!sandboxEnabled || !sandboxReady)
|
|
5636
5724
|
return { color: "warning", label: "Inactive" };
|
|
5637
5725
|
return { color: "success", label: "Active" };
|
|
5638
|
-
}
|
|
5639
|
-
|
|
5640
|
-
return value ? theme.fg("warning", "yes") : theme.fg("success", "no");
|
|
5641
|
-
}
|
|
5726
|
+
};
|
|
5727
|
+
const networkMode = config.network.allowNetwork ? "Unrestricted" : windowsImplementation === "appContainer" && !config.windows.allowLoopback ? "Blocked" : "Proxied";
|
|
5642
5728
|
return {
|
|
5643
5729
|
render(width) {
|
|
5644
5730
|
const innerWidth = Math.max(1, width - 4);
|
|
5645
|
-
const row = (content = "") => boxRow(theme, width, content);
|
|
5646
|
-
const lines = [boxTop(theme, width, "Sandbox")];
|
|
5647
5731
|
const status = sandboxStatus();
|
|
5648
|
-
const
|
|
5649
|
-
const pathSnippet = text(truncateToWidth3(landstripDisplayPath(), Math.max(20, innerWidth - 28)));
|
|
5650
|
-
const section = (title, detail) => {
|
|
5651
|
-
lines.push(row(""));
|
|
5652
|
-
lines.push(row(`${accent(title)}${detail ? dim(` · ${detail}`) : ""}`));
|
|
5653
|
-
};
|
|
5654
|
-
const item = (label, value) => {
|
|
5655
|
-
lines.push(row(` ${dim("•")} ${muted(label.padEnd(13))} ${value}`));
|
|
5656
|
-
};
|
|
5732
|
+
const tabs = dialogTabs(theme, ["Overview", "Policy"], tab);
|
|
5657
5733
|
const listValue = (values) => {
|
|
5658
5734
|
const value = values.join(", ") || "none";
|
|
5659
|
-
return text(truncateToWidth3(value, Math.max(10, innerWidth -
|
|
5735
|
+
return text(truncateToWidth3(value, Math.max(10, innerWidth - 20)));
|
|
5660
5736
|
};
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
item("session", theme.fg("accent", sessionAllowedDomains.join(", ")));
|
|
5673
|
-
}
|
|
5674
|
-
section("Filesystem");
|
|
5675
|
-
item("deny read", listValue(config.filesystem.denyRead));
|
|
5676
|
-
item("allow read", listValue(config.filesystem.allowRead));
|
|
5677
|
-
item("allow write", listValue(config.filesystem.allowWrite));
|
|
5678
|
-
item("deny write", listValue(config.filesystem.denyWrite));
|
|
5679
|
-
if (windowsImplementation !== undefined) {
|
|
5680
|
-
section("Windows");
|
|
5681
|
-
item("implementation", text(windowsImplementation === "restrictedUser" ? "restricted user" : "AppContainer"));
|
|
5682
|
-
if (windowsImplementation === "appContainer") {
|
|
5683
|
-
const mode = config.windows.appContainerMode;
|
|
5684
|
-
item("container", mode === "standard" ? theme.fg("warning", `${mode} (weaker)`) : text(mode));
|
|
5685
|
-
item("loopback", boolValue(config.windows.allowLoopback));
|
|
5686
|
-
}
|
|
5687
|
-
}
|
|
5688
|
-
if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
|
|
5689
|
-
section("Session grants");
|
|
5690
|
-
if (sessionAllowedReadPaths.length > 0) {
|
|
5691
|
-
item("read", theme.fg("accent", sessionAllowedReadPaths.join(", ")));
|
|
5692
|
-
}
|
|
5693
|
-
if (sessionAllowedWritePaths.length > 0) {
|
|
5694
|
-
item("write", theme.fg("accent", sessionAllowedWritePaths.join(", ")));
|
|
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})`)));
|
|
5695
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)));
|
|
5696
5751
|
}
|
|
5697
|
-
|
|
5698
|
-
return
|
|
5752
|
+
content.push("", footer);
|
|
5753
|
+
return [
|
|
5754
|
+
boxTop(theme, width, "Sandbox"),
|
|
5755
|
+
...content.map((line) => boxRow(theme, width, line)),
|
|
5756
|
+
boxBottom(theme, width)
|
|
5757
|
+
];
|
|
5699
5758
|
},
|
|
5700
5759
|
handleInput(data) {
|
|
5760
|
+
if (matchesKey2(data, "tab")) {
|
|
5761
|
+
tab = tab === "Overview" ? "Policy" : "Overview";
|
|
5762
|
+
tui.requestRender();
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5701
5765
|
if (matchesKey2(data, "return")) {
|
|
5702
5766
|
done(true);
|
|
5703
5767
|
return;
|
|
@@ -5714,8 +5778,16 @@ ${message}
|
|
|
5714
5778
|
if (!shouldToggle)
|
|
5715
5779
|
return;
|
|
5716
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
|
+
}
|
|
5717
5789
|
try {
|
|
5718
|
-
const scope = await setSandboxConfigEnabled(ctx.cwd, enabled,
|
|
5790
|
+
const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectTrusted);
|
|
5719
5791
|
if (!enabled) {
|
|
5720
5792
|
disableSandbox(ctx);
|
|
5721
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"
|