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

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -14,7 +14,7 @@ import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWi
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import { createFileRecorder, formatResultPath } from "./file-recorder";
17
- import { formatGroupedFiles } from "./grouped-file-output";
17
+ import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
18
18
  import { formatMatchLine } from "./match-line-format";
19
19
  import type { OutputMeta } from "./output-meta";
20
20
  import { resolveToolSearchScope } from "./path-utils";
@@ -29,7 +29,6 @@ import {
29
29
  formatParseErrors,
30
30
  formatParseErrorsCountLabel,
31
31
  PREVIEW_LIMITS,
32
- splitGroupsByBlankLine,
33
32
  } from "./render-utils";
34
33
  import { ToolError } from "./tool-errors";
35
34
  import { toolResult } from "./tool-result";
@@ -118,6 +117,9 @@ export interface AstGrepToolDetails {
118
117
  /** Absolute base directory used during search. Used by the renderer to resolve
119
118
  * display-relative paths to absolute paths for OSC 8 hyperlinks. */
120
119
  searchPath?: string;
120
+ /** Session cwd at search time. Display header/match paths are cwd-relative, so
121
+ * the renderer resolves them against this; `searchPath` is the scope target. */
122
+ cwd?: string;
121
123
  }
122
124
 
123
125
  export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
@@ -207,6 +209,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
207
209
  ...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
208
210
  scopePath,
209
211
  searchPath: resolvedSearchPath,
212
+ cwd: this.session.cwd,
210
213
  files: fileList,
211
214
  fileMatches: [],
212
215
  };
@@ -381,15 +384,41 @@ export const astGrepToolRenderer = {
381
384
  if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
382
385
  const description = args?.pat;
383
386
  const header = renderStatusLine(
384
- { icon: limitReached ? "warning" : "success", title: "AST Grep", description, meta },
387
+ {
388
+ ...(limitReached
389
+ ? { icon: "warning" as const }
390
+ : { iconOverride: uiTheme.fg("accent", uiTheme.symbol("icon.search")) }),
391
+ title: "AST Grep",
392
+ description,
393
+ meta,
394
+ },
385
395
  uiTheme,
386
396
  );
387
397
 
388
398
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
389
- const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
390
- const matchGroups = allGroups.filter(
391
- group => !group[0]?.startsWith("Result limit reached") && !group[0]?.startsWith("Parse issues:"),
392
- );
399
+ const allLines = textContent.split("\n");
400
+ // Resolve hyperlinks over the whole output so nested directory headers
401
+ // reconstruct across the blank-line groups the tree list collapses by.
402
+ const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
403
+ const styledLines = allLines.map((line, index) => {
404
+ const ctx = contexts[index]!;
405
+ if (ctx.kind === "dir") {
406
+ const styled = uiTheme.fg("accent", line);
407
+ return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
408
+ }
409
+ if (ctx.kind === "file") {
410
+ const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
411
+ return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
412
+ }
413
+ if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
414
+ return uiTheme.fg("toolOutput", line);
415
+ });
416
+ const matchGroups = groupLineIndicesByBlank(allLines)
417
+ .filter(indices => {
418
+ const first = allLines[indices[0]!]!;
419
+ return !first.startsWith("Result limit reached") && !first.startsWith("Parse issues:");
420
+ })
421
+ .map(indices => indices.map(index => styledLines[index]!));
393
422
 
394
423
  const extraLines: string[] = [];
395
424
  if (limitReached) {
@@ -404,7 +433,6 @@ export const astGrepToolRenderer = {
404
433
  return createCachedComponent(
405
434
  () => options.expanded,
406
435
  width => {
407
- const searchBase = details?.searchPath;
408
436
  const matchLines = renderTreeList(
409
437
  {
410
438
  items: matchGroups,
@@ -412,41 +440,7 @@ export const astGrepToolRenderer = {
412
440
  maxCollapsed: matchGroups.length,
413
441
  maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
414
442
  itemType: "match",
415
- renderItem: group => {
416
- let contextDir = searchBase ?? "";
417
- return group.map(line => {
418
- if (line.startsWith("## ")) {
419
- const fileName = line
420
- .slice(3)
421
- .trimEnd()
422
- .replace(/\s+\([^)]*\)\s*$/, "")
423
- .replace(/#[0-9a-f]+$/, "");
424
- const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
425
- const styled = uiTheme.fg("dim", line);
426
- return absPath ? fileHyperlink(absPath, styled) : styled;
427
- }
428
- if (line.startsWith("# ")) {
429
- const raw = line
430
- .slice(2)
431
- .trimEnd()
432
- .replace(/\s+\([^)]*\)\s*$/, "");
433
- const isDirectory = raw.endsWith("/");
434
- const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
435
- if (isDirectory) {
436
- if (searchBase) {
437
- contextDir = name === "." ? searchBase : path.join(searchBase, name);
438
- }
439
- return uiTheme.fg("accent", line);
440
- }
441
- // Root-level file (single # without trailing slash) from formatGroupedFiles.
442
- const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
443
- const styled = uiTheme.fg("accent", line);
444
- return absPath ? fileHyperlink(absPath, styled) : styled;
445
- }
446
- if (line.startsWith(" meta:")) return uiTheme.fg("dim", line);
447
- return uiTheme.fg("toolOutput", line);
448
- });
449
- },
443
+ renderItem: group => group,
450
444
  },
451
445
  uiTheme,
452
446
  );
package/src/tools/bash.ts CHANGED
@@ -29,6 +29,7 @@ import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-intera
29
29
  import { checkBashInterception } from "./bash-interceptor";
30
30
  import { canUseInteractiveBashPty } from "./bash-pty-selection";
31
31
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
32
+ import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
32
33
  import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
33
34
  import { resolveToCwd } from "./path-utils";
34
35
  import { capPreviewLines, formatToolWorkingDirectory, replaceTabs } from "./render-utils";
@@ -287,6 +288,35 @@ function formatExitCodeNotice(exitCode: number): string {
287
288
  return `Command exited with code ${exitCode}`;
288
289
  }
289
290
 
291
+ const RAW_OUTPUT_ARTIFACT_PREFIX = "[raw output: artifact://";
292
+ const RAW_OUTPUT_ARTIFACT_SUFFIX = "]";
293
+
294
+ function stripRawOutputArtifactNotice(text: string): { text: string; artifactId?: string } {
295
+ const trimmed = text.trimEnd();
296
+ const lineStart = trimmed.lastIndexOf("\n");
297
+ const candidateStart = lineStart === -1 ? 0 : lineStart + 1;
298
+ if (
299
+ !trimmed.startsWith(RAW_OUTPUT_ARTIFACT_PREFIX, candidateStart) ||
300
+ !trimmed.endsWith(RAW_OUTPUT_ARTIFACT_SUFFIX)
301
+ ) {
302
+ return { text };
303
+ }
304
+
305
+ const idStart = candidateStart + RAW_OUTPUT_ARTIFACT_PREFIX.length;
306
+ const idEnd = trimmed.length - RAW_OUTPUT_ARTIFACT_SUFFIX.length;
307
+ if (idStart === idEnd) return { text };
308
+ for (let i = idStart; i < idEnd; i++) {
309
+ const code = trimmed.charCodeAt(i);
310
+ if (code < 48 || code > 57) return { text };
311
+ }
312
+
313
+ const artifactId = trimmed.slice(idStart, idEnd);
314
+ return {
315
+ text: trimmed.slice(0, lineStart === -1 ? 0 : lineStart).trimEnd(),
316
+ artifactId,
317
+ };
318
+ }
319
+
290
320
  /**
291
321
  * Strip the trailing occurrence of `notice` (plus a single surrounding newline
292
322
  * on each side) so the TUI can echo the value via a styled footer label
@@ -692,6 +722,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
692
722
  cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
693
723
  }
694
724
 
725
+ // Best-effort cache invalidation: drop github-cache rows for any issue/PR
726
+ // number touched by a mutating `gh` subcommand inside this bash call so
727
+ // subsequent issue:// / pr:// reads pick up the post-mutation state
728
+ // instead of the cached pre-mutation snapshot.
729
+ invalidateGithubCacheForBashCommand(command);
730
+
695
731
  const commandCwd = cwd ? resolveToCwd(cwd, this.session.cwd) : this.session.cwd;
696
732
  let cwdStat: fs.Stats;
697
733
  try {
@@ -1028,6 +1064,7 @@ export interface ShellRendererConfig<TArgs> {
1028
1064
  resolveCommand?: (args: TArgs | undefined) => string | undefined;
1029
1065
  resolveCwd?: (args: TArgs | undefined) => string | undefined;
1030
1066
  resolveEnv?: (args: TArgs | undefined) => Record<string, string> | undefined;
1067
+ showHeader?: boolean;
1031
1068
  }
1032
1069
 
1033
1070
  function getPartialJson<TArgs>(args: TArgs | undefined): string | undefined {
@@ -1079,9 +1116,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1079
1116
  return {
1080
1117
  renderCall(args: TArgs, options: RenderResultOptions, uiTheme: Theme): Component {
1081
1118
  const renderArgs = toBashRenderArgs(args, config);
1082
- const title = config.resolveTitle(args, options);
1083
1119
  const cmdLines = formatBashCommandLines(renderArgs, uiTheme);
1084
- const header = renderStatusLine({ icon: "pending", title }, uiTheme);
1120
+ const header =
1121
+ config.showHeader === false
1122
+ ? undefined
1123
+ : renderStatusLine({ icon: "pending", title: config.resolveTitle(args, options) }, uiTheme);
1085
1124
  const outputBlock = new CachedOutputBlock();
1086
1125
  return markFramedBlockComponent({
1087
1126
  render: (width: number): string[] =>
@@ -1115,8 +1154,10 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1115
1154
  const cmdLines = args ? formatBashCommandLines(renderArgs, uiTheme) : undefined;
1116
1155
  const isError = result.isError === true;
1117
1156
  const icon = options.isPartial ? "pending" : isError ? "error" : "success";
1118
- const title = config.resolveTitle(args, options);
1119
- const header = renderStatusLine({ icon, title }, uiTheme);
1157
+ const header =
1158
+ config.showHeader === false
1159
+ ? undefined
1160
+ : renderStatusLine({ icon, title: config.resolveTitle(args, options) }, uiTheme);
1120
1161
  const details = result.details;
1121
1162
  const outputBlock = new CachedOutputBlock();
1122
1163
 
@@ -1133,7 +1174,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1133
1174
  const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
1134
1175
  const strippedOutput = stripOutputNotice(rawOutput, details?.meta);
1135
1176
  const withoutExit = stripExitCodeNotice(strippedOutput, details?.exitCode);
1136
- const output = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
1177
+ const withoutWall = stripWallTimeNotice(withoutExit, details?.wallTimeMs);
1178
+ const rawOutputArtifact = stripRawOutputArtifactNotice(withoutWall);
1179
+ const output = rawOutputArtifact.text;
1137
1180
  const displayOutput = output.trimEnd();
1138
1181
  const showingFullOutput = expanded && renderContext?.isFullOutput === true;
1139
1182
 
@@ -1152,6 +1195,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1152
1195
  : `Timeout: ${timeoutSeconds}s`,
1153
1196
  );
1154
1197
  }
1198
+ if (rawOutputArtifact.artifactId) {
1199
+ statsParts.push(`Artifact: ${rawOutputArtifact.artifactId}`);
1200
+ }
1155
1201
  if (isError && typeof details?.exitCode === "number") {
1156
1202
  statsParts.push(`Exit: ${details.exitCode}`);
1157
1203
  }
@@ -1215,7 +1261,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1215
1261
  { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
1216
1262
  ],
1217
1263
  width,
1218
- animate: options.isPartial && shimmerEnabled(),
1264
+ // Don't animate once the command has been backgrounded: the block
1265
+ // gets committed to scrollback and finalizes later via the async
1266
+ // update path, so a mid-sweep frame would freeze a stray dark
1267
+ // border segment.
1268
+ animate: options.isPartial && shimmerEnabled() && details?.async?.state !== "running",
1219
1269
  },
1220
1270
  uiTheme,
1221
1271
  );
@@ -1235,4 +1285,5 @@ export const bashToolRenderer = createShellRenderer<BashRenderArgs>({
1235
1285
  resolveCommand: args => args?.command,
1236
1286
  resolveCwd: args => args?.cwd,
1237
1287
  resolveEnv: args => args?.env,
1288
+ showHeader: false,
1238
1289
  });
@@ -101,11 +101,23 @@ export async function acquireTab(
101
101
  if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
102
102
  await releaseTab(name, { kill: false });
103
103
  } else {
104
+ const reuseSteps: string[] = [];
105
+ if (opts.viewport) {
106
+ const dsf = opts.viewport.deviceScaleFactor;
107
+ reuseSteps.push(
108
+ `await page.setViewport({ width: ${opts.viewport.width}, height: ${opts.viewport.height}, deviceScaleFactor: ${dsf === undefined ? "undefined" : String(dsf)} });`,
109
+ );
110
+ }
104
111
  if (opts.url) {
112
+ reuseSteps.push(
113
+ `await tab.goto(${JSON.stringify(opts.url)}, { waitUntil: ${JSON.stringify(opts.waitUntil ?? "load")} });`,
114
+ );
115
+ }
116
+ if (reuseSteps.length) {
105
117
  await runInTabWithSnapshot(
106
118
  name,
107
119
  {
108
- code: `await tab.goto(${JSON.stringify(opts.url)}, { waitUntil: ${JSON.stringify(opts.waitUntil ?? "load")} });`,
120
+ code: reuseSteps.join("\n"),
109
121
  timeoutMs: opts.timeoutMs,
110
122
  signal: opts.signal,
111
123
  },
@@ -27,7 +27,7 @@ import {
27
27
  DEFAULT_VIEWPORT,
28
28
  loadPuppeteerInWorker,
29
29
  } from "./launch";
30
- import { extractReadableFromHtml, type ReadableFormat, type ReadableResult } from "./readable";
30
+ import { extractReadableFromHtml, type ReadableFormat } from "./readable";
31
31
  import type {
32
32
  Observation,
33
33
  ObservationEntry,
@@ -97,7 +97,7 @@ interface TabApi {
97
97
  ): Promise<void>;
98
98
  observe(opts?: { includeAll?: boolean; viewportOnly?: boolean }): Promise<Observation>;
99
99
  screenshot(opts?: ScreenshotOptions): Promise<ScreenshotResult>;
100
- extract(format?: ReadableFormat): Promise<ReadableResult | null>;
100
+ extract(format?: ReadableFormat): Promise<string>;
101
101
  click(selector: string): Promise<void>;
102
102
  type(selector: string, text: string): Promise<void>;
103
103
  fill(selector: string, value: string): Promise<void>;
@@ -167,6 +167,25 @@ function cloneSafe(value: unknown): unknown {
167
167
  return String(value);
168
168
  }
169
169
 
170
+ /**
171
+ * Strip `user:pass@` from a URL before surfacing it in tool outputs / details
172
+ * so Basic Auth credentials don't leak into transcripts. Returns the original
173
+ * string verbatim when it doesn't parse as a URL or when there are no
174
+ * credentials to redact.
175
+ */
176
+ function redactUrlCredentials(url: string): string {
177
+ if (!url || (!url.includes("@") && !url.includes("//"))) return url;
178
+ try {
179
+ const parsed = new URL(url);
180
+ if (!parsed.username && !parsed.password) return url;
181
+ parsed.username = "";
182
+ parsed.password = "";
183
+ return parsed.toString();
184
+ } catch {
185
+ return url;
186
+ }
187
+ }
188
+
170
189
  function errorPayload(error: unknown): RunErrorPayload {
171
190
  if (error instanceof ToolAbortError) {
172
191
  return { name: error.name, message: error.message, stack: error.stack, isToolError: false, isAbort: true };
@@ -491,7 +510,7 @@ export class WorkerCore {
491
510
  const targetId = this.#targetId ?? (await targetIdForPage(page));
492
511
  this.#targetId = targetId;
493
512
  return {
494
- url: page.url(),
513
+ url: redactUrlCredentials(page.url()),
495
514
  title: await page.title().catch(() => undefined),
496
515
  viewport: page.viewport() ?? DEFAULT_VIEWPORT,
497
516
  targetId,
@@ -677,7 +696,17 @@ export class WorkerCore {
677
696
  screenshot: async opts => await this.#captureScreenshot(session, displays, screenshots, signal, opts),
678
697
  extract: async (format = "markdown") => {
679
698
  const html = (await untilAborted(signal, () => page.content())) as string;
680
- return extractReadableFromHtml(html, page.url(), format);
699
+ const result = await extractReadableFromHtml(html, page.url(), format);
700
+ if (!result) {
701
+ throw new ToolError(`tab.extract(${JSON.stringify(format)}) found no readable content on ${page.url()}`);
702
+ }
703
+ const content = format === "markdown" ? result.markdown : result.text;
704
+ if (!content) {
705
+ throw new ToolError(
706
+ `tab.extract(${JSON.stringify(format)}) produced empty ${format} content for ${page.url()}`,
707
+ );
708
+ }
709
+ return content;
681
710
  },
682
711
  click: async selector => {
683
712
  const resolved = normalizeSelector(selector);
@@ -22,6 +22,7 @@ import {
22
22
  type DapFunctionBreakpointRecord,
23
23
  type DapInstructionBreakpointRecord,
24
24
  type DapModule,
25
+ type DapResolvedAdapter,
25
26
  type DapScope,
26
27
  type DapSessionSummary,
27
28
  type DapSource,
@@ -30,6 +31,8 @@ import {
30
31
  type DapVariable,
31
32
  dapSessionManager,
32
33
  getAvailableAdapters,
34
+ type LaunchProgramKind,
35
+ resolveLaunchOverrides,
33
36
  selectAttachAdapter,
34
37
  selectLaunchAdapter,
35
38
  } from "../dap";
@@ -489,16 +492,23 @@ function getConfiguredAdapters(cwd: string): string {
489
492
  const adapters = getAvailableAdapters(cwd).map(adapter => adapter.name);
490
493
  return adapters.length > 0 ? adapters.join(", ") : "none";
491
494
  }
492
- async function validateLaunchProgram(program: string, cwd: string): Promise<void> {
493
- let isDirectory: boolean;
495
+
496
+ async function classifyLaunchProgram(program: string): Promise<LaunchProgramKind> {
494
497
  try {
495
- isDirectory = (await fs.stat(program)).isDirectory();
498
+ return (await fs.stat(program)).isDirectory() ? "directory" : "file";
496
499
  } catch (error) {
497
- if (isEnoent(error)) return;
500
+ if (isEnoent(error)) return "missing";
498
501
  throw error;
499
502
  }
500
- if (!isDirectory) return;
503
+ }
501
504
 
505
+ function validateLaunchProgram(
506
+ program: string,
507
+ cwd: string,
508
+ programKind: LaunchProgramKind,
509
+ adapter: DapResolvedAdapter,
510
+ ): void {
511
+ if (programKind !== "directory" || adapter.acceptsDirectoryProgram) return;
502
512
  const displayPath = formatPathRelativeToCwd(program, cwd, { trailingSlash: true });
503
513
  throw new ToolError(
504
514
  `launch program resolves to a directory: ${displayPath}. Pass an executable file path, or for Python use adapter "debugpy" with program set to the .py file.`,
@@ -676,8 +686,8 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
676
686
  }
677
687
  const commandCwd = params.cwd ? resolveToCwd(params.cwd, this.session.cwd) : this.session.cwd;
678
688
  const program = resolveToCwd(params.program, commandCwd);
679
- await validateLaunchProgram(program, commandCwd);
680
- const adapter = selectLaunchAdapter(program, commandCwd, params.adapter);
689
+ const programKind = await classifyLaunchProgram(program);
690
+ const adapter = selectLaunchAdapter(program, commandCwd, params.adapter, programKind);
681
691
  if (!adapter) {
682
692
  if (params.adapter === "debugpy") {
683
693
  throw new ToolError("adapter 'debugpy' is not available: python not found in PATH");
@@ -686,8 +696,10 @@ export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails
686
696
  `No debugger adapter available. Installed adapters: ${getConfiguredAdapters(commandCwd)}`,
687
697
  );
688
698
  }
699
+ validateLaunchProgram(program, commandCwd, programKind, adapter);
700
+ const extraLaunchArguments = resolveLaunchOverrides(adapter, program, programKind);
689
701
  const snapshot = await dapSessionManager.launch(
690
- { adapter, program, args: params.args, cwd: commandCwd },
702
+ { adapter, program, args: params.args, cwd: commandCwd, extraLaunchArguments },
691
703
  combinedSignal,
692
704
  timeoutSec * 1000,
693
705
  );
package/src/tools/eval.ts CHANGED
@@ -88,12 +88,21 @@ function formatDisplayOutputsForText(outputs: EvalDisplayOutput[]): string {
88
88
  export interface EvalToolDescriptionOptions {
89
89
  py?: boolean;
90
90
  js?: boolean;
91
+ /**
92
+ * Whether `agent()` is allowed in this session. Driven by the parent's
93
+ * spawn policy (`getSessionSpawns`). Defaults to `true` for backward
94
+ * compatibility — when the session forbids spawning, the prelude doc
95
+ * omits the `agent()` entry so the model does not promise itself a
96
+ * helper that will only ever throw "spawns disabled".
97
+ */
98
+ spawns?: boolean;
91
99
  }
92
100
 
93
101
  export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
94
102
  const py = options.py ?? true;
95
103
  const js = options.js ?? true;
96
- return prompt.render(evalDescription, { py, js });
104
+ const spawns = options.spawns ?? true;
105
+ return prompt.render(evalDescription, { py, js, spawns });
97
106
  }
98
107
 
99
108
  export interface EvalToolOptions {
@@ -169,7 +178,9 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
169
178
  get description(): string {
170
179
  if (!this.session) return getEvalToolDescription();
171
180
  const backends = resolveEvalBackends(this.session);
172
- return getEvalToolDescription({ py: backends.python, js: backends.js });
181
+ const sessionSpawns = this.session.getSessionSpawns?.() ?? "*";
182
+ const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
183
+ return getEvalToolDescription({ py: backends.python, js: backends.js, spawns: spawnsAllowed });
173
184
  }
174
185
  readonly parameters = evalSchema;
175
186
  readonly concurrency = "exclusive";