pi-landstrip 0.18.7 → 0.18.8

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 +147 -88
  2. package/package.json +2 -2
package/dist/index.ts CHANGED
@@ -1733,7 +1733,28 @@ var MAX_DEPTH = 3;
1733
1733
  var packageDir = dirname4(fileURLToPath(import.meta.url));
1734
1734
  var MAX_TASK_OUTPUT_BYTES = 64 * 1024;
1735
1735
  var INSPECTOR_BODY_LINES = 16;
1736
+ var TASK_PREVIEW_MIN_WIDTH = 92;
1737
+ var TASK_PREVIEW_LINES = 7;
1736
1738
  var PI_THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
1739
+ var AGENTS_HELP_ROWS = [
1740
+ ["Tab", "Next tab"],
1741
+ ["Shift+Tab / S", "Switch scope in Agents or Settings"],
1742
+ ["↑ / ↓", "Select item or scroll when follow is off"],
1743
+ ["F", "Toggle task log follow"],
1744
+ ["Page Up / Down", "Scroll task output by page"],
1745
+ ["Home / End", "Jump to task output boundary"],
1746
+ ["Enter", "Inspect task, set primary, or save"],
1747
+ ["Esc", "Back or close"],
1748
+ ["Ctrl+C", "Close"],
1749
+ ["Space", "Enable agent or toggle sandbox"],
1750
+ ["E", "Edit project agent"],
1751
+ ["X", "Delete project agent"],
1752
+ ["Y / N", "Confirm or cancel deletion"],
1753
+ ["I", "Use global value"],
1754
+ ["R", "Discard setting changes"],
1755
+ ["0-9 / + / -", "Set subagent limit"],
1756
+ ["Backspace", "Open parent task"]
1757
+ ];
1737
1758
  var MIN_SUPPORTED_PI_VERSION = [0, 82, 0];
1738
1759
  var cachedPiPackage;
1739
1760
  var piPackageResolved = false;
@@ -1845,6 +1866,40 @@ function taskDuration(details) {
1845
1866
  const seconds = Math.max(0, end - details.startedAt) / 1000;
1846
1867
  return seconds < 10 ? `${seconds.toFixed(1)}s` : `${Math.round(seconds)}s`;
1847
1868
  }
1869
+ function activeTaskRecords(tasks) {
1870
+ const byId = new Map(tasks.map((task) => [task.id, task]));
1871
+ const visibleIds = new Set;
1872
+ for (const task of tasks) {
1873
+ if (task.state !== "queued" && task.state !== "running")
1874
+ continue;
1875
+ const seen = new Set;
1876
+ let current = task;
1877
+ while (current && !seen.has(current.id)) {
1878
+ seen.add(current.id);
1879
+ visibleIds.add(current.id);
1880
+ current = current.parentTaskId ? byId.get(current.parentTaskId) : undefined;
1881
+ }
1882
+ }
1883
+ return tasks.filter((task) => visibleIds.has(task.id));
1884
+ }
1885
+ function taskProgress(task) {
1886
+ if (task.state === "queued")
1887
+ return ["waiting for slot"];
1888
+ if (task.state !== "running")
1889
+ return [];
1890
+ const progress = [];
1891
+ if (task.currentTool)
1892
+ progress.push(`→ ${task.currentTool}`);
1893
+ if (task.toolCalls !== undefined) {
1894
+ progress.push(`${task.toolCalls} call${task.toolCalls === 1 ? "" : "s"}`);
1895
+ }
1896
+ const duration = taskDuration(taskDetails(task));
1897
+ if (duration)
1898
+ progress.push(duration);
1899
+ if (task.retryAttempt)
1900
+ progress.push(`retry ${task.retryAttempt}`);
1901
+ return progress;
1902
+ }
1848
1903
  function taskOutput(details, fallback) {
1849
1904
  return details.error ?? details.output ?? fallback;
1850
1905
  }
@@ -2392,6 +2447,7 @@ ${this.primaryAgent.prompt}` };
2392
2447
  let editing = false;
2393
2448
  let confirmingDeleteAgent;
2394
2449
  let detail = requested.length > 0;
2450
+ let follow = detail;
2395
2451
  let scroll = 0;
2396
2452
  const reloadAgents = () => {
2397
2453
  const selectedName = agents[selectedAgent]?.name;
@@ -2444,7 +2500,7 @@ ${this.primaryAgent.prompt}` };
2444
2500
  return `${clipped}${" ".repeat(Math.max(0, cellWidth - visibleWidth2(clipped)))}`;
2445
2501
  };
2446
2502
  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"}`)}`;
2503
+ const tabs = `${dialogTabs(theme, ["Agents", "Tasks", "Settings", "Help"], `${tab[0]?.toUpperCase()}${tab.slice(1)}`)}${tab === "tasks" ? "" : theme.fg("dim", ` · Scope: ${scope === "project" ? "Project" : "Global"}`)}`;
2448
2504
  if (tab === "agents") {
2449
2505
  const start = Math.max(0, Math.min(selectedAgent - 3, agents.length - 7));
2450
2506
  const shown2 = agents.slice(start, start + 7);
@@ -2513,34 +2569,13 @@ ${this.primaryAgent.prompt}` };
2513
2569
  lines2.push(` ${theme.fg("error", diagnostic)}`);
2514
2570
  }
2515
2571
  }
2516
- lines2.push("");
2517
2572
  if (confirmingDeleteAgent) {
2518
- lines2.push(theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2573
+ lines2.push("", theme.fg("error", `Delete @${confirmingDeleteAgent}? This removes its project file.`), dialogKeys(theme, [
2519
2574
  ["Y", "delete"],
2520
2575
  ["N / Esc", "cancel"]
2521
2576
  ]));
2522
2577
  } 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));
2578
+ lines2.push("", theme.fg("dim", "Saving…"));
2544
2579
  }
2545
2580
  return box(lines2);
2546
2581
  }
@@ -2566,57 +2601,73 @@ ${this.primaryAgent.prompt}` };
2566
2601
  lines2.push(`${cursor} ${value} ${label}${dirty}`);
2567
2602
  }
2568
2603
  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) {
2604
+ if (saving)
2570
2605
  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
- }
2606
+ return box(lines2);
2607
+ }
2608
+ if (tab === "help") {
2609
+ const shortcutWidth = Math.max(visibleWidth2("Shortcut"), ...AGENTS_HELP_ROWS.map(([shortcut]) => visibleWidth2(shortcut)));
2610
+ const descriptionWidth = Math.max(1, contentWidth - shortcutWidth - 4);
2611
+ const lines2 = [
2612
+ tabs,
2613
+ "",
2614
+ theme.fg("dim", ` ${pad("Shortcut", shortcutWidth)} Description`),
2615
+ ...AGENTS_HELP_ROWS.map(([shortcut, description]) => ` ${theme.fg("accent", pad(shortcut, shortcutWidth))} ${theme.fg("text", truncateToWidth2(description, descriptionWidth))}`)
2616
+ ];
2590
2617
  return box(lines2);
2591
2618
  }
2592
2619
  if (!detail) {
2593
- const lines2 = [tabs, ""];
2620
+ const listLines = [];
2594
2621
  if (tasks.length === 0) {
2595
- lines2.push("No task sessions in this session.");
2622
+ listLines.push("No task sessions in this session.");
2596
2623
  } else {
2597
2624
  const start = Math.max(0, Math.min(selectedTask - 5, tasks.length - 11));
2598
2625
  const shown2 = tasks.slice(start, start + 11);
2599
- for (const [offset, task2] of shown2.entries()) {
2626
+ for (const [offset, task3] of shown2.entries()) {
2600
2627
  const index = start + offset;
2601
2628
  const cursor = index === selectedTask ? theme.fg("accent", "›") : " ";
2602
- 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))}`);
2629
+ const indent = " ".repeat(Math.max(0, task3.depth - 1));
2630
+ listLines.push(`${cursor} ${indent}${taskState(theme, task3)} ${theme.fg("accent", `@${task3.agent}`)} ${theme.fg("text", task3.description)} ${theme.fg("dim", task3.id.slice(0, 8))}`);
2604
2631
  }
2605
2632
  }
2606
- lines2.push("", dialogKeys(theme, [
2607
- ["↑↓", "select"],
2608
- ["Enter", "inspect"],
2609
- ["Tab", "next tab"],
2610
- ["Esc", "close"]
2611
- ]));
2612
- return box(lines2);
2633
+ if (width < TASK_PREVIEW_MIN_WIDTH || tasks.length === 0) {
2634
+ return box([tabs, "", ...listLines]);
2635
+ }
2636
+ const task2 = tasks[selectedTask];
2637
+ if (!task2)
2638
+ return box([tabs, "", ...listLines]);
2639
+ const leftWidth = Math.max(44, Math.min(64, Math.floor(contentWidth * 0.52)));
2640
+ const rightWidth = Math.max(1, contentWidth - leftWidth - 3);
2641
+ const transcript2 = taskTranscript(task2).flatMap((line) => wrapTextWithAnsi(line, rightWidth));
2642
+ const duration2 = taskDuration(taskDetails(task2));
2643
+ const metrics2 = [
2644
+ task2.currentTool ? `→ ${task2.currentTool}` : undefined,
2645
+ task2.toolCalls === undefined ? undefined : `${task2.toolCalls} tool call${task2.toolCalls === 1 ? "" : "s"}`,
2646
+ duration2,
2647
+ task2.retryAttempt ? `retry ${task2.retryAttempt}` : undefined
2648
+ ].filter(Boolean);
2649
+ const previewLines = [
2650
+ `${theme.fg("accent", theme.bold(`@${task2.agent}`))} ${theme.fg("text", task2.description)}`,
2651
+ `${taskState(theme, task2)} ${theme.fg("dim", task2.id)}`,
2652
+ metrics2.length > 0 ? theme.fg("dim", metrics2.join(" · ")) : "",
2653
+ theme.fg("dim", "Latest activity"),
2654
+ ...transcript2.slice(-TASK_PREVIEW_LINES).map((line) => theme.fg("toolOutput", line))
2655
+ ];
2656
+ const divider = theme.fg("border", "│");
2657
+ const rowCount = Math.max(listLines.length, previewLines.length);
2658
+ const lines2 = Array.from({ length: rowCount }, (_, index) => {
2659
+ const left = pad(listLines[index] ?? "", leftWidth);
2660
+ const right = truncateToWidth2(previewLines[index] ?? "", rightWidth);
2661
+ return `${left} ${divider} ${right}`;
2662
+ });
2663
+ return box([tabs, "", ...lines2]);
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,10 @@ ${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", "settings", "help"];
2701
2746
  tab = tabs[(tabs.indexOf(tab) + 1) % tabs.length] ?? "agents";
2702
2747
  detail = false;
2748
+ follow = false;
2703
2749
  editing = false;
2704
2750
  scroll = 0;
2705
2751
  tui.requestRender();
@@ -2865,6 +2911,11 @@ ${this.primaryAgent.prompt}` };
2865
2911
  tui.requestRender();
2866
2912
  return;
2867
2913
  }
2914
+ if (tab === "help") {
2915
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2916
+ done();
2917
+ return;
2918
+ }
2868
2919
  if (!detail) {
2869
2920
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c"))
2870
2921
  done();
@@ -2874,6 +2925,7 @@ ${this.primaryAgent.prompt}` };
2874
2925
  selectedTask = Math.min(tasks.length - 1, selectedTask + 1);
2875
2926
  } else if (matchesKey(data, "return") && tasks.length > 0) {
2876
2927
  detail = true;
2928
+ follow = true;
2877
2929
  scroll = 0;
2878
2930
  } else
2879
2931
  return;
@@ -2885,19 +2937,33 @@ ${this.primaryAgent.prompt}` };
2885
2937
  return;
2886
2938
  if (matchesKey(data, "escape")) {
2887
2939
  detail = false;
2940
+ follow = false;
2888
2941
  scroll = 0;
2889
2942
  } else if (matchesKey(data, "ctrl+c")) {
2890
2943
  done();
2891
- } else if (matchesKey(data, "up")) {
2944
+ } else if (data.toLowerCase() === "f") {
2945
+ follow = !follow;
2946
+ } else if (!follow && matchesKey(data, "up")) {
2892
2947
  scroll = Math.max(0, scroll - 1);
2893
- } else if (matchesKey(data, "down")) {
2948
+ } else if (!follow && matchesKey(data, "down")) {
2894
2949
  scroll += 1;
2950
+ } else if (!follow && matchesKey(data, "pageUp")) {
2951
+ scroll = Math.max(0, scroll - INSPECTOR_BODY_LINES);
2952
+ } else if (!follow && matchesKey(data, "pageDown")) {
2953
+ scroll += INSPECTOR_BODY_LINES;
2954
+ } else if (!follow && matchesKey(data, "home")) {
2955
+ scroll = 0;
2956
+ } else if (!follow && matchesKey(data, "end")) {
2957
+ scroll = Number.MAX_SAFE_INTEGER;
2895
2958
  } else if (matchesKey(data, "backspace")) {
2896
2959
  const parentIndex = task.parentTaskId ? tasks.findIndex((candidate) => candidate.id === task.parentTaskId) : -1;
2897
- if (parentIndex >= 0)
2960
+ if (parentIndex >= 0) {
2898
2961
  selectedTask = parentIndex;
2899
- else
2962
+ follow = true;
2963
+ } else {
2900
2964
  detail = false;
2965
+ follow = false;
2966
+ }
2901
2967
  scroll = 0;
2902
2968
  } else
2903
2969
  return;
@@ -3661,33 +3727,26 @@ ${renderTaskResult(task.id, "error", message)}`);
3661
3727
  if (!ctx.hasUI || ctx.mode !== undefined && ctx.mode !== "tui")
3662
3728
  return;
3663
3729
  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
3730
  const active = tasks.filter((task) => task.state === "queued" || task.state === "running");
3678
- const activeRoots = new Set(active.map(rootId));
3679
- if (activeRoots.size === 0) {
3731
+ if (active.length === 0) {
3680
3732
  ctx.ui.setWidget(TASK_WIDGET, undefined);
3681
3733
  return;
3682
3734
  }
3683
- const visible = tasks.filter((task) => activeRoots.has(rootId(task)));
3735
+ const visible = activeTaskRecords(tasks);
3736
+ const byId = new Map(tasks.map((task) => [task.id, task]));
3737
+ const running = active.filter((task) => task.state === "running").length;
3738
+ const queued = active.length - running;
3739
+ const runningSummary = this.maxSubagents > 0 ? `${running}/${this.maxSubagents} running` : `${running} running`;
3740
+ const summary = queued > 0 ? `${runningSummary} · ${queued} queued` : runningSummary;
3684
3741
  ctx.ui.setWidget(TASK_WIDGET, (_tui, theme) => ({
3685
3742
  render: (width) => {
3686
- const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${active.length} active`);
3743
+ const header = theme.fg("accent", theme.bold("Subagents")) + theme.fg("dim", ` ${summary}`);
3687
3744
  const tree = taskTreeLines(visible, (task) => {
3688
3745
  const agent = theme.fg("accent", `@${task.agent}`);
3689
3746
  const description = theme.fg("text", task.description);
3690
- return `${taskState(theme, task)} ${agent} ${description}`;
3747
+ const progress = taskProgress(byId.get(task.id));
3748
+ const suffix = progress.length > 0 ? theme.fg("dim", ` · ${progress.join(" · ")}`) : "";
3749
+ return `${taskState(theme, task)} ${agent} ${description}${suffix}`;
3691
3750
  });
3692
3751
  const shown = tree.slice(0, 8);
3693
3752
  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.8",
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.8",
43
43
  "jsonc-parser": "^3.3.1",
44
44
  "minimatch": "^10.2.5",
45
45
  "yaml": "^2.9.0"