pi-landstrip 0.17.13 → 0.17.14

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 +8 -9
  2. package/config.ts +17 -9
  3. package/index.ts +158 -231
  4. package/package.json +2 -2
package/README.md CHANGED
@@ -54,7 +54,7 @@ processes, but without the outer Landstrip OS sandbox. The extension warns once
54
54
  per session. Agent tool permissions still apply, but they are not an OS isolation
55
55
  boundary.
56
56
 
57
- Trusted project config overrides global config. `/landstrip` updates a trusted
57
+ Trusted project config overrides global config. `/sandbox` updates a trusted
58
58
  project sandbox file when present, otherwise the global file. Pi versions
59
59
  without a project-trust API use only global configuration.
60
60
 
@@ -84,13 +84,13 @@ 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 `/landstrip` inside Pi. Its Sandbox tab shows the active policy, its Agents
88
- tab selects the primary role, and its Settings tab controls sandboxing and the
89
- maximum number of concurrent subagents.
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.
90
90
 
91
91
  ## Primary agents
92
92
 
93
- The Agents tab provides OpenCode-compatible `build` and `plan` roles by default.
93
+ The `/agents` selector provides OpenCode-compatible `build` and `plan` roles by default.
94
94
  Build has normal development access; plan asks before shell commands and file
95
95
  changes. The selection controls the root system prompt and permissions and is
96
96
  restored with the session.
@@ -206,10 +206,9 @@ package entry. `subagents.json` accepts only top-level `maxSubagents` and
206
206
  ```
207
207
 
208
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 Agents
210
- tab and primary roles. There is no separate subagent enable switch. The Settings
211
- tab in `/landstrip` edits this limit and the persisted sandbox enable checkbox;
212
- the Sandbox tab only reports effective policy and status.
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.
213
212
 
214
213
  Agent modes, hidden/disabled agents, prompts, and ordered `allow`/`ask`/`deny`
215
214
  permissions apply to primary agents and subagents. Subagent workers also honor
package/config.ts CHANGED
@@ -76,21 +76,29 @@ export async function setMaxSubagentsConfig(
76
76
  includeProject = true,
77
77
  agentDir = getAgentDir(),
78
78
  ): Promise<'global' | 'project'> {
79
+ const { projectPath } = getLandstripConfigPaths(cwd, agentDir);
80
+ const scope = includeProject && existsSync(projectPath) ? 'project' : 'global';
81
+ await setMaxSubagentsConfigForScope(cwd, maxSubagents, scope, agentDir);
82
+ return scope;
83
+ }
84
+
85
+ export async function setMaxSubagentsConfigForScope(
86
+ cwd: string,
87
+ maxSubagents: number,
88
+ scope: 'global' | 'project',
89
+ agentDir = getAgentDir(),
90
+ ): Promise<void> {
79
91
  if (!Number.isInteger(maxSubagents) || maxSubagents < 0 || maxSubagents > MAX_SUBAGENTS) {
80
92
  throw new Error(`maxSubagents must be an integer from 0 to ${MAX_SUBAGENTS}`);
81
93
  }
82
94
  const { globalPath, projectPath } = getLandstripConfigPaths(cwd, agentDir);
83
- const target =
84
- includeProject && existsSync(projectPath)
85
- ? { scope: 'project' as const, path: projectPath }
86
- : { scope: 'global' as const, path: globalPath };
87
- await withFileMutationQueue(target.path, async () => {
88
- const config = readConfig(target.path);
95
+ const path = scope === 'project' ? projectPath : globalPath;
96
+ await withFileMutationQueue(path, async () => {
97
+ const config = readConfig(path);
89
98
  config.maxSubagents = maxSubagents;
90
- mkdirSync(dirname(target.path), { recursive: true });
91
- writeFileSync(target.path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
99
+ mkdirSync(dirname(path), { recursive: true });
100
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
92
101
  });
93
- return target.scope;
94
102
  }
95
103
 
96
104
  export function loadLandstripConfig(
package/index.ts CHANGED
@@ -61,7 +61,7 @@ import {
61
61
  workerConfigFromEnvironment,
62
62
  } from './subagents.ts';
63
63
  import { availablePrimaryAgents } from './agents.ts';
64
- import { MAX_SUBAGENTS, setMaxSubagentsConfig } from './config.ts';
64
+ import { loadLandstripConfig, MAX_SUBAGENTS, setMaxSubagentsConfigForScope } from './config.ts';
65
65
 
66
66
  interface SandboxFilesystemConfig {
67
67
  denyRead: string[];
@@ -814,10 +814,11 @@ function promptWriteBlock(
814
814
  );
815
815
  }
816
816
 
817
- // The broker knows only address:port, and no sandbox field can express a
818
- // grant for one: allowedDomains is a hostname list enforced by the proxy, and the
819
- // landstrip network policy is all-or-nothing (allowNetwork, allowLocalBinding).
820
- // So a connection is granted for the session or not at all.
817
+ // The broker knows only address:port, and no sandbox field can express a grant
818
+ // for one non-loopback endpoint: allowedDomains is enforced by the proxy,
819
+ // allowLocalBinding grants all loopback endpoints, and allowNetwork disables
820
+ // network enforcement. So a non-loopback connection is granted for the session
821
+ // or not at all.
821
822
  function promptNetworkBlock(
822
823
  ctx: ExtensionContext,
823
824
  operation: string,
@@ -2359,63 +2360,98 @@ export function createLandstripIntegration(
2359
2360
 
2360
2361
  enableSandbox(ctx);
2361
2362
  });
2362
- const landstripCommand: Parameters<ExtensionAPI['registerCommand']>[1] = {
2363
- description: 'Show Landstrip configuration',
2363
+ maybePi.registerCommand?.('sandbox', {
2364
+ description: 'Show config and toggle the sandbox',
2364
2365
  handler: async (_args, ctx) => {
2365
- let config = loadConfig(ctx.cwd);
2366
-
2366
+ if (!ctx.hasUI) return;
2367
+ const config = loadConfig(ctx.cwd);
2367
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?`,
2401
+ );
2402
+ if (!confirmed) return;
2368
2403
 
2404
+ try {
2405
+ const scope = await setSandboxConfigEnabled(ctx.cwd, enabled, projectConfigTrusted);
2406
+ if (!enabled) {
2407
+ disableSandbox(ctx);
2408
+ } else if (!noSandboxFlag) {
2409
+ enableSandbox(ctx);
2410
+ }
2411
+ if (enabled && noSandboxFlag) {
2412
+ notify(ctx, 'Sandbox remains disabled via --no-sandbox', 'warning');
2413
+ } else {
2414
+ notify(ctx, `Sandbox ${enabled ? 'enabled' : 'disabled'} in ${scope} config`, 'info');
2415
+ }
2416
+ } catch (error) {
2417
+ notify(ctx, `Could not update config: ${error}`, 'error');
2418
+ }
2419
+ },
2420
+ });
2421
+
2422
+ maybePi.registerCommand?.('agents', {
2423
+ description: 'Select the primary agent and configure subagents',
2424
+ handler: async (_args, ctx) => {
2369
2425
  if (!ctx.hasUI) return;
2370
2426
  await ctx.ui.custom(
2371
2427
  (tui, theme, _kb, done) => {
2372
- let tab: 'sandbox' | 'agents' | 'settings' = 'sandbox';
2373
- let selectedAgent = 0;
2428
+ let tab: 'agents' | 'settings' = 'agents';
2429
+ const initialAgents = runtime
2430
+ ? availablePrimaryAgents(runtime.getAgentCatalog(ctx))
2431
+ : [];
2432
+ let selectedAgent = Math.max(
2433
+ 0,
2434
+ initialAgents.findIndex((agent) => agent.name === runtime?.getPrimaryAgent()?.name),
2435
+ );
2374
2436
  let selectedSetting = 0;
2375
- let maxSubagentsValue = runtime?.getMaxSubagents() ?? 0;
2376
- let editingMaxSubagents = false;
2377
- const dim = (s: string) => theme.fg('dim', s);
2378
- const muted = (s: string) => theme.fg('muted', s);
2379
- const accent = (s: string) => theme.fg('accent', s);
2380
- const text = (s: string) => theme.fg('text', s);
2381
-
2382
- function sandboxStatus(): { color: 'success' | 'warning'; label: string } {
2383
- if (noSandboxFlag) return { color: 'warning', label: 'Disabled (--no-sandbox)' };
2384
- if (!config.enabled) return { color: 'warning', label: 'Disabled' };
2385
- if (!sandboxEnabled || !sandboxReady) return { color: 'warning', label: 'Inactive' };
2386
- return { color: 'success', label: 'Active' };
2387
- }
2388
-
2389
- function boolVal(v: boolean): string {
2390
- return v ? theme.fg('warning', 'yes') : theme.fg('success', 'no');
2391
- }
2437
+ let values = [
2438
+ loadLandstripConfig(ctx.cwd, false).maxSubagents,
2439
+ loadLandstripConfig(ctx.cwd, projectConfigTrusted).maxSubagents,
2440
+ ];
2441
+ let editing = false;
2442
+ const dim = (value: string) => theme.fg('dim', value);
2443
+ const muted = (value: string) => theme.fg('muted', value);
2444
+ const accent = (value: string) => theme.fg('accent', value);
2445
+ const text = (value: string) => theme.fg('text', value);
2392
2446
 
2393
2447
  return {
2394
2448
  render(width: number): string[] {
2395
- const innerW = Math.max(1, width - 4);
2396
2449
  const row = (content = '') => boxRow(theme, width, content);
2397
- const lines: string[] = [];
2398
- const status = sandboxStatus();
2399
- function section(titleText: string, detail?: string): void {
2400
- lines.push(row(''));
2401
- lines.push(row(`${accent(titleText)}${detail ? dim(` · ${detail}`) : ''}`));
2402
- }
2403
-
2404
- function item(label: string, value: string): void {
2405
- lines.push(row(` ${dim('•')} ${muted(label.padEnd(13))} ${value}`));
2406
- }
2407
-
2408
- function listValue(values: string[], maxWidth: number): string {
2409
- const value = values.join(', ') || 'none';
2410
- return text(truncateToWidth(value, Math.max(10, maxWidth)));
2411
- }
2412
-
2413
- lines.push(boxTop(theme, width, 'Landstrip'));
2414
- const sandboxTab = tab === 'sandbox' ? accent('Sandbox') : muted('Sandbox');
2415
- const agentsTab = tab === 'agents' ? accent('Agents') : muted('Agents');
2416
- const settingsTab = tab === 'settings' ? accent('Settings') : muted('Settings');
2450
+ const lines = [boxTop(theme, width, 'Agents')];
2417
2451
  lines.push(
2418
- row(` ${sandboxTab} ${dim('│')} ${agentsTab} ${dim('│')} ${settingsTab}`),
2452
+ row(
2453
+ ` ${tab === 'agents' ? accent('Agents') : muted('Agents')} ${dim('│')} ${tab === 'settings' ? accent('Settings') : muted('Settings')}`,
2454
+ ),
2419
2455
  row(''),
2420
2456
  );
2421
2457
 
@@ -2423,101 +2459,44 @@ export function createLandstripIntegration(
2423
2459
  const catalog = runtime?.getAgentCatalog(ctx);
2424
2460
  const agents = catalog ? availablePrimaryAgents(catalog) : [];
2425
2461
  const activeAgent = runtime?.getPrimaryAgent()?.name;
2426
- if (agents.length === 0) {
2427
- lines.push(row(muted('No primary agents configured')));
2428
- } else {
2429
- selectedAgent = Math.min(selectedAgent, agents.length - 1);
2430
- for (const [index, agent] of agents.entries()) {
2431
- const cursor = index === selectedAgent ? accent('›') : ' ';
2432
- const active =
2433
- agent.name === activeAgent ? theme.fg('success', '●') : dim('○');
2434
- const name = index === selectedAgent ? accent(agent.name) : text(agent.name);
2435
- const description = agent.description ? dim(` · ${agent.description}`) : '';
2436
- lines.push(row(`${cursor} ${active} ${name}${description}`));
2437
- }
2462
+ selectedAgent = Math.min(selectedAgent, Math.max(0, agents.length - 1));
2463
+ if (agents.length === 0) lines.push(row(muted('No primary agents configured')));
2464
+ for (const [index, agent] of agents.entries()) {
2465
+ const selected = index === selectedAgent;
2466
+ const cursor = selected ? accent('›') : ' ';
2467
+ const active = agent.name === activeAgent ? theme.fg('success', '●') : dim('○');
2468
+ const name = selected ? accent(agent.name) : text(agent.name);
2469
+ const description = agent.description ? dim(` ${agent.description}`) : '';
2470
+ lines.push(row(`${cursor} ${active} ${name}${description}`));
2438
2471
  }
2439
- lines.push(
2440
- row(''),
2441
- row(
2442
- `${dim('↑↓')} ${muted('select')} ${dim('enter')} ${muted('activate')} ${dim('tab')} ${muted('switch')} ${dim('esc')} ${muted('close')}`,
2443
- ),
2444
- boxBottom(theme, width),
2445
- );
2446
- return lines;
2447
- }
2448
-
2449
- if (tab === 'settings') {
2450
- const sandboxCursor = selectedSetting === 0 ? accent('›') : ' ';
2451
- const maxCursor = selectedSetting === 1 ? accent('›') : ' ';
2452
- const checkbox = config.enabled ? theme.fg('success', '[x]') : muted('[ ]');
2453
- const maxValue =
2454
- selectedSetting === 1
2455
- ? accent(`[ ${maxSubagentsValue} ]`)
2456
- : text(`[ ${maxSubagentsValue} ]`);
2457
- lines.push(
2458
- row(`${sandboxCursor} ${checkbox} ${text('Sandbox')}`),
2459
- row(
2460
- `${maxCursor} ${maxValue} ${text('Maximum concurrent subagents')} ${dim(`(0-${MAX_SUBAGENTS})`)}`,
2461
- ),
2462
- );
2463
- if (maxSubagentsValue === 0) {
2464
- lines.push(row(` ${dim('Task delegation is disabled')}`));
2472
+ } else {
2473
+ const scopes = ['Global', 'Project'] as const;
2474
+ for (const [index, scope] of scopes.entries()) {
2475
+ const selected = index === selectedSetting;
2476
+ const unavailable = scope === 'Project' && !projectConfigTrusted;
2477
+ const cursor = selected ? accent('›') : ' ';
2478
+ const value = selected
2479
+ ? accent(`[ ${values[index]} ]`)
2480
+ : text(`[ ${values[index]} ]`);
2481
+ const label = unavailable
2482
+ ? muted(`${scope} (project not trusted)`)
2483
+ : text(scope);
2484
+ lines.push(row(`${cursor} ${value} ${label}`));
2465
2485
  }
2466
- if (noSandboxFlag) {
2467
- lines.push(row(` ${dim('Sandbox is overridden by --no-sandbox')}`));
2486
+ if (values[selectedSetting] === 0) {
2487
+ lines.push(row(` ${dim('Task delegation is disabled for this scope')}`));
2468
2488
  }
2469
- lines.push(
2470
- row(''),
2471
- row(
2472
- `${dim('↑↓')} ${muted('select')} ${dim('space/enter')} ${muted('toggle or save')} ${dim('0-9')} ${muted('edit number')} ${dim('tab')} ${muted('switch')} ${dim('esc')} ${muted('close')}`,
2473
- ),
2474
- boxBottom(theme, width),
2475
- );
2476
- return lines;
2477
2489
  }
2478
2490
 
2479
- const statusDot = theme.fg(status.color, '●');
2480
- const pathSnippet = text(
2481
- truncateToWidth(landstripDisplayPath(), Math.max(20, innerW - 28)),
2482
- );
2483
2491
  lines.push(
2492
+ row(''),
2484
2493
  row(
2485
- `${statusDot} ${text(status.label)} ${dim('·')} ${muted('landstrip')} ${pathSnippet}`,
2494
+ 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')}`,
2486
2497
  ),
2498
+ boxBottom(theme, width),
2487
2499
  );
2488
-
2489
- section('Config');
2490
- item('project', text(projectPath));
2491
- item('global', text(globalPath));
2492
-
2493
- const netMode = config.network.allowNetwork ? 'unrestricted' : 'proxied';
2494
- section('Network', netMode);
2495
- item('allow network', boolVal(config.network.allowNetwork));
2496
- item('allowed', listValue(config.network.allowedDomains, innerW - 17));
2497
- item('denied', listValue(config.network.deniedDomains, innerW - 17));
2498
- if (sessionAllowedDomains.length > 0)
2499
- item('session', theme.fg('accent', sessionAllowedDomains.join(', ')));
2500
-
2501
- section('Filesystem');
2502
- item('deny read', listValue(config.filesystem.denyRead, innerW - 17));
2503
- item('allow read', listValue(config.filesystem.allowRead, innerW - 17));
2504
- item('allow write', listValue(config.filesystem.allowWrite, innerW - 17));
2505
- item('deny write', listValue(config.filesystem.denyWrite, innerW - 17));
2506
-
2507
- if (sessionAllowedReadPaths.length > 0 || sessionAllowedWritePaths.length > 0) {
2508
- section('Session grants');
2509
- if (sessionAllowedReadPaths.length > 0)
2510
- item('read', theme.fg('accent', sessionAllowedReadPaths.join(', ')));
2511
- if (sessionAllowedWritePaths.length > 0)
2512
- item('write', theme.fg('accent', sessionAllowedWritePaths.join(', ')));
2513
- }
2514
-
2515
- lines.push(row(''));
2516
- lines.push(
2517
- row(`${dim('tab')} ${muted('switch')} ${dim('esc')} ${muted('close')}`),
2518
- );
2519
- lines.push(boxBottom(theme, width));
2520
-
2521
2500
  return lines;
2522
2501
  },
2523
2502
 
@@ -2526,81 +2505,64 @@ export function createLandstripIntegration(
2526
2505
  done(undefined);
2527
2506
  return;
2528
2507
  }
2529
- if (matchesKey(data, 'tab') || matchesKey(data, 'right')) {
2530
- tab = tab === 'sandbox' ? 'agents' : tab === 'agents' ? 'settings' : 'sandbox';
2531
- if (tab === 'settings') {
2532
- maxSubagentsValue = runtime?.getMaxSubagents() ?? 0;
2533
- editingMaxSubagents = false;
2534
- }
2535
- tui.requestRender();
2536
- return;
2537
- }
2538
- if (matchesKey(data, 'left')) {
2539
- tab = tab === 'settings' ? 'agents' : tab === 'agents' ? 'sandbox' : 'settings';
2540
- if (tab === 'settings') {
2541
- maxSubagentsValue = runtime?.getMaxSubagents() ?? 0;
2542
- editingMaxSubagents = false;
2543
- }
2508
+ if (
2509
+ matchesKey(data, 'tab') ||
2510
+ matchesKey(data, 'left') ||
2511
+ matchesKey(data, 'right')
2512
+ ) {
2513
+ tab = tab === 'agents' ? 'settings' : 'agents';
2514
+ editing = false;
2544
2515
  tui.requestRender();
2545
2516
  return;
2546
2517
  }
2547
2518
  if (tab === 'agents') {
2548
2519
  const catalog = runtime?.getAgentCatalog(ctx);
2549
2520
  const agents = catalog ? availablePrimaryAgents(catalog) : [];
2550
- if (matchesKey(data, 'up')) {
2551
- selectedAgent = Math.max(0, selectedAgent - 1);
2552
- tui.requestRender();
2553
- } else if (matchesKey(data, 'down')) {
2521
+ if (matchesKey(data, 'up')) selectedAgent = Math.max(0, selectedAgent - 1);
2522
+ else if (matchesKey(data, 'down')) {
2554
2523
  selectedAgent = Math.min(Math.max(0, agents.length - 1), selectedAgent + 1);
2555
- tui.requestRender();
2556
2524
  } else if (matchesKey(data, 'return')) {
2557
2525
  const agent = agents[selectedAgent];
2558
- if (agent && runtime?.selectPrimaryAgent(agent.name, ctx)) tui.requestRender();
2559
- }
2526
+ if (agent) runtime?.selectPrimaryAgent(agent.name, ctx);
2527
+ } else return;
2528
+ tui.requestRender();
2560
2529
  return;
2561
2530
  }
2562
2531
 
2563
- if (tab !== 'settings') return;
2564
2532
  if (matchesKey(data, 'up')) {
2565
2533
  selectedSetting = Math.max(0, selectedSetting - 1);
2566
- editingMaxSubagents = false;
2567
- tui.requestRender();
2568
- return;
2569
- }
2570
- if (matchesKey(data, 'down')) {
2534
+ editing = false;
2535
+ } else if (matchesKey(data, 'down')) {
2571
2536
  selectedSetting = Math.min(1, selectedSetting + 1);
2572
- editingMaxSubagents = false;
2573
- tui.requestRender();
2574
- return;
2575
- }
2576
-
2577
- if (selectedSetting === 1) {
2578
- if (/^[0-9]$/.test(data)) {
2579
- const value = Number(
2580
- editingMaxSubagents ? `${maxSubagentsValue}${data}` : data,
2581
- );
2582
- if (value <= MAX_SUBAGENTS) {
2583
- maxSubagentsValue = value;
2584
- editingMaxSubagents = true;
2585
- tui.requestRender();
2586
- }
2587
- return;
2537
+ editing = false;
2538
+ } else if (/^[0-9]$/.test(data)) {
2539
+ const value = Number(editing ? `${values[selectedSetting]}${data}` : data);
2540
+ if (value <= MAX_SUBAGENTS) {
2541
+ values[selectedSetting] = value;
2542
+ editing = true;
2588
2543
  }
2589
- if (data === '+' || data === '-') {
2590
- maxSubagentsValue = Math.min(
2591
- MAX_SUBAGENTS,
2592
- Math.max(0, maxSubagentsValue + (data === '+' ? 1 : -1)),
2593
- );
2594
- editingMaxSubagents = false;
2595
- tui.requestRender();
2544
+ } else if (data === '+' || data === '-') {
2545
+ values[selectedSetting] = Math.min(
2546
+ MAX_SUBAGENTS,
2547
+ Math.max(0, values[selectedSetting] + (data === '+' ? 1 : -1)),
2548
+ );
2549
+ editing = false;
2550
+ } else if (matchesKey(data, 'return')) {
2551
+ const scope = selectedSetting === 0 ? 'global' : 'project';
2552
+ if (scope === 'project' && !projectConfigTrusted) {
2553
+ notify(ctx, 'Project settings require a trusted project', 'warning');
2596
2554
  return;
2597
2555
  }
2598
- if (!matchesKey(data, 'return')) return;
2599
- const maxSubagents = maxSubagentsValue;
2600
- void setMaxSubagentsConfig(ctx.cwd, maxSubagents, projectConfigTrusted)
2601
- .then((scope) => {
2602
- runtime?.setMaxSubagents(maxSubagents);
2603
- editingMaxSubagents = false;
2556
+ const maxSubagents = values[selectedSetting];
2557
+ void setMaxSubagentsConfigForScope(ctx.cwd, maxSubagents, scope)
2558
+ .then(() => {
2559
+ const effective = loadLandstripConfig(
2560
+ ctx.cwd,
2561
+ projectConfigTrusted,
2562
+ ).maxSubagents;
2563
+ runtime?.setMaxSubagents(effective);
2564
+ values = [loadLandstripConfig(ctx.cwd, false).maxSubagents, effective];
2565
+ editing = false;
2604
2566
  notify(
2605
2567
  ctx,
2606
2568
  `Maximum concurrent subagents set to ${maxSubagents} in ${scope} config`,
@@ -2612,34 +2574,8 @@ export function createLandstripIntegration(
2612
2574
  notify(ctx, `Could not update config: ${error}`, 'error');
2613
2575
  });
2614
2576
  return;
2615
- }
2616
-
2617
- if (data !== ' ' && !matchesKey(data, 'return')) return;
2618
-
2619
- void (async () => {
2620
- const enabled = !config.enabled;
2621
- const scope = await setSandboxConfigEnabled(
2622
- ctx.cwd,
2623
- enabled,
2624
- projectConfigTrusted,
2625
- );
2626
- config = loadConfig(ctx.cwd);
2627
-
2628
- if (!enabled) {
2629
- disableSandbox(ctx);
2630
- notify(ctx, `Sandbox disabled in ${scope} config`, 'info');
2631
- } else if (noSandboxFlag) {
2632
- notify(ctx, 'Sandbox remains disabled via --no-sandbox', 'warning');
2633
- } else if (!config.enabled) {
2634
- notify(ctx, 'Sandbox remains disabled via config', 'info');
2635
- } else if (enableSandbox(ctx)) {
2636
- notify(ctx, `Sandbox enabled in ${scope} config`, 'info');
2637
- }
2638
-
2639
- tui.requestRender();
2640
- })().catch((error: unknown) => {
2641
- notify(ctx, `Could not update config: ${error}`, 'error');
2642
- });
2577
+ } else return;
2578
+ tui.requestRender();
2643
2579
  },
2644
2580
 
2645
2581
  invalidate(): void {},
@@ -2647,19 +2583,10 @@ export function createLandstripIntegration(
2647
2583
  },
2648
2584
  {
2649
2585
  overlay: true,
2650
- overlayOptions: {
2651
- anchor: 'center',
2652
- width: 78,
2653
- margin: 2,
2654
- },
2586
+ overlayOptions: { anchor: 'center', width: 72, margin: 2 },
2655
2587
  },
2656
2588
  );
2657
2589
  },
2658
- };
2659
- maybePi.registerCommand?.('landstrip', landstripCommand);
2660
- maybePi.registerCommand?.('sandbox', {
2661
- ...landstripCommand,
2662
- description: 'Deprecated alias for /landstrip',
2663
2590
  });
2664
2591
  }
2665
2592
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-landstrip",
3
- "version": "0.17.13",
3
+ "version": "0.17.14",
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.13",
41
+ "@landstrip/landstrip": "^0.17.14",
42
42
  "minimatch": "^10.2.5"
43
43
  },
44
44
  "devDependencies": {