@oh-my-pi/pi-coding-agent 11.2.3 → 11.3.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 (92) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/hooks/qna.ts +1 -1
  4. package/examples/hooks/status-line.ts +1 -1
  5. package/examples/sdk/11-sessions.ts +1 -1
  6. package/package.json +8 -8
  7. package/src/cli/args.ts +9 -6
  8. package/src/cli/update-cli.ts +2 -2
  9. package/src/commands/index/index.ts +2 -5
  10. package/src/commit/agentic/agent.ts +1 -1
  11. package/src/commit/changelog/index.ts +2 -2
  12. package/src/config/keybindings.ts +16 -1
  13. package/src/config/model-registry.ts +25 -20
  14. package/src/config/model-resolver.ts +8 -8
  15. package/src/config/resolve-config-value.ts +92 -0
  16. package/src/config/settings-schema.ts +9 -0
  17. package/src/config.ts +14 -1
  18. package/src/export/html/template.css +7 -0
  19. package/src/export/html/template.generated.ts +1 -1
  20. package/src/export/html/template.js +33 -16
  21. package/src/extensibility/custom-commands/bundled/review/index.ts +1 -1
  22. package/src/extensibility/extensions/index.ts +18 -0
  23. package/src/extensibility/extensions/loader.ts +15 -0
  24. package/src/extensibility/extensions/runner.ts +78 -1
  25. package/src/extensibility/extensions/types.ts +131 -5
  26. package/src/extensibility/extensions/wrapper.ts +1 -1
  27. package/src/extensibility/plugins/git-url.ts +270 -0
  28. package/src/extensibility/plugins/index.ts +2 -0
  29. package/src/extensibility/slash-commands.ts +45 -0
  30. package/src/index.ts +7 -0
  31. package/src/lsp/render.ts +50 -43
  32. package/src/lsp/utils.ts +2 -2
  33. package/src/main.ts +11 -10
  34. package/src/mcp/transports/stdio.ts +3 -5
  35. package/src/modes/components/custom-message.ts +0 -8
  36. package/src/modes/components/diff.ts +1 -7
  37. package/src/modes/components/footer.ts +4 -4
  38. package/src/modes/components/model-selector.ts +4 -0
  39. package/src/modes/components/todo-display.ts +13 -3
  40. package/src/modes/components/tool-execution.ts +30 -16
  41. package/src/modes/components/tree-selector.ts +50 -19
  42. package/src/modes/controllers/event-controller.ts +1 -0
  43. package/src/modes/controllers/extension-ui-controller.ts +34 -2
  44. package/src/modes/controllers/input-controller.ts +47 -33
  45. package/src/modes/controllers/selector-controller.ts +10 -15
  46. package/src/modes/interactive-mode.ts +50 -38
  47. package/src/modes/print-mode.ts +6 -0
  48. package/src/modes/rpc/rpc-client.ts +4 -4
  49. package/src/modes/rpc/rpc-mode.ts +17 -2
  50. package/src/modes/rpc/rpc-types.ts +2 -2
  51. package/src/modes/types.ts +1 -0
  52. package/src/modes/utils/ui-helpers.ts +3 -1
  53. package/src/patch/applicator.ts +2 -3
  54. package/src/patch/fuzzy.ts +1 -1
  55. package/src/patch/shared.ts +74 -61
  56. package/src/prompts/system/system-prompt.md +1 -0
  57. package/src/prompts/tools/task.md +6 -0
  58. package/src/sdk.ts +15 -11
  59. package/src/session/agent-session.ts +72 -23
  60. package/src/session/auth-storage.ts +2 -1
  61. package/src/session/blob-store.ts +105 -0
  62. package/src/session/session-manager.ts +107 -44
  63. package/src/task/executor.ts +19 -9
  64. package/src/task/render.ts +80 -58
  65. package/src/tools/ask.ts +28 -5
  66. package/src/tools/bash.ts +47 -39
  67. package/src/tools/browser.ts +248 -26
  68. package/src/tools/calculator.ts +42 -23
  69. package/src/tools/fetch.ts +33 -16
  70. package/src/tools/find.ts +57 -22
  71. package/src/tools/grep.ts +54 -25
  72. package/src/tools/index.ts +5 -5
  73. package/src/tools/notebook.ts +19 -6
  74. package/src/tools/path-utils.ts +26 -1
  75. package/src/tools/python.ts +20 -14
  76. package/src/tools/read.ts +21 -8
  77. package/src/tools/render-utils.ts +5 -45
  78. package/src/tools/ssh.ts +59 -53
  79. package/src/tools/submit-result.ts +2 -2
  80. package/src/tools/todo-write.ts +32 -14
  81. package/src/tools/truncate.ts +1 -1
  82. package/src/tools/write.ts +39 -24
  83. package/src/tui/output-block.ts +61 -3
  84. package/src/tui/tree-list.ts +4 -4
  85. package/src/tui/utils.ts +71 -1
  86. package/src/utils/frontmatter.ts +1 -1
  87. package/src/utils/title-generator.ts +1 -1
  88. package/src/utils/tools-manager.ts +18 -2
  89. package/src/web/scrapers/osv.ts +4 -1
  90. package/src/web/scrapers/youtube.ts +1 -1
  91. package/src/web/search/index.ts +1 -1
  92. package/src/web/search/render.ts +96 -90
@@ -24,7 +24,7 @@ import {
24
24
  type ReportFindingDetails,
25
25
  type SubmitReviewDetails,
26
26
  } from "../tools/review";
27
- import { renderStatusLine } from "../tui";
27
+ import { Ellipsis, Hasher, type RenderCache, renderStatusLine } from "../tui";
28
28
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
29
29
  import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
30
30
 
@@ -382,7 +382,13 @@ function formatScalarInline(value: unknown, maxLen: number, _theme: Theme): stri
382
382
  if (value === undefined) return "undefined";
383
383
  if (typeof value === "boolean") return String(value);
384
384
  if (typeof value === "number") return String(value);
385
- if (typeof value === "string") return `"${truncateToWidth(value, maxLen)}"`;
385
+ if (typeof value === "string") {
386
+ const firstLine = value.split("\n")[0].trim();
387
+ if (firstLine.length === 0) return `"" (${value.split("\n").length} lines)`;
388
+ const preview = truncateToWidth(firstLine, maxLen);
389
+ if (value.includes("\n")) return `"${preview}…" (${value.split("\n").length} lines)`;
390
+ return `"${preview}"`;
391
+ }
386
392
  if (Array.isArray(value)) return `[${value.length} items]`;
387
393
  if (typeof value === "object") {
388
394
  const keys = Object.keys(value);
@@ -845,74 +851,90 @@ export function renderResult(
845
851
  options: RenderResultOptions,
846
852
  theme: Theme,
847
853
  ): Component {
848
- const { expanded, isPartial, spinnerFrame } = options;
849
854
  const fallbackText = result.content.find(c => c.type === "text")?.text ?? "";
850
855
  const details = result.details;
851
856
 
852
857
  if (!details) {
853
- // Fallback to simple text
854
858
  const text = result.content.find(c => c.type === "text")?.text || "";
855
859
  return new Text(theme.fg("dim", truncateToWidth(text, 100)), 0, 0);
856
860
  }
857
861
 
858
- const lines: string[] = [];
859
-
860
- if (isPartial && details.progress) {
861
- // Streaming progress view
862
- details.progress.forEach((progress, i) => {
863
- const isLast = i === details.progress!.length - 1;
864
- lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
865
- });
866
- } else if (details.results.length > 0) {
867
- // Final results view
868
- details.results.forEach((res, i) => {
869
- const isLast = i === details.results.length - 1;
870
- lines.push(...renderAgentResult(res, isLast, expanded, theme));
871
- });
872
-
873
- // Summary line
874
- const abortedCount = details.results.filter(r => r.aborted).length;
875
- const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0).length;
876
- const failCount = details.results.length - successCount - abortedCount;
877
- let summary = `${theme.fg("dim", "Total:")} `;
878
- if (abortedCount > 0) {
879
- summary += theme.fg("error", `${abortedCount} aborted`);
880
- if (successCount > 0 || failCount > 0) summary += theme.sep.dot;
881
- }
882
- if (successCount > 0) {
883
- summary += theme.fg("success", `${successCount} succeeded`);
884
- if (failCount > 0) summary += theme.sep.dot;
885
- }
886
- if (failCount > 0) {
887
- summary += theme.fg("error", `${failCount} failed`);
888
- }
889
- summary += `${theme.sep.dot}${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
890
- lines.push(summary);
891
-
892
- // Artifacts suppressed from user view - available via session file
893
- }
862
+ let cached: RenderCache | undefined;
863
+
864
+ return {
865
+ render(width) {
866
+ const { expanded, isPartial, spinnerFrame } = options;
867
+ const key = new Hasher()
868
+ .bool(expanded)
869
+ .bool(isPartial)
870
+ .u32(spinnerFrame ?? 0)
871
+ .u32(width)
872
+ .digest();
873
+ if (cached?.key === key) return cached.lines;
874
+
875
+ const lines: string[] = [];
876
+
877
+ if (isPartial && details.progress) {
878
+ details.progress.forEach((progress, i) => {
879
+ const isLast = i === details.progress!.length - 1;
880
+ lines.push(...renderAgentProgress(progress, isLast, expanded, theme, spinnerFrame));
881
+ });
882
+ } else if (details.results.length > 0) {
883
+ details.results.forEach((res, i) => {
884
+ const isLast = i === details.results.length - 1;
885
+ lines.push(...renderAgentResult(res, isLast, expanded, theme));
886
+ });
887
+
888
+ const abortedCount = details.results.filter(r => r.aborted).length;
889
+ const successCount = details.results.filter(r => !r.aborted && r.exitCode === 0).length;
890
+ const failCount = details.results.length - successCount - abortedCount;
891
+ let summary = `${theme.fg("dim", "Total:")} `;
892
+ if (abortedCount > 0) {
893
+ summary += theme.fg("error", `${abortedCount} aborted`);
894
+ if (successCount > 0 || failCount > 0) summary += theme.sep.dot;
895
+ }
896
+ if (successCount > 0) {
897
+ summary += theme.fg("success", `${successCount} succeeded`);
898
+ if (failCount > 0) summary += theme.sep.dot;
899
+ }
900
+ if (failCount > 0) {
901
+ summary += theme.fg("error", `${failCount} failed`);
902
+ }
903
+ summary += `${theme.sep.dot}${theme.fg("dim", formatDuration(details.totalDurationMs))}`;
904
+ lines.push(summary);
905
+ }
894
906
 
895
- if (lines.length === 0) {
896
- const text = fallbackText.trim() ? fallbackText : "No results";
897
- return new Text(theme.fg("dim", truncateToWidth(text, 140)), 0, 0);
898
- }
907
+ if (lines.length === 0) {
908
+ const text = fallbackText.trim() ? fallbackText : "No results";
909
+ const result = [theme.fg("dim", truncateToWidth(text, width))];
910
+ cached = { key, lines: result };
911
+ return result;
912
+ }
899
913
 
900
- if (fallbackText.trim()) {
901
- const summaryLines = fallbackText.split("\n");
902
- const markerIndex = summaryLines.findIndex(
903
- line => line.includes("<system-notification>") || line.startsWith("Applied patches:"),
904
- );
905
- if (markerIndex >= 0) {
906
- const extra = summaryLines.slice(markerIndex);
907
- for (const line of extra) {
908
- if (!line.trim()) continue;
909
- lines.push(theme.fg("dim", line));
914
+ if (fallbackText.trim()) {
915
+ const summaryLines = fallbackText.split("\n");
916
+ const markerIndex = summaryLines.findIndex(
917
+ line => line.includes("<system-notification>") || line.startsWith("Applied patches:"),
918
+ );
919
+ if (markerIndex >= 0) {
920
+ const extra = summaryLines.slice(markerIndex);
921
+ for (const line of extra) {
922
+ if (!line.trim()) continue;
923
+ lines.push(theme.fg("dim", line));
924
+ }
925
+ }
910
926
  }
911
- }
912
- }
913
927
 
914
- const indented = lines.map(line => (line.length > 0 ? ` ${line}` : ""));
915
- return new Text(indented.join("\n"), 0, 0);
928
+ const indented = lines.map(line =>
929
+ line.length > 0 ? truncateToWidth(` ${line}`, width, Ellipsis.Omit) : "",
930
+ );
931
+ cached = { key, lines: indented };
932
+ return indented;
933
+ },
934
+ invalidate() {
935
+ cached = undefined;
936
+ },
937
+ };
916
938
  }
917
939
 
918
940
  function isTaskToolDetails(value: unknown): value is TaskToolDetails {
package/src/tools/ask.ts CHANGED
@@ -446,7 +446,7 @@ export const askToolRenderer = {
446
446
 
447
447
  renderResult(
448
448
  result: { content: Array<{ type: string; text?: string }>; details?: AskToolDetails },
449
- _opts: RenderResultOptions,
449
+ _options: RenderResultOptions,
450
450
  uiTheme: Theme,
451
451
  ): Component {
452
452
  const { details } = result;
@@ -454,7 +454,13 @@ export const askToolRenderer = {
454
454
  const txt = result.content[0];
455
455
  const fallback = txt?.type === "text" && txt.text ? txt.text : "";
456
456
  const header = renderStatusLine({ icon: "warning", title: "Ask" }, uiTheme);
457
- return new Text([header, uiTheme.fg("dim", fallback)].join("\n"), 0, 0);
457
+ const renderedLines = [header, uiTheme.fg("dim", fallback)];
458
+ return {
459
+ render() {
460
+ return renderedLines;
461
+ },
462
+ invalidate() {},
463
+ };
458
464
  }
459
465
 
460
466
  // Multi-part results
@@ -506,13 +512,24 @@ export const askToolRenderer = {
506
512
  }
507
513
  }
508
514
 
509
- return new Text(lines.join("\n"), 0, 0);
515
+ return {
516
+ render() {
517
+ return lines;
518
+ },
519
+ invalidate() {},
520
+ };
510
521
  }
511
522
 
512
523
  // Single question result
513
524
  if (!details.question) {
514
525
  const txt = result.content[0];
515
- return new Text(txt?.type === "text" && txt.text ? txt.text : "", 0, 0);
526
+ const renderedLines = txt?.type === "text" && txt.text ? txt.text.split("\n") : [""];
527
+ return {
528
+ render() {
529
+ return renderedLines;
530
+ },
531
+ invalidate() {},
532
+ };
516
533
  }
517
534
 
518
535
  const hasSelection = details.customInput || (details.selectedOptions && details.selectedOptions.length > 0);
@@ -535,6 +552,12 @@ export const askToolRenderer = {
535
552
  text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
536
553
  }
537
554
 
538
- return new Text(text, 0, 0);
555
+ const renderedLines = text.split("\n");
556
+ return {
557
+ render() {
558
+ return renderedLines;
559
+ },
560
+ invalidate() {},
561
+ };
539
562
  },
540
563
  };
package/src/tools/bash.ts CHANGED
@@ -2,14 +2,15 @@ import * as path from "node:path";
2
2
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Text } from "@oh-my-pi/pi-tui";
5
- import { Type } from "@sinclair/typebox";
5
+ import { type Static, Type } from "@sinclair/typebox";
6
6
  import { renderPromptTemplate } from "../config/prompt-templates";
7
7
  import { type BashExecutorOptions, executeBash } from "../exec/bash-executor";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
12
- import { renderOutputBlock, renderStatusLine } from "../tui";
12
+ import { renderStatusLine } from "../tui";
13
+ import { CachedOutputBlock } from "../tui/output-block";
13
14
  import type { ToolSession } from ".";
14
15
  import { checkBashInterception } from "./bash-interceptor";
15
16
  import { applyHeadTail, normalizeBashCommand } from "./bash-normalize";
@@ -31,6 +32,8 @@ const bashSchema = Type.Object({
31
32
  tail: Type.Optional(Type.Number({ description: "Return only last N lines of output" })),
32
33
  });
33
34
 
35
+ export type BashToolInput = Static<typeof bashSchema>;
36
+
34
37
  export interface BashToolDetails {
35
38
  meta?: OutputMeta;
36
39
  }
@@ -221,46 +224,49 @@ export const bashToolRenderer = {
221
224
  const cmdText = args ? formatBashCommand(args, uiTheme) : undefined;
222
225
  const isError = result.isError === true;
223
226
  const header = renderStatusLine({ icon: isError ? "error" : "success", title: "Bash" }, uiTheme);
224
- const { renderContext } = options;
225
227
  const details = result.details;
226
- const expanded = renderContext?.expanded ?? options.expanded;
227
- const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
228
-
229
- // Get output from context (preferred) or fall back to result content
230
- const output = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
231
- const displayOutput = output.trimEnd();
232
- const showingFullOutput = expanded && renderContext?.isFullOutput === true;
233
-
234
- // Build truncation warning lines (static, doesn't depend on width)
235
228
  const truncation = details?.meta?.truncation;
236
- const timeoutSeconds = renderContext?.timeout;
237
- const timeoutLine =
238
- typeof timeoutSeconds === "number"
239
- ? uiTheme.fg(
240
- "dim",
241
- `${uiTheme.format.bracketLeft}Timeout: ${timeoutSeconds}s${uiTheme.format.bracketRight}`,
242
- )
243
- : undefined;
244
- let warningLine: string | undefined;
245
- if (truncation && !showingFullOutput) {
246
- const warnings: string[] = [];
247
- if (truncation?.artifactId) {
248
- warnings.push(`Full output: artifact://${truncation.artifactId}`);
249
- }
250
- if (truncation.truncatedBy === "lines") {
251
- warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
252
- } else {
253
- warnings.push(
254
- `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.outputBytes)} limit)`,
255
- );
256
- }
257
- if (warnings.length > 0) {
258
- warningLine = uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme));
259
- }
260
- }
229
+ const outputBlock = new CachedOutputBlock();
261
230
 
262
231
  return {
263
232
  render: (width: number): string[] => {
233
+ // REACTIVE: read mutable options at render time
234
+ const { renderContext } = options;
235
+ const expanded = renderContext?.expanded ?? options.expanded;
236
+ const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
237
+
238
+ // Get output from context (preferred) or fall back to result content
239
+ const output = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
240
+ const displayOutput = output.trimEnd();
241
+ const showingFullOutput = expanded && renderContext?.isFullOutput === true;
242
+
243
+ // Build truncation warning
244
+ const timeoutSeconds = renderContext?.timeout;
245
+ const timeoutLine =
246
+ typeof timeoutSeconds === "number"
247
+ ? uiTheme.fg(
248
+ "dim",
249
+ `${uiTheme.format.bracketLeft}Timeout: ${timeoutSeconds}s${uiTheme.format.bracketRight}`,
250
+ )
251
+ : undefined;
252
+ let warningLine: string | undefined;
253
+ if (truncation && !showingFullOutput) {
254
+ const warnings: string[] = [];
255
+ if (truncation?.artifactId) {
256
+ warnings.push(`Full output: artifact://${truncation.artifactId}`);
257
+ }
258
+ if (truncation.truncatedBy === "lines") {
259
+ warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
260
+ } else {
261
+ warnings.push(
262
+ `Truncated: ${truncation.outputLines} lines shown (${formatBytes(truncation.outputBytes)} limit)`,
263
+ );
264
+ }
265
+ if (warnings.length > 0) {
266
+ warningLine = uiTheme.fg("warning", wrapBrackets(warnings.join(". "), uiTheme));
267
+ }
268
+ }
269
+
264
270
  const outputLines: string[] = [];
265
271
  const hasOutput = displayOutput.trim().length > 0;
266
272
  if (hasOutput) {
@@ -287,7 +293,7 @@ export const bashToolRenderer = {
287
293
  if (timeoutLine) outputLines.push(timeoutLine);
288
294
  if (warningLine) outputLines.push(warningLine);
289
295
 
290
- return renderOutputBlock(
296
+ return outputBlock.render(
291
297
  {
292
298
  header,
293
299
  state: isError ? "error" : "success",
@@ -300,7 +306,9 @@ export const bashToolRenderer = {
300
306
  uiTheme,
301
307
  );
302
308
  },
303
- invalidate: () => {},
309
+ invalidate: () => {
310
+ outputBlock.invalidate();
311
+ },
304
312
  };
305
313
  },
306
314
  mergeCallAndResult: true,