@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.0

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 (192) hide show
  1. package/CHANGELOG.md +98 -1
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/gallery-cli.d.ts +43 -0
  4. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  5. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  6. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  8. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  9. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  10. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  11. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  12. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  15. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  16. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  17. package/dist/types/commands/gallery.d.ts +47 -0
  18. package/dist/types/config/keybindings.d.ts +10 -2
  19. package/dist/types/config/model-id-affixes.d.ts +2 -0
  20. package/dist/types/config/model-registry.d.ts +8 -1
  21. package/dist/types/config/settings-schema.d.ts +43 -7
  22. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  23. package/dist/types/eval/backend.d.ts +6 -6
  24. package/dist/types/eval/bridge-timeout.d.ts +27 -0
  25. package/dist/types/eval/idle-timeout.d.ts +16 -14
  26. package/dist/types/eval/js/executor.d.ts +3 -3
  27. package/dist/types/eval/py/executor.d.ts +2 -2
  28. package/dist/types/eval/py/spawn-options.d.ts +58 -0
  29. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  30. package/dist/types/lsp/types.d.ts +10 -0
  31. package/dist/types/main.d.ts +3 -2
  32. package/dist/types/memory-backend/index.d.ts +2 -1
  33. package/dist/types/memory-backend/resolve.d.ts +1 -1
  34. package/dist/types/memory-backend/types.d.ts +1 -1
  35. package/dist/types/modes/components/assistant-message.d.ts +5 -0
  36. package/dist/types/modes/components/copy-selector.d.ts +22 -0
  37. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  38. package/dist/types/modes/components/model-selector.d.ts +1 -0
  39. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
  42. package/dist/types/modes/index.d.ts +5 -4
  43. package/dist/types/modes/interactive-mode.d.ts +2 -2
  44. package/dist/types/modes/setup-version.d.ts +11 -0
  45. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  46. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/modes/utils/copy-targets.d.ts +53 -0
  49. package/dist/types/sdk.d.ts +1 -1
  50. package/dist/types/task/executor.d.ts +7 -0
  51. package/dist/types/telemetry-export.d.ts +1 -1
  52. package/dist/types/tools/eval-render.d.ts +1 -0
  53. package/dist/types/tools/fetch.d.ts +15 -7
  54. package/dist/types/tools/render-utils.d.ts +33 -0
  55. package/dist/types/tools/renderers.d.ts +16 -2
  56. package/dist/types/tools/search.d.ts +1 -1
  57. package/dist/types/tools/write.d.ts +2 -0
  58. package/dist/types/tui/code-cell.d.ts +6 -0
  59. package/dist/types/tui/output-block.d.ts +11 -0
  60. package/dist/types/web/scrapers/github.d.ts +22 -0
  61. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  62. package/dist/types/web/search/types.d.ts +1 -1
  63. package/package.json +9 -9
  64. package/scripts/dev-launch +42 -0
  65. package/scripts/dev-launch-preload.ts +19 -0
  66. package/src/autoresearch/dashboard.ts +11 -21
  67. package/src/cli/args.ts +2 -2
  68. package/src/cli/claude-trace-cli.ts +13 -1
  69. package/src/cli/gallery-cli.ts +223 -0
  70. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  71. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  72. package/src/cli/gallery-fixtures/edit.ts +194 -0
  73. package/src/cli/gallery-fixtures/fs.ts +153 -0
  74. package/src/cli/gallery-fixtures/index.ts +40 -0
  75. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  76. package/src/cli/gallery-fixtures/memory.ts +81 -0
  77. package/src/cli/gallery-fixtures/misc.ts +221 -0
  78. package/src/cli/gallery-fixtures/search.ts +213 -0
  79. package/src/cli/gallery-fixtures/shell.ts +167 -0
  80. package/src/cli/gallery-fixtures/types.ts +41 -0
  81. package/src/cli/gallery-fixtures/web.ts +158 -0
  82. package/src/cli/gallery-screenshot.ts +279 -0
  83. package/src/cli-commands.ts +1 -0
  84. package/src/commands/gallery.ts +52 -0
  85. package/src/commands/launch.ts +1 -1
  86. package/src/config/keybindings.ts +68 -2
  87. package/src/config/model-equivalence.ts +35 -12
  88. package/src/config/model-id-affixes.ts +39 -22
  89. package/src/config/model-registry.ts +16 -16
  90. package/src/config/settings-schema.ts +29 -6
  91. package/src/config/settings.ts +11 -0
  92. package/src/dap/client.ts +14 -16
  93. package/src/debug/raw-sse.ts +18 -4
  94. package/src/edit/file-snapshot-store.ts +1 -1
  95. package/src/edit/index.ts +1 -1
  96. package/src/edit/renderer.ts +43 -55
  97. package/src/edit/streaming.ts +1 -1
  98. package/src/eval/__tests__/agent-bridge.test.ts +102 -58
  99. package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
  100. package/src/eval/__tests__/idle-timeout.test.ts +26 -12
  101. package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
  102. package/src/eval/__tests__/llm-bridge.test.ts +10 -10
  103. package/src/eval/agent-bridge.ts +38 -12
  104. package/src/eval/backend.ts +6 -6
  105. package/src/eval/bridge-timeout.ts +44 -0
  106. package/src/eval/idle-timeout.ts +33 -15
  107. package/src/eval/js/executor.ts +10 -10
  108. package/src/eval/llm-bridge.ts +4 -5
  109. package/src/eval/py/executor.ts +6 -6
  110. package/src/eval/py/kernel.ts +11 -1
  111. package/src/eval/py/spawn-options.ts +126 -0
  112. package/src/export/ttsr.ts +9 -0
  113. package/src/extensibility/extensions/runner.ts +3 -0
  114. package/src/extensibility/plugins/doctor.ts +0 -1
  115. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  116. package/src/goals/tools/goal-tool.ts +2 -2
  117. package/src/internal-urls/docs-index.generated.ts +7 -6
  118. package/src/lsp/client.ts +179 -52
  119. package/src/lsp/index.ts +38 -4
  120. package/src/lsp/render.ts +3 -3
  121. package/src/lsp/types.ts +10 -0
  122. package/src/main.ts +47 -52
  123. package/src/memory-backend/index.ts +13 -1
  124. package/src/memory-backend/resolve.ts +3 -5
  125. package/src/memory-backend/types.ts +1 -1
  126. package/src/modes/components/agent-dashboard.ts +13 -4
  127. package/src/modes/components/assistant-message.ts +22 -1
  128. package/src/modes/components/copy-selector.ts +249 -0
  129. package/src/modes/components/custom-editor.ts +10 -1
  130. package/src/modes/components/extensions/extension-list.ts +17 -8
  131. package/src/modes/components/history-search.ts +19 -11
  132. package/src/modes/components/model-selector.ts +125 -29
  133. package/src/modes/components/oauth-selector.ts +28 -12
  134. package/src/modes/components/session-observer-overlay.ts +13 -15
  135. package/src/modes/components/session-selector.ts +24 -13
  136. package/src/modes/components/status-line.ts +3 -5
  137. package/src/modes/components/tool-execution.ts +83 -24
  138. package/src/modes/components/tree-selector.ts +19 -7
  139. package/src/modes/components/user-message-selector.ts +25 -14
  140. package/src/modes/controllers/command-controller.ts +13 -118
  141. package/src/modes/controllers/event-controller.ts +26 -10
  142. package/src/modes/controllers/input-controller.ts +11 -3
  143. package/src/modes/controllers/selector-controller.ts +40 -3
  144. package/src/modes/index.ts +5 -4
  145. package/src/modes/interactive-mode.ts +21 -7
  146. package/src/modes/setup-version.ts +11 -0
  147. package/src/modes/setup-wizard/index.ts +3 -2
  148. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  149. package/src/modes/theme/theme.ts +46 -10
  150. package/src/modes/types.ts +2 -2
  151. package/src/modes/utils/context-usage.ts +10 -6
  152. package/src/modes/utils/copy-targets.ts +254 -0
  153. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  154. package/src/prompts/tools/ast-edit.md +1 -1
  155. package/src/prompts/tools/ast-grep.md +1 -1
  156. package/src/prompts/tools/read.md +1 -1
  157. package/src/prompts/tools/search.md +1 -1
  158. package/src/sdk.ts +21 -23
  159. package/src/session/agent-session.ts +13 -9
  160. package/src/slash-commands/builtin-registry.ts +4 -12
  161. package/src/slash-commands/helpers/usage-report.ts +2 -0
  162. package/src/task/executor.ts +20 -2
  163. package/src/task/render.ts +37 -11
  164. package/src/telemetry-export.ts +25 -7
  165. package/src/tools/bash.ts +18 -8
  166. package/src/tools/browser/render.ts +5 -4
  167. package/src/tools/debug.ts +3 -3
  168. package/src/tools/eval-backends.ts +6 -17
  169. package/src/tools/eval-render.ts +28 -10
  170. package/src/tools/eval.ts +19 -23
  171. package/src/tools/fetch.ts +99 -89
  172. package/src/tools/read.ts +7 -7
  173. package/src/tools/render-utils.ts +63 -3
  174. package/src/tools/renderers.ts +16 -1
  175. package/src/tools/report-tool-issue.ts +1 -1
  176. package/src/tools/search.ts +173 -81
  177. package/src/tools/ssh.ts +21 -8
  178. package/src/tools/todo.ts +20 -7
  179. package/src/tools/write.ts +39 -9
  180. package/src/tui/code-cell.ts +19 -4
  181. package/src/tui/output-block.ts +14 -0
  182. package/src/web/scrapers/github.ts +255 -3
  183. package/src/web/scrapers/youtube.ts +3 -2
  184. package/src/web/search/providers/perplexity.ts +199 -51
  185. package/src/web/search/render.ts +42 -57
  186. package/src/web/search/types.ts +5 -1
  187. package/dist/types/eval/heartbeat.d.ts +0 -45
  188. package/src/eval/__tests__/heartbeat.test.ts +0 -84
  189. package/src/eval/__tests__/shared-executors.test.ts +0 -609
  190. package/src/eval/heartbeat.ts +0 -74
  191. /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
  192. /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
@@ -9,7 +9,7 @@ Searches files using powerful regex matching.
9
9
 
10
10
  <output>
11
11
  {{#if IS_HL_MODE}}
12
- - Text output emits a file snapshot tag header per matched file plus numbered lines: src/login.ts#1f`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
12
+ - Text output emits a file snapshot tag header per matched file plus numbered lines: `[src/login.ts#1A2B]`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
13
13
  {{else}}
14
14
  {{#if IS_LINE_NUMBER_MODE}}
15
15
  - Text output is line-number-prefixed
package/src/sdk.ts CHANGED
@@ -36,7 +36,6 @@ import {
36
36
  } from "@oh-my-pi/pi-utils";
37
37
  import chalk from "chalk";
38
38
  import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
39
- import { createAutoresearchExtension } from "./autoresearch";
40
39
  import { loadCapability } from "./capability";
41
40
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
42
41
  import { bucketRules } from "./capability/rule-buckets";
@@ -57,7 +56,6 @@ import { resolveConfigValue } from "./config/resolve-config-value";
57
56
  import { initializeWithSettings } from "./discovery";
58
57
  import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
59
58
  import { defaultEvalSessionId } from "./eval/session-id";
60
- import { TtsrManager } from "./export/ttsr";
61
59
  import {
62
60
  type CustomCommandsLoadResult,
63
61
  type LoadedCustomCommand,
@@ -90,7 +88,7 @@ import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls
90
88
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
91
89
  import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
92
90
  import { resolveMemoryBackend } from "./memory-backend";
93
- import { getMnemopiSessionState, type MnemopiSessionState } from "./mnemopi/state";
91
+ import type { MnemopiSessionState } from "./mnemopi/state";
94
92
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
95
93
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
96
94
  import {
@@ -1150,6 +1148,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1150
1148
 
1151
1149
  // Discover rules and bucket them in one pass to avoid repeated scans over large rule sets.
1152
1150
  const { ttsrManager, rulebookRules, alwaysApplyRules } = await logger.time("discoverTtsrRules", async () => {
1151
+ const { TtsrManager } = await import("./export/ttsr");
1153
1152
  const ttsrSettings = settings.getGroup("ttsr");
1154
1153
  const ttsrManager = new TtsrManager(ttsrSettings);
1155
1154
  const rulesResult =
@@ -1295,7 +1294,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1295
1294
  session ? session.trackEvalExecution(execution, abortController) : execution,
1296
1295
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
1297
1296
  getHindsightSessionState: () => session?.getHindsightSessionState(),
1298
- getMnemopiSessionState: () => getMnemopiSessionState(session),
1297
+ getMnemopiSessionState: () => session?.getMnemopiSessionState(),
1299
1298
  getAgentId: () => resolvedAgentId,
1300
1299
  getToolByName: name => session?.getToolByName(name),
1301
1300
  agentRegistry,
@@ -1472,7 +1471,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1472
1471
  }
1473
1472
 
1474
1473
  const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
1475
- inlineExtensions.push(createAutoresearchExtension);
1474
+ inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
1476
1475
  if (customTools.length > 0) {
1477
1476
  inlineExtensions.push(createCustomToolsExtension(customTools));
1478
1477
  }
@@ -1607,9 +1606,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1607
1606
  // `ExtensionToolWrapper` installed below is the only place the per-tool approval gate runs.
1608
1607
  // A conditional runner means the approval system silently disappears for users with no
1609
1608
  // extensions, contradicting non-yolo `tools.approvalMode` settings without feedback.
1610
- // (Today `createAutoresearchExtension` is unconditionally pushed below, so this scenario
1611
- // is unreachable; the unconditional construction makes that invariant explicit instead of
1612
- // implicit, so a future change to make autoresearch optional cannot silently re-open the hole.)
1609
+ // (The builtin autoresearch extension is unconditionally loaded above, so this scenario
1610
+ // is unreachable; unconditional runner construction keeps that invariant explicit and
1611
+ // prevents future optional extensions from silently re-opening the hole.)
1613
1612
  const extensionRunner: ExtensionRunner = new ExtensionRunner(
1614
1613
  extensionsResult.extensions,
1615
1614
  extensionsResult.runtime,
@@ -1723,7 +1722,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1723
1722
 
1724
1723
  const repeatToolDescriptions = settings.get("repeatToolDescriptions");
1725
1724
  const eagerTasks = settings.get("task.eager");
1726
- const intentField = settings.get("tools.intentTracing") || $flag("PI_INTENT_TRACING") ? INTENT_FIELD : undefined;
1725
+ const intentField = $flag("PI_INTENT_TRACING", settings.get("tools.intentTracing")) ? INTENT_FIELD : undefined;
1727
1726
  const rebuildSystemPrompt = async (
1728
1727
  toolNames: string[],
1729
1728
  tools: Map<string, AgentTool>,
@@ -1749,7 +1748,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1749
1748
  const promptTools = buildSystemPromptToolMetadata(tools, {
1750
1749
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableToolsForDesc) },
1751
1750
  });
1752
- const memoryBackend = resolveMemoryBackend(settings);
1751
+ const memoryBackend = await resolveMemoryBackend(settings);
1753
1752
  const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
1754
1753
 
1755
1754
  // Build combined append prompt: memory instructions + MCP server instructions
@@ -2267,19 +2266,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2267
2266
  }
2268
2267
  }
2269
2268
 
2270
- logger.time("startMemoryStartupTask", () =>
2271
- Promise.resolve(
2272
- resolveMemoryBackend(settings).start({
2273
- session,
2274
- settings,
2275
- modelRegistry,
2276
- agentDir,
2277
- taskDepth,
2278
- parentHindsightSessionState: options.parentHindsightSessionState,
2279
- parentMnemopiSessionState: options.parentMnemopiSessionState,
2280
- }),
2281
- ),
2282
- );
2269
+ logger.time("startMemoryStartupTask", async () => {
2270
+ const memoryBackend = await resolveMemoryBackend(settings);
2271
+ await memoryBackend.start({
2272
+ session,
2273
+ settings,
2274
+ modelRegistry,
2275
+ agentDir,
2276
+ taskDepth,
2277
+ parentHindsightSessionState: options.parentHindsightSessionState,
2278
+ parentMnemopiSessionState: options.parentMnemopiSessionState,
2279
+ });
2280
+ });
2283
2281
 
2284
2282
  // Wire MCP manager callbacks to session for reactive tool updates.
2285
2283
  // Skip when reusing a parent's manager — the parent owns the callbacks.
@@ -91,6 +91,7 @@ import {
91
91
  extractRetryHint,
92
92
  getAgentDbPath,
93
93
  getInstallId,
94
+ isBunTestRuntime,
94
95
  isEnoent,
95
96
  isUnexpectedSocketCloseMessage,
96
97
  logger,
@@ -127,7 +128,6 @@ import {
127
128
  } from "../eval/py/executor";
128
129
  import { defaultEvalSessionId } from "../eval/session-id";
129
130
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
130
- import { exportSessionToHtml } from "../export/html";
131
131
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
132
132
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
133
133
  import type { CustomTool, CustomToolContext } from "../extensibility/custom-tools/types";
@@ -1036,6 +1036,7 @@ export class AgentSession {
1036
1036
 
1037
1037
  #acquirePowerAssertion(): void {
1038
1038
  if (process.platform !== "darwin") return;
1039
+ if (isBunTestRuntime()) return;
1039
1040
  if (this.#powerAssertion) return;
1040
1041
  const idle = this.settings.get("power.preventIdleSleep");
1041
1042
  const system = this.settings.get("power.preventSystemSleep");
@@ -2965,14 +2966,14 @@ export class AgentSession {
2965
2966
  }
2966
2967
 
2967
2968
  #rekeyHindsightMemoryForCurrentSessionId(): void {
2968
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
2969
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2969
2970
  const sid = this.agent.sessionId;
2970
2971
  if (!sid) return;
2971
2972
  this.getHindsightSessionState()?.setSessionId(sid);
2972
2973
  }
2973
2974
 
2974
2975
  #rekeyMnemopiMemoryForCurrentSessionId(): void {
2975
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2976
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2976
2977
  const sid = this.agent.sessionId;
2977
2978
  if (!sid) return;
2978
2979
  this.getMnemopiSessionState()?.setSessionId(sid);
@@ -2980,14 +2981,14 @@ export class AgentSession {
2980
2981
 
2981
2982
  /** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
2982
2983
  #resetHindsightConversationTrackingIfHindsight(): void {
2983
- if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
2984
+ if (this.settings.get("memory.backend") !== "hindsight") return;
2984
2985
  const state = this.getHindsightSessionState();
2985
2986
  if (!state || state.aliasOf) return;
2986
2987
  state.resetConversationTracking();
2987
2988
  }
2988
2989
 
2989
2990
  #resetMnemopiConversationTrackingIfMnemopi(): void {
2990
- if (resolveMemoryBackend(this.settings).id !== "mnemopi") return;
2991
+ if (this.settings.get("memory.backend") !== "mnemopi") return;
2991
2992
  const state = this.getMnemopiSessionState();
2992
2993
  if (!state || state.aliasOf) return;
2993
2994
  state.resetConversationTracking();
@@ -3668,7 +3669,7 @@ export class AgentSession {
3668
3669
  }
3669
3670
 
3670
3671
  async #buildSystemPromptForAgentStart(promptText: string): Promise<string[]> {
3671
- const backend = resolveMemoryBackend(this.settings);
3672
+ const backend = await resolveMemoryBackend(this.settings);
3672
3673
  if (!backend.beforeAgentStartPrompt) return this.#baseSystemPrompt;
3673
3674
 
3674
3675
  try {
@@ -6094,7 +6095,7 @@ export class AgentSession {
6094
6095
  messagesToSummarize: AgentMessage[];
6095
6096
  turnPrefixMessages: AgentMessage[];
6096
6097
  }): Promise<string | undefined> {
6097
- const backend = resolveMemoryBackend(this.settings);
6098
+ const backend = await resolveMemoryBackend(this.settings);
6098
6099
  if (!backend.preCompactionContext) return undefined;
6099
6100
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
6100
6101
  try {
@@ -8136,8 +8137,10 @@ export class AgentSession {
8136
8137
 
8137
8138
  const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
8138
8139
  if (!switchedCredential && currentSelector) {
8139
- this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
8140
- switchedModel = await this.#tryRetryModelFallback(currentSelector);
8140
+ if (retrySettings.modelFallback) {
8141
+ this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
8142
+ switchedModel = await this.#tryRetryModelFallback(currentSelector);
8143
+ }
8141
8144
  if (switchedModel) {
8142
8145
  delayMs = 0;
8143
8146
  } else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
@@ -9584,6 +9587,7 @@ export class AgentSession {
9584
9587
  */
9585
9588
  async exportToHtml(outputPath?: string): Promise<string> {
9586
9589
  const themeName = getCurrentThemeName();
9590
+ const { exportSessionToHtml } = await import("../export/html");
9587
9591
  return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
9588
9592
  }
9589
9593
 
@@ -392,17 +392,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
392
392
  },
393
393
  {
394
394
  name: "copy",
395
- description: "Copy last agent message to clipboard",
396
- subcommands: [
397
- { name: "last", description: "Copy full last agent message" },
398
- { name: "code", description: "Copy last code block" },
399
- { name: "all", description: "Copy all code blocks from last message" },
400
- { name: "cmd", description: "Copy last bash/python command" },
401
- ],
402
- allowArgs: true,
403
- handleTui: async (command, runtime) => {
404
- const sub = command.args.trim().toLowerCase() || undefined;
405
- await runtime.ctx.handleCopyCommand(sub);
395
+ description: "Pick text or code from the conversation to copy",
396
+ handleTui: (_command, runtime) => {
397
+ runtime.ctx.showCopySelector();
406
398
  runtime.ctx.editor.setText("");
407
399
  },
408
400
  },
@@ -942,7 +934,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
942
934
  allowArgs: true,
943
935
  handle: async (command, runtime) => {
944
936
  const verb = (command.args.trim().split(/\s+/)[0] ?? "").toLowerCase() || "view";
945
- const backend = resolveMemoryBackend(runtime.settings);
937
+ const backend = await resolveMemoryBackend(runtime.settings);
946
938
  switch (verb) {
947
939
  case "view": {
948
940
  const payload = await backend.buildDeveloperInstructions(
@@ -26,6 +26,8 @@ function formatUsageReportAccount(report: UsageReport, limit: UsageLimit, index:
26
26
  if (typeof email === "string" && email) return email;
27
27
  const accountId = report.metadata?.accountId ?? limit.scope.accountId;
28
28
  if (typeof accountId === "string" && accountId) return accountId;
29
+ const projectId = report.metadata?.projectId ?? limit.scope.projectId;
30
+ if (typeof projectId === "string" && projectId) return projectId;
29
31
  return `account ${index + 1}`;
30
32
  }
31
33
 
@@ -166,6 +166,13 @@ export interface ExecutorOptions {
166
166
  outputSchema?: unknown;
167
167
  /** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
168
168
  taskDepth?: number;
169
+ /**
170
+ * Override the `task.maxRuntimeMs` wall-clock cap for this run. When provided
171
+ * it wins over the settings value; `0` disables the per-subagent wall-clock
172
+ * limit entirely. Used by the eval `agent()` bridge, whose parent cell
173
+ * watchdog is already suspended for the call's duration.
174
+ */
175
+ maxRuntimeMs?: number;
169
176
  enableLsp?: boolean;
170
177
  signal?: AbortSignal;
171
178
  onProgress?: (progress: AgentProgress) => void;
@@ -625,7 +632,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
625
632
  agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
626
633
  );
627
634
  const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
628
- const maxRuntimeMs = Math.max(0, Math.trunc(Number(settings.get("task.maxRuntimeMs") ?? 0) || 0));
635
+ const maxRuntimeMs = Math.max(
636
+ 0,
637
+ Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
638
+ );
629
639
  const parentDepth = options.taskDepth ?? 0;
630
640
  const childDepth = parentDepth + 1;
631
641
  const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
@@ -1484,7 +1494,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
1484
1494
  if (lastAssistant.stopReason === "aborted") {
1485
1495
  aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
1486
1496
  if (aborted) {
1487
- abortReasonText ??= resolveAbortReasonText();
1497
+ // A real caller signal or the wall-clock timer carries a precise
1498
+ // reason (signal.reason / "runtime limit exceeded"). An internal
1499
+ // turn abort (abortReason === undefined) does NOT — prefer the
1500
+ // assistant message's own errorMessage ("Request was aborted" or a
1501
+ // specific stream error) over the misleading "Cancelled by caller".
1502
+ abortReasonText ??=
1503
+ abortReason === "signal" || runtimeLimitExceeded
1504
+ ? resolveAbortReasonText()
1505
+ : lastAssistant.errorMessage?.trim() || resolveAbortReasonText();
1488
1506
  }
1489
1507
  exitCode = 1;
1490
1508
  } else if (lastAssistant.stopReason === "error") {
@@ -117,6 +117,26 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
117
117
  return findings;
118
118
  }
119
119
 
120
+ /**
121
+ * Normalize the `yield` slot of `extractedToolData` into an array of
122
+ * yield-detail records. The subprocess executor always populates this slot as
123
+ * `unknown[]` (see `executor.ts` `extractData` handler), but the renderer
124
+ * MUST also tolerate a stray single object — optional chaining short-circuits
125
+ * on `null`/`undefined` only, so calling `.map` on a plain object would throw
126
+ * `TypeError: completeData?.map is not a function` and crash the TUI.
127
+ * A single object is wrapped as a 1-element array so the review verdict still
128
+ * renders; non-object primitives drop out.
129
+ */
130
+ function normalizeYieldData(value: unknown): Array<{ data: unknown }> {
131
+ if (Array.isArray(value)) {
132
+ return value.filter((item): item is { data: unknown } => item !== null && typeof item === "object");
133
+ }
134
+ if (value !== null && typeof value === "object") {
135
+ return [value as { data: unknown }];
136
+ }
137
+ return [];
138
+ }
139
+
120
140
  function formatJsonScalar(value: unknown, _theme: Theme): string {
121
141
  if (value === null) return "null";
122
142
  if (typeof value === "string") {
@@ -541,10 +561,11 @@ export function renderCall(
541
561
 
542
562
  if (hasContext) {
543
563
  lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
544
- for (const line of context.split("\n")) {
564
+ const contextLines = context.split("\n").map(line => {
545
565
  const content = line ? theme.fg("muted", replaceTabs(line)) : "";
546
- lines.push(` ${vertical} ${content}`);
547
- }
566
+ return ` ${vertical} ${content}`;
567
+ });
568
+ lines.push(...contextLines);
548
569
  }
549
570
 
550
571
  // `Tasks` is the last child unless the isolation flag follows it.
@@ -671,12 +692,12 @@ function renderAgentProgress(
671
692
  if (progress.extractedToolData) {
672
693
  // For completed tasks, check for review verdict from yield tool
673
694
  if (progress.status === "completed") {
674
- const completeData = progress.extractedToolData.yield as Array<{ data: unknown }> | undefined;
695
+ const completeData = normalizeYieldData(progress.extractedToolData.yield);
675
696
  const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
676
697
  const reviewData = completeData
677
- ?.map(c => c.data as SubmitReviewDetails)
698
+ .map(c => c.data as SubmitReviewDetails)
678
699
  .filter(d => d && typeof d === "object" && "overall_correctness" in d);
679
- if (reviewData && reviewData.length > 0) {
700
+ if (reviewData.length > 0) {
680
701
  const summary = reviewData[reviewData.length - 1];
681
702
  const findings = reportFindingData;
682
703
  lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
@@ -912,16 +933,21 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
912
933
  );
913
934
  }
914
935
  // Check for review result (yield with review schema + report_finding)
915
- const completeData = result.extractedToolData?.yield as Array<{ data: unknown }> | undefined;
936
+ // Check for review result (yield with review schema + report_finding).
937
+ // `normalizeYieldData` guards against a stray non-array `yield` slot —
938
+ // optional chaining on `.map` only short-circuits on null/undefined and
939
+ // would otherwise crash the renderer with `TypeError: completeData?.map
940
+ // is not a function` when the slot is a plain object (see issue #1987).
941
+ const completeData = normalizeYieldData(result.extractedToolData?.yield);
916
942
  const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
917
943
 
918
944
  // Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
919
945
  const reviewData = completeData
920
- ?.map(c => c.data as SubmitReviewDetails)
946
+ .map(c => c.data as SubmitReviewDetails)
921
947
  .filter(d => d && typeof d === "object" && "overall_correctness" in d);
922
- const submitReviewData = reviewData && reviewData.length > 0 ? reviewData : undefined;
948
+ const submitReviewData = reviewData.length > 0 ? reviewData : undefined;
923
949
 
924
- if (submitReviewData && submitReviewData.length > 0) {
950
+ if (submitReviewData) {
925
951
  // Use combined review renderer
926
952
  const summary = submitReviewData[submitReviewData.length - 1];
927
953
  const findings = reportFindingData;
@@ -929,7 +955,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
929
955
  return lines;
930
956
  }
931
957
  if (reportFindingData.length > 0) {
932
- const hasCompleteData = completeData && completeData.length > 0;
958
+ const hasCompleteData = completeData.length > 0;
933
959
  const message = hasCompleteData
934
960
  ? "Review verdict missing expected fields"
935
961
  : "Review incomplete (yield not called)";
@@ -23,11 +23,7 @@
23
23
  * `sdk-trace-base@2.7` exports cleanly on Bun.
24
24
  */
25
25
  import { logger, postmortem } from "@oh-my-pi/pi-utils";
26
- import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
27
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
28
- import { resourceFromAttributes } from "@opentelemetry/resources";
29
- import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
30
- import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
26
+ import type * as TraceNode from "@opentelemetry/sdk-trace-node";
31
27
 
32
28
  /**
33
29
  * Periodic flush interval. A long-lived `omp` process (the ACP server is
@@ -36,7 +32,8 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
36
32
  */
37
33
  const FLUSH_INTERVAL_MS = 30_000;
38
34
 
39
- let provider: NodeTracerProvider | undefined;
35
+ let provider: TraceNode.NodeTracerProvider | undefined;
36
+ let initPromise: Promise<void> | undefined;
40
37
 
41
38
  /**
42
39
  * Whether {@link initTelemetryExport} registered a real provider. The CLI uses
@@ -53,8 +50,10 @@ export function isTelemetryExportEnabled(): boolean {
53
50
  * the OTEL kill-switches are engaged), so it is safe to call unconditionally at
54
51
  * startup.
55
52
  */
56
- export function initTelemetryExport(): void {
53
+ export async function initTelemetryExport(): Promise<void> {
57
54
  if (provider) return;
55
+ if (initPromise) return initPromise;
56
+
58
57
  // The OTEL env contract parses booleans and enum lists case-insensitively, so
59
58
  // OTEL_SDK_DISABLED=TRUE and OTEL_TRACES_EXPORTER=None must also disable export.
60
59
  if (process.env.OTEL_SDK_DISABLED?.trim().toLowerCase() === "true") return;
@@ -77,6 +76,25 @@ export function initTelemetryExport(): void {
77
76
  return;
78
77
  }
79
78
 
79
+ initPromise = registerProvider();
80
+ return initPromise;
81
+ }
82
+
83
+ async function registerProvider(): Promise<void> {
84
+ const [
85
+ { AsyncLocalStorageContextManager },
86
+ { OTLPTraceExporter },
87
+ { resourceFromAttributes },
88
+ { BatchSpanProcessor },
89
+ { NodeTracerProvider },
90
+ ] = await Promise.all([
91
+ import("@opentelemetry/context-async-hooks"),
92
+ import("@opentelemetry/exporter-trace-otlp-proto"),
93
+ import("@opentelemetry/resources"),
94
+ import("@opentelemetry/sdk-trace-base"),
95
+ import("@opentelemetry/sdk-trace-node"),
96
+ ]);
97
+
80
98
  // The exporter reads endpoint/headers/timeout from OTEL_EXPORTER_OTLP_* itself,
81
99
  // so there is nothing to thread through here.
82
100
  const exporter = new OTLPTraceExporter();
package/src/tools/bash.ts CHANGED
@@ -20,7 +20,7 @@ import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
20
20
  import type { ClientBridgeTerminalExitStatus, ClientBridgeTerminalOutput } from "../session/client-bridge";
21
21
  import { DEFAULT_MAX_BYTES, streamTailUpdates, TailBuffer } from "../session/streaming-output";
22
22
  import { renderStatusLine } from "../tui";
23
- import { CachedOutputBlock } from "../tui/output-block";
23
+ import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
24
24
  import { getSixelLineMask } from "../utils/sixel";
25
25
  import type { ToolSession } from ".";
26
26
  import { truncateForPrompt } from "./approval";
@@ -31,7 +31,7 @@ import { canUseInteractiveBashPty } from "./bash-pty-selection";
31
31
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
32
32
  import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
33
33
  import { resolveToCwd } from "./path-utils";
34
- import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
34
+ import { capPreviewLines, formatToolWorkingDirectory, replaceTabs } from "./render-utils";
35
35
  import { ToolAbortError, ToolError } from "./tool-errors";
36
36
  import { toolResult } from "./tool-result";
37
37
  import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
@@ -1083,16 +1083,22 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1083
1083
  const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
1084
1084
  const header = renderStatusLine({ icon: "pending", title }, uiTheme);
1085
1085
  const outputBlock = new CachedOutputBlock();
1086
- return {
1086
+ return markFramedBlockComponent({
1087
1087
  render: (width: number): string[] =>
1088
1088
  outputBlock.render(
1089
- { header, state: "pending", sections: [{ lines: cmdLines }], width, animate: true },
1089
+ {
1090
+ header,
1091
+ state: "pending",
1092
+ sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
1093
+ width,
1094
+ animate: true,
1095
+ },
1090
1096
  uiTheme,
1091
1097
  ),
1092
1098
  invalidate: () => {
1093
1099
  outputBlock.invalidate();
1094
1100
  },
1095
- };
1101
+ });
1096
1102
  },
1097
1103
 
1098
1104
  renderResult(
@@ -1114,7 +1120,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1114
1120
  const details = result.details;
1115
1121
  const outputBlock = new CachedOutputBlock();
1116
1122
 
1117
- return {
1123
+ return markFramedBlockComponent({
1118
1124
  render: (width: number): string[] => {
1119
1125
  // REACTIVE: read mutable options at render time
1120
1126
  const { renderContext } = options;
@@ -1201,7 +1207,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1201
1207
  header,
1202
1208
  state: options.isPartial ? "pending" : isError ? "error" : "success",
1203
1209
  sections: [
1204
- { lines: cmdLines ?? [] },
1210
+ {
1211
+ lines: options.isPartial
1212
+ ? capPreviewLines(cmdLines ?? [], uiTheme, { expanded })
1213
+ : (cmdLines ?? []),
1214
+ },
1205
1215
  { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
1206
1216
  ],
1207
1217
  width,
@@ -1213,7 +1223,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1213
1223
  invalidate: () => {
1214
1224
  outputBlock.invalidate();
1215
1225
  },
1216
- };
1226
+ });
1217
1227
  },
1218
1228
  mergeCallAndResult: true,
1219
1229
  inline: true,
@@ -9,7 +9,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
9
9
  import { Text } from "@oh-my-pi/pi-tui";
10
10
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
11
11
  import type { Theme } from "../../modes/theme/theme";
12
- import { Hasher, renderCodeCell, renderStatusLine } from "../../tui";
12
+ import { Hasher, isFramedBlockComponent, markFramedBlockComponent, renderCodeCell, renderStatusLine } from "../../tui";
13
13
  import type { BrowserToolDetails } from "../browser";
14
14
  import { formatStyledTruncationWarning, stripOutputNotice } from "../output-meta";
15
15
  import { replaceTabs, shortenPath } from "../render-utils";
@@ -65,13 +65,14 @@ function dropTrailingBlankLines(text: string): string {
65
65
 
66
66
  function appendLine(component: Component, line: string | undefined): Component {
67
67
  if (!line) return component;
68
- return {
68
+ const wrapped = {
69
69
  render: (width: number): string[] => {
70
70
  const base = component.render(width);
71
71
  return [...base, line];
72
72
  },
73
73
  invalidate: () => component.invalidate?.(),
74
74
  };
75
+ return isFramedBlockComponent(component) ? markFramedBlockComponent(wrapped) : wrapped;
75
76
  }
76
77
 
77
78
  function renderRunCell(
@@ -93,7 +94,7 @@ function renderRunCell(
93
94
  const title = titleParts.join(" · ");
94
95
 
95
96
  let cached: { key: bigint; width: number; lines: string[] } | undefined;
96
- return {
97
+ return markFramedBlockComponent({
97
98
  render: (width: number): string[] => {
98
99
  const expanded = options.renderContext?.expanded ?? options.expanded;
99
100
  const previewLines = options.renderContext?.previewLines ?? BROWSER_DEFAULT_PREVIEW_LINES;
@@ -131,7 +132,7 @@ function renderRunCell(
131
132
  invalidate: () => {
132
133
  cached = undefined;
133
134
  },
134
- };
135
+ });
135
136
  }
136
137
 
137
138
  function renderOpenOrCloseLine(
@@ -36,7 +36,7 @@ import {
36
36
  import type { Theme } from "../modes/theme/theme";
37
37
  import debugDescription from "../prompts/tools/debug.md" with { type: "text" };
38
38
  import { renderStatusLine } from "../tui";
39
- import { CachedOutputBlock } from "../tui/output-block";
39
+ import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
40
40
  import type { ToolSession } from ".";
41
41
  import { truncateForPrompt } from "./approval";
42
42
  import type { OutputMeta } from "./output-meta";
@@ -581,7 +581,7 @@ export const debugToolRenderer = {
581
581
  args?: DebugRenderArgs,
582
582
  ): Component {
583
583
  const outputBlock = new CachedOutputBlock();
584
- return {
584
+ return markFramedBlockComponent({
585
585
  render(width: number): string[] {
586
586
  const action = (args?.action ?? result.details?.action ?? "debug").replaceAll("_", " ");
587
587
  const status = options.isPartial ? "running" : result.isError ? "error" : "success";
@@ -620,7 +620,7 @@ export const debugToolRenderer = {
620
620
  invalidate() {
621
621
  outputBlock.invalidate();
622
622
  },
623
- };
623
+ });
624
624
  },
625
625
  mergeCallAndResult: true,
626
626
  inline: true,
@@ -1,4 +1,4 @@
1
- import { $env, $flag } from "@oh-my-pi/pi-utils";
1
+ import { $flag } from "@oh-my-pi/pi-utils";
2
2
  import type { ToolSession } from ".";
3
3
 
4
4
  export interface EvalBackendsAllowance {
@@ -6,21 +6,6 @@ export interface EvalBackendsAllowance {
6
6
  js: boolean;
7
7
  }
8
8
 
9
- /**
10
- * Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
11
- * means "not specified, defer to settings". Returns null when neither is set
12
- * so the caller can fall through to `readEvalBackendsAllowance` per key.
13
- */
14
- function getEvalBackendsFromEnv(): EvalBackendsAllowance | null {
15
- const pyEnv = $env.PI_PY;
16
- const jsEnv = $env.PI_JS;
17
- if (pyEnv === undefined && jsEnv === undefined) return null;
18
- return {
19
- python: pyEnv === undefined ? true : $flag("PI_PY"),
20
- js: jsEnv === undefined ? true : $flag("PI_JS"),
21
- };
22
- }
23
-
24
9
  /** Read per-backend allowance from settings (defaults true). */
25
10
  export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
26
11
  return {
@@ -34,5 +19,9 @@ export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAll
34
19
  * override the per-key settings; otherwise settings (defaults true) win.
35
20
  */
36
21
  export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
37
- return getEvalBackendsFromEnv() ?? readEvalBackendsAllowance(session);
22
+ const settings = readEvalBackendsAllowance(session);
23
+ return {
24
+ python: $flag("PI_PY", settings.python),
25
+ js: $flag("PI_JS", settings.js),
26
+ };
38
27
  }