pum-agent 0.2.1-beta.1 → 0.2.3-beta.1

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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ pum
23
23
  PUM opens the login panel automatically on the first start.
24
24
 
25
25
  > [!WARNING]
26
- > PUM can read, write, and delete files. PUM can also run commands and supervised trigger processes. Check mode can verify or ask about these operations, but it is not a sandbox. Start PUM only inside a workspace where these actions are acceptable.
26
+ > PUM can read, write, and delete files. Check mode adds deterministic policy checks and can enforce native Bash isolation on supported Linux and Windows hosts. Other tools, extensions, and external triggers still run in the PUM process boundary described below. Review the safeguards and prerequisites before using untrusted workspaces.
27
27
 
28
28
  ## See PUM in action
29
29
 
@@ -46,7 +46,7 @@ The following screens are real OpenTUI renders captured through `tmux`. A local
46
46
  - **Prompt control:** Steer active work, answer model questionnaires, use an ownership-aware message cache, attach clipboard images, and resume sessions with metadata-rich history.
47
47
  - **External triggers:** Supervise background commands such as `gh run watch` and automatically wake the exact target agent when they exit.
48
48
  - **Provider choice:** Search the providers exposed by pi, or add an OpenAI-compatible custom endpoint.
49
- - **Optional safeguards:** Use strict, balanced, or ask Check mode for `bash`, `edit`, `apply_patch`, and external-trigger process proposals.
49
+ - **Optional safeguards:** Use strict, balanced, or ask Check mode, plus native Bash sandboxing through Bubblewrap or Windows CreateProcessInSandbox when available.
50
50
  - **Terminal-first appearance:** Nine themes, semantic color overrides, Unicode glyphs, and optional animation.
51
51
 
52
52
  PUM uses [pi](https://github.com/earendil-works/pi) for the agent loop and [OpenTUI](https://github.com/anomalyco/opentui) for rendering.
@@ -60,7 +60,22 @@ PUM uses [pi](https://github.com/earendil-works/pi) for the agent loop and [Open
60
60
 
61
61
  Linux and macOS are the primary environments. Windows CI checks the code and Windows path behavior. Native Windows TUI operation remains provisional because it has not been fully validated in a Windows terminal.
62
62
 
63
- On Windows, install Git for Windows. Ensure that `bash.exe` is in `PATH` or remains in its standard location. Use Windows Terminal with PowerShell. Do not use PowerShell ISE.
63
+ On Linux, native Bash sandboxing requires Bubblewrap (`bwrap`) and working unprivileged user namespaces. PUM probes a minimal sandbox launch; finding the executable alone is not sufficient. On Arch Linux, install the prerequisite separately with `sudo pacman -S --needed bubblewrap`.
64
+
65
+ On Windows, install Git for Windows. Ensure that `bash.exe` is in `PATH` or remains in its standard location. Use Windows Terminal with PowerShell. Do not use PowerShell ISE. Native Bash sandboxing uses the optional alpha `@microsoft/mxc-sdk` package and requires its `base-container` CreateProcessInSandbox tier. PUM deliberately rejects the SDK's AppContainer+DACL fallback because it can modify host ACLs.
66
+
67
+ ### Terminal title
68
+
69
+ PUM sets a compact terminal title such as `Pum · working · 2 subagents`. The title reports overall activity and counts only starting or running subagents. PUM clears the title during graceful shutdown.
70
+
71
+ Windows Terminal and common Linux terminal emulators accept the title through OpenTUI. Inside `tmux`, PUM sets the active pane title. To copy that pane title to the outer terminal title, add this configuration:
72
+
73
+ ```tmux
74
+ set -g set-titles on
75
+ set -g set-titles-string '#T'
76
+ ```
77
+
78
+ Keep `allow-set-title` enabled so applications can update the pane title. A `tmux` configuration can replace or suppress application titles. PUM cannot override that server policy.
64
79
 
65
80
  ## Install and start
66
81
 
@@ -123,6 +138,7 @@ Set `PUM_DIR` to override PUM's complete configuration and data directory. Run `
123
138
  | `Ctrl+L` | Open the agent transcript selector |
124
139
  | `Shift+Tab` / `Ctrl+Shift+Tab` | Cycle through agent transcripts |
125
140
  | `Ctrl+H` | Open session history when the terminal reports the key distinctly |
141
+ | `Ctrl+End` | Scroll to the end of the selected transcript |
126
142
  | `Ctrl+P` | Open settings |
127
143
  | `Ctrl+T` | Open supervised external triggers |
128
144
  | `Esc` twice | Cancel the selected working agent |
@@ -162,7 +178,7 @@ PUM runs up to 10 active subagents by default. Configure a limit from 1 through
162
178
  - Its own transcript, draft, usage data, and cancellation state
163
179
  - Tools for progress messages and a single final completion report
164
180
 
165
- Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
181
+ Select a range of stashed prompts and press `Enter`. The main agent can group related work and run independent groups in parallel. A managed merge requires both authoritative `completed` status and a persisted completion notice. Idle settlement is not completion. Successful managed merges remove the completed worktree and branch. A parent cannot finish, merge, or be removed until every retained descendant closes deepest-first.
166
182
 
167
183
  Use `Ctrl+L` to select an agent transcript. Input then goes to that agent. Finished or interrupted agents remain available until PUM merges or removes them.
168
184
 
@@ -170,7 +186,7 @@ The public `spawn_subagent` tool accepts `preview: true`. PUM then shows the exa
170
186
 
171
187
  Press `↑` on an empty single-line prompt to recall the newest queued user-authored message for the selected transcript. PUM removes the message from the authoritative queue before restoring its text. PUM does not recall inter-agent, trigger, lifecycle, cache, delivered, or image-bearing messages.
172
188
 
173
- Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM persists completion intent and delivery state so interrupted notification delivery can resume without duplicate completion messages.
189
+ Idle notices report settled work cycles to the direct spawner. They are not completion notices. PUM acknowledges completion delivery only after the notice enters the parent session. Persisted completion intent and stable message identifiers let interrupted delivery resume without duplicate completion messages.
174
190
 
175
191
  ## Tools and safeguards
176
192
 
@@ -186,7 +202,7 @@ Main and managed child agents can list and read the current workspace message ca
186
202
 
187
203
  Agents can add entries. An agent can delete only entries created by that exact agent. User-created and legacy entries remain user-owned.
188
204
 
189
- The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration.
205
+ The `message_cache_send` tool accepts stable entry IDs. Single entries use the selected agent delivery path. Multiple entries use main-agent worktree orchestration. PUM reserves selected entries during delivery and marks them executed only after delivery succeeds. Failed main or child delivery leaves the entries pending.
190
206
 
191
207
  ### External triggers
192
208
 
@@ -205,10 +221,10 @@ Trigger events target one exact main or retained child session. A missing sessio
205
221
  Select a Check mode profile in `Ctrl+P`. It applies to `bash`, `edit`, `apply_patch`, and external-trigger process execution:
206
222
 
207
223
  - **Strict:** Run deterministic hard rules, then require a clear verifier approval.
208
- - **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
224
+ - **Balanced:** Block deterministic hard-rule or suspicious findings. Allow ordinary complete project-local calls and explicit non-sensitive external reads. Verifier review is non-blocking unless the verifier returns explicit `UNSAFE`.
209
225
  - **Ask:** Show the approval popup for every checked call that passes hard rules, unless an exact session or project approval already matches. A verifier `SAFE`, unclear, error, or unavailable result still requires approval.
210
226
 
211
- Every active profile hard-blocks project escape, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use the exception.
227
+ Every active profile hard-blocks external writes, location changes, execution operands, ambiguous path access, escaping links, credential access, privilege escalation, persistence, remote-script execution, destructive Git operations, and broad deletion. Balanced permits only explicit, deterministically classified, non-sensitive external reads. These hard blocks cannot be overridden and do not open the popup. An explicit verifier `UNSAFE` verdict also blocks without a popup. The only exception is a deterministic match for direct main-agent `npm publish` or `npm dist-tag add`. The verifier category does not control this exception. The exception still requires explicit popup approval. Managed subagents cannot use the exception.
212
228
 
213
229
  Use `/check-path list`, `/check-path add <directory>`, `/check-path remove <directory>`, or `/check-path clear` to manage up to 16 additional directory roots for the current launch project. Bash, edit, and external-trigger checks can use these roots; `apply_patch` remains project-local. Added roots are canonicalized and remain subject to credential, traversal, symlink or junction, broad-deletion, and other hard blocks.
214
230
 
@@ -216,7 +232,25 @@ For `edit` and `apply_patch`, PUM validates the complete proposed change before
216
232
 
217
233
  Ask mode can allow an exact call once, for the current session, or for the current project. Approvals match the authoritative main or child identity, tool, verifier model, project, and canonical complete input. Chat text is not approval. Use **Clear approvals** in Settings to remove project approvals.
218
234
 
219
- The verifier uses a structured decision schema. One unclear response can receive one adjudication under the shared 15-second watchdog. Strict blocks malformed replies, errors, aborts, and timeouts. Balanced allows a fully validated call after an unclear, unavailable, failed, or timed-out review. Balanced still blocks explicit verifier `UNSAFE`, aborts, deterministic suspicious findings, malformed structures, and incomplete analysis. Ask requires the popup after hard rules for verifier `SAFE`, unclear, error, and unavailable results. Check mode is off by default and is not a sandbox.
235
+ The verifier uses a structured decision schema. One unclear response can receive one adjudication under the shared 15-second watchdog. Strict blocks malformed replies, errors, aborts, and timeouts. Balanced allows a fully validated call after an unclear, unavailable, failed, or timed-out review. Balanced still blocks explicit verifier `UNSAFE`, aborts, deterministic suspicious findings, malformed structures, and incomplete analysis. Ask requires the popup after hard rules for verifier `SAFE`, unclear, error, and unavailable results. Check mode is off by default.
236
+
237
+ #### Native Bash sandbox
238
+
239
+ The **Sandbox** setting has three modes:
240
+
241
+ - **Auto:** Enforce the platform sandbox for Bash when available. If probing fails, retain deterministic Check mode and show one process-local warning that is not written to session context.
242
+ - **Require:** Block checked Bash calls unless native enforcement is available.
243
+ - **Off:** Do not sandbox Bash. Check mode policy and approval behavior remain unchanged.
244
+
245
+ Check mode **Off** always uses pi's normal unsandboxed Bash backend. For an active Check mode, PUM recomputes the sandbox policy from the exact approved command, authoritative working directory, configured additional roots, and deterministic access analysis. Model input cannot supply policy fields.
246
+
247
+ The project and configured additional roots are writable. Explicit Balanced external reads are mounted read-only. PUM configuration and common credential paths are denied, and credential-shaped or process-injection environment variables are removed. A private temporary directory is supplied for the command. Safe pi metadata such as `PI_PROVIDER`, `PI_MODEL`, and `PI_REASONING_LEVEL` remains available; session paths and identifiers are withheld.
248
+
249
+ Network access is denied unless deterministic analysis recognizes an approved network operation. Bubblewrap's host-network mode is all-or-nothing and is **not domain-filtered**. Windows similarly grants or withholds the SDK's broad network capabilities; it does not provide hostname allowlists.
250
+
251
+ The override uses pi's `createBashTool` implementation and custom Bash operations, preserving streaming, truncation, full-output files, rendering, timeout messages, abort handling, shell configuration, and child-tree cleanup. Only Bash commands are routed through this backend. PUM does not sandbox the TUI/model process itself.
252
+
253
+ External triggers preserve direct executable/argument boundaries and continue to use deterministic Check mode, but they are not routed through the native sandbox in this release. The trigger manager's synchronous spawn boundary does not carry the exact approved policy object into execution; silently recomputing a second process policy there would weaken approval identity. Trigger output, environment, limits, and process supervision remain unchanged.
220
254
 
221
255
  Verifier prompts stay bounded. For an oversized Balanced review, PUM sends complete validation metadata, counts, findings, and SHA-256 digests. PUM does not send a raw prefix or suffix as if it were complete. Strict and Ask keep their fail-closed oversized-input behavior.
222
256
 
@@ -254,7 +288,7 @@ Set `PUM_DIR` to override the complete PUM data directory.
254
288
  | `auth.json` | Provider credentials and custom-provider keys |
255
289
  | `models.json` | Custom endpoints and model metadata; submitted keys are not stored here |
256
290
  | `settings.json` | Model and thinking level managed by pi |
257
- | `pum.json` | Theme, animation, search, writing, explanation, and check settings |
291
+ | `pum.json` | Theme, animation, search, writing, explanation, Check mode, sandbox, and subagent settings |
258
292
  | `theme.json` | Optional semantic color overrides |
259
293
  | `history.json` | Prompt history by working directory |
260
294
  | `prompt-stash.json` | Stashed prompts by working directory |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.1-beta.1",
3
+ "version": "0.2.3-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -60,7 +60,8 @@
60
60
  "ws": "^8.21.3"
61
61
  },
62
62
  "optionalDependencies": {
63
- "@mariozechner/clipboard": "0.3.9"
63
+ "@mariozechner/clipboard": "0.3.9",
64
+ "@microsoft/mxc-sdk": "^0.7.0"
64
65
  },
65
66
  "devDependencies": {
66
67
  "@types/bun": "^1.3.14",
package/src/app.tsx CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  checkPathsForProject,
23
23
  MAX_ACTIVE_SUBAGENTS,
24
24
  MIN_ACTIVE_SUBAGENTS,
25
+ SANDBOX_MODES,
25
26
  saveSettings,
26
27
  WORKING_RULE_ANIMATION_MODES,
27
28
  type PumSettings,
@@ -81,7 +82,7 @@ import {
81
82
  removePendingImage,
82
83
  type PendingImage,
83
84
  } from "./image-paste";
84
- import type { SubagentManager } from "./subagents/manager";
85
+ import { countActiveSubagents, type SubagentManager } from "./subagents/manager";
85
86
  import type { SpawnPreviewManager } from "./subagents/spawn-preview";
86
87
  import { SpawnPreviewPopup } from "./subagents/spawn-preview-popup";
87
88
  import { recallNewestQueuedUserMessage } from "./queue-recall";
@@ -119,6 +120,7 @@ import {
119
120
  type TriggerAction,
120
121
  type TriggerManagerLike,
121
122
  } from "./triggers/popup";
123
+ import type { TerminalTitleController } from "./terminal-title";
122
124
 
123
125
  type Stream = { kind: "assistant" | "thinking"; text: string } | null;
124
126
  type Transcript = { lines: Line[]; stream: Stream; pending: PendingLine[] };
@@ -344,6 +346,10 @@ export function App({
344
346
  checkApprovalStore,
345
347
  triggerManager,
346
348
  messageCacheController,
349
+ terminalTitle,
350
+ startupWarnings = [],
351
+ onSandboxModeChange,
352
+ sandboxWarningSource,
347
353
  }: {
348
354
  session: AgentSession;
349
355
  modelRuntime: ModelRuntime;
@@ -365,16 +371,24 @@ export function App({
365
371
  checkApprovalStore?: CheckApprovalStore;
366
372
  triggerManager?: TriggerManagerLike;
367
373
  messageCacheController?: MessageCacheController;
374
+ terminalTitle?: TerminalTitleController;
375
+ /** Visible process-local warnings. These lines never enter pi session context. */
376
+ startupWarnings?: readonly string[];
377
+ onSandboxModeChange?: (mode: NonNullable<PumSettings["sandboxMode"]>) => void;
378
+ sandboxWarningSource?: { subscribeWarnings(listener: (warning: string) => void): () => void };
368
379
  }) {
369
380
  const cwd = process.cwd();
370
381
  const [session, setSession] = useState(initialSession);
371
382
  const [tx, setTx] = useState<Transcript>(() => ({
372
383
  // A resumed session already holds messages; show them instead of a blank pane.
373
- lines: replayEntries(
374
- initialSession.sessionManager.buildContextEntries(),
375
- cwd,
376
- initial.showThinking,
377
- ),
384
+ lines: [
385
+ ...replayEntries(
386
+ initialSession.sessionManager.buildContextEntries(),
387
+ cwd,
388
+ initial.showThinking,
389
+ ),
390
+ ...startupWarnings.map((text): Line => ({ kind: "text", role: "system", text })),
391
+ ],
378
392
  stream: null,
379
393
  pending: [],
380
394
  }));
@@ -436,6 +450,7 @@ export function App({
436
450
  const syntaxStyle = useMemo(() => buildSyntaxStyle(theme), [theme]);
437
451
  const animations = settings.animations && supportsTrueColor();
438
452
  const agents = subagentManager.getAgents();
453
+ const activeSubagentCount = countActiveSubagents(agents);
439
454
  const activeAgent = activeAgentId
440
455
  ? agents.find((agent) => agent.id === activeAgentId)
441
456
  : undefined;
@@ -471,6 +486,7 @@ export function App({
471
486
  ), [modelRuntime, modelId, modelQuery, loginPage]);
472
487
 
473
488
  const inputRef = useRef<TextareaRenderable>(null);
489
+ const transcriptScrollRef = useRef<ScrollBoxRenderable>(null);
474
490
  const questionnaireInputRef = useRef<TextareaRenderable>(null);
475
491
  const spawnPreviewInputRef = useRef<TextareaRenderable>(null);
476
492
  const settingsOpenRef = useRef(settingsOpen);
@@ -553,6 +569,7 @@ export function App({
553
569
  };
554
570
  // The event subscription is set up once, so it reads the toggle via a ref.
555
571
  const showThinkingRef = useRef(initial.showThinking);
572
+ const startupWarningsRef = useRef([...startupWarnings]);
556
573
  const sessionRef = useRef(session);
557
574
  sessionRef.current = session;
558
575
  const loginControllerRef = useRef<LoginController | null>(null);
@@ -794,6 +811,10 @@ export function App({
794
811
  return pending ? resolvePendingDelivery(value, pending.id) : value;
795
812
  });
796
813
 
814
+ useEffect(() => sandboxWarningSource?.subscribeWarnings((warning) => {
815
+ append({ kind: "text", role: "system", text: warning });
816
+ }), [sandboxWarningSource]);
817
+
797
818
  useEffect(() => checkApprovalCoordinator?.subscribe((request) => {
798
819
  setCheckApproval(request);
799
820
  setCheckApprovalDecision("allowOnce");
@@ -818,6 +839,13 @@ export function App({
818
839
  [subagentManager],
819
840
  );
820
841
 
842
+ useEffect(() => {
843
+ terminalTitle?.update({
844
+ working: busy || activeSubagentCount > 0,
845
+ activeSubagentCount,
846
+ });
847
+ }, [terminalTitle, busy, activeSubagentCount]);
848
+
821
849
  useEffect(() => spawnPreviewManager?.subscribe(() => {
822
850
  setSpawnPreviewRevision((revision) => revision + 1);
823
851
  }), [spawnPreviewManager]);
@@ -865,8 +893,13 @@ export function App({
865
893
  .catch((error) => append({ kind: "text", role: "error", text: String(error) }));
866
894
  setThinkingLevel(session.agent.state.thinkingLevel as ThinkingLevel);
867
895
  setModelId(session.agent.state.model.id);
896
+ const visibleStartupWarnings = startupWarningsRef.current;
897
+ startupWarningsRef.current = [];
868
898
  setTx({
869
- lines: replayEntries(session.sessionManager.buildContextEntries(), cwd, showThinkingRef.current),
899
+ lines: [
900
+ ...replayEntries(session.sessionManager.buildContextEntries(), cwd, showThinkingRef.current),
901
+ ...visibleStartupWarnings.map((text): Line => ({ kind: "text", role: "system", text })),
902
+ ],
870
903
  stream: null,
871
904
  pending: [],
872
905
  });
@@ -1029,6 +1062,7 @@ export function App({
1029
1062
  if (patch.explanationStrength !== undefined) {
1030
1063
  setExplanationStrength(patch.explanationStrength);
1031
1064
  }
1065
+ if (patch.sandboxMode !== undefined) onSandboxModeChange?.(patch.sandboxMode);
1032
1066
  if (patch.checkMode !== undefined || patch.checkModel !== undefined || patch.checkPaths !== undefined) {
1033
1067
  setCheckModeConfig({
1034
1068
  profile: next.checkMode,
@@ -1328,12 +1362,12 @@ export function App({
1328
1362
  return true;
1329
1363
  };
1330
1364
 
1331
- const deliverMainPrompt = (
1365
+ const deliverMainPrompt = async (
1332
1366
  promptText: string,
1333
1367
  displayText: string,
1334
1368
  images: ReturnType<typeof imageContent>[] = [],
1335
1369
  recallable = images.length === 0,
1336
- ) => {
1370
+ ): Promise<void> => {
1337
1371
  const userLine: Extract<Line, { kind: "text" }> = {
1338
1372
  kind: "text",
1339
1373
  role: "user",
@@ -1350,20 +1384,26 @@ export function App({
1350
1384
  recallable,
1351
1385
  };
1352
1386
  addPending(pending);
1353
- withSearchRoute(session.sessionId, () => session.steer(promptText, images)).catch((error) => {
1387
+ try {
1388
+ await withSearchRoute(session.sessionId, () => session.steer(promptText, images));
1389
+ } catch (error) {
1354
1390
  dropPending(pending.id);
1355
1391
  append({ kind: "text", role: "error", text: String(error) });
1356
- });
1392
+ throw error;
1393
+ }
1357
1394
  return;
1358
1395
  }
1359
1396
 
1360
1397
  append(userLine);
1361
1398
  inFlight.current = promptText;
1362
1399
  setWorking(true);
1363
- withSearchRoute(session.sessionId, () => session.prompt(promptText, { images })).catch((error) => {
1400
+ try {
1401
+ await withSearchRoute(session.sessionId, () => session.prompt(promptText, { images }));
1402
+ } catch (error) {
1364
1403
  append({ kind: "text", role: "error", text: String(error) });
1365
1404
  setWorking(false);
1366
- });
1405
+ throw error;
1406
+ }
1367
1407
  };
1368
1408
 
1369
1409
  const submitPrompt = (value?: string, stashIndex?: number) => {
@@ -1410,7 +1450,7 @@ export function App({
1410
1450
  histCursor.current = null;
1411
1451
  draft.current = "";
1412
1452
  setSelectedStash(-1);
1413
- deliverMainPrompt(promptText, displayText, images);
1453
+ void deliverMainPrompt(promptText, displayText, images).catch(() => {});
1414
1454
  };
1415
1455
 
1416
1456
  const cachedBatchDisplay = (prompts: readonly string[]): string => [
@@ -1444,7 +1484,7 @@ export function App({
1444
1484
  setStash(next);
1445
1485
  refreshHistoryAfterStashMutation();
1446
1486
  resetAfterCacheExecution();
1447
- deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
1487
+ void deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false).catch(() => {});
1448
1488
  };
1449
1489
 
1450
1490
  useEffect(() => {
@@ -1460,24 +1500,22 @@ export function App({
1460
1500
  const detach = messageCacheController.bindExecutor(
1461
1501
  session.sessionId,
1462
1502
  async (request: MessageCacheSendRequest): Promise<MessageCacheSendResult> => {
1463
- const { entries, state } = messageCacheController.execute(request.ids);
1464
- stashRef.current = state.stash;
1465
- setStash(state.stash);
1466
- history.current = state.history;
1467
- resetAfterCacheExecution(
1468
- request.requester.kind === "subagent" ? request.requester.id : null,
1469
- );
1470
- const prompts = entries.map((entry) => entry.text);
1503
+ const prompts = request.entries.map((entry) => entry.text);
1471
1504
  if (prompts.length > 1) {
1472
- deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
1505
+ await deliverMainPrompt(buildStashBatchPrompt(prompts), cachedBatchDisplay(prompts), [], false);
1506
+ resetAfterCacheExecution(
1507
+ request.requester.kind === "subagent" ? request.requester.id : null,
1508
+ );
1473
1509
  return { count: prompts.length, route: "main" };
1474
1510
  }
1475
1511
  const prompt = prompts[0]!;
1476
1512
  if (request.requester.kind === "subagent") {
1477
1513
  await subagentManager.sendUserMessage(request.requester.id, prompt, [], prompt, false);
1514
+ resetAfterCacheExecution(request.requester.id);
1478
1515
  return { count: 1, route: "subagent" };
1479
1516
  }
1480
- deliverMainPrompt(prompt, prompt, [], false);
1517
+ await deliverMainPrompt(prompt, prompt, [], false);
1518
+ resetAfterCacheExecution(null);
1481
1519
  return { count: 1, route: "main" };
1482
1520
  },
1483
1521
  );
@@ -1519,6 +1557,12 @@ export function App({
1519
1557
  update({ checkMode: CHECK_MODE_PROFILES[(index + step + CHECK_MODE_PROFILES.length) % CHECK_MODE_PROFILES.length]! });
1520
1558
  };
1521
1559
 
1560
+ const stepSandboxMode = (step: number) => {
1561
+ const current = settings.sandboxMode ?? "auto";
1562
+ const index = SANDBOX_MODES.indexOf(current);
1563
+ update({ sandboxMode: SANDBOX_MODES[(index + step + SANDBOX_MODES.length) % SANDBOX_MODES.length]! });
1564
+ };
1565
+
1522
1566
  const rowActions: Record<SettingRowId, { step?: (n: number) => void; enter?: () => void }> = {
1523
1567
  theme: { step: stepTheme },
1524
1568
  providers: { enter: openLogin },
@@ -1528,6 +1572,7 @@ export function App({
1528
1572
  writingStyle: { step: stepWritingStyle },
1529
1573
  explanationStrength: { step: stepExplanationStrength },
1530
1574
  checkMode: { step: stepCheckMode },
1575
+ sandboxMode: { step: stepSandboxMode },
1531
1576
  checkModel: { enter: () => {
1532
1577
  setModelQuery("");
1533
1578
  setModelSearchFocused(false);
@@ -1575,6 +1620,7 @@ export function App({
1575
1620
  writingStyle: `‹ ${settings.writingStyle} ›`,
1576
1621
  explanationStrength: `‹ ${settings.explanationStrength} ›`,
1577
1622
  checkMode: `‹ ${settings.checkMode} ›`,
1623
+ sandboxMode: `‹ ${settings.sandboxMode ?? "auto"} ›`,
1578
1624
  checkModel: `${settings.checkModel} ›`,
1579
1625
  checkPaths: `${checkPathsForProject(settings, cwd).length} additional · /check-path ›`,
1580
1626
  clearCheckApprovals: "clear ›",
@@ -1948,6 +1994,13 @@ export function App({
1948
1994
  return;
1949
1995
  }
1950
1996
 
1997
+ if (key.ctrl && key.name === "end") {
1998
+ key.stopPropagation();
1999
+ const transcriptScroll = transcriptScrollRef.current;
2000
+ if (transcriptScroll) transcriptScroll.scrollTop = transcriptScroll.scrollHeight;
2001
+ return;
2002
+ }
2003
+
1951
2004
  const isAgentCycle =
1952
2005
  (key.name === "tab" && key.shift) ||
1953
2006
  key.name === "backtab" ||
@@ -2236,7 +2289,7 @@ export function App({
2236
2289
  busy={visibleBusy}
2237
2290
  elapsedSec={visibleElapsedSec}
2238
2291
  agentCount={agents.length}
2239
- runningAgentCount={agents.filter((agent) => agent.status === "running" || agent.status === "starting").length}
2292
+ runningAgentCount={activeSubagentCount}
2240
2293
  maxActiveAgentCount={settings.maxActiveSubagents}
2241
2294
  activeAgentName={activeAgent?.name}
2242
2295
  />
@@ -2249,6 +2302,8 @@ export function App({
2249
2302
  />
2250
2303
  <scrollbox
2251
2304
  key={activeAgentId ?? "main"}
2305
+ ref={transcriptScrollRef}
2306
+ id="transcript-scrollbox"
2252
2307
  style={{ flexGrow: 1, paddingLeft: 1, paddingRight: 1 }}
2253
2308
  stickyScroll
2254
2309
  stickyStart="bottom"
@@ -2389,7 +2444,7 @@ export function App({
2389
2444
  textColor={theme.fg}
2390
2445
  cursorColor={theme.accent}
2391
2446
  selectionBg={theme.selectionBg}
2392
- wrapMode="char"
2447
+ wrapMode="word"
2393
2448
  scrollMargin={1}
2394
2449
  focused={!settingsOpen && !helpOpen && !historyOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !questionnaire && !spawnPreview && !checkApproval}
2395
2450
  onContentChange={handleTextareaChange}
package/src/check-mode.ts CHANGED
@@ -262,12 +262,13 @@ export async function prepareCheck(
262
262
  cwd: input.cwd,
263
263
  projectCwd: cwd,
264
264
  allowedPaths: additionalPaths,
265
+ protectedPaths: [AGENT_DIR],
265
266
  profile,
266
267
  });
267
268
  } else {
268
269
  const command = input && typeof input === "object" ? (input as { command?: unknown }).command : undefined;
269
270
  if (typeof command !== "string") return { block: "Bash safety check requires a complete command string or process proposal" };
270
- policy = analyzeCheckPolicy({ command, cwd, profile, allowedPaths: additionalPaths });
271
+ policy = analyzeCheckPolicy({ command, cwd, profile, allowedPaths: additionalPaths, protectedPaths: [AGENT_DIR] });
271
272
  }
272
273
  bash = policy.analysis;
273
274
  if (!bash.complete || bash.truncated || !bash.syntaxBalanced) {
@@ -316,6 +317,7 @@ export async function prepareCheck(
316
317
  decision: policy.decision,
317
318
  reason: policy.reason,
318
319
  findings: policy.findings,
320
+ accesses: policy.accesses,
319
321
  } : undefined,
320
322
  shell: processProposal ? undefined : bash,
321
323
  process: processProposal ? {
@@ -379,6 +381,7 @@ export async function prepareCheck(
379
381
  substitutionCount: bash.substitutions.length,
380
382
  mutationIntent: bash.mutationIntent,
381
383
  errors: bash.errors,
384
+ accesses: policy?.accesses,
382
385
  } : undefined,
383
386
  process: processProposal ? {
384
387
  source: processProposal.source,
@@ -1,7 +1,7 @@
1
1
  import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
2
2
  import { createHash } from "node:crypto";
3
3
  import { lstat, readFile, realpath } from "node:fs/promises";
4
- import { basename, dirname, relative, resolve, sep } from "node:path";
4
+ import { basename, dirname, parse, relative, resolve, sep } from "node:path";
5
5
  import { previewApplyPatch } from "./apply-patch";
6
6
  import type { CheckedToolName } from "./check-approvals";
7
7
  import {
@@ -85,6 +85,11 @@ async function validateEditPath(
85
85
  const roots = await Promise.all([projectRoot, ...allowedPaths].map((path) => realpath(path)));
86
86
  const absolute = resolve(projectRoot, inputPath);
87
87
  const sortedRoots = roots.sort((first, second) => second.length - first.length);
88
+ if (process.platform === "win32" && windowsAbsolute(inputPath)) {
89
+ const targetRoot = parse(absolute).root.toLowerCase();
90
+ const hasAllowedVolume = sortedRoots.some((candidate) => parse(candidate).root.toLowerCase() === targetRoot);
91
+ if (!hasAllowedVolume) throw new Error(`Edit path is outside the allowed Check mode paths: ${inputPath}`);
92
+ }
88
93
  let root = sortedRoots.find((candidate) => isPathInsideOrSame(candidate, absolute));
89
94
  if (!root) {
90
95
  let targetIdentity: string;