@oh-my-pi/pi-coding-agent 15.11.3 → 15.11.4

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 (103) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/cli.js +353 -294
  3. package/dist/types/config/api-key-resolver.d.ts +9 -3
  4. package/dist/types/config/keybindings.d.ts +1 -1
  5. package/dist/types/config/model-discovery.d.ts +6 -4
  6. package/dist/types/config/model-registry.d.ts +7 -4
  7. package/dist/types/config/settings-schema.d.ts +458 -155
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/mnemopi/config.d.ts +3 -1
  10. package/dist/types/modes/components/settings-defs.d.ts +9 -2
  11. package/dist/types/modes/components/settings-selector.d.ts +9 -4
  12. package/dist/types/modes/components/tool-execution.d.ts +12 -1
  13. package/dist/types/modes/components/transcript-container.d.ts +12 -0
  14. package/dist/types/modes/controllers/input-controller.d.ts +9 -1
  15. package/dist/types/modes/theme/theme.d.ts +23 -3
  16. package/dist/types/session/agent-session.d.ts +14 -7
  17. package/dist/types/session/auth-storage.d.ts +1 -1
  18. package/dist/types/session/snapcompact-inline.d.ts +28 -0
  19. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +14 -0
  20. package/dist/types/system-prompt.d.ts +3 -1
  21. package/dist/types/task/render.d.ts +16 -6
  22. package/dist/types/tools/gh.d.ts +3 -0
  23. package/dist/types/tools/render-utils.d.ts +8 -16
  24. package/dist/types/utils/session-color.d.ts +15 -3
  25. package/dist/types/web/kagi.d.ts +1 -2
  26. package/dist/types/web/search/providers/codex.d.ts +1 -1
  27. package/dist/types/web/search/providers/gemini.d.ts +9 -6
  28. package/package.json +11 -11
  29. package/src/auto-thinking/classifier.ts +1 -5
  30. package/src/commit/model-selection.ts +3 -6
  31. package/src/config/api-key-resolver.ts +10 -3
  32. package/src/config/keybindings.ts +1 -1
  33. package/src/config/model-discovery.ts +60 -46
  34. package/src/config/model-registry.ts +21 -8
  35. package/src/config/model-resolver.ts +57 -3
  36. package/src/config/settings-schema.ts +601 -153
  37. package/src/eval/completion-bridge.ts +1 -5
  38. package/src/export/html/template.generated.ts +1 -1
  39. package/src/export/html/template.js +13 -6
  40. package/src/internal-urls/docs-index.generated.ts +5 -5
  41. package/src/internal-urls/issue-pr-protocol.ts +10 -4
  42. package/src/memories/index.ts +2 -10
  43. package/src/mnemopi/backend.ts +30 -8
  44. package/src/mnemopi/config.ts +6 -1
  45. package/src/mnemopi/state.ts +6 -0
  46. package/src/modes/components/extensions/inspector-panel.ts +6 -2
  47. package/src/modes/components/plan-review-overlay.ts +15 -17
  48. package/src/modes/components/plugin-settings.ts +22 -5
  49. package/src/modes/components/settings-defs.ts +19 -4
  50. package/src/modes/components/settings-selector.ts +493 -93
  51. package/src/modes/components/status-line/component.ts +3 -1
  52. package/src/modes/components/status-line/segments.ts +3 -1
  53. package/src/modes/components/tool-execution.ts +69 -12
  54. package/src/modes/components/transcript-container.ts +26 -0
  55. package/src/modes/components/tree-selector.ts +16 -6
  56. package/src/modes/controllers/command-controller.ts +37 -7
  57. package/src/modes/controllers/event-controller.ts +1 -0
  58. package/src/modes/controllers/input-controller.ts +68 -6
  59. package/src/modes/controllers/selector-controller.ts +81 -61
  60. package/src/modes/interactive-mode.ts +4 -2
  61. package/src/modes/rpc/rpc-mode.ts +2 -1
  62. package/src/modes/shared.ts +2 -0
  63. package/src/modes/theme/theme.ts +100 -7
  64. package/src/modes/utils/context-usage.ts +3 -1
  65. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  66. package/src/modes/utils/ui-helpers.ts +9 -5
  67. package/src/prompts/system/personalities/default.md +26 -0
  68. package/src/prompts/system/personalities/friendly.md +17 -0
  69. package/src/prompts/system/personalities/pragmatic.md +15 -0
  70. package/src/prompts/system/snapcompact-system-frames-note.md +1 -0
  71. package/src/prompts/system/snapcompact-system-stub.md +1 -0
  72. package/src/prompts/system/snapcompact-toolresult-note.md +1 -0
  73. package/src/prompts/system/system-prompt.md +5 -22
  74. package/src/prompts/tools/task.md +3 -3
  75. package/src/sdk.ts +22 -1
  76. package/src/session/agent-session.ts +91 -24
  77. package/src/session/auth-storage.ts +1 -0
  78. package/src/session/session-dump-format.ts +8 -1
  79. package/src/session/session-manager.ts +5 -5
  80. package/src/session/snapcompact-inline.ts +187 -0
  81. package/src/slash-commands/helpers/active-oauth-account.ts +44 -0
  82. package/src/slash-commands/helpers/usage-report.ts +24 -3
  83. package/src/system-prompt.ts +15 -1
  84. package/src/task/render.ts +29 -19
  85. package/src/tool-discovery/tool-index.ts +2 -0
  86. package/src/tools/bash.ts +10 -3
  87. package/src/tools/eval-render.ts +13 -8
  88. package/src/tools/gh.ts +39 -1
  89. package/src/tools/image-gen.ts +114 -78
  90. package/src/tools/inspect-image.ts +1 -5
  91. package/src/tools/job.ts +25 -5
  92. package/src/tools/read.ts +1 -57
  93. package/src/tools/render-utils.ts +29 -31
  94. package/src/tools/ssh.ts +3 -3
  95. package/src/tools/tts.ts +40 -20
  96. package/src/utils/clipboard.ts +56 -4
  97. package/src/utils/commit-message-generator.ts +1 -5
  98. package/src/utils/session-color.ts +83 -9
  99. package/src/utils/title-generator.ts +1 -1
  100. package/src/web/kagi.ts +26 -27
  101. package/src/web/search/providers/codex.ts +42 -40
  102. package/src/web/search/providers/gemini.ts +42 -22
  103. package/src/web/search/providers/perplexity.ts +22 -10
@@ -9,17 +9,27 @@ import { $ } from "bun";
9
9
  import { contextFileCapability } from "./capability/context-file";
10
10
  import { systemPromptCapability } from "./capability/system-prompt";
11
11
  import { findConfigFile } from "./config";
12
- import type { SkillsSettings } from "./config/settings";
12
+ import type { Personality, SkillsSettings } from "./config/settings";
13
13
  import { type ContextFile, loadCapability, type SystemPrompt as SystemPromptFile } from "./discovery";
14
14
  import { expandAtImports } from "./discovery/at-imports";
15
15
  import { loadSkills, type Skill } from "./extensibility/skills";
16
16
  import { hasObsidian } from "./internal-urls/vault-protocol";
17
17
  import customSystemPromptTemplate from "./prompts/system/custom-system-prompt.md" with { type: "text" };
18
+ import defaultPersonality from "./prompts/system/personalities/default.md" with { type: "text" };
19
+ import friendlyPersonality from "./prompts/system/personalities/friendly.md" with { type: "text" };
20
+ import pragmaticPersonality from "./prompts/system/personalities/pragmatic.md" with { type: "text" };
18
21
  import projectPromptTemplate from "./prompts/system/project-prompt.md" with { type: "text" };
19
22
  import systemPromptTemplate from "./prompts/system/system-prompt.md" with { type: "text" };
20
23
  import { shortenPath } from "./tools/render-utils";
21
24
  import { AGENTS_MD_LIMIT, buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
22
25
 
26
+ /** Bundled personality specs, keyed by the `personality` setting value. */
27
+ const PERSONALITY_SPECS: Record<Exclude<Personality, "none">, string> = {
28
+ default: defaultPersonality,
29
+ friendly: friendlyPersonality,
30
+ pragmatic: pragmaticPersonality,
31
+ };
32
+
23
33
  interface AlwaysApplyRule {
24
34
  name: string;
25
35
  content: string;
@@ -385,6 +395,8 @@ export interface BuildSystemPromptOptions {
385
395
  memoryRootEnabled?: boolean;
386
396
  /** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
387
397
  model?: string;
398
+ /** Personality preset rendered into the default system prompt. "none" omits the block. Default: "default" */
399
+ personality?: Personality;
388
400
  }
389
401
 
390
402
  /** Result of building provider-facing system prompt messages. */
@@ -419,6 +431,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
419
431
  workspaceTree: providedWorkspaceTree,
420
432
  memoryRootEnabled = false,
421
433
  model,
434
+ personality = "default",
422
435
  } = options;
423
436
  const resolvedCwd = cwd ?? getProjectDir();
424
437
 
@@ -590,6 +603,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
590
603
  dateTime,
591
604
  cwd: promptCwd,
592
605
  model: model ?? "",
606
+ personality: personality === "none" ? "" : PERSONALITY_SPECS[personality].trim(),
593
607
  intentTracing: !!intentField,
594
608
  intentField: intentField ?? "",
595
609
  mcpDiscoveryMode,
@@ -11,7 +11,6 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
11
11
  import { settings } from "../config/settings";
12
12
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
13
  import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
14
- import { shimmerEnabled, shimmerText } from "../modes/theme/shimmer";
15
14
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
16
15
  import {
17
16
  formatBadge,
@@ -35,6 +34,18 @@ import { repairDoubleEncodedJsonString } from "./repair-args";
35
34
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
36
35
  import type { AgentProgress, SingleResult, TaskItem, TaskParams, TaskToolDetails } from "./types";
37
36
 
37
+ /** Render context threaded in from `ToolExecutionComponent.#buildRenderContext`. */
38
+ interface TaskRenderContext {
39
+ hasResult?: boolean;
40
+ /**
41
+ * The block left the transcript live region (detached spawn the transcript
42
+ * has moved past, or a sealed block): progress rows render static gray, so
43
+ * commit-eligible rows do not repaint after entering native scrollback.
44
+ */
45
+ frozen?: boolean;
46
+ }
47
+ type TaskRenderOptions = RenderResultOptions & { renderContext?: TaskRenderContext };
48
+
38
49
  /**
39
50
  * Get status icon for agent state.
40
51
  * For running status, uses animated spinner if spinnerFrame is provided.
@@ -614,11 +625,7 @@ function createMarkdownSectionRenderer(text: string, theme: Theme): AssignmentSe
614
625
  /**
615
626
  * Render the tool call arguments.
616
627
  */
617
- export function renderCall(
618
- args: TaskParams,
619
- options: RenderResultOptions & { renderContext?: { hasResult?: boolean } },
620
- theme: Theme,
621
- ): Component {
628
+ export function renderCall(args: TaskParams, options: TaskRenderOptions, theme: Theme): Component {
622
629
  const showIsolated = "isolated" in args && args.isolated === true;
623
630
  const header = renderStatusLine({ icon: "pending", title: "Task", description: args.agent }, theme);
624
631
  const assignmentSection = createAssignmentSectionRenderer(args, theme);
@@ -666,6 +673,7 @@ function renderAgentProgress(
666
673
  expanded: boolean,
667
674
  theme: Theme,
668
675
  spinnerFrame?: number,
676
+ frozen = false,
669
677
  ): string[] {
670
678
  const lines: string[] = [];
671
679
 
@@ -684,16 +692,16 @@ function renderAgentProgress(
684
692
  const indent = prefix ? `${prefix} ` : "";
685
693
  let statusLine: string;
686
694
  if (progress.status === "running" || progress.status === "pending") {
687
- // Live (or queued) agents shimmer their description so the row reads as
688
- // in-flight even after the block freezes the async spawn result keeps
689
- // the agent on "pending" while the detached job runs.
690
- const bullet =
691
- progress.status === "running" ? theme.styledSymbol("status.done", "text") : theme.fg(iconColor, icon);
692
- const name = theme.fg("accent", description ? theme.bold(displayId) : displayId);
693
- statusLine = `${indent}${bullet} ${name}`;
695
+ // Live (or queued) agents use the task icon: detached async spawns can
696
+ // stay "pending" while real work is running, so a pending/hourglass glyph
697
+ // reads wrong in the transcript. Keep the row static; the Task tool header
698
+ // already carries any live animation.
699
+ const taskIcon = theme.styledSymbol("tool.task", frozen ? "dim" : "accent");
700
+ const nameColor = frozen ? "dim" : "accent";
701
+ const name = theme.fg(nameColor, description ? theme.bold(displayId) : displayId);
702
+ statusLine = `${indent}${taskIcon} ${name}`;
694
703
  if (description) {
695
- const desc = shimmerEnabled() ? shimmerText(description, theme) : theme.fg("accent", description);
696
- statusLine += `${theme.fg("accent", ":")} ${desc}`;
704
+ statusLine += `${theme.fg(nameColor, ":")} ${theme.fg(nameColor, description)}`;
697
705
  }
698
706
  } else {
699
707
  const glyph =
@@ -836,7 +844,7 @@ function renderAgentProgress(
836
844
  const inflight = progress.inflightTaskDetails;
837
845
  if (completedTaskCalls.length > 0 || inflight) {
838
846
  const snapshots = inflight ? [...completedTaskCalls, inflight] : completedTaskCalls;
839
- const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame);
847
+ const nestedLines = renderNestedTaskTree(snapshots, expanded, theme, spinnerFrame, frozen);
840
848
  for (const line of nestedLines) {
841
849
  lines.push(`${continuePrefix}${line}`);
842
850
  }
@@ -1160,7 +1168,7 @@ function orderResultsForDisplay(results: readonly SingleResult[]): SingleResult[
1160
1168
  */
1161
1169
  export function renderResult(
1162
1170
  result: { content: Array<{ type: string; text?: string }>; details?: TaskToolDetails; isError?: boolean },
1163
- options: RenderResultOptions,
1171
+ options: TaskRenderOptions,
1164
1172
  theme: Theme,
1165
1173
  args?: TaskParams,
1166
1174
  ): Component {
@@ -1219,13 +1227,14 @@ export function renderResult(
1219
1227
 
1220
1228
  return framedBlock(theme, width => {
1221
1229
  const { expanded, isPartial, spinnerFrame } = options;
1230
+ const frozen = options.renderContext?.frozen === true;
1222
1231
  const lines: string[] = [];
1223
1232
 
1224
1233
  const shouldRenderProgress =
1225
1234
  Boolean(details.progress && details.progress.length > 0) && (isPartial || details.results.length === 0);
1226
1235
  if (shouldRenderProgress && details.progress) {
1227
1236
  orderProgressForDisplay(details.progress).forEach(progress => {
1228
- lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame));
1237
+ lines.push(...renderAgentProgress(progress, "", " ", expanded, theme, spinnerFrame, frozen));
1229
1238
  });
1230
1239
  } else if (details.results && details.results.length > 0) {
1231
1240
  orderResultsForDisplay(details.results).forEach(res => {
@@ -1345,6 +1354,7 @@ function renderNestedTaskTree(
1345
1354
  expanded: boolean,
1346
1355
  theme: Theme,
1347
1356
  spinnerFrame?: number,
1357
+ frozen = false,
1348
1358
  ): string[] {
1349
1359
  const lines: string[] = [];
1350
1360
  for (const details of detailsList) {
@@ -1362,7 +1372,7 @@ function renderNestedTaskTree(
1362
1372
  const ordered = orderProgressForDisplay(inflight);
1363
1373
  ordered.forEach((prog, index) => {
1364
1374
  const { prefix, continuePrefix } = nestedMarkers(index === ordered.length - 1, theme);
1365
- lines.push(...renderAgentProgress(prog, prefix, continuePrefix, expanded, theme, spinnerFrame));
1375
+ lines.push(...renderAgentProgress(prog, prefix, continuePrefix, expanded, theme, spinnerFrame, frozen));
1366
1376
  });
1367
1377
  }
1368
1378
  }
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
2
3
 
3
4
  // ─── Generic Tool Discovery Types ────────────────────────────────────────────
4
5
 
@@ -65,6 +66,7 @@ export function isMCPToolName(name: string): boolean {
65
66
  }
66
67
 
67
68
  function getSchemaPropertyKeys(parameters: unknown): string[] {
69
+ if (isZodSchema(parameters)) parameters = zodToWireSchema(parameters);
68
70
  if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return [];
69
71
  const properties = (parameters as { properties?: unknown }).properties;
70
72
  if (!properties || typeof properties !== "object" || Array.isArray(properties)) return [];
package/src/tools/bash.ts CHANGED
@@ -31,7 +31,7 @@ import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-ski
31
31
  import { invalidateGithubCacheForBashCommand } from "./gh-cache-invalidation";
32
32
  import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
33
33
  import { resolveToCwd } from "./path-utils";
34
- import { capPreviewLines, formatToolWorkingDirectory, replaceTabs } from "./render-utils";
34
+ import { capPreviewLines, formatToolWorkingDirectory, previewWindowRows, 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";
@@ -1241,6 +1241,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1241
1241
  let cachedRawOutput: string | undefined;
1242
1242
  let cachedIsPartial: boolean | undefined;
1243
1243
  let cachedLines: readonly string[] | undefined;
1244
+ let cachedPreviewWindow: number | undefined;
1244
1245
 
1245
1246
  return markFramedBlockComponent({
1246
1247
  render: (width: number): readonly string[] => {
@@ -1255,6 +1256,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1255
1256
  const rawOutput = renderContext?.output ?? result.content?.find(c => c.type === "text")?.text ?? "";
1256
1257
 
1257
1258
  const isPartial = options.isPartial === true;
1259
+ const previewWindow = previewWindowRows();
1258
1260
 
1259
1261
  if (
1260
1262
  cachedLines !== undefined &&
@@ -1262,7 +1264,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1262
1264
  cachedPreviewLines === previewLines &&
1263
1265
  cachedExpanded === expanded &&
1264
1266
  cachedRawOutput === rawOutput &&
1265
- cachedIsPartial === isPartial
1267
+ cachedIsPartial === isPartial &&
1268
+ cachedPreviewWindow === previewWindow
1266
1269
  ) {
1267
1270
  return cachedLines;
1268
1271
  }
@@ -1348,7 +1351,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1348
1351
  state: isPartial ? "pending" : isError ? "error" : "success",
1349
1352
  sections: [
1350
1353
  {
1351
- lines: isPartial ? capPreviewLines(cmdLines ?? [], uiTheme, { expanded }) : (cmdLines ?? []),
1354
+ // Viewport-sized tail window in every state streaming and final
1355
+ // render identically; only ctrl+o uncaps.
1356
+ lines: capPreviewLines(cmdLines ?? [], uiTheme, { expanded }),
1352
1357
  },
1353
1358
  { label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
1354
1359
  ],
@@ -1362,6 +1367,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1362
1367
  cachedExpanded = expanded;
1363
1368
  cachedRawOutput = rawOutput;
1364
1369
  cachedIsPartial = isPartial;
1370
+ cachedPreviewWindow = previewWindow;
1365
1371
  cachedLines = framed;
1366
1372
  return framed;
1367
1373
  },
@@ -1373,6 +1379,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
1373
1379
  cachedExpanded = undefined;
1374
1380
  cachedRawOutput = undefined;
1375
1381
  cachedIsPartial = undefined;
1382
+ cachedPreviewWindow = undefined;
1376
1383
  },
1377
1384
  });
1378
1385
  },
@@ -33,6 +33,7 @@ import {
33
33
  formatDuration,
34
34
  formatStatusIcon,
35
35
  formatTitle,
36
+ previewWindowRows,
36
37
  replaceTabs,
37
38
  shortenPath,
38
39
  truncateToWidth,
@@ -493,7 +494,7 @@ export const evalToolRenderer = {
493
494
 
494
495
  return markFramedBlockComponent({
495
496
  render: (width: number): readonly string[] => {
496
- const key = `${options.expanded ? 1 : 0}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
497
+ const key = `${options.expanded ? 1 : 0}|${previewWindowRows()}|${cells.map(c => `${c.language}:${c.title ?? ""}:${c.code.length}`).join("|")}`;
497
498
  if (cached && cached.key === key && cached.width === width) {
498
499
  return cached.result;
499
500
  }
@@ -510,9 +511,11 @@ export const evalToolRenderer = {
510
511
  title: cell.title,
511
512
  status: "pending",
512
513
  width,
513
- // Always render the full source: the code is fixed input, not the
514
- // streaming part, so it is never compacted.
515
- codeMaxLines: Number.POSITIVE_INFINITY,
514
+ // Viewport-sized tail window following the newest streamed code
515
+ // line; renderResult keeps the same cap so the cell never snaps
516
+ // open on completion. Only ctrl+o uncaps.
517
+ codeTail: true,
518
+ codeMaxLines: previewWindowRows(),
516
519
  expanded: options.expanded,
517
520
  },
518
521
  uiTheme,
@@ -576,7 +579,7 @@ export const evalToolRenderer = {
576
579
  render: (width: number): readonly string[] => {
577
580
  const expanded = options.renderContext?.expanded ?? options.expanded;
578
581
  const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
579
- const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
582
+ const key = `${expanded}|${previewLines}|${options.spinnerFrame}|${previewWindowRows()}`;
580
583
  if (cached && cached.key === key && cached.width === width) {
581
584
  return cached.result;
582
585
  }
@@ -613,9 +616,11 @@ export const evalToolRenderer = {
613
616
  duration: cell.durationMs,
614
617
  output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
615
618
  outputMaxLines: outputLines.length,
616
- // Code is fixed input always shown in full, never compacted.
617
- // Only `output` honors the collapsed preview cap above.
618
- codeMaxLines: Number.POSITIVE_INFINITY,
619
+ // Same viewport-sized tail window as the pending preview so the
620
+ // cell never snaps open on completion; only ctrl+o uncaps.
621
+ // `output` keeps its own preview cap from above.
622
+ codeTail: true,
623
+ codeMaxLines: previewWindowRows(),
619
624
  expanded,
620
625
  width,
621
626
  },
package/src/tools/gh.ts CHANGED
@@ -63,6 +63,44 @@ const GH_ISSUE_FIELDS_NO_COMMENTS = [
63
63
  "updatedAt",
64
64
  "url",
65
65
  ];
66
+
67
+ const GH_ISSUE_STATE_REASON_FIELD = "stateReason";
68
+
69
+ function ghJsonErrorNamesField(err: unknown, field: string): boolean {
70
+ if (!(err instanceof Error) || !err.message.includes("Unknown JSON field")) return false;
71
+ return err.message.includes(`"${field}"`) || err.message.includes(`'${field}'`) || err.message.includes(field);
72
+ }
73
+
74
+ function dropJsonField(args: readonly string[], field: string): string[] | undefined {
75
+ const next = [...args];
76
+ const jsonIndex = next.indexOf("--json");
77
+ if (jsonIndex < 0) return undefined;
78
+ const fields = next[jsonIndex + 1];
79
+ if (!fields) return undefined;
80
+ const splitFields = fields.split(",");
81
+ const kept = splitFields.filter(candidate => candidate !== field);
82
+ if (kept.length === splitFields.length) return undefined;
83
+ next[jsonIndex + 1] = kept.join(",");
84
+ return next;
85
+ }
86
+
87
+ /** Runs `gh --json` for issue data, retrying without optional stateReason on older gh releases. */
88
+ export async function githubIssueJsonWithStateReasonFallback<T>(
89
+ cwd: string,
90
+ args: readonly string[],
91
+ signal: AbortSignal | undefined,
92
+ options?: git.GhCommandOptions,
93
+ ): Promise<T> {
94
+ try {
95
+ return await git.github.json<T>(cwd, [...args], signal, options);
96
+ } catch (err) {
97
+ if (!ghJsonErrorNamesField(err, GH_ISSUE_STATE_REASON_FIELD)) throw err;
98
+ const retryArgs = dropJsonField(args, GH_ISSUE_STATE_REASON_FIELD);
99
+ if (!retryArgs) throw err;
100
+ return await git.github.json<T>(cwd, retryArgs, signal, options);
101
+ }
102
+ }
103
+
66
104
  const GH_PR_FIELDS = [
67
105
  "author",
68
106
  "baseRefName",
@@ -2549,7 +2587,7 @@ async function fetchIssueViewFresh(
2549
2587
  const args = ["issue", "view", identifier];
2550
2588
  appendRepoFlag(args, repo, identifier);
2551
2589
  args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
2552
- const data = await git.github.json<GhIssueViewData>(cwd, args, signal, {
2590
+ const data = await githubIssueJsonWithStateReasonFallback<GhIssueViewData>(cwd, args, signal, {
2553
2591
  repoProvided: Boolean(repo),
2554
2592
  });
2555
2593
  const rendered = formatIssueView(data, { issue: identifier, repo, comments: includeComments });
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { type ApiKey, type FetchImpl, getEnvApiKey, type Model, withAuth } from "@oh-my-pi/pi-ai";
3
+ import { type ApiKey, type FetchImpl, getEnvApiKey, type Model, ProviderHttpError, withAuth } from "@oh-my-pi/pi-ai";
4
4
  import {
5
5
  CODEX_BASE_URL,
6
6
  getCodexAccountId,
@@ -47,7 +47,7 @@ export type ImageProviderPreference = Exclude<ImageProvider, "openai-codex"> | "
47
47
 
48
48
  interface ImageApiKey {
49
49
  provider: ImageProvider;
50
- apiKey: string;
50
+ apiKey: ApiKey;
51
51
  projectId?: string;
52
52
  model?: Model;
53
53
  }
@@ -502,6 +502,39 @@ async function findXAIImageCredentials(modelRegistry?: ModelRegistry): Promise<I
502
502
  return null;
503
503
  }
504
504
 
505
+ async function findOpenRouterImageCredentials(
506
+ modelRegistry?: ModelRegistry,
507
+ sessionId?: string,
508
+ ): Promise<ImageApiKey | null> {
509
+ if (modelRegistry) {
510
+ // AuthStorage.getApiKey already falls back to env keys, so this covers OPENROUTER_API_KEY too.
511
+ const apiKey = await modelRegistry.getApiKeyForProvider("openrouter", sessionId);
512
+ if (apiKey) return { provider: "openrouter", apiKey: modelRegistry.resolver("openrouter", { sessionId }) };
513
+ return null;
514
+ }
515
+ const apiKey = getEnvApiKey("openrouter");
516
+ if (apiKey) return { provider: "openrouter", apiKey };
517
+ return null;
518
+ }
519
+
520
+ async function findGeminiImageCredentials(
521
+ modelRegistry?: ModelRegistry,
522
+ sessionId?: string,
523
+ ): Promise<ImageApiKey | null> {
524
+ if (modelRegistry) {
525
+ // AuthStorage.getApiKey already falls back to env keys (GEMINI_API_KEY), so only
526
+ // GOOGLE_API_KEY needs the explicit check below.
527
+ const apiKey = await modelRegistry.getApiKeyForProvider("google", sessionId);
528
+ if (apiKey) return { provider: "gemini", apiKey: modelRegistry.resolver("google", { sessionId }) };
529
+ } else {
530
+ const envKey = getEnvApiKey("google");
531
+ if (envKey) return { provider: "gemini", apiKey: envKey };
532
+ }
533
+ const googleKey = $env.GOOGLE_API_KEY;
534
+ if (googleKey) return { provider: "gemini", apiKey: googleKey };
535
+ return null;
536
+ }
537
+
505
538
  async function findOpenAIHostedImageCredentials(
506
539
  modelRegistry: ModelRegistry | undefined,
507
540
  activeModel: Model | undefined,
@@ -532,14 +565,12 @@ async function findImageApiKey(
532
565
  if (antigravity) return antigravity;
533
566
  // Fall through to auto-detect if preferred provider key not found.
534
567
  } else if (preferredImageProvider === "gemini") {
535
- const geminiKey = getEnvApiKey("google");
536
- if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
537
- const googleKey = $env.GOOGLE_API_KEY;
538
- if (googleKey) return { provider: "gemini", apiKey: googleKey };
568
+ const gemini = await findGeminiImageCredentials(modelRegistry, sessionId);
569
+ if (gemini) return gemini;
539
570
  // Fall through to auto-detect if preferred provider key not found.
540
571
  } else if (preferredImageProvider === "openrouter") {
541
- const openRouterKey = getEnvApiKey("openrouter");
542
- if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
572
+ const openRouter = await findOpenRouterImageCredentials(modelRegistry, sessionId);
573
+ if (openRouter) return openRouter;
543
574
  // Fall through to auto-detect if preferred provider key not found.
544
575
  } else if (preferredImageProvider === "xai") {
545
576
  const xai = await findXAIImageCredentials(modelRegistry);
@@ -559,14 +590,11 @@ async function findImageApiKey(
559
590
  const xai = await findXAIImageCredentials(modelRegistry);
560
591
  if (xai) return xai;
561
592
 
562
- const openRouterKey = getEnvApiKey("openrouter");
563
- if (openRouterKey) return { provider: "openrouter", apiKey: openRouterKey };
593
+ const openRouter = await findOpenRouterImageCredentials(modelRegistry, sessionId);
594
+ if (openRouter) return openRouter;
564
595
 
565
- const geminiKey = getEnvApiKey("google");
566
- if (geminiKey) return { provider: "gemini", apiKey: geminiKey };
567
-
568
- const googleKey = $env.GOOGLE_API_KEY;
569
- if (googleKey) return { provider: "gemini", apiKey: googleKey };
596
+ const gemini = await findGeminiImageCredentials(modelRegistry, sessionId);
597
+ if (gemini) return gemini;
570
598
 
571
599
  return null;
572
600
  }
@@ -1054,11 +1082,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1054
1082
  }
1055
1083
 
1056
1084
  const hostedModel = apiKey.model;
1057
- const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel.provider, {
1058
- sessionId,
1059
- baseUrl: hostedModel.baseUrl,
1060
- modelId: hostedModel.id,
1061
- });
1085
+ const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel, sessionId);
1062
1086
 
1063
1087
  const parsed = await withAuth(
1064
1088
  hostedKey,
@@ -1161,9 +1185,11 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1161
1185
  } catch {
1162
1186
  // Keep raw text.
1163
1187
  }
1164
- throw Object.assign(new Error(`Antigravity image request failed (${resp.status}): ${message}`), {
1165
- status: resp.status,
1166
- });
1188
+ throw new ProviderHttpError(
1189
+ `Antigravity image request failed (${resp.status}): ${message}`,
1190
+ resp.status,
1191
+ { headers: resp.headers },
1192
+ );
1167
1193
  }
1168
1194
  return resp;
1169
1195
  },
@@ -1267,8 +1293,8 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1267
1293
  } catch {
1268
1294
  // Keep raw text.
1269
1295
  }
1270
- throw Object.assign(new Error(`xAI image request failed (${resp.status}): ${message}`), {
1271
- status: resp.status,
1296
+ throw new ProviderHttpError(`xAI image request failed (${resp.status}): ${message}`, resp.status, {
1297
+ headers: resp.headers,
1272
1298
  });
1273
1299
  }
1274
1300
  return rawText;
@@ -1331,34 +1357,40 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1331
1357
  messages: [{ role: "user" as const, content: contentParts }],
1332
1358
  };
1333
1359
 
1334
- const rawText = await withAuth(apiKey.apiKey, async key => {
1335
- const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
1336
- method: "POST",
1337
- headers: {
1338
- "Content-Type": "application/json",
1339
- Authorization: `Bearer ${key}`,
1340
- "HTTP-Referer": "https://omp.sh/",
1341
- "X-OpenRouter-Title": "Oh-My-Pi",
1342
- "X-OpenRouter-Categories": "cli-agent",
1343
- },
1344
- body: JSON.stringify(requestBody),
1345
- signal: requestSignal,
1346
- });
1347
- const text = await resp.text();
1348
- if (!resp.ok) {
1349
- let message = text;
1350
- try {
1351
- const parsed = JSON.parse(text) as { error?: { message?: string } };
1352
- message = parsed.error?.message ?? message;
1353
- } catch {
1354
- // Keep raw text.
1355
- }
1356
- throw Object.assign(new Error(`OpenRouter image request failed (${resp.status}): ${message}`), {
1357
- status: resp.status,
1360
+ const rawText = await withAuth(
1361
+ apiKey.apiKey,
1362
+ async key => {
1363
+ const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
1364
+ method: "POST",
1365
+ headers: {
1366
+ "Content-Type": "application/json",
1367
+ Authorization: `Bearer ${key}`,
1368
+ "HTTP-Referer": "https://omp.sh/",
1369
+ "X-OpenRouter-Title": "Oh-My-Pi",
1370
+ "X-OpenRouter-Categories": "cli-agent",
1371
+ },
1372
+ body: JSON.stringify(requestBody),
1373
+ signal: requestSignal,
1358
1374
  });
1359
- }
1360
- return text;
1361
- });
1375
+ const text = await resp.text();
1376
+ if (!resp.ok) {
1377
+ let message = text;
1378
+ try {
1379
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1380
+ message = parsed.error?.message ?? message;
1381
+ } catch {
1382
+ // Keep raw text.
1383
+ }
1384
+ throw new ProviderHttpError(
1385
+ `OpenRouter image request failed (${resp.status}): ${message}`,
1386
+ resp.status,
1387
+ { headers: resp.headers },
1388
+ );
1389
+ }
1390
+ return text;
1391
+ },
1392
+ { signal: requestSignal },
1393
+ );
1362
1394
 
1363
1395
  const data = JSON.parse(rawText) as OpenRouterResponse;
1364
1396
  const message = data.choices?.[0]?.message;
@@ -1426,34 +1458,38 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1426
1458
  generationConfig,
1427
1459
  };
1428
1460
 
1429
- const rawText = await withAuth(apiKey.apiKey, async key => {
1430
- const resp = await fetchImpl(
1431
- `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1432
- {
1433
- method: "POST",
1434
- headers: {
1435
- "Content-Type": "application/json",
1436
- "x-goog-api-key": key,
1461
+ const rawText = await withAuth(
1462
+ apiKey.apiKey,
1463
+ async key => {
1464
+ const resp = await fetchImpl(
1465
+ `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1466
+ {
1467
+ method: "POST",
1468
+ headers: {
1469
+ "Content-Type": "application/json",
1470
+ "x-goog-api-key": key,
1471
+ },
1472
+ body: JSON.stringify(requestBody),
1473
+ signal: requestSignal,
1437
1474
  },
1438
- body: JSON.stringify(requestBody),
1439
- signal: requestSignal,
1440
- },
1441
- );
1442
- const text = await resp.text();
1443
- if (!resp.ok) {
1444
- let message = text;
1445
- try {
1446
- const parsed = JSON.parse(text) as { error?: { message?: string } };
1447
- message = parsed.error?.message ?? message;
1448
- } catch {
1449
- // Keep raw text.
1475
+ );
1476
+ const text = await resp.text();
1477
+ if (!resp.ok) {
1478
+ let message = text;
1479
+ try {
1480
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1481
+ message = parsed.error?.message ?? message;
1482
+ } catch {
1483
+ // Keep raw text.
1484
+ }
1485
+ throw new ProviderHttpError(`Gemini image request failed (${resp.status}): ${message}`, resp.status, {
1486
+ headers: resp.headers,
1487
+ });
1450
1488
  }
1451
- throw Object.assign(new Error(`Gemini image request failed (${resp.status}): ${message}`), {
1452
- status: resp.status,
1453
- });
1454
- }
1455
- return text;
1456
- });
1489
+ return text;
1490
+ },
1491
+ { signal: requestSignal },
1492
+ );
1457
1493
 
1458
1494
  const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1459
1495
  const responseParts = combineParts(data);
@@ -138,11 +138,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
138
138
  ],
139
139
  },
140
140
  {
141
- apiKey: modelRegistry.resolver(model.provider, {
142
- sessionId: this.session.getSessionId?.() ?? undefined,
143
- baseUrl: model.baseUrl,
144
- modelId: model.id,
145
- }),
141
+ apiKey: modelRegistry.resolver(model, this.session.getSessionId?.() ?? undefined),
146
142
  signal,
147
143
  },
148
144
  { telemetry, oneshotKind: "inspect_image", completeImpl: this.completeImageRequest },