@oh-my-pi/pi-coding-agent 16.1.8 → 16.1.10

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 (68) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/cli.js +4775 -3942
  3. package/dist/types/cli/flag-tables.d.ts +17 -0
  4. package/dist/types/cli-commands.d.ts +4 -2
  5. package/dist/types/config/settings-schema.d.ts +10 -0
  6. package/dist/types/edit/streaming.d.ts +5 -5
  7. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  8. package/dist/types/modes/controllers/selector-controller.d.ts +0 -1
  9. package/dist/types/modes/interactive-mode.d.ts +1 -1
  10. package/dist/types/modes/types.d.ts +1 -1
  11. package/dist/types/sdk.d.ts +1 -0
  12. package/dist/types/session/agent-session.d.ts +13 -0
  13. package/dist/types/system-prompt.d.ts +2 -0
  14. package/dist/types/tools/browser/aria/aria-snapshot.d.ts +39 -0
  15. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -0
  16. package/dist/types/tools/browser/launch.d.ts +5 -0
  17. package/dist/types/tools/browser.d.ts +1 -0
  18. package/dist/types/tools/find.d.ts +1 -2
  19. package/dist/types/tools/write.d.ts +1 -2
  20. package/package.json +12 -12
  21. package/scripts/generate-aria-snapshot.ts +134 -0
  22. package/src/cli/args.ts +0 -1
  23. package/src/cli/flag-tables.ts +42 -0
  24. package/src/cli/profile-bootstrap.ts +1 -11
  25. package/src/cli-commands.ts +48 -3
  26. package/src/config/model-registry.ts +6 -7
  27. package/src/config/settings-schema.ts +12 -0
  28. package/src/edit/streaming.ts +5 -5
  29. package/src/internal-urls/docs-index.generated.txt +1 -1
  30. package/src/lsp/client.ts +39 -25
  31. package/src/modes/controllers/command-controller.ts +18 -3
  32. package/src/modes/controllers/event-controller.ts +20 -0
  33. package/src/modes/controllers/mcp-command-controller.ts +40 -12
  34. package/src/modes/controllers/selector-controller.ts +3 -5
  35. package/src/modes/interactive-mode.ts +1 -1
  36. package/src/modes/types.ts +1 -1
  37. package/src/prompts/system/project-prompt.md +2 -0
  38. package/src/prompts/system/system-prompt.md +0 -1
  39. package/src/prompts/tools/browser.md +2 -0
  40. package/src/sdk.ts +8 -1
  41. package/src/session/agent-session.ts +36 -1
  42. package/src/slash-commands/builtin-registry.ts +24 -66
  43. package/src/system-prompt.ts +15 -3
  44. package/src/tools/bash.ts +3 -3
  45. package/src/tools/browser/aria/aria-snapshot.bundle.txt +7 -0
  46. package/src/tools/browser/aria/aria-snapshot.ts +103 -0
  47. package/src/tools/browser/cmux/cmux-tab.ts +36 -1
  48. package/src/tools/browser/launch.ts +107 -31
  49. package/src/tools/browser/tab-worker.ts +89 -17
  50. package/src/tools/browser.ts +5 -0
  51. package/src/tools/find.ts +1 -2
  52. package/src/tools/index.ts +1 -1
  53. package/src/tools/puppeteer/00_stealth_tampering.txt +16 -35
  54. package/src/tools/puppeteer/01_stealth_activity.txt +79 -19
  55. package/src/tools/puppeteer/02_stealth_hairline.txt +57 -11
  56. package/src/tools/puppeteer/03_stealth_botd.txt +10 -14
  57. package/src/tools/puppeteer/04_stealth_iframe.txt +156 -63
  58. package/src/tools/puppeteer/05_stealth_webgl.txt +222 -64
  59. package/src/tools/puppeteer/06_stealth_screen.txt +255 -67
  60. package/src/tools/puppeteer/07_stealth_fonts.txt +17 -15
  61. package/src/tools/puppeteer/08_stealth_audio.txt +32 -20
  62. package/src/tools/puppeteer/09_stealth_locale.txt +45 -40
  63. package/src/tools/puppeteer/10_stealth_plugins.txt +12 -8
  64. package/src/tools/puppeteer/11_stealth_hardware.txt +57 -6
  65. package/src/tools/puppeteer/12_stealth_codecs.txt +20 -18
  66. package/src/tools/puppeteer/13_stealth_worker.txt +206 -45
  67. package/src/tools/write.ts +1 -2
  68. package/src/utils/git.ts +3 -2
package/src/lsp/client.ts CHANGED
@@ -327,9 +327,45 @@ async function startMessageReader(client: LspClient): Promise<void> {
327
327
  try {
328
328
  const message: LspJsonRpcResponse | LspJsonRpcNotification = JSON.parse(messageText);
329
329
 
330
- // Route message
331
- if ("id" in message && message.id !== undefined) {
332
- // Response to a request
330
+ // Route message. A JSON-RPC message carrying a `method` is always
331
+ // server-originated: a request when it also has an `id`, a
332
+ // notification otherwise. A message with only an `id` is a response
333
+ // to one of our requests. Disambiguate on `method` FIRST: a
334
+ // server's request ids live in its own id space and routinely
335
+ // collide with our in-flight client request ids (e.g. a
336
+ // basedpyright `workspace/configuration` pull arriving while a
337
+ // `documentSymbol` request with the same id is pending). Matching
338
+ // pending requests first would swallow that pull as a bogus
339
+ // response -- dropping the config answer the server blocks on and
340
+ // resolving our request with `undefined`, wedging the lazy
341
+ // cold-start handshake (#3001).
342
+ if ("method" in message) {
343
+ if ("id" in message && message.id !== undefined) {
344
+ // Server-initiated request: must be answered.
345
+ await handleServerRequest(client, message as LspJsonRpcRequest);
346
+ } else {
347
+ // Server notification
348
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
349
+ const params = message.params as PublishDiagnosticsParams;
350
+ client.diagnostics.set(params.uri, {
351
+ diagnostics: params.diagnostics,
352
+ version: params.version ?? null,
353
+ });
354
+ client.diagnosticsVersion += 1;
355
+ } else if (message.method === "$/progress" && message.params) {
356
+ const params = message.params as { token: string | number; value?: { kind?: string } };
357
+ if (params.value?.kind === "begin") {
358
+ client.activeProgressTokens.add(params.token);
359
+ } else if (params.value?.kind === "end") {
360
+ client.activeProgressTokens.delete(params.token);
361
+ if (client.activeProgressTokens.size === 0) {
362
+ client.resolveProjectLoaded();
363
+ }
364
+ }
365
+ }
366
+ }
367
+ } else if ("id" in message && message.id !== undefined) {
368
+ // Response to one of our requests.
333
369
  const pending = client.pendingRequests.get(message.id);
334
370
  if (pending) {
335
371
  client.pendingRequests.delete(message.id);
@@ -338,28 +374,6 @@ async function startMessageReader(client: LspClient): Promise<void> {
338
374
  } else {
339
375
  pending.resolve(message.result);
340
376
  }
341
- } else if ("method" in message) {
342
- await handleServerRequest(client, message as LspJsonRpcRequest);
343
- }
344
- } else if ("method" in message) {
345
- // Server notification
346
- if (message.method === "textDocument/publishDiagnostics" && message.params) {
347
- const params = message.params as PublishDiagnosticsParams;
348
- client.diagnostics.set(params.uri, {
349
- diagnostics: params.diagnostics,
350
- version: params.version ?? null,
351
- });
352
- client.diagnosticsVersion += 1;
353
- } else if (message.method === "$/progress" && message.params) {
354
- const params = message.params as { token: string | number; value?: { kind?: string } };
355
- if (params.value?.kind === "begin") {
356
- client.activeProgressTokens.add(params.token);
357
- } else if (params.value?.kind === "end") {
358
- client.activeProgressTokens.delete(params.token);
359
- if (client.activeProgressTokens.size === 0) {
360
- client.resolveProjectLoaded();
361
- }
362
- }
363
377
  }
364
378
  }
365
379
  } catch (err) {
@@ -85,15 +85,30 @@ export class CommandController {
85
85
  }
86
86
  }
87
87
 
88
- handleDumpCommand() {
88
+ async handleDumpCommand(): Promise<void> {
89
89
  try {
90
90
  const formatted = this.ctx.session.formatSessionAsText();
91
91
  if (!formatted) {
92
92
  this.ctx.showError("No messages to dump yet.");
93
93
  return;
94
94
  }
95
- copyToClipboard(formatted);
96
- this.ctx.showStatus("Session copied to clipboard");
95
+ // Build the LLM request JSON sidecar first so its path (and a
96
+ // raw-context warning) can be appended to the copied transcript.
97
+ let sidecarPath: string | undefined;
98
+ let sidecarError: string | undefined;
99
+ try {
100
+ sidecarPath = await this.ctx.session.dumpLlmRequestToTmpDir();
101
+ } catch (error: unknown) {
102
+ sidecarError = error instanceof Error ? error.message : "Unknown error";
103
+ }
104
+ const doc = sidecarPath
105
+ ? `${formatted}\n\n---\nLLM request JSON: ${sidecarPath}\nThis file persists on disk and may contain raw context/secrets — treat accordingly.`
106
+ : formatted;
107
+ await copyToClipboard(doc);
108
+ const statusParts = ["Session copied to clipboard"];
109
+ if (sidecarPath) statusParts.push(`LLM request JSON: ${sidecarPath}`);
110
+ if (sidecarError) statusParts.push(`LLM request JSON unavailable: ${sidecarError}`);
111
+ this.ctx.showStatus(statusParts.join("\n"));
97
112
  } catch (error: unknown) {
98
113
  this.ctx.showError(`Failed to copy session: ${error instanceof Error ? error.message : "Unknown error"}`);
99
114
  }
@@ -738,6 +738,26 @@ export class EventController {
738
738
  this.ctx.chatContainer.addChild(component);
739
739
  this.ctx.pendingTools.set(event.toolCallId, component);
740
740
  this.ctx.ui.requestRender();
741
+ } else {
742
+ // The tool is about to run, so its arguments are final and validated.
743
+ // A pending component created while args streamed (message_update) may
744
+ // still show a mid-reveal prefix — or, when the closing full-args
745
+ // `message_update` never lands (smooth-streaming off leaving the
746
+ // throttled `arguments` stale, an owned-dialect projector, or a
747
+ // superseded/aborted turn that still executes the call), a stale body
748
+ // the result render then freezes at its `…` placeholder. Reconcile the
749
+ // authoritative args here and drop any live reveal so a late tick can't
750
+ // re-truncate them: tool_execution_start is the one event every
751
+ // execution path emits with the full args immediately before the result.
752
+ this.#toolArgsReveal.finish(event.toolCallId);
753
+ const component = this.ctx.pendingTools.get(event.toolCallId);
754
+ if (component && typeof component.updateArgs === "function") {
755
+ component.updateArgs(event.args, event.toolCallId);
756
+ if (typeof component.setArgsComplete === "function") {
757
+ component.setArgsComplete(event.toolCallId);
758
+ }
759
+ this.ctx.ui.requestRender();
760
+ }
741
761
  }
742
762
  }
743
763
 
@@ -8,7 +8,7 @@ import { type Component, replaceTabs, Spacer, Text } from "@oh-my-pi/pi-tui";
8
8
  import { getMCPConfigPath, getProjectDir } from "@oh-my-pi/pi-utils";
9
9
  import type { SourceMeta } from "../../capability/types";
10
10
  import { expandEnvVarsDeep } from "../../discovery/helpers";
11
- import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
11
+ import { analyzeAuthError, discoverOAuthEndpoints, loadAllMCPConfigs, MCPManager } from "../../mcp";
12
12
  import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
13
13
  import {
14
14
  addMCPServer,
@@ -1383,7 +1383,7 @@ export class MCPCommandController {
1383
1383
  }
1384
1384
  await setServerDisabled(userConfigPath, name, !enabled);
1385
1385
  if (enabled) {
1386
- await this.#reloadMCP();
1386
+ await this.#connectEnabledMCPServer(name);
1387
1387
  const state = await this.#waitForServerConnectionWithAnimation(name);
1388
1388
  const status =
1389
1389
  state === "connected"
@@ -1419,7 +1419,12 @@ export class MCPCommandController {
1419
1419
 
1420
1420
  const updated: MCPServerConfig = { ...found.config, enabled };
1421
1421
  await updateMCPServer(found.filePath, name, updated);
1422
- await this.#reloadMCP();
1422
+ if (enabled) {
1423
+ await this.#connectEnabledMCPServer(name);
1424
+ } else {
1425
+ await this.ctx.mcpManager?.disconnectServer(name);
1426
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager?.getTools() ?? []);
1427
+ }
1423
1428
 
1424
1429
  let status = "";
1425
1430
  if (enabled) {
@@ -1671,6 +1676,37 @@ export class MCPCommandController {
1671
1676
  }
1672
1677
  }
1673
1678
 
1679
+ async #connectEnabledMCPServer(name: string): Promise<void> {
1680
+ if (!this.ctx.mcpManager) {
1681
+ return;
1682
+ }
1683
+
1684
+ const { configs, sources } = await loadAllMCPConfigs(getProjectDir());
1685
+ const config = configs[name];
1686
+ if (!config) {
1687
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1688
+ return;
1689
+ }
1690
+
1691
+ const source = sources[name];
1692
+ const result = await this.ctx.mcpManager.connectServers({ [name]: config }, source ? { [name]: source } : {});
1693
+ await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1694
+ this.#showMCPConnectionErrors(result.errors);
1695
+ }
1696
+
1697
+ #showMCPConnectionErrors(errors: Map<string, string>): void {
1698
+ if (errors.size === 0) {
1699
+ return;
1700
+ }
1701
+
1702
+ const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
1703
+ for (const [serverName, error] of errors.entries()) {
1704
+ errorLines.push(` ${serverName}: ${error}`);
1705
+ }
1706
+ errorLines.push("");
1707
+ this.#showMessage(errorLines.join("\n"));
1708
+ }
1709
+
1674
1710
  /**
1675
1711
  * Reload MCP manager with new configs
1676
1712
  */
@@ -1686,15 +1722,7 @@ export class MCPCommandController {
1686
1722
  const result = await this.ctx.mcpManager.discoverAndConnect();
1687
1723
  await this.ctx.session.refreshMCPTools(this.ctx.mcpManager.getTools());
1688
1724
 
1689
- // Show any connection errors
1690
- if (result.errors.size > 0) {
1691
- const errorLines = ["", theme.fg("warning", "Some servers failed to connect:"), ""];
1692
- for (const [serverName, error] of result.errors.entries()) {
1693
- errorLines.push(` ${serverName}: ${error}`);
1694
- }
1695
- errorLines.push("");
1696
- this.#showMessage(errorLines.join("\n"));
1697
- }
1725
+ this.#showMCPConnectionErrors(result.errors);
1698
1726
  }
1699
1727
 
1700
1728
  /**
@@ -71,10 +71,6 @@ import { buildCopyTargets } from "../utils/copy-targets";
71
71
 
72
72
  const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
73
73
 
74
- export function formatTemporaryModelStatus(modelLabel: string, roleSelectorHint = "Alt+M"): string {
75
- return `Temporary model selection is session-only: ${modelLabel}. Use ${roleSelectorHint} or /model for role models (default/smol/plan/task/slow/custom roles).`;
76
- }
77
-
78
74
  export class SelectorController {
79
75
  constructor(private ctx: InteractiveModeContext) {}
80
76
 
@@ -499,7 +495,9 @@ export class SelectorController {
499
495
  this.ctx.statusLine.invalidate();
500
496
  this.ctx.updateEditorBorderColor();
501
497
  const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
502
- this.ctx.showStatus(formatTemporaryModelStatus(selector ?? model.id, roleSelectorHint));
498
+ this.ctx.showStatus(
499
+ `Session-only model: ${selector ?? model.id}. Use ${roleSelectorHint} or /model for roles.`,
500
+ );
503
501
  done();
504
502
  this.ctx.ui.requestRender();
505
503
  } else if (role === "default") {
@@ -3491,7 +3491,7 @@ export class InteractiveMode implements InteractiveModeContext {
3491
3491
  return this.#commandController.handleExportCommand(text);
3492
3492
  }
3493
3493
 
3494
- handleDumpCommand() {
3494
+ async handleDumpCommand(): Promise<void> {
3495
3495
  return this.#commandController.handleDumpCommand();
3496
3496
  }
3497
3497
 
@@ -290,7 +290,7 @@ export interface InteractiveModeContext {
290
290
  handleHotkeysCommand(): void;
291
291
  handleToolsCommand(): void;
292
292
  handleContextCommand(): void;
293
- handleDumpCommand(): void;
293
+ handleDumpCommand(): Promise<void>;
294
294
  handleAdvisorDumpCommand(isRaw?: boolean): void;
295
295
  handleDebugTranscriptCommand(): Promise<void>;
296
296
  handleClearCommand(): Promise<void>;
@@ -29,6 +29,7 @@ Before making changes within these directories, you MUST read:
29
29
  The context files above are loaded automatically. You NEVER `search`/`find` for `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, or similar agent/context files — the relevant ones are already in your context; any others are noise.
30
30
  {{/ifAny}}
31
31
 
32
+ {{#if includeWorkspaceTree}}
32
33
  {{#if workspaceTree.rendered}}
33
34
  <workspace-tree>
34
35
  Working directory layout (sorted by mtime, recent first; depth ≤ 3):
@@ -38,6 +39,7 @@ Working directory layout (sorted by mtime, recent first; depth ≤ 3):
38
39
  {{/if}}
39
40
  </workspace-tree>
40
41
  {{/if}}
42
+ {{/if}}
41
43
 
42
44
  Today is {{date}}, and the current working directory is '{{cwd}}'.
43
45
 
@@ -17,7 +17,6 @@ You are a helpful assistant the team trusts with load-bearing changes, operating
17
17
  - You are not alone in this repo. Treat unexpected changes as the user's work and adapt.
18
18
  - In terminal prose and final chat, you MAY use LaTeX math (`$`, `$$`, `\text`, `\times`) and color (`\textcolor`, `\colorbox`, `\fcolorbox`).
19
19
  - To show a diagram, you MAY emit a ` ```mermaid ` block — the terminal renders it as ASCII. Use it for genuine structure or flow, not trivia.
20
- - For a visual separator between sections, use `─` (U+2500).
21
20
 
22
21
  RUNTIME
23
22
  ==============
@@ -15,6 +15,8 @@ Drives real Chromium tab; full puppeteer access via JS.
15
15
  - `tab` helpers; drop to raw puppeteer `page` for anything uncovered:
16
16
  - `tab.goto(url, { waitUntil? })` — navigate.
17
17
  - `tab.observe({ includeAll?, viewportOnly? })` — accessibility snapshot: `{ url, title, viewport, scroll, elements: [{ id, role, name, value, states, … }] }`. Ids stable until next observe/goto.
18
+ - `tab.ariaSnapshot(selector?, { depth?, boxes? })` — Playwright-format ARIA-tree YAML (nested roles + accessible names + `/url`/`/placeholder`), scoped to `selector` or the whole document. Every node carries a `[ref=eN]` id; `[cursor=pointer]` flags clickables. Captures dense, hierarchical structure/text that `observe()`'s flat list flattens away. Refs renumber from e1 each call and stay valid until the next `ariaSnapshot()`.
19
+ - `tab.ref("e5")` — `[ref=eN]` from the last ariaSnapshot → element handle with the common action methods (`.click()`, `.type()`, `.fill()`, `.hover()`, `.evaluate()`, …); the primary way to act on a ref. For convenience `aria-ref=e5` also works inline in `tab.click`/`type`/`fill`/`waitFor`/`scrollIntoView` (e.g. `tab.click("aria-ref=e5")`).
18
20
  - `tab.id(n)` — id from last observe → `ElementHandle` (`.click()`, `.type()`, …).
19
21
  - `tab.click(selector)` / `tab.type(selector, text)` / `tab.fill(selector, value)` / `tab.press(key, { selector? })` / `tab.scroll(dx, dy)`.
20
22
  - `tab.waitFor(selector)` — wait until attached; returns `ElementHandle`.
package/src/sdk.ts CHANGED
@@ -796,6 +796,7 @@ export interface BuildSystemPromptOptions {
796
796
  customPrompt?: string;
797
797
  appendPrompt?: string;
798
798
  inlineToolDescriptors?: boolean;
799
+ includeWorkspaceTree?: boolean;
799
800
  }
800
801
 
801
802
  /**
@@ -813,6 +814,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
813
814
  contextFiles: options.contextFiles,
814
815
  appendSystemPrompt: options.appendPrompt,
815
816
  inlineToolDescriptors: options.inlineToolDescriptors,
817
+ includeWorkspaceTree: options.includeWorkspaceTree,
816
818
  toolNames: options.tools?.map(tool => tool.name),
817
819
  tools: toolMap ? buildSystemPromptToolMetadata(toolMap) : undefined,
818
820
  });
@@ -1120,9 +1122,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1120
1122
  // startup does not perform a second recursive filesystem search. Subagents
1121
1123
  // inherit the parent's resolved values via options.
1122
1124
  const STARTUP_SCAN_DEADLINE_MS = 5000;
1125
+ const includeWorkspaceTree = settings.get("includeWorkspaceTree") ?? false;
1123
1126
  const workspaceTreePromise: Promise<WorkspaceTree> = options.workspaceTree
1124
1127
  ? Promise.resolve(options.workspaceTree)
1125
- : logger.time("buildWorkspaceTree", () => buildWorkspaceTree(cwd, { timeoutMs: STARTUP_SCAN_DEADLINE_MS }));
1128
+ : includeWorkspaceTree
1129
+ ? logger.time("buildWorkspaceTree", () => buildWorkspaceTree(cwd, { timeoutMs: STARTUP_SCAN_DEADLINE_MS }))
1130
+ : Promise.resolve({ rootPath: cwd, rendered: "", truncated: false, totalLines: 0, agentsMdFiles: [] });
1126
1131
  workspaceTreePromise.catch(() => {});
1127
1132
 
1128
1133
  // Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
@@ -2101,6 +2106,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2101
2106
  const eagerTasks = settings.get("task.eager") !== "default";
2102
2107
  const eagerTasksAlways = settings.get("task.eager") === "always";
2103
2108
  const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
2109
+ const includeWorkspaceTree = settings.get("includeWorkspaceTree") ?? false;
2104
2110
  const rebuildSystemPrompt = async (
2105
2111
  toolNames: string[],
2106
2112
  tools: Map<string, AgentTool>,
@@ -2194,6 +2200,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2194
2200
  taskBatch: settings.get("task.batch"),
2195
2201
  secretsEnabled,
2196
2202
  workspaceTree: workspaceTreePromise,
2203
+ includeWorkspaceTree,
2197
2204
  memoryRootEnabled: memoryBackend.id === "local",
2198
2205
  model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
2199
2206
  personality: agentKind === "sub" ? "none" : settings.get("personality"),
@@ -103,7 +103,7 @@ import {
103
103
  resolveServiceTier,
104
104
  streamSimple,
105
105
  } from "@oh-my-pi/pi-ai";
106
- import { stripToolDescriptions } from "@oh-my-pi/pi-ai/utils/schema";
106
+ import { stripToolDescriptions, toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
107
107
  import { THINKING_LOOP_ERROR_MARKER } from "@oh-my-pi/pi-ai/utils/thinking-loop";
108
108
  import { isFireworksFastModelId, toFireworksBaseModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
109
109
  import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
@@ -12606,6 +12606,41 @@ export class AgentSession {
12606
12606
  });
12607
12607
  }
12608
12608
 
12609
+ /**
12610
+ * Dump the current session's LLM-facing request context as JSON to a
12611
+ * auto-named file in `os.tmpdir()`. This is the synchronous
12612
+ * `convertToLlm`-boundary snapshot — system prompt, tools (wire schemas),
12613
+ * thinking/service tier, and converted messages — with no network round-trip
12614
+ * and no arming flag, so advisor/side requests cannot intercept it.
12615
+ *
12616
+ * The file persists on disk and may contain the same raw context/secrets
12617
+ * as `/dump`; treat the path accordingly.
12618
+ *
12619
+ * @returns the written file path, or `undefined` when there are no messages.
12620
+ */
12621
+ async dumpLlmRequestToTmpDir(): Promise<string | undefined> {
12622
+ const messages = this.messages;
12623
+ if (messages.length === 0) return undefined;
12624
+ const llmMessages = await this.convertMessagesToLlm(messages);
12625
+ const payload = {
12626
+ model: this.agent.state.model ?? null,
12627
+ thinkingLevel: this.#thinkingLevel ?? null,
12628
+ serviceTier: this.agent.serviceTier ?? null,
12629
+ systemPrompt: this.agent.state.systemPrompt,
12630
+ tools: this.agent.state.tools.map(tool => ({
12631
+ name: tool.name,
12632
+ description: tool.description,
12633
+ parameters: toolWireSchema(tool),
12634
+ ...(tool.strict !== undefined ? { strict: tool.strict } : {}),
12635
+ ...(tool.customWireName ? { customWireName: tool.customWireName } : {}),
12636
+ })),
12637
+ messages: llmMessages,
12638
+ };
12639
+ const filePath = path.join(os.tmpdir(), `omp-llm-request-${Snowflake.next()}.json`);
12640
+ await Bun.write(filePath, `${JSON.stringify(payload, null, 2)}\n`);
12641
+ return filePath;
12642
+ }
12643
+
12609
12644
  /**
12610
12645
  * Enable or disable the advisor for this session. The setting is overridden for the session,
12611
12646
  * and the runtime is started or stopped to match.
@@ -1,8 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
4
3
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
5
- import { setNextRequestDebugPath } from "@oh-my-pi/pi-ai/utils/request-debug";
6
4
  import { type AutocompleteItem, Spacer } from "@oh-my-pi/pi-tui";
7
5
  import { APP_NAME, setProjectDir } from "@oh-my-pi/pi-utils";
8
6
  import { COLLAB_GUEST_ALLOWED_COMMANDS, CollabGuestLink } from "../collab/guest";
@@ -192,46 +190,6 @@ async function handleUsageResetCommand(
192
190
  await output(describeRedeemOutcome(outcome, target.label));
193
191
  }
194
192
 
195
- const DEBUG_DUMP_NEXT_REQUEST_USAGE = "Usage: /debug dump-next-request <path>";
196
-
197
- function resolveDebugRequestDumpPath(target: string, cwd: string): string {
198
- const expanded =
199
- target === "~"
200
- ? os.homedir()
201
- : target.startsWith("~/") || target.startsWith("~\\")
202
- ? path.join(os.homedir(), target.slice(2))
203
- : target;
204
- return path.resolve(cwd, expanded);
205
- }
206
-
207
- async function handleDebugSubcommand(
208
- args: string,
209
- cwd: string,
210
- output: (text: string) => Promise<void> | void,
211
- ): Promise<SlashCommandResult> {
212
- const { verb, rest } = parseSubcommand(args);
213
- switch (verb) {
214
- case "":
215
- await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
216
- return commandConsumed();
217
- case "dump-next-request":
218
- case "dump-request":
219
- case "next-request": {
220
- if (!rest) {
221
- await output(DEBUG_DUMP_NEXT_REQUEST_USAGE);
222
- return commandConsumed();
223
- }
224
- const requestPath = resolveDebugRequestDumpPath(rest, cwd);
225
- setNextRequestDebugPath(requestPath);
226
- await output(`Next AI provider request will be dumped to ${requestPath}`);
227
- return commandConsumed();
228
- }
229
- default:
230
- await output(`Unknown /debug subcommand "${verb}". ${DEBUG_DUMP_NEXT_REQUEST_USAGE}`);
231
- return commandConsumed();
232
- }
233
- }
234
-
235
193
  /** Parse the `/shake` subcommand into a {@link ShakeMode}; empty defaults to elide. */
236
194
  function parseShakeMode(args: string): ShakeMode | { error: string } {
237
195
  const verb = args.trim().toLowerCase();
@@ -612,16 +570,33 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
612
570
  },
613
571
  {
614
572
  name: "dump",
615
- description: "Copy session transcript to clipboard",
616
- acpDescription: "Return full transcript as plain text",
573
+ description: "Copy session transcript to clipboard (and write LLM request JSON to tmp)",
574
+ acpDescription: "Return full transcript as plain text, with LLM request JSON path",
617
575
  allowArgs: true,
618
576
  handle: async (_command, runtime) => {
619
577
  const text = runtime.session.formatSessionAsText();
620
- await runtime.output(text || "No messages to dump yet.");
578
+ if (!text) {
579
+ await runtime.output("No messages to dump yet.");
580
+ return commandConsumed();
581
+ }
582
+ let sidecarPath: string | undefined;
583
+ try {
584
+ sidecarPath = await runtime.session.dumpLlmRequestToTmpDir();
585
+ } catch {
586
+ // Sidecar is best-effort; the transcript is still output below.
587
+ }
588
+ const lines = [text];
589
+ if (sidecarPath)
590
+ lines.push(
591
+ "",
592
+ `LLM request JSON: ${sidecarPath}`,
593
+ "This file persists on disk and may contain raw context/secrets — treat accordingly.",
594
+ );
595
+ await runtime.output(lines.join("\n"));
621
596
  return commandConsumed();
622
597
  },
623
- handleTui: (_command, runtime) => {
624
- runtime.ctx.handleDumpCommand();
598
+ handleTui: async (_command, runtime) => {
599
+ await runtime.ctx.handleDumpCommand();
625
600
  runtime.ctx.editor.setText("");
626
601
  },
627
602
  },
@@ -1509,25 +1484,8 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
1509
1484
  {
1510
1485
  name: "debug",
1511
1486
  description: "Open debug tools selector",
1512
- allowArgs: true,
1513
- subcommands: [
1514
- {
1515
- name: "dump-next-request",
1516
- description: "Dump the next AI provider HTTP request as JSON",
1517
- usage: "<path>",
1518
- },
1519
- ],
1520
- handle: async (command, runtime) =>
1521
- handleDebugSubcommand(command.args, runtime.cwd, text => runtime.output(text)),
1522
- handleTui: async (command, runtime) => {
1523
- const args = command.args.trim();
1524
- if (args.length === 0) {
1525
- runtime.ctx.showDebugSelector();
1526
- } else {
1527
- await handleDebugSubcommand(args, runtime.ctx.sessionManager.getCwd(), text =>
1528
- runtime.ctx.showStatus(text),
1529
- );
1530
- }
1487
+ handleTui: async (_command, runtime) => {
1488
+ await runtime.ctx.showDebugSelector();
1531
1489
  runtime.ctx.editor.setText("");
1532
1490
  },
1533
1491
  },
@@ -421,6 +421,8 @@ export interface BuildSystemPromptOptions {
421
421
  model?: string;
422
422
  /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
423
423
  personality?: Personality;
424
+ /** Whether to include the workspace directory tree in the system prompt. Default: false */
425
+ includeWorkspaceTree?: boolean;
424
426
  }
425
427
 
426
428
  /** Result of building provider-facing system prompt messages. */
@@ -461,6 +463,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
461
463
  memoryRootEnabled = false,
462
464
  model,
463
465
  personality = "default",
466
+ includeWorkspaceTree = false,
464
467
  } = options;
465
468
  const inlineToolDescriptors = providedInlineToolDescriptors ?? false;
466
469
  const resolvedCwd = cwd ?? getProjectDir();
@@ -523,9 +526,17 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
523
526
  const workspaceTreePromise =
524
527
  providedWorkspaceTree !== undefined
525
528
  ? Promise.resolve(providedWorkspaceTree)
526
- : logger.time("buildWorkspaceTree", () =>
527
- buildWorkspaceTree(resolvedCwd, { timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS }),
528
- );
529
+ : includeWorkspaceTree
530
+ ? logger.time("buildWorkspaceTree", () =>
531
+ buildWorkspaceTree(resolvedCwd, { timeoutMs: SYSTEM_PROMPT_PREP_TIMEOUT_MS }),
532
+ )
533
+ : Promise.resolve({
534
+ rootPath: resolvedCwd,
535
+ rendered: "",
536
+ truncated: false,
537
+ totalLines: 0,
538
+ agentsMdFiles: [],
539
+ });
529
540
  const skillsPromise: Promise<Skill[]> =
530
541
  providedSkills !== undefined
531
542
  ? Promise.resolve(providedSkills)
@@ -670,6 +681,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
670
681
  secretsEnabled,
671
682
  hasMemoryRoot: memoryRootEnabled,
672
683
  hasObsidian: hasObsidian(),
684
+ includeWorkspaceTree,
673
685
  };
674
686
  const rendered = prompt.render(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
675
687
  const systemPrompt = [rendered];
package/src/tools/bash.ts CHANGED
@@ -1132,9 +1132,9 @@ function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
1132
1132
  }
1133
1133
 
1134
1134
  export function getBashEnvForDisplay(args: BashRenderArgs): Record<string, string> | undefined {
1135
- // During streaming, partial-json parsing often does not surface env values until the object closes.
1136
- // Recover them from the raw JSON buffer so the pending bash preview can show `NAME="..." cmd` immediately,
1137
- // instead of rendering only the command and making the env assignment appear at the very end.
1135
+ // The parsed args don't always mirror the exact current stream prefix, so recover
1136
+ // env from the raw JSON buffer to surface `NAME="..." cmd` in the preview as it
1137
+ // streams rather than only once the args object finishes.
1138
1138
  const partialEnv = extractPartialBashEnv(args.__partialJson);
1139
1139
  if (partialEnv && args.env) return { ...partialEnv, ...args.env };
1140
1140
  return args.env ?? partialEnv;