pi-landstrip 0.18.7 → 0.18.9

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 +171 -104
  2. package/package.json +2 -2
package/dist/index.ts CHANGED
@@ -1006,19 +1006,33 @@ function loadAgentCatalog(cwd, piAgentDir = getAgentDir2(), includeProject = tru
1006
1006
  }
1007
1007
  return { agents: normalized, permissions, diagnostics, maxSubagents };
1008
1008
  }
1009
+ function permissionMatches(pattern, permission) {
1010
+ return pattern === "*" || minimatch(permission, pattern, { dot: true });
1011
+ }
1009
1012
  function permissionDecision(rules, permission, resource = "*") {
1010
1013
  let decision = "ask";
1011
1014
  for (const rule of rules) {
1012
- const matchesPermission = rule.permission === "*" || minimatch(permission, rule.permission, { dot: true });
1013
1015
  const matchesResource = rule.pattern === "*" || minimatch(normalizeAbsolutePath(resource), normalizeAbsolutePath(rule.pattern), {
1014
1016
  dot: true,
1015
1017
  matchBase: false
1016
1018
  });
1017
- if (matchesPermission && matchesResource)
1019
+ if (permissionMatches(rule.permission, permission) && matchesResource)
1018
1020
  decision = rule.action;
1019
1021
  }
1020
1022
  return decision;
1021
1023
  }
1024
+ function permissionAlwaysDenied(rules, permission) {
1025
+ let denied = false;
1026
+ for (const rule of rules) {
1027
+ if (!permissionMatches(rule.permission, permission))
1028
+ continue;
1029
+ if (rule.pattern === "*")
1030
+ denied = rule.action === "deny";
1031
+ else if (rule.action !== "deny")
1032
+ denied = false;
1033
+ }
1034
+ return denied;
1035
+ }
1022
1036
  function mergePermissionRules(...values) {
1023
1037
  return values.flatMap((value) => value);
1024
1038
  }
@@ -1734,6 +1748,25 @@ var packageDir = dirname4(fileURLToPath(import.meta.url));
1734
1748
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
1735
1749
  var INSPECTOR_BODY_LINES = 16;
1736
1750
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
1751
+ var AGENTS_HELP_ROWS = [
1752
+ ["Tab", "Next tab"],
1753
+ ["Shift+Tab / S", "Switch scope in Agents or Settings"],
1754
+ ["↑ / ↓", "Select item or scroll when follow is off"],
1755
+ ["F", "Toggle task log follow"],
1756
+ ["Page Up / Down", "Scroll task output by page"],
1757
+ ["Home / End", "Jump to task output boundary"],
1758
+ ["Enter", "Open logs, set primary, or save"],
1759
+ ["Esc", "Back or close"],
1760
+ ["Ctrl+C", "Close"],
1761
+ ["Space", "Enable agent or toggle sandbox"],
1762
+ ["E", "Edit project agent"],
1763
+ ["X", "Delete project agent"],
1764
+ ["Y / N", "Confirm or cancel deletion"],
1765
+ ["I", "Use global value"],
1766
+ ["R", "Discard setting changes"],
1767
+ ["0-9 / + / -", "Set subagent limit"],
1768
+ ["Backspace", "Open parent task"]
1769
+ ];
1737
1770
  var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
1738
1771
  var cachedPiPackage;
1739
1772
  var piPackageResolved = false;
@@ -1845,6 +1878,40 @@ function taskDuration(details) {
1845
1878
  const seconds = Math.max(0, end - details.startedAt) / 1000;
1846
1879
  return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
1847
1880
  }
1881
+ function activeTaskRecords(tasks) {
1882
+ const byId = new Map(tasks.map((task) => [task.id, task]));
1883
+ const visibleIds = new Set;
1884
+ for (const task of tasks) {
1885
+ if (task.state !== "queued" && task.state !== "running")
1886
+ continue;
1887
+ const seen = new Set;
1888
+ let current = task;
1889
+ while (current && !seen.has(current.id)) {
1890
+ seen.add(current.id);
1891
+ visibleIds.add(current.id);
1892
+ current = current.parentTaskId ? byId.get(current.parentTaskId) : undefined;
1893
+ }
1894
+ }
1895
+ return tasks.filter((task) => visibleIds.has(task.id));
1896
+ }
1897
+ function taskProgress(task) {
1898
+ if (task.state === "queued")
1899
+ return ["waiting for slot"];
1900
+ if (task.state !== "running")
1901
+ return [];
1902
+ const progress = [];
1903
+ if (task.currentTool)
1904
+ progress.push(`→ ${task.currentTool}`);
1905
+ if (task.toolCalls !== undefined) {
1906
+ progress.push(`${task.toolCalls} call${task.toolCalls === 1 ? "" : "s"}`);
1907
+ }
1908
+ const duration = taskDuration(taskDetails(task));
1909
+ if (duration)
1910
+ progress.push(duration);
1911
+ if (task.retryAttempt)
1912
+ progress.push(`retry ${task.retryAttempt}`);
1913
+ return progress;
1914
+ }
1848
1915
  function taskOutput(details, fallback) {
1849
1916
  return details.error ?? details.output ?? fallback;
1850
1917
  }
@@ -2099,6 +2166,15 @@ function permissionName(tool) {
2099
2166
  return "list";
2100
2167
  return tool;
2101
2168
  }
2169
+ function permittedToolNames(tools, rules) {
2170
+ return tools.filter((tool) => !permissionAlwaysDenied(rules, permissionName(tool)));
2171
+ }
2172
+ function enforcePermittedTools(pi, rules) {
2173
+ const active = pi.getActiveTools();
2174
+ const permitted = permittedToolNames(active, rules);
2175
+ if (permitted.join("\x00") !== active.join("\x00"))
2176
+ pi.setActiveTools(permitted);
2177
+ }
2102
2178
  function canonicalPermissionPath(path, seen = new Set) {
2103
2179
  const missing = [];
2104
2180
  let existing = path;
@@ -2188,6 +2264,10 @@ function parseControlResponse(value) {
2188
2264
  function registerSubagentWorker(pi, config) {
2189
2265
  pi.on("session_start", () => {
2190
2266
  delete process.env[WORKER_ENV];
2267
+ enforcePermittedTools(pi, config.rules);
2268
+ });
2269
+ pi.on("before_agent_start", () => {
2270
+ enforcePermittedTools(pi, config.rules);
2191
2271
  });
2192
2272
  pi.on("tool_call", async (event, ctx) => {
2193
2273
  if (event.toolName === "task")
@@ -2379,7 +2459,7 @@ ${this.primaryAgent.prompt}` };
2379
2459
  let maxSubagentsSettings = loadMaxSubagentsSettings(ctx.cwd, projectTrusted);
2380
2460
  const sandboxCallbacks = this.integration.sandboxCallbacks;
2381
2461
  let sandboxSettings = sandboxCallbacks?.load(ctx.cwd, projectTrusted) ?? { global: true };
2382
- let tab = requested ? "tasks" : "agents";
2462
+ let tab = requested ? "logs" : "agents";
2383
2463
  let selectedAgent = Math.max(0, agents.findIndex((agent) => agent.name === this.primaryAgent?.name));
2384
2464
  let selectedTask = requested ? tasks.findIndex((task) => task.id === requested || task.id.startsWith(requested)) : 0;
2385
2465
  if (selectedTask < 0) {
@@ -2391,7 +2471,7 @@ ${this.primaryAgent.prompt}` };
2391
2471
  let saving = false;
2392
2472
  let editing = false;
2393
2473
  let confirmingDeleteAgent;
2394
- let detail = requested.length > 0;
2474
+ let follow = requested.length > 0;
2395
2475
  let scroll = 0;
2396
2476
  const reloadAgents = () => {
2397
2477
  const selectedName = agents[selectedAgent]?.name;
@@ -2444,7 +2524,7 @@ ${this.primaryAgent.prompt}` };
2444
2524
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
2445
2525
  };
2446
2526
  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"}`)}`;
2527
+ const tabs = `${dialogTabs(theme, ["Agents", "Tasks", "Logs", "Settings", "Help"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`)}${tab === "tasks" || tab === "logs" ? "" : theme.fg("dim", ` · Scope: ${scope === "project" ? "Project" : "Global"}`)}`;
2448
2528
  if (tab === "agents") {
2449
2529
  const start = Math.max(0, Math.min(selectedAgent - 3, agents.length - 7));
2450
2530
  const shown2 = agents.slice(start, start + 7);
@@ -2513,45 +2593,30 @@ ${this.primaryAgent.prompt}` };
2513
2593
  lines2.push(` ${theme.fg("error", diagnostic)}`);
2514
2594
  }
2515
2595
  }
2516
- lines2.push("");
2517
2596
  if (confirmingDeleteAgent) {
2518
- lines2.push(theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2597
+ lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2519
2598
  ["Y", "delete"],
2520
2599
  ["N / Esc", "cancel"]
2521
2600
  ]));
2522
2601
  } 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));
2602
+ lines2.push("", theme.fg("dim", "Saving…"));
2544
2603
  }
2545
2604
  return box(lines2);
2546
2605
  }
2547
2606
  if (tab === "settings") {
2548
- const maxSubagents = scope === "project" && maxSubagentsSettings.project === undefined ? "-" : String(selectedMaxSubagents());
2549
- const sandbox = scope === "project" && sandboxSettings.project === undefined ? "-" : selectedSandboxEnabled() ? "on" : "off";
2607
+ const maxSubagentsInherited = scope === "project" && maxSubagentsSettings.project === undefined;
2608
+ const sandboxInherited = scope === "project" && sandboxSettings.project === undefined;
2550
2609
  const rows = [
2551
- { label: "Maximum subagents", value: maxSubagents, unavailable: false },
2610
+ {
2611
+ label: "Maximum subagents",
2612
+ value: String(selectedMaxSubagents()),
2613
+ inherited: maxSubagentsInherited,
2614
+ unavailable: false
2615
+ },
2552
2616
  {
2553
2617
  label: "Sandbox enabled",
2554
- value: sandbox,
2618
+ value: selectedSandboxEnabled() ? "on" : "off",
2619
+ inherited: sandboxInherited,
2555
2620
  unavailable: sandboxCallbacks === undefined
2556
2621
  }
2557
2622
  ];
@@ -2560,39 +2625,31 @@ ${this.primaryAgent.prompt}` };
2560
2625
  const selected = index === selectedSetting;
2561
2626
  const cursor = selected ? theme.fg("accent", "›") : " ";
2562
2627
  const text = `[ ${row.value} ]`;
2563
- const value = selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
2628
+ const value = row.inherited ? theme.fg("dim", text) : selected ? theme.fg("accent", text) : theme.fg(row.unavailable ? "muted" : "text", text);
2564
2629
  const label = theme.fg(row.unavailable ? "muted" : "text", row.label);
2565
2630
  const dirty = dirtySettings.has(index) ? theme.fg("warning", " *") : "";
2566
2631
  lines2.push(`${cursor} ${value} ${label}${dirty}`);
2567
2632
  }
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) {
2633
+ lines2.push("", theme.fg("dim", selectedSetting === 0 ? rows[0]?.inherited ? "Sets concurrent subagents. Zero disables the task tool. Using the Global value." : "Sets concurrent subagents. Zero disables the task tool." : rows[1]?.inherited ? "Controls OS sandboxing. Use /sandbox to inspect its policy. Using the Global value." : "Controls OS sandboxing. Use /sandbox to inspect its policy."));
2634
+ if (saving)
2570
2635
  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));
2589
- }
2590
2636
  return box(lines2);
2591
2637
  }
2592
- if (!detail) {
2593
- const lines2 = [tabs, ""];
2638
+ if (tab === "help") {
2639
+ const shortcutWidth = Math.max(visibleWidth2("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth2(shortcut)));
2640
+ const descriptionWidth = Math.max(1, contentWidth - shortcutWidth - 4);
2641
+ const lines2 = [
2642
+ tabs,
2643
+ "",
2644
+ theme.fg("dim", ` ${pad("Shortcut", shortcutWidth)} Description`),
2645
+ ...AGENTS_HELP_ROWS.map(([shortcut, description]) => ` ${theme.fg("accent", pad(shortcut, shortcutWidth))} ${theme.fg("text", truncateToWidth2(description, descriptionWidth))}`)
2646
+ ];
2647
+ return box(lines2);
2648
+ }
2649
+ if (tab === "tasks") {
2650
+ const listLines = [];
2594
2651
  if (tasks.length === 0) {
2595
- lines2.push("No task sessions in this session.");
2652
+ listLines.push("No task sessions in this session.");
2596
2653
  } else {
2597
2654
  const start = Math.max(0, Math.min(selectedTask - 5, tasks.length - 11));
2598
2655
  const shown2 = tasks.slice(start, start + 11);
@@ -2600,23 +2657,17 @@ ${this.primaryAgent.prompt}` };
2600
2657
  const index = start + offset;
2601
2658
  const cursor = index === selectedTask ? theme.fg("accent", "›") : " ";
2602
2659
  const indent = " ".repeat(Math.max(0, task2.depth - 1));
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))}`);
2660
+ listLines.push(`${cursor} ${indent}${taskState(theme, task2)} ${theme.fg("accent", `@${task2.agent}`)} ${theme.fg("text", task2.description)} ${theme.fg("dim", task2.id.slice(0, 8))}`);
2604
2661
  }
2605
2662
  }
2606
- lines2.push("", dialogKeys(theme, [
2607
- ["↑↓", "select"],
2608
- ["Enter", "inspect"],
2609
- ["Tab", "next tab"],
2610
- ["Esc", "close"]
2611
- ]));
2612
- return box(lines2);
2663
+ return box([tabs, "", ...listLines]);
2613
2664
  }
2614
2665
  const task = tasks[selectedTask];
2615
2666
  if (!task)
2616
2667
  return box([tabs, "", "No task sessions in this session."]);
2617
2668
  const transcript = taskTranscript(task).flatMap((line) => wrapTextWithAnsi(line, contentWidth));
2618
2669
  const maxScroll = Math.max(0, transcript.length - INSPECTOR_BODY_LINES);
2619
- scroll = Math.min(scroll, maxScroll);
2670
+ scroll = follow ? maxScroll : Math.min(scroll, maxScroll);
2620
2671
  const shown = transcript.slice(scroll, scroll + INSPECTOR_BODY_LINES);
2621
2672
  const duration = taskDuration(taskDetails(task));
2622
2673
  const metrics = [
@@ -2628,7 +2679,7 @@ ${this.primaryAgent.prompt}` };
2628
2679
  tabs,
2629
2680
  "",
2630
2681
  `${theme.fg("accent", theme.bold(`@${task.agent}`))} ${theme.fg("text", task.description)}`,
2631
- `${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))}`,
2682
+ `${taskState(theme, task)} ${theme.fg("dim", metrics.join(" · "))} ${theme.fg("dim", "·")} ${theme.fg(follow ? "accent" : "muted", `Follow: ${follow ? "on" : "off"}`)}`,
2632
2683
  "",
2633
2684
  ...shown.map((line) => theme.fg("toolOutput", line))
2634
2685
  ];
@@ -2637,12 +2688,6 @@ ${this.primaryAgent.prompt}` };
2637
2688
  if (transcript.length > INSPECTOR_BODY_LINES) {
2638
2689
  lines.push(theme.fg("dim", `${scroll + 1}–${scroll + shown.length} of ${transcript.length}`));
2639
2690
  }
2640
- lines.push("", dialogKeys(theme, [
2641
- ["↑↓", "scroll"],
2642
- ...task.parentTaskId ? [["Backspace", "parent"]] : [],
2643
- ["Esc", "task list"],
2644
- ["Tab", "next tab"]
2645
- ]));
2646
2691
  return box(lines.map((line) => truncateToWidth2(line, contentWidth)));
2647
2692
  },
2648
2693
  handleInput: (data) => {
@@ -2674,7 +2719,7 @@ ${this.primaryAgent.prompt}` };
2674
2719
  }
2675
2720
  return;
2676
2721
  }
2677
- if (data.toLowerCase() === "s" && tab !== "tasks") {
2722
+ if ((matchesKey(data, "shift+tab") || data.toLowerCase() === "s") && (tab === "agents" || tab === "settings")) {
2678
2723
  if (saving)
2679
2724
  return;
2680
2725
  if (dirtySettings.size > 0) {
@@ -2697,9 +2742,12 @@ ${this.primaryAgent.prompt}` };
2697
2742
  ctx.ui.notify("Save changes with Enter or discard them with R before leaving", "warning");
2698
2743
  return;
2699
2744
  }
2700
- const tabs = ["agents", "tasks", "settings"];
2745
+ const tabs = ["agents", "tasks", "logs", "settings", "help"];
2701
2746
  tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "agents";
2702
- detail = false;
2747
+ if (tab === "logs" && tasks.length > 0)
2748
+ follow = true;
2749
+ else
2750
+ follow = false;
2703
2751
  editing = false;
2704
2752
  scroll = 0;
2705
2753
  tui.requestRender();
@@ -2865,7 +2913,12 @@ ${this.primaryAgent.prompt}` };
2865
2913
  tui.requestRender();
2866
2914
  return;
2867
2915
  }
2868
- if (!detail) {
2916
+ if (tab === "help") {
2917
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2918
+ done();
2919
+ return;
2920
+ }
2921
+ if (tab === "tasks") {
2869
2922
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2870
2923
  done();
2871
2924
  else if (matchesKey(data, "up"))
@@ -2873,31 +2926,51 @@ ${this.primaryAgent.prompt}` };
2873
2926
  else if (matchesKey(data, "down") && tasks.length > 0) {
2874
2927
  selectedTask = Math.min(tasks.length - 1, selectedTask + 1);
2875
2928
  } else if (matchesKey(data, "return") && tasks.length > 0) {
2876
- detail = true;
2929
+ tab = "logs";
2930
+ follow = true;
2877
2931
  scroll = 0;
2878
2932
  } else
2879
2933
  return;
2880
2934
  tui.requestRender();
2881
2935
  return;
2882
2936
  }
2937
+ if (tab !== "logs")
2938
+ return;
2883
2939
  const task = tasks[selectedTask];
2884
- if (!task)
2940
+ if (!task) {
2941
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2942
+ done();
2885
2943
  return;
2944
+ }
2886
2945
  if (matchesKey(data, "escape")) {
2887
- detail = false;
2946
+ tab = "tasks";
2947
+ follow = false;
2888
2948
  scroll = 0;
2889
2949
  } else if (matchesKey(data, "ctrl+c")) {
2890
2950
  done();
2891
- } else if (matchesKey(data, "up")) {
2951
+ } else if (data.toLowerCase() === "f") {
2952
+ follow = !follow;
2953
+ } else if (!follow && matchesKey(data, "up")) {
2892
2954
  scroll = Math.max(0, scroll - 1);
2893
- } else if (matchesKey(data, "down")) {
2955
+ } else if (!follow && matchesKey(data, "down")) {
2894
2956
  scroll += 1;
2957
+ } else if (!follow && matchesKey(data, "pageUp")) {
2958
+ scroll = Math.max(0, scroll - INSPECTOR_BODY_LINES);
2959
+ } else if (!follow && matchesKey(data, "pageDown")) {
2960
+ scroll += INSPECTOR_BODY_LINES;
2961
+ } else if (!follow && matchesKey(data, "home")) {
2962
+ scroll = 0;
2963
+ } else if (!follow && matchesKey(data, "end")) {
2964
+ scroll = Number.MAX_SAFE_INTEGER;
2895
2965
  } else if (matchesKey(data, "backspace")) {
2896
2966
  const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
2897
- if (parentIndex >= 0)
2967
+ if (parentIndex >= 0) {
2898
2968
  selectedTask = parentIndex;
2899
- else
2900
- detail = false;
2969
+ follow = true;
2970
+ } else {
2971
+ tab = "tasks";
2972
+ follow = false;
2973
+ }
2901
2974
  scroll = 0;
2902
2975
  } else
2903
2976
  return;
@@ -3368,7 +3441,8 @@ ${catalog.diagnostics.join(`
3368
3441
  ctx.ui.notify(`Agent ${agent.name} options are not supported by Pi RPC mode: ${providerOptionNames.join(", ")}`, "warning");
3369
3442
  }
3370
3443
  const taskEnabled = agent.permissions.some((rule) => rule.permission === "task" && rule.action !== "deny");
3371
- const tools = taskEnabled ? [...new Set([...this.pi.getActiveTools(), "task"])] : this.pi.getActiveTools().filter((tool) => tool !== "task");
3444
+ const activeTools = taskEnabled ? [...new Set([...this.pi.getActiveTools(), "task"])] : this.pi.getActiveTools().filter((tool) => tool !== "task");
3445
+ const tools = permittedToolNames(activeTools, rules);
3372
3446
  const workerExtensions = this.integration.getWorkerExtensions?.() ?? [];
3373
3447
  const args = [
3374
3448
  ...invocation.args,
@@ -3661,33 +3735,26 @@ ${renderTaskResult(task.id, "error", message)}`);
3661
3735
  if (!ctx.hasUI || ctx.mode !== undefined && ctx.mode !== "tui")
3662
3736
  return;
3663
3737
  const tasks = [...this.tasks.values()];
3664
- const byId = new Map(tasks.map((task) => [task.id, task]));
3665
- const rootId = (task) => {
3666
- const seen = new Set;
3667
- let current = task;
3668
- while (current.parentTaskId && !seen.has(current.id)) {
3669
- seen.add(current.id);
3670
- const parent = byId.get(current.parentTaskId);
3671
- if (!parent)
3672
- break;
3673
- current = parent;
3674
- }
3675
- return current.id;
3676
- };
3677
3738
  const active = tasks.filter((task) => task.state === "queued" || task.state === "running");
3678
- const activeRoots = new Set(active.map(rootId));
3679
- if (activeRoots.size === 0) {
3739
+ if (active.length === 0) {
3680
3740
  ctx.ui.setWidget(TASK_WIDGET, undefined);
3681
3741
  return;
3682
3742
  }
3683
- const visible = tasks.filter((task) => activeRoots.has(rootId(task)));
3743
+ const visible = activeTaskRecords(tasks);
3744
+ const byId = new Map(tasks.map((task) => [task.id, task]));
3745
+ const running = active.filter((task) => task.state === "running").length;
3746
+ const queued = active.length - running;
3747
+ const runningSummary = this.maxSubagents > 0 ? `${running}/${this.maxSubagents} running` : `${running} running`;
3748
+ const summary = queued > 0 ? `${runningSummary} · ${queued} queued` : runningSummary;
3684
3749
  ctx.ui.setWidget(TASK_WIDGET, (_tui, theme) => ({
3685
3750
  render: (width) => {
3686
- const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${active.length} active`);
3751
+ const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${summary}`);
3687
3752
  const tree = taskTreeLines(visible, (task) => {
3688
3753
  const agent = theme.fg("accent", `@${task.agent}`);
3689
3754
  const description = theme.fg("text", task.description);
3690
- return `${taskState(theme, task)} ${agent} ${description}`;
3755
+ const progress = taskProgress(byId.get(task.id));
3756
+ const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
3757
+ return `${taskState(theme, task)} ${agent} ${description}${suffix}`;
3691
3758
  });
3692
3759
  const shown = tree.slice(0, 8);
3693
3760
  if (tree.length > shown.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.18.7",
3
+ "version": "0.18.9",
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.7",
42
+ "@landstrip/landstrip": "^0.18.9",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"