pi-landstrip 0.19.19 → 0.19.20

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 +541 -69
  2. package/package.json +2 -2
package/dist/index.ts CHANGED
@@ -17,6 +17,7 @@ import { StringDecoder as StringDecoder3 } from "node:string_decoder";
17
17
  import { fileURLToPath as fileURLToPath2 } from "node:url";
18
18
  import {
19
19
  createBashToolDefinition,
20
+ createLocalBashOperations,
20
21
  getAgentDir as getAgentDir5,
21
22
  getShellConfig,
22
23
  SettingsManager as SettingsManager2,
@@ -228,16 +229,19 @@ function serveWorkerAuth(pipe, resolve) {
228
229
  // subagents.ts
229
230
  import { randomUUID } from "node:crypto";
230
231
  import {
232
+ closeSync,
231
233
  existsSync as existsSync5,
232
234
  lstatSync,
233
235
  mkdirSync as mkdirSync3,
234
236
  mkdtempSync,
237
+ openSync,
235
238
  readFileSync as readFileSync6,
236
239
  readdirSync as readdirSync2,
237
240
  readlinkSync,
238
241
  realpathSync as realpathSync2,
239
242
  rmSync,
240
- writeFileSync as writeFileSync3
243
+ writeFileSync as writeFileSync3,
244
+ writeSync
241
245
  } from "node:fs";
242
246
  import { homedir as homedir3, tmpdir } from "node:os";
243
247
  import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute3, join as join7, relative as relative3, resolve as resolve3, sep as sep4 } from "node:path";
@@ -1457,12 +1461,15 @@ function parseArgv(args) {
1457
1461
  };
1458
1462
  }
1459
1463
  function taskItems(tasks, prefix) {
1460
- return tasks.map((task) => ({
1461
- value: `${prefix}${task.id}`,
1462
- label: task.id.slice(0, 8),
1463
- description: `@${task.agent} [${task.state}]`,
1464
- search: task.id
1465
- }));
1464
+ return tasks.map((task) => {
1465
+ const label = task.kind === "command" || task.agent === "bash" ? "$ bash" : `@${task.agent}`;
1466
+ return {
1467
+ value: `${prefix}${task.id}`,
1468
+ label: task.id.slice(0, 8),
1469
+ description: `${label} [${task.state}]`,
1470
+ search: task.id
1471
+ };
1472
+ });
1466
1473
  }
1467
1474
  function matchingTasksById(tasks, taskId) {
1468
1475
  const exact = tasks.find((task) => task.id === taskId);
@@ -1580,22 +1587,46 @@ Scope: ${scope} configuration`);
1580
1587
  {
1581
1588
  name: "tasks",
1582
1589
  summary: "Inspect and manage running task sessions",
1583
- pane: (argv) => argv.words[0] === "kill" || argv.words[0] === "list" ? undefined : "tasks",
1590
+ pane: (argv) => {
1591
+ const first = argv.words[0]?.toLowerCase() ?? "";
1592
+ if (["cancel", "delete", "kill"].includes(first))
1593
+ return;
1594
+ if (first === "inspect" && argv.words[1])
1595
+ return argv.words[1];
1596
+ if (first && first !== "list")
1597
+ return first;
1598
+ return "tasks";
1599
+ },
1584
1600
  complete: (argv, { runtime }) => {
1585
1601
  const tasks = runtime?.getTasks() ?? [];
1586
- if (argv.words[0]?.toLowerCase() === "kill")
1587
- return taskItems(tasks, "tasks kill ");
1602
+ const action = argv.words[0]?.toLowerCase();
1603
+ if (action === "cancel" || action === "kill")
1604
+ return taskItems(tasks, `tasks ${action} `);
1605
+ if (action === "delete")
1606
+ return taskItems(tasks, "tasks delete ");
1607
+ if (action === "inspect")
1608
+ return taskItems(tasks, "tasks inspect ");
1588
1609
  return [
1589
- { value: "tasks list", label: "list", description: "List all task sessions" },
1590
- { value: "tasks kill", label: "kill", description: "Kill a task session by ID" },
1610
+ { value: "tasks cancel", label: "cancel", description: "Cancel an active task session" },
1611
+ { value: "tasks delete", label: "delete", description: "Delete a task session by ID" },
1612
+ { value: "tasks inspect", label: "inspect", description: "Inspect a task session in TUI" },
1591
1613
  ...taskItems(tasks, "tasks ")
1592
1614
  ];
1593
1615
  },
1594
- run: (argv, { ctx, runtime }) => {
1595
- const [action = "", taskId = ""] = argv.words;
1596
- if (action === "kill") {
1616
+ run: async (argv, { ctx, runtime }) => {
1617
+ const [rawAction = "", rawTaskId = ""] = argv.words;
1618
+ let action = rawAction.toLowerCase();
1619
+ let taskId = rawTaskId;
1620
+ if (!["cancel", "delete", "kill", "inspect", "list"].includes(action) && action) {
1621
+ const matched = matchingTasksById(runtime.getTasks(), action);
1622
+ if (matched.length > 0) {
1623
+ action = "inspect";
1624
+ taskId = matched[0].id;
1625
+ }
1626
+ }
1627
+ if (action === "cancel" || action === "kill") {
1597
1628
  if (!taskId) {
1598
- ctx.ui.notify("Task ID required to kill a task", "error");
1629
+ ctx.ui.notify("Task ID required to cancel a task", "error");
1599
1630
  return;
1600
1631
  }
1601
1632
  const matched = matchingTasksById(runtime.getTasks(), taskId);
@@ -1607,17 +1638,36 @@ Scope: ${scope} configuration`);
1607
1638
  ctx.ui.notify(`Task ID prefix ${taskId} matches ${matched.length} tasks`, "error");
1608
1639
  return;
1609
1640
  }
1610
- const deleted = runtime.deleteTasks([matched[0].id], ctx);
1611
- ctx.ui.notify(`Killed ${deleted} task${deleted === 1 ? "" : "s"}`, "info");
1641
+ const cancelled = await runtime.cancelTasks([matched[0].id], ctx);
1642
+ ctx.ui.notify(`Cancelled ${cancelled} task${cancelled === 1 ? "" : "s"}`, "info");
1612
1643
  return;
1613
1644
  }
1614
- const tasks = runtime.getTasks();
1615
- if (tasks.length === 0) {
1616
- ctx.ui.notify("No task sessions", "info");
1645
+ if (action === "delete") {
1646
+ if (!taskId) {
1647
+ ctx.ui.notify("Task ID required to delete a task", "error");
1648
+ return;
1649
+ }
1650
+ const matched = matchingTasksById(runtime.getTasks(), taskId);
1651
+ if (matched.length === 0) {
1652
+ ctx.ui.notify(`No task found matching ${taskId}`, "error");
1653
+ return;
1654
+ }
1655
+ if (matched.length > 1) {
1656
+ ctx.ui.notify(`Task ID prefix ${taskId} matches ${matched.length} tasks`, "error");
1657
+ return;
1658
+ }
1659
+ const deleted = runtime.deleteTasks([matched[0].id], ctx);
1660
+ ctx.ui.notify(`Deleted ${deleted} task${deleted === 1 ? "" : "s"}`, "info");
1617
1661
  return;
1618
1662
  }
1619
- ctx.ui.notify(tasks.map((task) => `${task.id.slice(0, 8)}: @${task.agent} [${task.state}]`).join(`
1620
- `), "info");
1663
+ if (action === "inspect" && taskId) {
1664
+ const matched = matchingTasksById(runtime.getTasks(), taskId);
1665
+ if (matched.length === 1) {
1666
+ await runtime.openLandstrip(matched[0].id, ctx);
1667
+ return;
1668
+ }
1669
+ }
1670
+ await runtime.openLandstrip("tasks", ctx);
1621
1671
  }
1622
1672
  },
1623
1673
  {
@@ -2485,6 +2535,7 @@ var AGENTS_HELP_ROWS = [
2485
2535
  ["Esc", "Back one level, or close from a top-level tab"],
2486
2536
  ["Ctrl+C", "Close"],
2487
2537
  ["Space", "Toggle agent enabled or select task"],
2538
+ ["C", "Cancel running task or execution"],
2488
2539
  ["X", "Delete selected agent or task sessions"],
2489
2540
  ["E", "Edit project agent"],
2490
2541
  ["I", "Inherit global agent state"],
@@ -2596,6 +2647,14 @@ var taskParameters = Type.Object({
2596
2647
  command: Type.Optional(Type.String({ description: "Originating command, if any" })),
2597
2648
  background: Type.Optional(Type.Boolean({ description: "Return immediately and run in background" }))
2598
2649
  });
2650
+ var TasksToolSchema = Type.Object({
2651
+ action: Type.Union([Type.Literal("list"), Type.Literal("inspect"), Type.Literal("cancel")], {
2652
+ description: 'Task management action: "list" to view task sessions, "inspect" for status/output, or "cancel" to stop an active task'
2653
+ }),
2654
+ task_id: Type.Optional(Type.String({
2655
+ description: 'Task ID or unique prefix (required for "inspect" and "cancel")'
2656
+ }))
2657
+ });
2599
2658
  var TASK_STATES = new Set([
2600
2659
  "queued",
2601
2660
  "running",
@@ -2609,6 +2668,8 @@ function taskDetails(task, state = task.state) {
2609
2668
  taskId: task.id,
2610
2669
  state,
2611
2670
  agent: task.agent,
2671
+ kind: task.kind ?? "subagent",
2672
+ command: task.command,
2612
2673
  description: task.description,
2613
2674
  background: task.background,
2614
2675
  currentTool: task.currentTool,
@@ -2618,7 +2679,12 @@ function taskDetails(task, state = task.state) {
2618
2679
  startedAt: task.startedAt,
2619
2680
  finishedAt: task.finishedAt,
2620
2681
  output: task.output,
2621
- error: task.error
2682
+ error: task.error,
2683
+ outputFile: task.outputFile,
2684
+ errorFile: task.errorFile,
2685
+ exitCode: task.exitCode,
2686
+ parentTaskId: task.parentTaskId,
2687
+ depth: task.depth
2622
2688
  };
2623
2689
  }
2624
2690
  function taskDuration(details) {
@@ -2808,6 +2874,12 @@ function taskProgress(task) {
2808
2874
  if (task.state !== "running")
2809
2875
  return [];
2810
2876
  const progress = [];
2877
+ if (task.kind === "command") {
2878
+ const duration = taskDuration(taskDetails(task));
2879
+ if (duration)
2880
+ progress.push(duration);
2881
+ return progress;
2882
+ }
2811
2883
  if (task.currentTool)
2812
2884
  progress.push(`→ ${task.currentTool}`);
2813
2885
  if (task.toolCalls !== undefined) {
@@ -2848,6 +2920,19 @@ function messageContentText(message) {
2848
2920
  `);
2849
2921
  }
2850
2922
  function taskTranscript(task) {
2923
+ if (task.kind === "command") {
2924
+ if (task.outputFile) {
2925
+ try {
2926
+ const content = readFileSync6(task.outputFile, "utf8");
2927
+ const lines = content.split(`
2928
+ `);
2929
+ return lines.length > 0 && lines[0] !== "" ? lines : ["Command running... (no output yet)"];
2930
+ } catch {}
2931
+ }
2932
+ const fallback = task.error ?? task.output ?? "Command output is not available yet.";
2933
+ return fallback.split(`
2934
+ `);
2935
+ }
2851
2936
  if (!task.sessionFile || !existsSync5(task.sessionFile)) {
2852
2937
  const fallback = task.error ?? task.output ?? "Child session is not available yet.";
2853
2938
  return fallback.split(`
@@ -3105,7 +3190,7 @@ ${requested.map(({ permission, resource }) => `${permission}: ${resource}`).join
3105
3190
  }
3106
3191
  }
3107
3192
  function renderTaskResult(id, state, value) {
3108
- const tag = state === "error" ? "task_error" : "task_result";
3193
+ const tag = state === "error" || state === "cancelled" || state === "interrupted" ? "task_error" : "task_result";
3109
3194
  return `<task id="${id}" state="${state}">
3110
3195
  <${tag}>
3111
3196
  ${value}
@@ -3401,6 +3486,8 @@ class SubagentRuntime {
3401
3486
  primaryAgentSwitching = false;
3402
3487
  shuttingDown = false;
3403
3488
  activeSessionId;
3489
+ taskUpdateListeners = new Set;
3490
+ recentCompletions = new Map;
3404
3491
  constructor(pi, integration, createWorker = (...args) => this.defaultWorker(...args), loadCatalog = loadAgentCatalog, permissionPrompts = new PermissionPromptCoordinator) {
3405
3492
  this.pi = pi;
3406
3493
  this.integration = integration;
@@ -3417,14 +3504,31 @@ class SubagentRuntime {
3417
3504
  primaryAgentName() {
3418
3505
  return this.primaryAgent?.name;
3419
3506
  }
3507
+ onTaskUpdate(listener) {
3508
+ this.taskUpdateListeners.add(listener);
3509
+ return () => this.taskUpdateListeners.delete(listener);
3510
+ }
3511
+ notifyTaskUpdate() {
3512
+ for (const listener of this.taskUpdateListeners) {
3513
+ try {
3514
+ listener();
3515
+ } catch {}
3516
+ }
3517
+ }
3420
3518
  register() {
3421
3519
  this.pi.registerTool(this.createTaskTool());
3520
+ this.pi.registerTool(this.createTasksTool());
3422
3521
  registerLandstripCommands(this.pi, this, this.integration);
3423
3522
  this.pi.registerShortcut("ctrl+shift+a", {
3424
3523
  description: "Cycle to the next primary agent",
3425
3524
  handler: async (ctx) => this.cyclePrimaryAgent(ctx)
3426
3525
  });
3526
+ this.pi.registerShortcut("ctrl+shift+t", {
3527
+ description: "Open Landstrip task sessions dashboard",
3528
+ handler: async (ctx) => this.openLandstrip("tasks", ctx)
3529
+ });
3427
3530
  this.pi.on("session_start", async (_event, ctx) => {
3531
+ this.recentCompletions.clear();
3428
3532
  this.activeSessionId = undefined;
3429
3533
  await this.dispose();
3430
3534
  this.shuttingDown = false;
@@ -3439,6 +3543,10 @@ class SubagentRuntime {
3439
3543
  if (nextTools.join("\x00") !== activeTools.join("\x00"))
3440
3544
  this.pi.setActiveTools(nextTools);
3441
3545
  });
3546
+ this.pi.on("turn_start", async (_event, ctx) => {
3547
+ this.recentCompletions.clear();
3548
+ this.updateTaskWidget(ctx);
3549
+ });
3442
3550
  this.pi.on("session_before_switch", async (_event, ctx) => {
3443
3551
  this.activeSessionId = undefined;
3444
3552
  if (ctx.hasUI)
@@ -3784,6 +3892,14 @@ ${this.primaryAgent.prompt}` };
3784
3892
  return settingsList;
3785
3893
  };
3786
3894
  return ctx.ui.custom((tui, theme, _keybindings, done) => {
3895
+ const unsubscribe = this.onTaskUpdate(() => {
3896
+ refreshTasks();
3897
+ tui.requestRender();
3898
+ });
3899
+ const finish = () => {
3900
+ unsubscribe();
3901
+ done(undefined);
3902
+ };
3787
3903
  const renderAgentsTab = (frame) => {
3788
3904
  const { tabs, pane, contentWidth, pad } = frame;
3789
3905
  const listedAgents = agentsForTab();
@@ -4008,7 +4124,7 @@ ${this.primaryAgent.prompt}` };
4008
4124
  const shellReadMode = overview.windowsMode !== undefined ? "Policy (required)" : overview.shellReadMode === "host" ? "Host-aligned" : "Policy";
4009
4125
  const changePath = overview.changeScope === "project" ? overview.paths.project : overview.paths.global;
4010
4126
  const item = (label, value) => ` ${theme.fg("dim", label.padEnd(16))} ${value}`;
4011
- lines.push(`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`, item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")), item("Runtime", overview.running ? theme.fg("success", "Running") : theme.fg("dim", "Not running")), "", theme.fg("accent", "Protection"), item("Network", networkMode), item("Shell reads", shellReadMode), item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`), item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length)), item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)), "", theme.fg("accent", "Configuration"), item("Change scope", overview.changeScope === "project" ? "Project" : "Global"), item("Change file", truncateToWidth(changePath, Math.max(10, contentWidth - 20))), item("Project file", truncateToWidth(overview.paths.project, Math.max(10, contentWidth - 20))), item("Global file", truncateToWidth(overview.paths.global, Math.max(10, contentWidth - 20))), item("Landstrip binary", truncateToWidth(overview.paths.binary, Math.max(10, contentWidth - 20))));
4127
+ lines.push(`${theme.fg(status.color, "●")} ${theme.fg("text", status.label)}`, item("Configured", overview.enabled ? theme.fg("text", "Enabled") : theme.fg("warning", "Disabled")), item("Runtime", overview.running ? theme.fg("success", "Running") : theme.fg("dim", "Not running")), item("Tasks", `${tasks.filter((t) => t.state === "running").length} running · ${tasks.length} total`), "", theme.fg("accent", "Protection"), item("Network", networkMode), item("Shell reads", shellReadMode), item("Read rules", `${overview.config.filesystem.denyRead.length + overview.config.filesystem.allowRead.length} · ${overview.readRuleScope}`), item("Write rules", String(overview.config.filesystem.allowWrite.length + overview.config.filesystem.denyWrite.length)), item("Session grants", String(overview.sessionDomains.length + overview.sessionReadPaths.length + overview.sessionWritePaths.length)), "", theme.fg("accent", "Configuration"), item("Change scope", overview.changeScope === "project" ? "Project" : "Global"), item("Change file", truncateToWidth(changePath, Math.max(10, contentWidth - 20))), item("Project file", truncateToWidth(overview.paths.project, Math.max(10, contentWidth - 20))), item("Global file", truncateToWidth(overview.paths.global, Math.max(10, contentWidth - 20))), item("Landstrip binary", truncateToWidth(overview.paths.binary, Math.max(10, contentWidth - 20))));
4012
4128
  if (overview.windowsMode !== undefined)
4013
4129
  lines.push(item("Windows mode", overview.windowsMode));
4014
4130
  lines.push("", theme.fg("accent", "Network"), item("Allowed domains", listValue(overview.config.network.allowedDomains)), item("Denied domains", listValue(overview.config.network.deniedDomains)), item("Unix sockets", overview.config.network.allowAllUnixSockets ? "all" : listValue(overview.config.network.allowUnixSockets)), "", theme.fg("accent", "Filesystem"), item("Shell reads", shellReadMode), item("Read rules for", overview.readRuleScope), item("Denied reads", listValue(overview.config.filesystem.denyRead)), item("Allowed reads", listValue(overview.config.filesystem.allowRead)), item("Allowed writes", listValue(overview.config.filesystem.allowWrite)), item("Denied writes", listValue(overview.config.filesystem.denyWrite)), "", theme.fg("accent", "Session grants"), item("Domains", listValue(overview.sessionDomains)), item("Read paths", listValue(overview.sessionReadPaths)), item("Write paths", listValue(overview.sessionWritePaths)));
@@ -4054,7 +4170,8 @@ ${this.primaryAgent.prompt}` };
4054
4170
  const cursor = index === selectedTask ? theme.fg("accent", "›") : " ";
4055
4171
  const selected = selectedTaskIds.has(task.id) ? theme.fg("accent", "✓") : " ";
4056
4172
  const indent = " ".repeat(Math.max(0, task.depth - 1));
4057
- listLines.push(`${cursor} ${selected} ${indent}${taskState(theme, task)} ${theme.fg("accent", `@${task.agent}`)} ${theme.fg("text", task.description)} ${theme.fg("dim", task.id.slice(0, 8))}`);
4173
+ const tag = task.kind === "command" ? "$ bash" : `@${task.agent}`;
4174
+ listLines.push(`${cursor} ${selected} ${indent}${taskState(theme, task)} ${theme.fg("accent", tag)} ${theme.fg("text", task.description)} ${theme.fg("dim", task.id.slice(0, 8))}`);
4058
4175
  }
4059
4176
  if (status)
4060
4177
  listLines.push(theme.fg("dim", ` ${status}`));
@@ -4066,7 +4183,8 @@ ${this.primaryAgent.prompt}` };
4066
4183
  listLines.push("");
4067
4184
  if (pendingTaskAction) {
4068
4185
  const count = pendingTaskAction.taskIds.length;
4069
- const label = count === 1 ? `Delete ${pendingTaskAction.taskIds[0]?.slice(0, 8) ?? "task"}?` : `Delete ${count} selected task sessions?`;
4186
+ const actionLabel = pendingTaskAction.type === "cancel" ? "Cancel" : "Delete";
4187
+ const label = count === 1 ? `${actionLabel} ${pendingTaskAction.taskIds[0]?.slice(0, 8) ?? "task"}?` : `${actionLabel} ${count} selected tasks?`;
4070
4188
  listLines.push(`${theme.fg("warning", label)} ${dialogKeys(theme, [
4071
4189
  ["Enter", "confirm"],
4072
4190
  ["Esc", "cancel"]
@@ -4076,6 +4194,7 @@ ${this.primaryAgent.prompt}` };
4076
4194
  ["↑ / ↓", "move"],
4077
4195
  ["Space", "select"],
4078
4196
  ["Enter", "inspect"],
4197
+ ["C", "cancel"],
4079
4198
  ["X", "delete"],
4080
4199
  ["Esc", "close"]
4081
4200
  ]));
@@ -4098,10 +4217,11 @@ ${this.primaryAgent.prompt}` };
4098
4217
  duration
4099
4218
  ].filter(Boolean);
4100
4219
  const usage = formatTaskUsage(task.usage);
4220
+ const tag = task.kind === "command" ? "$ bash" : `@${task.agent}`;
4101
4221
  const lines = [
4102
4222
  tabs,
4103
4223
  "",
4104
- `${theme.fg("accent", theme.bold(`@${task.agent}`))} ${theme.fg("text", task.description)}`,
4224
+ `${theme.fg("accent", theme.bold(tag))} ${theme.fg("text", task.description)}`,
4105
4225
  `${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))} ${theme.fg("dim", "·")} ${theme.fg(follow ? "accent" : "muted", `Follow: ${follow ? "on" : "off"}`)}`,
4106
4226
  ...usage ? [theme.fg("dim", usage)] : [],
4107
4227
  "",
@@ -4116,11 +4236,11 @@ ${this.primaryAgent.prompt}` };
4116
4236
  const input = steering.render(Math.max(2, contentWidth - visibleWidth("Steer ")))[0] ?? "";
4117
4237
  lines.push(`${theme.fg("accent", "Steer")} ${input.replace(/^>/, "›")}`);
4118
4238
  } else if (task.state === "queued" || task.state === "running") {
4119
- lines.push(dialogKeys(theme, [
4120
- ["Enter", "steer"],
4121
- ["F", "follow"],
4122
- ["Esc", "tasks"]
4123
- ]));
4239
+ const keys = [];
4240
+ if (task.kind !== "command")
4241
+ keys.push(["Enter", "steer"]);
4242
+ keys.push(["C", "cancel"], ["F", "follow"], ["Esc", "tasks"]);
4243
+ lines.push(dialogKeys(theme, keys));
4124
4244
  }
4125
4245
  return pane(lines.map((line) => truncateToWidth(line, contentWidth)));
4126
4246
  },
@@ -4149,7 +4269,7 @@ ${this.primaryAgent.prompt}` };
4149
4269
  return;
4150
4270
  }
4151
4271
  if (matchesKey(data, "escape")) {
4152
- done(undefined);
4272
+ finish();
4153
4273
  return;
4154
4274
  }
4155
4275
  if (!matchesKey(data, "return"))
@@ -4196,10 +4316,20 @@ ${this.primaryAgent.prompt}` };
4196
4316
  tasks: (data) => {
4197
4317
  if (pendingTaskAction) {
4198
4318
  if (matchesKey(data, "return")) {
4199
- const taskIds = pendingTaskAction.taskIds;
4319
+ const action = pendingTaskAction;
4200
4320
  pendingTaskAction = undefined;
4201
- const deleted = this.deleteTasks(taskIds, ctx);
4202
- for (const taskId of taskIds)
4321
+ if (action.type === "cancel") {
4322
+ this.cancelTasks(action.taskIds, ctx).then((cancelled) => {
4323
+ if (cancelled > 0) {
4324
+ ctx.ui.notify(`Cancelled ${cancelled} task${cancelled === 1 ? "" : "s"}`, "info");
4325
+ }
4326
+ refreshTasks();
4327
+ tui.requestRender();
4328
+ });
4329
+ return;
4330
+ }
4331
+ const deleted = this.deleteTasks(action.taskIds, ctx);
4332
+ for (const taskId of action.taskIds)
4203
4333
  selectedTaskIds.delete(taskId);
4204
4334
  refreshTasks();
4205
4335
  if (deleted > 0) {
@@ -4215,7 +4345,7 @@ ${this.primaryAgent.prompt}` };
4215
4345
  return;
4216
4346
  }
4217
4347
  if (matchesKey(data, "escape"))
4218
- done(undefined);
4348
+ finish();
4219
4349
  else if (matchesKey(data, "up"))
4220
4350
  selectedTask = Math.max(0, selectedTask - 1);
4221
4351
  else if (matchesKey(data, "down") && tasks.length > 0) {
@@ -4226,6 +4356,17 @@ ${this.primaryAgent.prompt}` };
4226
4356
  return;
4227
4357
  if (!selectedTaskIds.delete(task.id))
4228
4358
  selectedTaskIds.add(task.id);
4359
+ } else if (data.toLowerCase() === "c") {
4360
+ const targets = taskTargets();
4361
+ const cancellable = targets.filter((t) => t.state === "running" || t.state === "queued");
4362
+ if (cancellable.length === 0) {
4363
+ ctx.ui.notify("No running or queued tasks selected to cancel", "warning");
4364
+ return;
4365
+ }
4366
+ pendingTaskAction = {
4367
+ type: "cancel",
4368
+ taskIds: cancellable.map((task) => task.id)
4369
+ };
4229
4370
  } else if (data.toLowerCase() === "x") {
4230
4371
  const targets = taskTargets();
4231
4372
  if (targets.length === 0)
@@ -4247,13 +4388,22 @@ ${this.primaryAgent.prompt}` };
4247
4388
  const task = tasks[selectedTask];
4248
4389
  if (!task) {
4249
4390
  if (matchesKey(data, "escape"))
4250
- done(undefined);
4391
+ finish();
4251
4392
  return;
4252
4393
  }
4253
4394
  if (matchesKey(data, "escape")) {
4254
4395
  tab = "tasks";
4255
4396
  follow = false;
4256
4397
  scroll = 0;
4398
+ } else if (data.toLowerCase() === "c" && (task.state === "running" || task.state === "queued")) {
4399
+ this.cancelTasks([task.id], ctx).then((count) => {
4400
+ if (count > 0) {
4401
+ ctx.ui.notify(`Cancelled task ${task.id.slice(0, 8)}`, "info");
4402
+ }
4403
+ refreshTasks();
4404
+ tui.requestRender();
4405
+ });
4406
+ return;
4257
4407
  } else if (matchesKey(data, "return") && (task.state === "queued" || task.state === "running")) {
4258
4408
  const input = new Input;
4259
4409
  input.focused = true;
@@ -4314,7 +4464,7 @@ ${this.primaryAgent.prompt}` };
4314
4464
  },
4315
4465
  help: (data) => {
4316
4466
  if (matchesKey(data, "escape"))
4317
- done(undefined);
4467
+ finish();
4318
4468
  return;
4319
4469
  }
4320
4470
  };
@@ -4408,7 +4558,9 @@ ${this.primaryAgent.prompt}` };
4408
4558
  }
4409
4559
  handleTabInput[tab](data);
4410
4560
  },
4411
- invalidate() {}
4561
+ invalidate() {
4562
+ unsubscribe();
4563
+ }
4412
4564
  };
4413
4565
  }, {
4414
4566
  overlay: true,
@@ -4460,6 +4612,199 @@ ${this.primaryAgent.prompt}` };
4460
4612
  this.primaryAgentSwitching = false;
4461
4613
  }
4462
4614
  }
4615
+ createTasksTool(boundCtx) {
4616
+ return {
4617
+ name: "tasks",
4618
+ label: "Tasks",
4619
+ description: "Inspect, list, and cancel background tasks and subagents managed by Landstrip.",
4620
+ parameters: TasksToolSchema,
4621
+ executionMode: "parallel",
4622
+ renderCall: (input, theme) => {
4623
+ const action = input.action ?? "list";
4624
+ const target = input.task_id ? ` ${input.task_id.slice(0, 8)}` : "";
4625
+ return new Text(`${theme.fg("toolTitle", theme.bold("Tasks"))} ${theme.fg("accent", action)}${theme.fg("dim", target)}`, 0, 0);
4626
+ },
4627
+ execute: async (_toolCallId, input, _signal, _onUpdate, callCtx) => {
4628
+ const ctx = boundCtx ?? callCtx;
4629
+ if (!ctx)
4630
+ throw new Error("Task context is unavailable");
4631
+ const action = input.action;
4632
+ if (action === "list") {
4633
+ const list = [...this.tasks.values()].map((task) => {
4634
+ const details = taskDetails(task);
4635
+ return {
4636
+ id: details.taskId,
4637
+ kind: details.kind,
4638
+ agent: details.agent,
4639
+ state: details.state,
4640
+ command: details.command,
4641
+ description: details.description,
4642
+ duration: taskDuration(details),
4643
+ exitCode: details.exitCode
4644
+ };
4645
+ });
4646
+ return {
4647
+ content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
4648
+ details: { count: list.length }
4649
+ };
4650
+ }
4651
+ if (action === "inspect") {
4652
+ if (!input.task_id)
4653
+ throw new Error("task_id is required for inspect action");
4654
+ const matched = matchingTasksById([...this.tasks.values()], input.task_id);
4655
+ if (matched.length === 0)
4656
+ throw new Error(`No task found matching prefix "${input.task_id}"`);
4657
+ if (matched.length > 1)
4658
+ throw new Error(`Task ID prefix "${input.task_id}" matches ${matched.length} tasks`);
4659
+ const task = matched[0];
4660
+ const details = taskDetails(task);
4661
+ const transcript = taskTranscript(task);
4662
+ const tail = transcript.slice(-100).join(`
4663
+ `);
4664
+ const header = task.kind === "command" ? `$ bash ${task.command ?? task.description}
4665
+ CWD: ${task.cwd}
4666
+ State: ${details.state}
4667
+ Duration: ${taskDuration(details) ?? "unknown"}
4668
+ Exit code: ${details.exitCode ?? "none"}` : `@${task.agent}: ${task.description}
4669
+ State: ${details.state}
4670
+ Duration: ${taskDuration(details) ?? "unknown"}
4671
+ Tool calls: ${task.toolCalls ?? 0}`;
4672
+ return {
4673
+ content: [{ type: "text", text: `${header}
4674
+
4675
+ Recent Output:
4676
+ ${tail}` }],
4677
+ details
4678
+ };
4679
+ }
4680
+ if (action === "cancel") {
4681
+ if (!input.task_id)
4682
+ throw new Error("task_id is required for cancel action");
4683
+ const matched = matchingTasksById([...this.tasks.values()], input.task_id);
4684
+ if (matched.length === 0)
4685
+ throw new Error(`No task found matching prefix "${input.task_id}"`);
4686
+ if (matched.length > 1)
4687
+ throw new Error(`Task ID prefix "${input.task_id}" matches ${matched.length} tasks`);
4688
+ const count = await this.cancelTasks([matched[0].id], ctx);
4689
+ return {
4690
+ content: [
4691
+ {
4692
+ type: "text",
4693
+ text: `Cancelled task ${matched[0].id.slice(0, 8)} (${count} task cancelled)`
4694
+ }
4695
+ ],
4696
+ details: { taskId: matched[0].id, cancelled: count > 0 }
4697
+ };
4698
+ }
4699
+ throw new Error(`Unknown action: ${action}`);
4700
+ }
4701
+ };
4702
+ }
4703
+ async runBackgroundShellTask(params, ctx) {
4704
+ const taskId = randomUUID();
4705
+ const sessionId = this.activeSessionId ?? ctx.sessionManager.getSessionId();
4706
+ const sessionDir = join7(getAgentDir4(), "sessions", "pi-landstrip", sessionId, taskId);
4707
+ mkdirSync3(sessionDir, { recursive: true });
4708
+ const outputFile = join7(sessionDir, "output.txt");
4709
+ const outputFd = openSync(outputFile, "w");
4710
+ const label = params.name?.trim() || truncateToWidth(params.command.trim(), 60);
4711
+ const task = {
4712
+ version: 1,
4713
+ id: taskId,
4714
+ kind: "command",
4715
+ agent: "bash",
4716
+ command: params.command,
4717
+ description: label,
4718
+ cwd: ctx.cwd,
4719
+ parentSessionId: sessionId,
4720
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
4721
+ depth: 1,
4722
+ state: "running",
4723
+ startedAt: Date.now(),
4724
+ sessionDir,
4725
+ outputFile,
4726
+ background: true
4727
+ };
4728
+ this.tasks.set(taskId, task);
4729
+ this.persist(task);
4730
+ this.notifyTaskUpdate();
4731
+ this.updateTaskWidget(ctx);
4732
+ const controller = new AbortController;
4733
+ this.controllers.set(taskId, controller);
4734
+ (async () => {
4735
+ try {
4736
+ const result = await this.integration.runSandboxedCommand(params.command, ctx.cwd, {
4737
+ timeout: params.timeout,
4738
+ signal: controller.signal,
4739
+ promptOnBlock: false,
4740
+ onData: (chunk) => {
4741
+ try {
4742
+ writeSync(outputFd, chunk);
4743
+ } catch {}
4744
+ this.notifyTaskUpdate();
4745
+ }
4746
+ }, ctx);
4747
+ task.exitCode = result.exitCode;
4748
+ if (task.state !== "cancelled") {
4749
+ task.state = result.exitCode === 0 ? "completed" : "error";
4750
+ if (result.exitCode !== 0) {
4751
+ task.error = `Process exited with code ${result.exitCode}`;
4752
+ }
4753
+ }
4754
+ } catch (err) {
4755
+ if (task.state !== "cancelled") {
4756
+ task.state = "error";
4757
+ task.error = formatError(err);
4758
+ }
4759
+ } finally {
4760
+ try {
4761
+ closeSync(outputFd);
4762
+ } catch {}
4763
+ task.finishedAt = Date.now();
4764
+ this.controllers.delete(taskId);
4765
+ this.persist(task);
4766
+ const durationSec = taskDuration(taskDetails(task)) ?? "";
4767
+ const exitMsg = task.exitCode !== undefined && task.exitCode !== null ? task.exitCode === 0 ? "completed" : `failed (exit ${task.exitCode})` : task.state === "cancelled" ? "cancelled" : "error";
4768
+ this.recentCompletions.set(taskId, {
4769
+ label: task.description,
4770
+ status: task.state === "completed" ? "completed" : task.state === "cancelled" ? "cancelled" : "error",
4771
+ text: `$ bash ${task.description} ${exitMsg} (${durationSec})`,
4772
+ finishedAt: Date.now()
4773
+ });
4774
+ const commandOutput = task.outputFile && existsSync5(task.outputFile) ? readFileSync6(task.outputFile, "utf8").trim() : task.output ?? "";
4775
+ const deliveryText = `Background command ${task.state === "completed" ? "completed" : "failed"}: ${task.description}
4776
+
4777
+ ${renderTaskResult(task.id, task.state, commandOutput || (task.error ?? ""))}`;
4778
+ const delivered = await this.deliverBackground(task, deliveryText);
4779
+ if (delivered) {
4780
+ task.delivered = true;
4781
+ this.persist(task);
4782
+ }
4783
+ if (task.state === "error") {
4784
+ ctx.ui.notify(`Background command failed: ${task.description}`, "error");
4785
+ }
4786
+ this.notifyTaskUpdate();
4787
+ this.updateTaskWidget(ctx);
4788
+ }
4789
+ })();
4790
+ return {
4791
+ content: [
4792
+ {
4793
+ type: "text",
4794
+ text: `Command launched in background.
4795
+ Task ID: ${taskId}
4796
+ Output file: ${outputFile}
4797
+ Use /landstrip tasks to view live progress or cancel.`
4798
+ }
4799
+ ],
4800
+ details: {
4801
+ taskId,
4802
+ state: "running",
4803
+ description: label,
4804
+ outputFile
4805
+ }
4806
+ };
4807
+ }
4463
4808
  createTaskTool(parentTask, callerRules, boundCtx) {
4464
4809
  const cwd = boundCtx?.cwd ?? process.cwd();
4465
4810
  const catalog = this.loadCatalog(cwd, getAgentDir4(), boundCtx ? isProjectTrusted(boundCtx) : false);
@@ -5198,6 +5543,13 @@ ${catalog.diagnostics.join(`
5198
5543
  const output = await run;
5199
5544
  if (this.shuttingDown || task.delivered || this.foregroundClaims.has(task.id))
5200
5545
  return;
5546
+ const durationSec = taskDuration(taskDetails(task)) ?? "";
5547
+ this.recentCompletions.set(task.id, {
5548
+ label: task.description,
5549
+ status: "completed",
5550
+ text: `@${task.agent}: ${task.description} completed (${durationSec})`,
5551
+ finishedAt: Date.now()
5552
+ });
5201
5553
  const delivered = await this.deliverBackground(task, `Background task completed: ${task.description}
5202
5554
 
5203
5555
  ${renderTaskResult(task.id, "completed", output)}`);
@@ -5205,10 +5557,18 @@ ${renderTaskResult(task.id, "completed", output)}`);
5205
5557
  return;
5206
5558
  task.delivered = true;
5207
5559
  this.persist(task);
5560
+ this.notifyTaskUpdate();
5208
5561
  } catch (error) {
5209
5562
  if (this.shuttingDown || task.delivered || this.foregroundClaims.has(task.id))
5210
5563
  return;
5211
5564
  const message = formatError(error);
5565
+ const durationSec = taskDuration(taskDetails(task)) ?? "";
5566
+ this.recentCompletions.set(task.id, {
5567
+ label: task.description,
5568
+ status: task.state === "cancelled" ? "cancelled" : "error",
5569
+ text: `@${task.agent}: ${task.description} failed (${durationSec})`,
5570
+ finishedAt: Date.now()
5571
+ });
5212
5572
  const delivered = await this.deliverBackground(task, `Background task failed: ${task.description}
5213
5573
 
5214
5574
  ${renderTaskResult(task.id, "error", message)}`);
@@ -5217,6 +5577,7 @@ ${renderTaskResult(task.id, "error", message)}`);
5217
5577
  ctx.ui.notify(`Background task failed: ${task.description}`, "error");
5218
5578
  task.delivered = true;
5219
5579
  this.persist(task);
5580
+ this.notifyTaskUpdate();
5220
5581
  }
5221
5582
  }
5222
5583
  async deliverBackground(task, content) {
@@ -5262,7 +5623,7 @@ ${renderTaskResult(task.id, "error", message)}`);
5262
5623
  return;
5263
5624
  const tasks = [...this.tasks.values()];
5264
5625
  const active = tasks.filter((task) => task.state === "queued" || task.state === "running");
5265
- if (active.length === 0) {
5626
+ if (active.length === 0 && this.recentCompletions.size === 0) {
5266
5627
  ctx.ui.setWidget(TASK_WIDGET, undefined);
5267
5628
  return;
5268
5629
  }
@@ -5274,10 +5635,11 @@ ${renderTaskResult(task.id, "error", message)}`);
5274
5635
  const summary = queued > 0 ? `${runningSummary} · ${queued} queued` : runningSummary;
5275
5636
  ctx.ui.setWidget(TASK_WIDGET, (_tui, theme) => ({
5276
5637
  render: (width) => {
5277
- const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${summary}`);
5638
+ const header = theme.fg("accent", theme.bold("Tasks")) + theme.fg("dim", ` ${summary}`);
5278
5639
  const tree = taskTreeLines(visible, (task) => {
5279
5640
  const state = taskState(theme, task);
5280
- const agent = theme.fg("accent", `@${task.agent}`);
5641
+ const tag = task.kind === "command" ? "$ bash" : `@${task.agent}`;
5642
+ const agent = theme.fg("accent", tag);
5281
5643
  const record = byId.get(task.id);
5282
5644
  const progress = taskProgress(record);
5283
5645
  const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
@@ -5290,32 +5652,97 @@ ${renderTaskResult(task.id, "error", message)}`);
5290
5652
  if (tree.length > shown.length) {
5291
5653
  shown.push(theme.fg("dim", ` … ${tree.length - shown.length} more`));
5292
5654
  }
5655
+ if (this.recentCompletions.size > 0 && shown.length < 8) {
5656
+ for (const comp of this.recentCompletions.values()) {
5657
+ if (shown.length >= 8)
5658
+ break;
5659
+ const icon = comp.status === "completed" ? theme.fg("success", "✓") : comp.status === "cancelled" ? theme.fg("muted", "⊘") : theme.fg("error", "✗");
5660
+ shown.push(` ${icon} ${theme.fg("dim", comp.text)}`);
5661
+ }
5662
+ }
5293
5663
  return [header, ...shown].map((line) => truncateToWidth(line, Math.max(1, width)));
5294
5664
  },
5295
5665
  invalidate() {}
5296
5666
  }));
5297
5667
  }
5668
+ abortTaskProcess(taskId) {
5669
+ const running = this.running.get(taskId);
5670
+ if (running) {
5671
+ try {
5672
+ running.rpc.abort();
5673
+ } catch {}
5674
+ }
5675
+ const controller = this.controllers.get(taskId);
5676
+ if (controller) {
5677
+ controller.abort();
5678
+ this.controllers.delete(taskId);
5679
+ }
5680
+ }
5681
+ async cancelTasks(taskIds, ctx) {
5682
+ let cancelled = 0;
5683
+ const toCancel = new Set;
5684
+ const collectRecursive = (id) => {
5685
+ toCancel.add(id);
5686
+ for (const t of this.tasks.values()) {
5687
+ if (t.parentTaskId === id && !toCancel.has(t.id)) {
5688
+ collectRecursive(t.id);
5689
+ }
5690
+ }
5691
+ };
5692
+ for (const id of taskIds) {
5693
+ const task = this.tasks.get(id);
5694
+ if (task)
5695
+ collectRecursive(task.id);
5696
+ }
5697
+ for (const taskId of toCancel) {
5698
+ const task = this.tasks.get(taskId);
5699
+ if (!task)
5700
+ continue;
5701
+ if (task.state === "completed" || task.state === "error" || task.state === "cancelled") {
5702
+ continue;
5703
+ }
5704
+ task.state = "cancelled";
5705
+ task.finishedAt = Date.now();
5706
+ this.abortTaskProcess(taskId);
5707
+ this.persist(task);
5708
+ cancelled += 1;
5709
+ }
5710
+ if (cancelled > 0) {
5711
+ this.updateTaskWidget(ctx);
5712
+ this.notifyTaskUpdate();
5713
+ }
5714
+ return cancelled;
5715
+ }
5298
5716
  deleteTasks(taskIds, ctx) {
5299
5717
  let deleted = 0;
5300
5718
  for (const taskId of taskIds) {
5301
5719
  const task = this.tasks.get(taskId);
5302
5720
  if (!task)
5303
5721
  continue;
5722
+ if (task.state === "running" || task.state === "queued") {
5723
+ task.state = "cancelled";
5724
+ task.finishedAt = Date.now();
5725
+ this.abortTaskProcess(taskId);
5726
+ }
5304
5727
  task.deleted = true;
5305
5728
  task.delivered = true;
5306
- this.controllers.get(taskId)?.abort();
5307
5729
  if (task.sessionDir && existsSync5(task.sessionDir)) {
5308
- rmSync(task.sessionDir, { recursive: true, force: true });
5730
+ try {
5731
+ rmSync(task.sessionDir, { recursive: true, force: true });
5732
+ } catch {}
5309
5733
  }
5310
5734
  this.tasks.delete(taskId);
5311
5735
  this.pendingPrompts.delete(taskId);
5312
5736
  this.pendingSteers.delete(taskId);
5313
5737
  this.foregroundClaims.delete(taskId);
5738
+ this.recentCompletions.delete(taskId);
5314
5739
  this.persist(task);
5315
5740
  deleted += 1;
5316
5741
  }
5317
- if (deleted > 0)
5742
+ if (deleted > 0) {
5318
5743
  this.updateTaskWidget(ctx);
5744
+ this.notifyTaskUpdate();
5745
+ }
5319
5746
  return deleted;
5320
5747
  }
5321
5748
  async cleanupOrphanSubagentSessions(ctx) {
@@ -5536,6 +5963,18 @@ function registerSubagents(pi, integration, permissionPrompts = new PermissionPr
5536
5963
  }
5537
5964
 
5538
5965
  // index.ts
5966
+ import { Type as Type2 } from "typebox";
5967
+ import { Text as Text2 } from "@earendil-works/pi-tui";
5968
+ var LandstripBashSchema = Type2.Object({
5969
+ command: Type2.String({ description: "Shell command to execute" }),
5970
+ timeout: Type2.Optional(Type2.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
5971
+ background: Type2.Optional(Type2.Boolean({
5972
+ description: "Run command asynchronously in the background and return a task ID"
5973
+ })),
5974
+ name: Type2.Optional(Type2.String({
5975
+ description: "Concise label for the background command execution"
5976
+ }))
5977
+ });
5539
5978
  function enforcesShellReadPolicy(shellReadAccess, platform = process.platform) {
5540
5979
  return shellReadAccess !== "host" || platform === "win32";
5541
5980
  }
@@ -5928,7 +6367,8 @@ function pi_landstrip_default(pi) {
5928
6367
  }
5929
6368
  const permissionPrompts = new PermissionPromptCoordinator;
5930
6369
  const integration = createLandstripIntegrationWithPrompts({}, permissionPrompts);
5931
- registerSubagents(pi, integration, permissionPrompts);
6370
+ const subagentRuntime = registerSubagents(pi, integration, permissionPrompts);
6371
+ integration.setSubagentRuntime?.(subagentRuntime);
5932
6372
  integration.register(pi);
5933
6373
  }
5934
6374
  function createLandstripIntegration(options = {}) {
@@ -5937,6 +6377,7 @@ function createLandstripIntegration(options = {}) {
5937
6377
  function createLandstripIntegrationWithPrompts(options, permissionPrompts) {
5938
6378
  const shouldRegisterBashTool = options.registerBashTool ?? true;
5939
6379
  const localCwd = options.cwd ?? process.cwd();
6380
+ let subagentRuntime = options.subagentRuntime;
5940
6381
  let projectConfigTrusted = false;
5941
6382
  function loadConfig(cwd) {
5942
6383
  return loadSandboxConfig(cwd, projectConfigTrusted);
@@ -6548,14 +6989,7 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6548
6989
  if (spawned)
6549
6990
  throw new Error("Prepared process can only be spawned once");
6550
6991
  spawned = true;
6551
- const landstripArgs = [
6552
- "run",
6553
- "-p",
6554
- workerPolicy.path,
6555
- "--",
6556
- options.command,
6557
- ...options.args
6558
- ];
6992
+ const landstripArgs = ["-p", workerPolicy.path, "--", options.command, ...options.args];
6559
6993
  const authPipe = Array.isArray(spawnOptions.stdio) && spawnOptions.stdio[WORKER_AUTH_FD] === "pipe";
6560
6994
  if (authPipe && process.platform === "win32") {
6561
6995
  throw new Error("Sandboxed worker authentication requires inherited Unix file descriptors");
@@ -6565,10 +6999,10 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6565
6999
  stdio.push(workerChildEnd ?? "ignore");
6566
7000
  if (authPipe) {
6567
7001
  stdio.push("pipe");
6568
- landstripArgs.splice(1, 0, "--inherit", String(WORKER_AUTH_FD));
7002
+ landstripArgs.unshift("--inherit", String(WORKER_AUTH_FD));
6569
7003
  }
6570
7004
  if (workerChildEnd)
6571
- landstripArgs.splice(1, 0, "--trap", "3");
7005
+ landstripArgs.unshift("--trap", "3");
6572
7006
  const child = spawn2(binaryPath(), landstripArgs, {
6573
7007
  ...spawnOptions,
6574
7008
  cwd: options.cwd,
@@ -6776,16 +7210,9 @@ ${paths}${note}`, PERMISSION_OPTIONS, promptSignal);
6776
7210
  };
6777
7211
  processAllowances = withWindowsProcessReadAccess(processAllowances, invocation.executable, cwd);
6778
7212
  policy = writePolicyFile(cwd, proxy?.port ?? null, "shell", processAllowances);
6779
- landstripArgs = [
6780
- "run",
6781
- "-p",
6782
- policy.path,
6783
- "--",
6784
- invocation.executable,
6785
- ...invocation.args
6786
- ];
7213
+ landstripArgs = ["-p", policy.path, "--", invocation.executable, ...invocation.args];
6787
7214
  if (process.platform !== "win32")
6788
- landstripArgs.splice(1, 0, "--trap", "3");
7215
+ landstripArgs.unshift("--trap", "3");
6789
7216
  } catch (error) {
6790
7217
  await teardownResources();
6791
7218
  throw error;
@@ -7215,13 +7642,45 @@ ${message}
7215
7642
  const localBash = createBashToolDefinition(cwd, {
7216
7643
  shellPath: SettingsManager2.create(cwd).getShellPath()
7217
7644
  });
7645
+ const baseRenderCall = localBash.renderCall;
7646
+ const baseRenderResult = localBash.renderResult;
7218
7647
  return {
7219
7648
  ...localBash,
7220
7649
  label: "bash (landstrip)",
7221
7650
  description: "Execute commands with the active Landstrip shell provider.",
7222
- async execute(id, params, signal, onUpdate, ctx) {
7651
+ parameters: LandstripBashSchema,
7652
+ renderCall(args, theme, context) {
7653
+ const bashArgs = args;
7654
+ if (bashArgs.background) {
7655
+ const nameSuffix = bashArgs.name ? theme.fg("dim", ` (${bashArgs.name})`) : "";
7656
+ return new Text2(theme.fg("accent", "$ " + bashArgs.command) + theme.fg("muted", " [bg]") + nameSuffix, 0, 0);
7657
+ }
7658
+ return baseRenderCall ? baseRenderCall(args, theme, context) : new Text2(args.command, 0, 0);
7659
+ },
7660
+ renderResult(result, options, theme, context) {
7661
+ const details = result.details;
7662
+ if (details?.taskId) {
7663
+ const text = result.content[0]?.type === "text" ? result.content[0].text : "";
7664
+ return new Text2(theme.fg("success", `✓ Task ${details.taskId.slice(0, 8)} started`) + `
7665
+ ` + theme.fg("dim", text), 0, 0);
7666
+ }
7667
+ return baseRenderResult ? baseRenderResult(result, options, theme, context) : new Text2("", 0, 0);
7668
+ },
7669
+ async execute(id, rawParams, signal, onUpdate, ctx) {
7223
7670
  if (!ctx)
7224
7671
  throw new Error("Sandbox context is unavailable");
7672
+ const params = rawParams;
7673
+ if (params.background) {
7674
+ if (!ensureSandboxState(ctx)) {
7675
+ if (sandboxState === "unavailable") {
7676
+ throw new Error("Sandbox is unavailable; refusing command");
7677
+ }
7678
+ }
7679
+ if (!subagentRuntime) {
7680
+ throw new Error("Background task runtime is unavailable");
7681
+ }
7682
+ return subagentRuntime.runBackgroundShellTask(params, ctx);
7683
+ }
7225
7684
  if (!ensureSandboxState(ctx)) {
7226
7685
  if (sandboxState === "unavailable") {
7227
7686
  throw new Error("Sandbox is unavailable; refusing command");
@@ -7349,11 +7808,24 @@ ${message}
7349
7808
  on,
7350
7809
  register,
7351
7810
  emit: emitEvent,
7811
+ runSandboxedCommand(command, cwd, execOptions, ctx) {
7812
+ const ops = ensureSandboxState(ctx) ? createLandstripBashOps(ctx, { promptOnBlock: execOptions.promptOnBlock ?? false }) : externalShellProvider !== undefined ? createProviderBashOps() : createLocalBashOperations();
7813
+ return ops.exec(command, cwd, {
7814
+ onData: execOptions.onData ?? (() => {}),
7815
+ signal: execOptions.signal,
7816
+ timeout: execOptions.timeout,
7817
+ env: execOptions.env
7818
+ });
7819
+ },
7820
+ setSubagentRuntime(runtime) {
7821
+ subagentRuntime = runtime;
7822
+ },
7352
7823
  sandboxCallbacks
7353
7824
  };
7354
7825
  return integration;
7355
7826
  }
7356
7827
  export {
7828
+ LandstripBashSchema,
7357
7829
  POSIX_SHELL_PROVIDER_ID,
7358
7830
  controlResponseLine,
7359
7831
  createLandstripIntegration,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.19.19",
3
+ "version": "0.19.20",
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-api": "^0.19.19",
42
+ "@landstrip/landstrip-api": "^0.19.20",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "undici": "^8.9.0",