pi-landstrip 0.17.16 → 0.17.18

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 (4) hide show
  1. package/README.md +85 -82
  2. package/index.ts +209 -40
  3. package/package.json +2 -2
  4. package/subagents.ts +431 -62
package/README.md CHANGED
@@ -1,26 +1,26 @@
1
1
  # pi-landstrip
2
2
 
3
- `pi-landstrip` is an extension for [pi](https://pi.dev/) providing sandbox-aware
4
- subagents and a sandbox defined with a policy compatible with Anthropic's JSON
5
- format. It uses [`landstrip`](https://github.com/landstrip/landstrip) to
6
- implement the sandbox.
3
+ `pi-landstrip` is a [Pi](https://pi.dev/) extension that provides sandboxed Bash
4
+ execution, OpenCode-compatible primary agents, and sandbox-aware subagents. It
5
+ uses an Anthropic-compatible policy and delegates OS-level enforcement to
6
+ [`landstrip`](https://github.com/landstrip/landstrip).
7
7
 
8
- `pi-landstrip` has a shared sandbox policy [sandbox.json](./sandbox.json) and a
9
- separate [subagents.json](./subagents.json) for primary-agent and subagent
10
- configuration. Global and trusted project files can override both.
8
+ The extension includes a shared [sandbox policy](./sandbox.json) and separate
9
+ [agent configuration](./subagents.json). Global and trusted-project files can
10
+ override both.
11
11
 
12
- Pi >=0.80.6 <0.81.0 and Node.js >=22.19.0 are required for process-backed subagents.
12
+ Process-backed subagents require Pi >= 0.80.6 and < 0.81.0, and Node.js >= 22.19.0.
13
13
 
14
- ## Installing the extension
14
+ ## Installation
15
15
 
16
16
  ### Automatic install
17
17
 
18
- ```
18
+ ```sh
19
19
  pi install npm:pi-landstrip
20
20
  ```
21
21
 
22
22
  This installs `pi-landstrip` and its `@landstrip/landstrip` dependency. Native
23
- binaries are currently published for Linux x64, Windows x64, and macOS x64/arm64.
23
+ binaries are published for Linux, macOS, and Windows on x64 and Arm64.
24
24
 
25
25
  ### Manual install
26
26
 
@@ -33,13 +33,13 @@ Add the extension to `~/.pi/agent/settings.json` (global) or
33
33
  }
34
34
  ```
35
35
 
36
- Alternatively, drop the extension under `~/.pi/agent/extensions/` (global) or
37
- `.pi/extensions/` (project). See the pi
38
- [extensions](https://pi.dev/docs/latest/extensions) documentation for details.
36
+ Alternatively, place the extension under `~/.pi/agent/extensions/` (global) or
37
+ `.pi/extensions/` (project). See Pi's
38
+ [extension documentation](https://pi.dev/docs/latest/extensions) for details.
39
39
 
40
40
  On unsupported platforms the extension loads but leaves sandboxing disabled.
41
41
 
42
- ## Disable
42
+ ## Disabling
43
43
 
44
44
  Use the `--no-sandbox` flag, or set `enabled` to `false` in `sandbox.json`:
45
45
 
@@ -60,23 +60,23 @@ without a project-trust API use only global configuration.
60
60
 
61
61
  ## Behavior
62
62
 
63
- When pi asks for a sandboxed permission, the extension emits a host
64
- notification. After that the extension opens a dialog with the choices to allow
65
- once, allow for the session, persist for the project, persist globally, or
66
- reject. The dialog shows the exact path or domain being approved.
63
+ When Pi requests a sandboxed permission, the extension sends a host notification
64
+ and opens a dialog. The user can allow once, allow for the session, persist for
65
+ the project or globally, or reject. The dialog shows the exact path or domain
66
+ being approved.
67
67
 
68
68
  Project approvals are written to `.pi/sandbox.json`; global approvals are
69
69
  written to `~/.pi/agent/sandbox.json`.
70
70
 
71
- The main agent remains a normal Pi process. Pi performs its usual filesystem
72
- tool and plugin composition; `pi-landstrip` replaces Bash execution, including
73
- AI `bash` calls and manually typed shell-mode commands (`!` and `!!`), with a
74
- Landstrip-wrapped implementation. Network traffic is routed through an
75
- allowlist proxy when network access is off. Main-agent filesystem tools and
76
- plugin callbacks remain trusted Pi code and are not confined by Landstrip. The
77
- default policy for Bash and subagent processes is strict: network access is off
78
- unless domains are allowed, reads are limited to the project, `~/.gitconfig`,
79
- and `/dev/null`, and writes are limited to the project and `/dev/null`.
71
+ The main agent remains a normal Pi process. `pi-landstrip` replaces Bash
72
+ execution, including AI `bash` calls and manually typed shell commands (`!` and
73
+ `!!`), with a Landstrip-wrapped implementation. Network traffic uses an
74
+ allowlist proxy when direct access is disabled. Pi's filesystem tools and plugin
75
+ callbacks remain trusted code outside the Landstrip sandbox.
76
+
77
+ By default, Bash and subagent processes have no direct network access. Reads are
78
+ limited to the project, Git configuration, and `/dev/null`; writes are limited
79
+ to the project and `/dev/null`.
80
80
 
81
81
  Subagent startup adds read-only bootstrap access to the selected Pi runtime,
82
82
  global settings, model/auth configuration, installed plugins, skills, and the
@@ -84,32 +84,30 @@ task's dedicated session directory. These paths are required to construct a
84
84
  normal Pi worker and are not persisted into `sandbox.json`. The worker receives
85
85
  write access only to its own session and temporary directories.
86
86
 
87
- Use `/sandbox` inside Pi to inspect the active policy and toggle sandboxing. Use
88
- `/agents` to select the primary role; its Settings tab controls the global and
89
- trusted-project maximum number of concurrent subagents.
87
+ Use `/sandbox` to inspect the active policy and toggle sandboxing. Use `/agents`
88
+ to select the primary role, inspect worker configuration and status, and set the
89
+ global or trusted-project concurrency limit.
90
90
 
91
91
  ## Primary agents
92
92
 
93
- The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by default.
94
- Build has normal development access; plan asks before shell commands and file
95
- changes. The selection controls the root system prompt and permissions and is
96
- restored with the session.
93
+ The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by
94
+ default. Build has normal development access; plan asks before shell commands
95
+ and file changes. The selection controls the root system prompt and permissions
96
+ and is restored with the session.
97
97
 
98
98
  ## Subagents
99
99
 
100
- Landstrip provides an OpenCode-compatible `task` tool. Each active task is a
101
- full `pi --mode rpc` process under one outer Landstrip instance. The sandbox
102
- covers Pi itself, its plugins and tools, model requests, and every descendant
103
- process. The root Pi supervises RPC, permissions, nesting, persistence, and
104
- completion delivery.
100
+ Landstrip provides an OpenCode-compatible `task` tool. Each active task runs as a
101
+ full `pi --mode rpc` process inside an outer Landstrip sandbox. The sandbox
102
+ covers Pi, its plugins and tools, model requests, and descendant processes. The
103
+ root Pi process supervises RPC, permissions, nesting, persistence, and result
104
+ delivery.
105
105
 
106
- The worker uses normal Pi resource discovery and plugin loading from the Pi
107
- filesystem, plus the pi-landstrip worker extension. Its requested active tool
108
- names come from the parent, but their implementations are composed normally in
109
- the worker: plugins can add or replace tools, including replacements such as
110
- `pi-readseek`. Every task run starts a fresh Pi process and fresh plugin
111
- instances. Continuing a `task_id` restores its persisted Pi session in a new
112
- worker process.
106
+ Workers use normal Pi resource discovery and plugin loading, plus the
107
+ `pi-landstrip` worker extension. Requested tools are composed inside the worker,
108
+ so plugins can add or replace implementations. Each task starts a fresh Pi
109
+ process and plugin instances; continuing a `task_id` restores its persisted Pi
110
+ session in a new process.
113
111
 
114
112
  The tool accepts the OpenCode task fields:
115
113
 
@@ -125,21 +123,24 @@ The tool accepts the OpenCode task fields:
125
123
  ```
126
124
 
127
125
  Foreground tasks return the child result directly. Background tasks return a
128
- running result while the worker continues, then deliver completion to the
129
- parent automatically. The process exits when the run settles; its Pi session
130
- remains on disk and can be continued with `task_id`. While tasks are active, Pi
131
- shows their parent-child tree automatically above the editor.
126
+ queued result and deliver completion automatically. Task rows show lifecycle
127
+ state, current activity, tool-call count, elapsed time, and expandable output.
128
+ Use `/subagents` or the task row's `/subagents <id>` command to inspect a child
129
+ transcript. Completed and failed task metadata remains available after reload,
130
+ and persisted sessions can be continued with `task_id`.
131
+
132
132
  Session switching or shutdown stops live workers. After an unclean restart,
133
- unfinished work is marked interrupted rather than silently rerun; a completed
134
- but undelivered background result is delivered when the root session resumes.
133
+ unfinished work is marked interrupted; completed but undelivered background
134
+ results are delivered when the root session resumes.
135
135
 
136
- OpenCode-style worker permissions are enforced around the worker's composed
137
- tools: `deny` blocks a call, `ask` suspends that worker and asks in the root UI,
138
- and `allow` lets the tool run. Other workers may continue while a prompt is
139
- open. An "Allow for this session" worker-permission decision applies to the
140
- root session and its descendants. These permissions are separate from the
141
- outer Landstrip policy and cannot widen it. Sandbox approvals continue to use
142
- `.pi/sandbox.json` and `~/.pi/agent/sandbox.json`.
136
+ OpenCode-style permissions wrap each worker's composed tools: `deny` blocks,
137
+ `ask` prompts in the root UI, and `allow` runs the tool. Other workers may
138
+ continue while a prompt is open. Forwarded worker dialogs identify the agent,
139
+ task summary, and task ID. An "Allow for this session" decision applies to the
140
+ root session and its descendants.
141
+
142
+ Tool permissions cannot widen the outer Landstrip policy. Sandbox approvals
143
+ continue to use `.pi/sandbox.json` and `~/.pi/agent/sandbox.json`.
143
144
 
144
145
  Subagents fail closed when sandboxing is expected but unavailable, unsupported,
145
146
  or missing. An explicit `--no-sandbox` flag or `enabled: false` configuration is
@@ -172,13 +173,14 @@ first.
172
173
 
173
174
  ## Configuration
174
175
 
175
- Subagent configuration is read from `~/.pi/agent/subagents.json` and, for
176
- trusted projects, `.pi/subagents.json`. Project values override global values;
177
- both are merged over the packaged [default](./subagents.json). Sandbox policy
178
- continues to use `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi's
179
- `settings.json` contains only normal Pi settings such as the `pi-landstrip`
180
- package entry. `subagents.json` accepts only top-level `maxSubagents` and
181
- `subagents`; sandbox fields belong only in `sandbox.json`.
176
+ Subagent configuration is read from `~/.pi/agent/subagents.json` and, for trusted
177
+ projects, `.pi/subagents.json`. Project values override global values; both are
178
+ merged over the packaged [defaults](./subagents.json). Sandbox policy remains in
179
+ `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`. Pi's `settings.json` contains
180
+ only normal Pi settings, such as the `pi-landstrip` package entry.
181
+
182
+ `subagents.json` accepts only top-level `maxSubagents` and `subagents`; sandbox
183
+ fields belong in `sandbox.json`.
182
184
 
183
185
  ```json
184
186
  {
@@ -205,10 +207,10 @@ package entry. `subagents.json` accepts only top-level `maxSubagents` and
205
207
  }
206
208
  ```
207
209
 
208
- `maxSubagents` is an integer from 0 through 16 controlling concurrent worker
209
- processes. Setting it to zero removes the `task` tool while retaining the primary
210
- roles. There is no separate subagent enable switch. The Settings tab in `/agents`
211
- edits this limit separately for global and trusted-project configuration.
210
+ `maxSubagents` is an integer from 0 through 16 controlling concurrent workers.
211
+ The packaged default is 0, which removes the `task` tool while retaining primary
212
+ roles. There is no separate subagent enable switch. The Settings tab in
213
+ `/agents` edits this limit for global and trusted-project configuration.
212
214
 
213
215
  Agent modes, hidden/disabled agents, prompts, and ordered `allow`/`ask`/`deny`
214
216
  permissions apply to primary agents and subagents. Subagent workers also honor
@@ -218,7 +220,7 @@ matching permission rules win. Put provider-specific values under `options`;
218
220
  unknown agent fields are rejected. Agent permissions cannot weaken an enabled
219
221
  OS sandbox, grant filesystem access, or grant network access.
220
222
 
221
- ## Migrating configuration
223
+ ## Configuration migration
222
224
 
223
225
  There is no compatibility loader for previous subagent configuration sources.
224
226
  Legacy keys in `settings.json` produce a migration warning. Move configuration as
@@ -234,22 +236,23 @@ follows, then remove the old fields:
234
236
  - Leave sandbox policy in `~/.pi/agent/sandbox.json` and `.pi/sandbox.json`;
235
237
  those files are unchanged.
236
238
 
237
- Do not put `subagents`, `agent`, `permission`, or `maxSubagents` in Pi's
238
- `settings.json`; Pi-Landstrip does not read them there.
239
+ Do not put these fields in Pi's `settings.json`; `pi-landstrip` does not read
240
+ them there.
239
241
 
240
242
  ## Limits
241
243
 
242
- Pi-Landstrip runs at most `maxSubagents` worker processes concurrently and
243
- allows nesting to three levels. A foreground parent hands its scheduler permit to a child
244
- while waiting, preventing nested-task deadlocks. A worker receives a nested
245
- `task` tool only when its agent has an explicit `task` permission. Nested tasks
246
- are separate Pi processes supervised by the root and Landstrip-wrapped unless
247
- sandboxing is explicitly disabled. Persisted sessions remain resumable by `task_id`.
244
+ `pi-landstrip` grants at most `maxSubagents` scheduler permits to active
245
+ subagent work and allows nesting to three levels. A foreground parent returns
246
+ its permit while waiting for a child and reacquires it before resuming,
247
+ preventing nested-task deadlocks. A worker receives a nested `task` tool only
248
+ when its agent has an explicit `task` permission. Nested tasks are separate Pi
249
+ processes supervised by the root and Landstrip-wrapped unless sandboxing is
250
+ explicitly disabled, so an inactive parent process can remain alive during the
251
+ handoff. Persisted sessions remain resumable by `task_id`.
248
252
 
249
253
  ## License
250
254
 
251
- `pi-landstrip` is licensed under `Apache-2.0`. See [LICENSE](LICENSE) for more
252
- information.
255
+ `pi-landstrip` is licensed under `Apache-2.0`. See [LICENSE](LICENSE).
253
256
 
254
257
  The bundled `@landstrip/landstrip` package is licensed separately as
255
258
  `Apache-2.0 AND LGPL-2.1-or-later`.
package/index.ts CHANGED
@@ -60,7 +60,7 @@ import {
60
60
  registerSubagentWorker,
61
61
  workerConfigFromEnvironment,
62
62
  } from './subagents.ts';
63
- import { availablePrimaryAgents } from './agents.ts';
63
+ import { availablePrimaryAgents, availableSubagents } from './agents.ts';
64
64
  import { loadLandstripConfig, MAX_SUBAGENTS, setMaxSubagentsConfigForScope } from './config.ts';
65
65
 
66
66
  interface SandboxFilesystemConfig {
@@ -2366,41 +2366,116 @@ export function createLandstripIntegration(
2366
2366
  if (!ctx.hasUI) return;
2367
2367
  const config = loadConfig(ctx.cwd);
2368
2368
  const { globalPath, projectPath } = getConfigPaths(ctx.cwd);
2369
- const status = noSandboxFlag
2370
- ? 'disabled by --no-sandbox'
2371
- : config.enabled && sandboxEnabled && sandboxReady
2372
- ? 'active'
2373
- : config.enabled
2374
- ? 'inactive'
2375
- : 'disabled by config';
2376
- const list = (values: string[]): string => values.join(', ') || 'none';
2377
- const summary = [
2378
- `Status: ${status}`,
2379
- `landstrip package binary: ${landstripDisplayPath()}`,
2380
- '',
2381
- 'Config files',
2382
- `${projectPath} ${existsSync(projectPath) ? '(found)' : '(missing)'}`,
2383
- `${globalPath} ${existsSync(globalPath) ? '(found)' : '(missing)'}`,
2384
- '',
2385
- `Network: ${config.network.allowNetwork ? 'unrestricted' : 'proxied'}`,
2386
- `allow network: ${config.network.allowNetwork ? 'yes' : 'no'}`,
2387
- `allowed: ${list(config.network.allowedDomains)}`,
2388
- `denied: ${list(config.network.deniedDomains)}`,
2389
- `unix sockets: ${config.network.allowAllUnixSockets ? 'all' : list(config.network.allowUnixSockets)}`,
2390
- '',
2391
- 'Filesystem',
2392
- `deny read: ${list(config.filesystem.denyRead)}`,
2393
- `allow read: ${list(config.filesystem.allowRead)}`,
2394
- `allow write: ${list(config.filesystem.allowWrite)}`,
2395
- `deny write: ${list(config.filesystem.denyWrite)}`,
2396
- ].join('\n');
2397
- const enabled = !config.enabled;
2398
- const confirmed = await ctx.ui.confirm(
2399
- 'Sandbox',
2400
- `${summary}\n\n${enabled ? 'Enable' : 'Disable'} the sandbox?`,
2369
+ const shouldToggle = await ctx.ui.custom<boolean>(
2370
+ (_tui, theme, _kb, done) => {
2371
+ const dim = (value: string) => theme.fg('dim', value);
2372
+ const muted = (value: string) => theme.fg('muted', value);
2373
+ const accent = (value: string) => theme.fg('accent', value);
2374
+ const text = (value: string) => theme.fg('text', value);
2375
+
2376
+ function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
2377
+ if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
2378
+ if (!config.enabled) return { color: 'warning', label: 'Disabled' };
2379
+ if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
2380
+ return { color: 'success', label: 'Active' };
2381
+ }
2382
+
2383
+ function boolValue(value: boolean): string {
2384
+ return value ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
2385
+ }
2386
+
2387
+ return {
2388
+ render(width: number): string[] {
2389
+ const innerWidth = Math.max(1, width - 4);
2390
+ const row = (content = '') => boxRow(theme, width, content);
2391
+ const lines = [boxTop(theme, width, 'Sandbox')];
2392
+ const status = sandboxStatus();
2393
+ const statusDot = theme.fg(status.color, '●');
2394
+ const pathSnippet = text(
2395
+ truncateToWidth(landstripDisplayPath(), Math.max(20, innerWidth - 28)),
2396
+ );
2397
+ const section = (title: string, detail?: string): void => {
2398
+ lines.push(row(''));
2399
+ lines.push(row(`${accent(title)}${detail ? dim(` · ${detail}`) : ''}`));
2400
+ };
2401
+ const item = (label: string, value: string): void => {
2402
+ lines.push(row(` ${dim('•')} ${muted(label.padEnd(13))} ${value}`));
2403
+ };
2404
+ const listValue = (values: string[]): string => {
2405
+ const value = values.join(', ') || 'none';
2406
+ return text(truncateToWidth(value, Math.max(10, innerWidth - 17)));
2407
+ };
2408
+
2409
+ lines.push(
2410
+ row(
2411
+ `${statusDot} ${text(status.label)} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
2412
+ ),
2413
+ );
2414
+
2415
+ section('Config');
2416
+ item('project', text(projectPath));
2417
+ item('global', text(globalPath));
2418
+
2419
+ const networkMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
2420
+ section('Network', networkMode);
2421
+ item('allow network', boolValue(config.network.allowNetwork));
2422
+ item('allowed', listValue(config.network.allowedDomains));
2423
+ item('denied', listValue(config.network.deniedDomains));
2424
+ item(
2425
+ 'unix sockets',
2426
+ config.network.allowAllUnixSockets
2427
+ ? text('all')
2428
+ : listValue(config.network.allowUnixSockets),
2429
+ );
2430
+ if (sessionAllowedDomains.length > 0) {
2431
+ item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
2432
+ }
2433
+
2434
+ section('Filesystem');
2435
+ item('deny read', listValue(config.filesystem.denyRead));
2436
+ item('allow read', listValue(config.filesystem.allowRead));
2437
+ item('allow write', listValue(config.filesystem.allowWrite));
2438
+ item('deny write', listValue(config.filesystem.denyWrite));
2439
+
2440
+ if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
2441
+ section('Session grants');
2442
+ if (sessionAllowedReadPaths.length > 0) {
2443
+ item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
2444
+ }
2445
+ if (sessionAllowedWritePaths.length > 0) {
2446
+ item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
2447
+ }
2448
+ }
2449
+
2450
+ lines.push(
2451
+ row(''),
2452
+ row(
2453
+ `${dim('enter')} ${muted(config.enabled ? 'disable' : 'enable')} ${dim('esc')} ${muted('close')}`,
2454
+ ),
2455
+ boxBottom(theme, width),
2456
+ );
2457
+ return lines;
2458
+ },
2459
+
2460
+ handleInput(data: string): void {
2461
+ if (matchesKey(data, 'return')) {
2462
+ done(true);
2463
+ return;
2464
+ }
2465
+ if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) done(false);
2466
+ },
2467
+
2468
+ invalidate(): void {},
2469
+ };
2470
+ },
2471
+ {
2472
+ overlay: true,
2473
+ overlayOptions: { anchor: 'center', width: 78, margin: 2 },
2474
+ },
2401
2475
  );
2402
- if (!confirmed) return;
2476
+ if (!shouldToggle) return;
2403
2477
 
2478
+ const enabled = !config.enabled;
2404
2479
  try {
2405
2480
  const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectConfigTrusted);
2406
2481
  if (!enabled) {
@@ -2425,7 +2500,7 @@ export function createLandstripIntegration(
2425
2500
  if (!ctx.hasUI) return;
2426
2501
  await ctx.ui.custom(
2427
2502
  (tui, theme, _kb, done) => {
2428
- let tab: 'agents' | 'settings' = 'agents';
2503
+ let tab: 'agents' | 'subagents' | 'settings' = 'agents';
2429
2504
  const initialAgents = runtime
2430
2505
  ? availablePrimaryAgents(runtime.getAgentCatalog(ctx))
2431
2506
  : [];
@@ -2433,12 +2508,22 @@ export function createLandstripIntegration(
2433
2508
  0,
2434
2509
  initialAgents.findIndex((agent) => agent.name === runtime?.getPrimaryAgent()?.name),
2435
2510
  );
2511
+ let selectedSubagent = 0;
2436
2512
  let selectedSetting = 0;
2437
2513
  let values = [
2438
2514
  loadLandstripConfig(ctx.cwd, false).maxSubagents,
2439
2515
  loadLandstripConfig(ctx.cwd, projectConfigTrusted).maxSubagents,
2440
2516
  ];
2441
2517
  let editing = false;
2518
+ const supportedVariants = new Set([
2519
+ 'off',
2520
+ 'minimal',
2521
+ 'low',
2522
+ 'medium',
2523
+ 'high',
2524
+ 'xhigh',
2525
+ 'max',
2526
+ ]);
2442
2527
  const dim = (value: string) => theme.fg('dim', value);
2443
2528
  const muted = (value: string) => theme.fg('muted', value);
2444
2529
  const accent = (value: string) => theme.fg('accent', value);
@@ -2450,7 +2535,7 @@ export function createLandstripIntegration(
2450
2535
  const lines = [boxTop(theme, width, 'Agents')];
2451
2536
  lines.push(
2452
2537
  row(
2453
- ` ${tab === 'agents' ? accent('Agents') : muted('Agents')} ${dim('│')} ${tab === 'settings' ? accent('Settings') : muted('Settings')}`,
2538
+ ` ${tab === 'agents' ? accent('Agents') : muted('Agents')} ${dim('│')} ${tab === 'subagents' ? accent('Subagents') : muted('Subagents')} ${dim('│')} ${tab === 'settings' ? accent('Settings') : muted('Settings')}`,
2454
2539
  ),
2455
2540
  row(''),
2456
2541
  );
@@ -2466,8 +2551,73 @@ export function createLandstripIntegration(
2466
2551
  const cursor = selected ? accent('›') : ' ';
2467
2552
  const active = agent.name === activeAgent ? theme.fg('success', '●') : dim('○');
2468
2553
  const name = selected ? accent(agent.name) : text(agent.name);
2554
+ const model = dim(` ${agent.model ?? 'current model'}`);
2469
2555
  const description = agent.description ? dim(` ${agent.description}`) : '';
2470
- lines.push(row(`${cursor} ${active} ${name}${description}`));
2556
+ lines.push(row(`${cursor} ${active} ${name}${model}${description}`));
2557
+ }
2558
+ } else if (tab === 'subagents') {
2559
+ const catalog = runtime?.getAgentCatalog(ctx);
2560
+ const agents = catalog ? availableSubagents(catalog) : [];
2561
+ selectedSubagent = Math.min(selectedSubagent, Math.max(0, agents.length - 1));
2562
+ if (agents.length === 0) lines.push(row(muted('No subagents configured')));
2563
+ const start = Math.max(0, Math.min(selectedSubagent - 3, agents.length - 7));
2564
+ for (const [offset, agent] of agents.slice(start, start + 7).entries()) {
2565
+ const index = start + offset;
2566
+ const selected = index === selectedSubagent;
2567
+ const cursor = selected ? accent('›') : ' ';
2568
+ const name = selected ? accent(`@${agent.name}`) : text(`@${agent.name}`);
2569
+ const flags = [agent.model ?? 'current model', agent.mode];
2570
+ if (agent.hidden) flags.push('hidden');
2571
+ lines.push(row(`${cursor} ${name} ${dim(flags.join(' · '))}`));
2572
+ }
2573
+
2574
+ const agent = agents[selectedSubagent];
2575
+ if (agent) {
2576
+ lines.push(row(''));
2577
+ const details = [
2578
+ `model ${agent.model ?? 'current model'}`,
2579
+ agent.variant ? `variant ${agent.variant}` : undefined,
2580
+ agent.steps ? `${agent.steps} steps` : undefined,
2581
+ ].filter(Boolean);
2582
+ lines.push(row(` ${text(details.join(' · '))}`));
2583
+ const permissions = [...(catalog?.permissions ?? []), ...agent.permissions];
2584
+ lines.push(row(` ${dim('Permissions')}`));
2585
+ if (permissions.length === 0) lines.push(row(` ${muted('default: ask')}`));
2586
+ for (const rule of permissions.slice(0, 4)) {
2587
+ lines.push(
2588
+ row(
2589
+ ` ${text(`${rule.permission}:${rule.pattern}`)} ${dim('→')} ${theme.fg(
2590
+ rule.action === 'deny'
2591
+ ? 'error'
2592
+ : rule.action === 'allow'
2593
+ ? 'success'
2594
+ : 'warning',
2595
+ rule.action,
2596
+ )}`,
2597
+ ),
2598
+ );
2599
+ }
2600
+ if (permissions.length > 4) {
2601
+ lines.push(row(` ${muted(`… ${permissions.length - 4} more`)}`));
2602
+ }
2603
+ const unsupported = Object.keys(agent.providerOptions);
2604
+ if (agent.variant && !supportedVariants.has(agent.variant)) {
2605
+ unsupported.push(`variant=${agent.variant}`);
2606
+ }
2607
+ const unsupportedText =
2608
+ unsupported.length > 0 ? unsupported.join(', ') : 'none';
2609
+ lines.push(
2610
+ row(` ${dim('Unsupported RPC options:')} ${text(unsupportedText)}`),
2611
+ );
2612
+ }
2613
+ if ((catalog?.diagnostics.length ?? 0) > 0) {
2614
+ lines.push(row(` ${theme.fg('error', 'Catalog diagnostics')}`));
2615
+ for (const diagnostic of catalog?.diagnostics.slice(0, 3) ?? []) {
2616
+ lines.push(row(` ${theme.fg('error', diagnostic)}`));
2617
+ }
2618
+ }
2619
+ for (const warning of catalog?.warnings.slice(0, 2) ?? []) {
2620
+ lines.push(row(` ${theme.fg('warning', warning)}`));
2471
2621
  }
2472
2622
  } else {
2473
2623
  const scopes = ['Global', 'Project'] as const;
@@ -2492,8 +2642,10 @@ export function createLandstripIntegration(
2492
2642
  row(''),
2493
2643
  row(
2494
2644
  tab === 'agents'
2495
- ? `${dim('↑↓')} ${muted('select')} ${dim('enter')} ${muted('activate')} ${dim('tab')} ${muted('settings')} ${dim('esc')} ${muted('close')}`
2496
- : `${dim('↑↓')} ${muted('scope')} ${dim('0-9/+/-')} ${muted('change')} ${dim('enter')} ${muted('save')} ${dim('tab')} ${muted('agents')} ${dim('esc')} ${muted('close')}`,
2645
+ ? `${dim('↑↓')} ${muted('select')} ${dim('enter')} ${muted('activate')} ${dim('tab')} ${muted('subagents')} ${dim('esc')} ${muted('close')}`
2646
+ : tab === 'subagents'
2647
+ ? `${dim('↑↓')} ${muted('inspect')} ${dim('tab')} ${muted('settings')} ${dim('esc')} ${muted('close')}`
2648
+ : `${dim('↑↓')} ${muted('scope')} ${dim('0-9/+/-')} ${muted('change')} ${dim('enter')} ${muted('save')} ${dim('tab')} ${muted('agents')} ${dim('esc')} ${muted('close')}`,
2497
2649
  ),
2498
2650
  boxBottom(theme, width),
2499
2651
  );
@@ -2510,7 +2662,10 @@ export function createLandstripIntegration(
2510
2662
  matchesKey(data, 'left') ||
2511
2663
  matchesKey(data, 'right')
2512
2664
  ) {
2513
- tab = tab === 'agents' ? 'settings' : 'agents';
2665
+ const tabs = ['agents', 'subagents', 'settings'] as const;
2666
+ const current = tabs.indexOf(tab);
2667
+ const offset = matchesKey(data, 'left') ? -1 : 1;
2668
+ tab = tabs[(current + offset + tabs.length) % tabs.length] ?? 'agents';
2514
2669
  editing = false;
2515
2670
  tui.requestRender();
2516
2671
  return;
@@ -2528,6 +2683,20 @@ export function createLandstripIntegration(
2528
2683
  tui.requestRender();
2529
2684
  return;
2530
2685
  }
2686
+ if (tab === 'subagents') {
2687
+ const catalog = runtime?.getAgentCatalog(ctx);
2688
+ const agents = catalog ? availableSubagents(catalog) : [];
2689
+ if (matchesKey(data, 'up')) {
2690
+ selectedSubagent = Math.max(0, selectedSubagent - 1);
2691
+ } else if (matchesKey(data, 'down')) {
2692
+ selectedSubagent = Math.min(
2693
+ Math.max(0, agents.length - 1),
2694
+ selectedSubagent + 1,
2695
+ );
2696
+ } else return;
2697
+ tui.requestRender();
2698
+ return;
2699
+ }
2531
2700
 
2532
2701
  if (matchesKey(data, 'up')) {
2533
2702
  selectedSetting = Math.max(0, selectedSetting - 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.17.16",
3
+ "version": "0.17.18",
4
4
  "description": "Sandbox-aware Pi subagents and interactive command permissions",
5
5
  "keywords": [
6
6
  "landstrip",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@earendil-works/pi-tui": "^0.80.6",
41
- "@landstrip/landstrip": "^0.17.16",
41
+ "@landstrip/landstrip": "^0.17.18",
42
42
  "minimatch": "^10.2.5"
43
43
  },
44
44
  "devDependencies": {