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

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 (176) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  4. package/dist/types/commit/analysis/summary.d.ts +2 -2
  5. package/dist/types/commit/changelog/generate.d.ts +2 -2
  6. package/dist/types/commit/changelog/index.d.ts +2 -2
  7. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  8. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  10. package/dist/types/commit/model-selection.d.ts +10 -4
  11. package/dist/types/config/api-key-resolver.d.ts +34 -0
  12. package/dist/types/config/model-registry.d.ts +17 -1
  13. package/dist/types/config/settings-schema.d.ts +9 -0
  14. package/dist/types/dap/config.d.ts +14 -1
  15. package/dist/types/dap/types.d.ts +10 -0
  16. package/dist/types/lsp/utils.d.ts +3 -2
  17. package/dist/types/modes/components/chat-block.d.ts +64 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  19. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  20. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  21. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  22. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  23. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  25. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  26. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  27. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  28. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  29. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  30. package/dist/types/modes/interactive-mode.d.ts +15 -5
  31. package/dist/types/modes/theme/theme.d.ts +1 -1
  32. package/dist/types/modes/types.d.ts +18 -5
  33. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  34. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  35. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  36. package/dist/types/sdk.d.ts +2 -0
  37. package/dist/types/session/agent-session.d.ts +21 -0
  38. package/dist/types/session/messages.d.ts +12 -0
  39. package/dist/types/session/session-manager.d.ts +3 -1
  40. package/dist/types/slash-commands/types.d.ts +4 -6
  41. package/dist/types/task/executor.d.ts +7 -0
  42. package/dist/types/task/index.d.ts +1 -0
  43. package/dist/types/task/render.d.ts +3 -2
  44. package/dist/types/tools/archive-reader.d.ts +5 -0
  45. package/dist/types/tools/ast-edit.d.ts +3 -0
  46. package/dist/types/tools/ast-grep.d.ts +3 -0
  47. package/dist/types/tools/bash.d.ts +1 -0
  48. package/dist/types/tools/find.d.ts +8 -4
  49. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  50. package/dist/types/tools/memory-render.d.ts +4 -1
  51. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  52. package/dist/types/tools/render-utils.d.ts +5 -9
  53. package/dist/types/tools/search.d.ts +4 -0
  54. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  55. package/dist/types/tools/todo.d.ts +3 -2
  56. package/dist/types/tools/write.d.ts +3 -0
  57. package/dist/types/tui/output-block.d.ts +16 -4
  58. package/dist/types/tui/status-line.d.ts +3 -0
  59. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  60. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  61. package/package.json +9 -9
  62. package/src/auto-thinking/classifier.ts +5 -1
  63. package/src/cli/dry-balance-cli.ts +52 -17
  64. package/src/cli/gallery-cli.ts +4 -1
  65. package/src/cli/gallery-fixtures/misc.ts +29 -0
  66. package/src/commit/analysis/conventional.ts +2 -2
  67. package/src/commit/analysis/summary.ts +2 -2
  68. package/src/commit/changelog/generate.ts +2 -2
  69. package/src/commit/changelog/index.ts +2 -2
  70. package/src/commit/map-reduce/index.ts +3 -3
  71. package/src/commit/map-reduce/map-phase.ts +2 -2
  72. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  73. package/src/commit/model-selection.ts +33 -9
  74. package/src/commit/pipeline.ts +4 -4
  75. package/src/config/api-key-resolver.ts +58 -0
  76. package/src/config/model-registry.ts +25 -2
  77. package/src/config/settings-schema.ts +10 -0
  78. package/src/config/settings.ts +20 -2
  79. package/src/dap/config.ts +41 -2
  80. package/src/dap/defaults.json +1 -0
  81. package/src/dap/session.ts +1 -0
  82. package/src/dap/types.ts +10 -0
  83. package/src/debug/index.ts +40 -54
  84. package/src/edit/renderer.ts +82 -78
  85. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  86. package/src/eval/llm-bridge.ts +8 -3
  87. package/src/goals/tools/goal-tool.ts +36 -26
  88. package/src/internal-urls/docs-index.generated.ts +6 -6
  89. package/src/lsp/utils.ts +3 -2
  90. package/src/main.ts +9 -7
  91. package/src/memories/index.ts +12 -5
  92. package/src/mnemopi/backend.ts +5 -1
  93. package/src/modes/acp/acp-agent.ts +33 -26
  94. package/src/modes/components/assistant-message.ts +2 -9
  95. package/src/modes/components/chat-block.ts +111 -0
  96. package/src/modes/components/copy-selector.ts +1 -44
  97. package/src/modes/components/custom-editor.ts +23 -0
  98. package/src/modes/components/custom-message.ts +1 -3
  99. package/src/modes/components/execution-shared.ts +1 -2
  100. package/src/modes/components/hook-message.ts +1 -3
  101. package/src/modes/components/overlay-box.ts +108 -0
  102. package/src/modes/components/plan-review-overlay.ts +799 -0
  103. package/src/modes/components/plan-toc.ts +138 -0
  104. package/src/modes/components/read-tool-group.ts +20 -4
  105. package/src/modes/components/skill-message.ts +0 -1
  106. package/src/modes/components/tips.txt +1 -0
  107. package/src/modes/components/todo-reminder.ts +0 -2
  108. package/src/modes/components/tool-execution.ts +68 -88
  109. package/src/modes/components/transcript-container.ts +84 -24
  110. package/src/modes/components/user-message.ts +1 -2
  111. package/src/modes/controllers/command-controller-shared.ts +7 -6
  112. package/src/modes/controllers/command-controller.ts +57 -55
  113. package/src/modes/controllers/event-controller.ts +41 -40
  114. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  115. package/src/modes/controllers/input-controller.ts +124 -119
  116. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  117. package/src/modes/controllers/selector-controller.ts +23 -25
  118. package/src/modes/controllers/streaming-reveal.ts +212 -0
  119. package/src/modes/controllers/tan-command-controller.ts +173 -0
  120. package/src/modes/interactive-mode.ts +169 -94
  121. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  122. package/src/modes/theme/theme-schema.json +1 -1
  123. package/src/modes/theme/theme.ts +8 -4
  124. package/src/modes/types.ts +18 -7
  125. package/src/modes/utils/copy-targets.ts +133 -27
  126. package/src/modes/utils/ui-helpers.ts +44 -46
  127. package/src/plan-mode/approved-plan.ts +66 -43
  128. package/src/plan-mode/plan-protection.ts +4 -4
  129. package/src/prompts/system/background-tan-dispatch.md +8 -0
  130. package/src/prompts/system/plan-mode-active.md +67 -58
  131. package/src/prompts/system/plan-mode-approved.md +1 -1
  132. package/src/sdk.ts +11 -37
  133. package/src/session/agent-session.ts +82 -6
  134. package/src/session/messages.ts +26 -0
  135. package/src/session/session-manager.ts +13 -5
  136. package/src/slash-commands/builtin-registry.ts +36 -9
  137. package/src/slash-commands/types.ts +4 -6
  138. package/src/task/executor.ts +5 -2
  139. package/src/task/index.ts +4 -0
  140. package/src/task/render.ts +212 -147
  141. package/src/tools/archive-reader.ts +64 -0
  142. package/src/tools/ask.ts +119 -164
  143. package/src/tools/ast-edit.ts +98 -71
  144. package/src/tools/ast-grep.ts +37 -43
  145. package/src/tools/bash.ts +50 -6
  146. package/src/tools/debug.ts +20 -8
  147. package/src/tools/fetch.ts +297 -7
  148. package/src/tools/find.ts +44 -30
  149. package/src/tools/gh-renderer.ts +81 -42
  150. package/src/tools/grouped-file-output.ts +272 -48
  151. package/src/tools/image-gen.ts +150 -103
  152. package/src/tools/inspect-image-renderer.ts +63 -41
  153. package/src/tools/inspect-image.ts +8 -1
  154. package/src/tools/job.ts +3 -4
  155. package/src/tools/memory-render.ts +4 -1
  156. package/src/tools/plan-mode-guard.ts +21 -39
  157. package/src/tools/read.ts +23 -16
  158. package/src/tools/render-utils.ts +21 -37
  159. package/src/tools/resolve.ts +14 -0
  160. package/src/tools/search-tool-bm25.ts +36 -23
  161. package/src/tools/search.ts +80 -78
  162. package/src/tools/sqlite-reader.ts +9 -12
  163. package/src/tools/todo.ts +118 -52
  164. package/src/tools/write.ts +81 -62
  165. package/src/tui/output-block.ts +60 -13
  166. package/src/tui/status-line.ts +5 -1
  167. package/src/utils/commit-message-generator.ts +9 -1
  168. package/src/utils/enhanced-paste.ts +202 -0
  169. package/src/utils/title-generator.ts +2 -1
  170. package/src/web/search/providers/anthropic.ts +25 -19
  171. package/src/web/search/providers/exa.ts +11 -3
  172. package/src/web/search/providers/kimi.ts +28 -17
  173. package/src/web/search/providers/parallel.ts +35 -24
  174. package/src/web/search/providers/synthetic.ts +8 -6
  175. package/src/web/search/providers/tavily.ts +9 -8
  176. package/src/web/search/providers/zai.ts +8 -6
@@ -1,6 +1,6 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
- import { getAntigravityUserAgent, getEnvApiKey, type Model } from "@oh-my-pi/pi-ai";
3
+ import { type ApiKey, getAntigravityUserAgent, getEnvApiKey, type Model, withAuth } from "@oh-my-pi/pi-ai";
4
4
  import {
5
5
  CODEX_BASE_URL,
6
6
  getCodexAccountId,
@@ -20,6 +20,7 @@ import {
20
20
  } from "@oh-my-pi/pi-utils";
21
21
  import * as z from "zod/v4";
22
22
  import packageJson from "../../package.json" with { type: "json" };
23
+
23
24
  import { isAuthenticated, type ModelRegistry } from "../config/model-registry";
24
25
  import type { CustomTool } from "../extensibility/custom-tools/types";
25
26
  import { ohMyPiXAIUserAgent, resolveXAIHttpCredentials } from "../lib/xai-http";
@@ -864,7 +865,10 @@ async function generateOpenAIHostedImage(
864
865
 
865
866
  if (!response.ok) {
866
867
  const errorText = await response.text();
867
- throw new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`);
868
+ throw Object.assign(
869
+ new Error(`OpenAI image request failed (${response.status}): ${getOpenAIResponseErrorMessage(errorText)}`),
870
+ { status: response.status },
871
+ );
868
872
  }
869
873
 
870
874
  const contentType = response.headers.get("content-type") ?? "";
@@ -1037,13 +1041,16 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1037
1041
  throw new Error("Missing active GPT model for OpenAI image generation");
1038
1042
  }
1039
1043
 
1040
- const parsed = await generateOpenAIHostedImage(
1041
- apiKey.apiKey,
1042
- apiKey.model,
1043
- params,
1044
- resolvedImages,
1045
- requestSignal,
1044
+ const hostedModel = apiKey.model;
1045
+ const hostedKey: ApiKey = ctx.modelRegistry.resolver(hostedModel.provider, {
1046
1046
  sessionId,
1047
+ baseUrl: hostedModel.baseUrl,
1048
+ });
1049
+
1050
+ const parsed = await withAuth(
1051
+ hostedKey,
1052
+ key => generateOpenAIHostedImage(key, hostedModel, params, resolvedImages, requestSignal, sessionId),
1053
+ { signal: requestSignal },
1047
1054
  );
1048
1055
 
1049
1056
  if (parsed.images.length === 0) {
@@ -1088,38 +1095,57 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1088
1095
  }
1089
1096
 
1090
1097
  const prompt = assemblePrompt(params);
1091
- const requestBody = buildAntigravityRequest(
1092
- prompt,
1093
- model,
1094
- apiKey.projectId,
1095
- params.aspect_ratio,
1096
- params.image_size,
1097
- resolvedImages,
1098
- );
1099
-
1100
- const response = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
1101
- method: "POST",
1102
- headers: {
1103
- Authorization: `Bearer ${apiKey.apiKey}`,
1104
- "Content-Type": "application/json",
1105
- Accept: "text/event-stream",
1106
- "User-Agent": getAntigravityUserAgent(),
1107
- },
1108
- body: JSON.stringify(requestBody),
1109
- signal: requestSignal,
1098
+ const antigravityKey: ApiKey = ctx.modelRegistry.resolver("google-antigravity", {
1099
+ sessionId,
1110
1100
  });
1111
1101
 
1112
- if (!response.ok) {
1113
- const errorText = await response.text();
1114
- let message = errorText;
1115
- try {
1116
- const parsed = JSON.parse(errorText) as { error?: { message?: string } };
1117
- message = parsed.error?.message ?? message;
1118
- } catch {
1119
- // Keep raw text.
1120
- }
1121
- throw new Error(`Antigravity image request failed (${response.status}): ${message}`);
1122
- }
1102
+ const response = await withAuth(
1103
+ antigravityKey,
1104
+ async key => {
1105
+ // On a retry the resolver yields the raw stored credential JSON
1106
+ // ({ token, projectId }); the initial seed is the already-parsed
1107
+ // access token. Tolerate both, falling back to the seed projectId.
1108
+ const rotated = parseAntigravityCredentials(key);
1109
+ const bearer = rotated?.accessToken ?? key;
1110
+ const projectId = rotated?.projectId ?? apiKey.projectId!;
1111
+ const requestBody = buildAntigravityRequest(
1112
+ prompt,
1113
+ model,
1114
+ projectId,
1115
+ params.aspect_ratio,
1116
+ params.image_size,
1117
+ resolvedImages,
1118
+ );
1119
+
1120
+ const resp = await fetch(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
1121
+ method: "POST",
1122
+ headers: {
1123
+ Authorization: `Bearer ${bearer}`,
1124
+ "Content-Type": "application/json",
1125
+ Accept: "text/event-stream",
1126
+ "User-Agent": getAntigravityUserAgent(),
1127
+ },
1128
+ body: JSON.stringify(requestBody),
1129
+ signal: requestSignal,
1130
+ });
1131
+
1132
+ if (!resp.ok) {
1133
+ const errorText = await resp.text();
1134
+ let message = errorText;
1135
+ try {
1136
+ const parsedErr = JSON.parse(errorText) as { error?: { message?: string } };
1137
+ message = parsedErr.error?.message ?? message;
1138
+ } catch {
1139
+ // Keep raw text.
1140
+ }
1141
+ throw Object.assign(new Error(`Antigravity image request failed (${resp.status}): ${message}`), {
1142
+ status: resp.status,
1143
+ });
1144
+ }
1145
+ return resp;
1146
+ },
1147
+ { signal: requestSignal },
1148
+ );
1123
1149
 
1124
1150
  const parsed = await parseAntigravitySseForImage(response, requestSignal);
1125
1151
  const responseText = parsed.text.length > 0 ? parsed.text.join(" ") : undefined;
@@ -1191,28 +1217,41 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1191
1217
  : xaiBaseBody;
1192
1218
  const xaiEndpoint = isEdit ? "/images/edits" : "/images/generations";
1193
1219
 
1194
- const xaiResponse = await fetch(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1195
- method: "POST",
1196
- headers: {
1197
- Authorization: `Bearer ${xaiCreds.apiKey}`,
1198
- "Content-Type": "application/json",
1199
- "User-Agent": ohMyPiXAIUserAgent(),
1200
- },
1201
- body: JSON.stringify(xaiBody),
1202
- signal: requestSignal,
1220
+ const xaiKey: ApiKey = ctx.modelRegistry.resolver(xaiCreds.provider, {
1221
+ sessionId,
1222
+ baseUrl: xaiCreds.baseURL,
1203
1223
  });
1204
1224
 
1205
- const xaiRawText = await xaiResponse.text();
1206
- if (!xaiResponse.ok) {
1207
- let message = xaiRawText;
1208
- try {
1209
- const parsedErr = JSON.parse(xaiRawText) as { error?: { message?: string } };
1210
- message = parsedErr.error?.message ?? message;
1211
- } catch {
1212
- // Keep raw text.
1213
- }
1214
- throw new Error(`xAI image request failed (${xaiResponse.status}): ${message}`);
1215
- }
1225
+ const xaiRawText = await withAuth(
1226
+ xaiKey,
1227
+ async key => {
1228
+ const resp = await fetch(`${xaiCreds.baseURL}${xaiEndpoint}`, {
1229
+ method: "POST",
1230
+ headers: {
1231
+ Authorization: `Bearer ${key}`,
1232
+ "Content-Type": "application/json",
1233
+ "User-Agent": ohMyPiXAIUserAgent(),
1234
+ },
1235
+ body: JSON.stringify(xaiBody),
1236
+ signal: requestSignal,
1237
+ });
1238
+ const rawText = await resp.text();
1239
+ if (!resp.ok) {
1240
+ let message = rawText;
1241
+ try {
1242
+ const parsedErr = JSON.parse(rawText) as { error?: { message?: string } };
1243
+ message = parsedErr.error?.message ?? message;
1244
+ } catch {
1245
+ // Keep raw text.
1246
+ }
1247
+ throw Object.assign(new Error(`xAI image request failed (${resp.status}): ${message}`), {
1248
+ status: resp.status,
1249
+ });
1250
+ }
1251
+ return rawText;
1252
+ },
1253
+ { signal: requestSignal },
1254
+ );
1216
1255
 
1217
1256
  const xaiData = JSON.parse(xaiRawText) as {
1218
1257
  data?: Array<{ b64_json?: string; url?: string }>;
@@ -1269,30 +1308,34 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1269
1308
  messages: [{ role: "user" as const, content: contentParts }],
1270
1309
  };
1271
1310
 
1272
- const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1273
- method: "POST",
1274
- headers: {
1275
- "Content-Type": "application/json",
1276
- Authorization: `Bearer ${apiKey.apiKey}`,
1277
- "HTTP-Referer": "https://omp.sh/",
1278
- "X-OpenRouter-Title": "Oh-My-Pi",
1279
- "X-OpenRouter-Categories": "cli-agent",
1280
- },
1281
- body: JSON.stringify(requestBody),
1282
- signal: requestSignal,
1283
- });
1284
-
1285
- const rawText = await response.text();
1286
- if (!response.ok) {
1287
- let message = rawText;
1288
- try {
1289
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1290
- message = parsed.error?.message ?? message;
1291
- } catch {
1292
- // Keep raw text.
1311
+ const rawText = await withAuth(apiKey.apiKey, async key => {
1312
+ const resp = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1313
+ method: "POST",
1314
+ headers: {
1315
+ "Content-Type": "application/json",
1316
+ Authorization: `Bearer ${key}`,
1317
+ "HTTP-Referer": "https://omp.sh/",
1318
+ "X-OpenRouter-Title": "Oh-My-Pi",
1319
+ "X-OpenRouter-Categories": "cli-agent",
1320
+ },
1321
+ body: JSON.stringify(requestBody),
1322
+ signal: requestSignal,
1323
+ });
1324
+ const text = await resp.text();
1325
+ if (!resp.ok) {
1326
+ let message = text;
1327
+ try {
1328
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1329
+ message = parsed.error?.message ?? message;
1330
+ } catch {
1331
+ // Keep raw text.
1332
+ }
1333
+ throw Object.assign(new Error(`OpenRouter image request failed (${resp.status}): ${message}`), {
1334
+ status: resp.status,
1335
+ });
1293
1336
  }
1294
- throw new Error(`OpenRouter image request failed (${response.status}): ${message}`);
1295
- }
1337
+ return text;
1338
+ });
1296
1339
 
1297
1340
  const data = JSON.parse(rawText) as OpenRouterResponse;
1298
1341
  const message = data.choices?.[0]?.message;
@@ -1360,30 +1403,34 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
1360
1403
  generationConfig,
1361
1404
  };
1362
1405
 
1363
- const response = await fetch(
1364
- `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1365
- {
1366
- method: "POST",
1367
- headers: {
1368
- "Content-Type": "application/json",
1369
- "x-goog-api-key": apiKey.apiKey,
1406
+ const rawText = await withAuth(apiKey.apiKey, async key => {
1407
+ const resp = await fetch(
1408
+ `https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
1409
+ {
1410
+ method: "POST",
1411
+ headers: {
1412
+ "Content-Type": "application/json",
1413
+ "x-goog-api-key": key,
1414
+ },
1415
+ body: JSON.stringify(requestBody),
1416
+ signal: requestSignal,
1370
1417
  },
1371
- body: JSON.stringify(requestBody),
1372
- signal: requestSignal,
1373
- },
1374
- );
1375
-
1376
- const rawText = await response.text();
1377
- if (!response.ok) {
1378
- let message = rawText;
1379
- try {
1380
- const parsed = JSON.parse(rawText) as { error?: { message?: string } };
1381
- message = parsed.error?.message ?? message;
1382
- } catch {
1383
- // Keep raw text.
1418
+ );
1419
+ const text = await resp.text();
1420
+ if (!resp.ok) {
1421
+ let message = text;
1422
+ try {
1423
+ const parsed = JSON.parse(text) as { error?: { message?: string } };
1424
+ message = parsed.error?.message ?? message;
1425
+ } catch {
1426
+ // Keep raw text.
1427
+ }
1428
+ throw Object.assign(new Error(`Gemini image request failed (${resp.status}): ${message}`), {
1429
+ status: resp.status,
1430
+ });
1384
1431
  }
1385
- throw new Error(`Gemini image request failed (${response.status}): ${message}`);
1386
- }
1432
+ return text;
1433
+ });
1387
1434
 
1388
1435
  const data = JSON.parse(rawText) as GeminiGenerateContentResponse;
1389
1436
  const responseParts = combineParts(data);
@@ -2,8 +2,8 @@ import type { Component } from "@oh-my-pi/pi-tui";
2
2
  import { Text } from "@oh-my-pi/pi-tui";
3
3
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
4
4
  import type { Theme } from "../modes/theme/theme";
5
- import { renderStatusLine } from "../tui";
6
- import { formatExpandHint, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
5
+ import { framedBlock, renderStatusLine } from "../tui";
6
+ import { formatErrorDetail, formatExpandHint, replaceTabs, shortenPath, truncateToWidth } from "./render-utils";
7
7
 
8
8
  interface InspectImageRenderArgs {
9
9
  path?: string;
@@ -27,17 +27,21 @@ const INSPECT_OUTPUT_COLLAPSED_LINES = 4;
27
27
  const INSPECT_OUTPUT_EXPANDED_LINES = 16;
28
28
  const INSPECT_OUTPUT_LINE_WIDTH = 120;
29
29
 
30
+ function questionLine(question: string, uiTheme: Theme): string {
31
+ return `${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(question), INSPECT_QUESTION_PREVIEW_WIDTH))}`;
32
+ }
33
+
30
34
  export const inspectImageToolRenderer = {
31
35
  renderCall(args: InspectImageRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
32
36
  const rawPath = args.path ?? "";
33
37
  const pathDisplay = rawPath ? shortenPath(rawPath) : "…";
34
- const header = renderStatusLine({ icon: "pending", title: "Inspect Image", description: pathDisplay }, uiTheme);
38
+ const header = renderStatusLine({ icon: "pending", title: "Inspect", description: pathDisplay }, uiTheme);
35
39
  const question = args.question?.trim();
36
- if (!question) {
37
- return new Text(header, 0, 0);
38
- }
39
- const questionLine = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(question), INSPECT_QUESTION_PREVIEW_WIDTH))}`;
40
- return new Text(`${header}\n${questionLine}`, 0, 0);
40
+ // Call is at most a status line plus a one-line question — too small to box.
41
+ // The container renders a lone Text cleanly with no chrome.
42
+ if (!question) return new Text(header, 0, 0);
43
+ const tree = ` ${uiTheme.fg("dim", uiTheme.tree.last)} ${questionLine(question, uiTheme)}`;
44
+ return new Text(`${header}\n${tree}`, 0, 0);
41
45
  },
42
46
 
43
47
  renderResult(
@@ -49,55 +53,73 @@ export const inspectImageToolRenderer = {
49
53
  const details = result.details;
50
54
  const rawPath = details?.imagePath ?? args?.path ?? "";
51
55
  const pathDisplay = rawPath ? shortenPath(rawPath) : "image";
52
- const metaParts: string[] = [];
53
- if (details?.model) metaParts.push(details.model);
54
- if (details?.mimeType) metaParts.push(details.mimeType);
55
56
  const header = renderStatusLine(
56
57
  {
57
58
  icon: result.isError ? "error" : "success",
58
- title: "Inspect Image",
59
+ title: "Inspect",
59
60
  description: pathDisplay,
60
61
  },
61
62
  uiTheme,
62
63
  );
63
64
 
64
- const lines: string[] = [header];
65
65
  const question = args?.question?.trim();
66
- if (question) {
67
- lines.push(
68
- ` ${uiTheme.fg("dim", uiTheme.tree.branch)} ${uiTheme.fg("dim", "Question:")} ${uiTheme.fg("accent", truncateToWidth(replaceTabs(question), INSPECT_QUESTION_PREVIEW_WIDTH))}`,
69
- );
70
- }
71
-
72
66
  const outputText = result.content.find(content => content.type === "text")?.text?.trimEnd() ?? "";
73
- if (!outputText) {
74
- lines.push(uiTheme.fg("dim", "(no output)"));
75
- if (metaParts.length > 0) {
76
- lines.push("");
77
- lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
78
- }
79
- return new Text(lines.join("\n"), 0, 0);
80
- }
81
67
 
82
- lines.push("");
83
- const outputLines = replaceTabs(outputText).split("\n");
84
- const maxLines = options.expanded ? INSPECT_OUTPUT_EXPANDED_LINES : INSPECT_OUTPUT_COLLAPSED_LINES;
85
- for (const line of outputLines.slice(0, maxLines)) {
86
- lines.push(uiTheme.fg("toolOutput", truncateToWidth(line, INSPECT_OUTPUT_LINE_WIDTH)));
68
+ if (result.isError) {
69
+ return framedBlock(uiTheme, width => {
70
+ const bodyLines: string[] = [];
71
+ if (question) bodyLines.push(questionLine(question, uiTheme));
72
+ bodyLines.push(formatErrorDetail(outputText || "inspection failed", uiTheme));
73
+ return {
74
+ header,
75
+ sections: [{ lines: bodyLines }],
76
+ state: "error",
77
+ borderColor: "error",
78
+ applyBg: false,
79
+ width,
80
+ };
81
+ });
87
82
  }
88
83
 
89
- if (outputLines.length > maxLines) {
90
- const remaining = outputLines.length - maxLines;
91
- const hint = formatExpandHint(uiTheme, options.expanded, true);
92
- lines.push(`${uiTheme.fg("dim", `… ${remaining} more lines`)}${hint ? ` ${hint}` : ""}`);
93
- }
84
+ const metaParts: string[] = [];
85
+ if (details?.model) metaParts.push(details.model);
86
+ if (details?.mimeType) metaParts.push(details.mimeType);
87
+ const metaLine = metaParts.length > 0 ? uiTheme.fg("dim", metaParts.join(" · ")) : "";
94
88
 
95
- if (metaParts.length > 0) {
96
- lines.push("");
97
- lines.push(uiTheme.fg("dim", metaParts.join(" · ")));
89
+ // No answer text: nothing worth boxing — keep it to a clean status line
90
+ // (plus a trailing meta line, when present).
91
+ if (!outputText) {
92
+ return new Text(metaLine ? `${header}\n${metaLine}` : header, 0, 0);
98
93
  }
99
94
 
100
- return new Text(lines.join("\n"), 0, 0);
95
+ return framedBlock(uiTheme, width => {
96
+ const bodyLines: string[] = [];
97
+ if (question) {
98
+ bodyLines.push(questionLine(question, uiTheme));
99
+ bodyLines.push("");
100
+ }
101
+
102
+ const outputLines = replaceTabs(outputText).split("\n");
103
+ const maxLines = options.expanded ? INSPECT_OUTPUT_EXPANDED_LINES : INSPECT_OUTPUT_COLLAPSED_LINES;
104
+ for (const line of outputLines.slice(0, maxLines)) {
105
+ bodyLines.push(uiTheme.fg("toolOutput", truncateToWidth(line, INSPECT_OUTPUT_LINE_WIDTH)));
106
+ }
107
+ if (outputLines.length > maxLines) {
108
+ const remaining = outputLines.length - maxLines;
109
+ const hint = formatExpandHint(uiTheme, options.expanded, true);
110
+ bodyLines.push(`${uiTheme.fg("dim", `… ${remaining} more lines`)}${hint ? ` ${hint}` : ""}`);
111
+ }
112
+
113
+ return {
114
+ header,
115
+ headerMeta: metaLine || undefined,
116
+ sections: [{ lines: bodyLines }],
117
+ state: "success",
118
+ borderColor: "borderMuted",
119
+ applyBg: false,
120
+ width,
121
+ };
122
+ });
101
123
  },
102
124
  mergeCallAndResult: true,
103
125
  };
@@ -4,6 +4,7 @@ import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
6
6
  import { extractTextContent } from "../commit/utils";
7
+
7
8
  import { expandRoleAlias, resolveModelFromString } from "../config/model-resolver";
8
9
  import inspectImageDescription from "../prompts/tools/inspect-image.md" with { type: "text" };
9
10
  import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-system.md" with { type: "text" };
@@ -136,7 +137,13 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
136
137
  },
137
138
  ],
138
139
  },
139
- { apiKey, signal },
140
+ {
141
+ apiKey: modelRegistry.resolver(model.provider, {
142
+ sessionId: this.session.getSessionId?.() ?? undefined,
143
+ baseUrl: model.baseUrl,
144
+ }),
145
+ signal,
146
+ },
140
147
  { telemetry, oneshotKind: "inspect_image", completeImpl: this.completeImageRequest },
141
148
  );
142
149
 
package/src/tools/job.ts CHANGED
@@ -396,7 +396,7 @@ export const jobToolRenderer = {
396
396
  inline: true,
397
397
 
398
398
  renderCall(args: JobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
399
- const text = renderStatusLine({ icon: "pending", title: "Job", description: describeTarget(args) }, uiTheme);
399
+ const text = renderStatusLine({ icon: "pending", title: describeTarget(args) || "Job" }, uiTheme);
400
400
  return new Text(text, 0, 0);
401
401
  },
402
402
 
@@ -410,7 +410,7 @@ export const jobToolRenderer = {
410
410
 
411
411
  if (jobs.length === 0) {
412
412
  const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process";
413
- const header = renderStatusLine({ icon: "warning", title: "Job", description: describeTarget(args) }, uiTheme);
413
+ const header = renderStatusLine({ icon: "warning", title: describeTarget(args) || "Job" }, uiTheme);
414
414
  return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0);
415
415
  }
416
416
 
@@ -433,8 +433,7 @@ export const jobToolRenderer = {
433
433
  {
434
434
  icon: headerIcon,
435
435
  spinnerFrame: counts.running > 0 ? options.spinnerFrame : undefined,
436
- title: "Job",
437
- description,
436
+ title: description,
438
437
  meta,
439
438
  },
440
439
  uiTheme,
@@ -4,7 +4,10 @@
4
4
  *
5
5
  * These keep the transcript terse — one status line plus, for `retain`, one
6
6
  * `Remember: …` line per stored item — instead of the generic JSON arg tree,
7
- * which exploded multi-line memory blobs into an unreadable wall.
7
+ * which exploded multi-line memory blobs into an unreadable wall. The tool
8
+ * container is a transparent passthrough, so these renderers stay frameless:
9
+ * a status line with a couple of dim bullets reads far cleaner than boxing a
10
+ * one-line memory note.
8
11
  */
9
12
  import type { Component } from "@oh-my-pi/pi-tui";
10
13
  import { Text } from "@oh-my-pi/pi-tui";
@@ -1,4 +1,3 @@
1
- import * as path from "node:path";
2
1
  import { resolveLocalUrlToPath, resolveVaultUrlToPath } from "../internal-urls";
3
2
  import type { ToolSession } from ".";
4
3
  import { normalizeLocalScheme, resolveToCwd } from "./path-utils";
@@ -6,10 +5,19 @@ import { ToolError } from "./tool-errors";
6
5
 
7
6
  const VAULT_SCHEME_PREFIX = "vault:";
8
7
  const LOCAL_SCHEME_PREFIX = "local:";
9
- const PLAN_ALIAS_FILE = "PLAN.md";
10
- const LOCAL_PLAN_ALIAS = "local://PLAN.md";
11
8
 
12
- function resolveRawPath(session: ToolSession, targetPath: string): string {
9
+ /** True when `targetPath` addresses the session-local artifact sandbox
10
+ * (`local://…`). Those files are not part of the working tree, so plan mode
11
+ * treats them as freely writable scratch/plan space. */
12
+ function targetsLocalSandbox(targetPath: string): boolean {
13
+ return normalizeLocalScheme(targetPath).startsWith(LOCAL_SCHEME_PREFIX);
14
+ }
15
+
16
+ /**
17
+ * Resolve a write/edit target to its absolute filesystem path, honoring the
18
+ * `local://` and `vault://` schemes. Plain paths resolve against the session cwd.
19
+ */
20
+ export function resolvePlanPath(session: ToolSession, targetPath: string): string {
13
21
  const normalized = normalizeLocalScheme(targetPath);
14
22
  if (normalized.startsWith(LOCAL_SCHEME_PREFIX)) {
15
23
  return resolveLocalUrlToPath(normalized, {
@@ -25,37 +33,12 @@ function resolveRawPath(session: ToolSession, targetPath: string): string {
25
33
  return resolveToCwd(normalized, session.cwd);
26
34
  }
27
35
 
28
- function isPlanAliasTarget(session: ToolSession, targetPath: string, resolved: string): boolean {
29
- const normalized = normalizeLocalScheme(targetPath);
30
- if (normalized === LOCAL_PLAN_ALIAS) return true;
31
- return resolved === resolveToCwd(PLAN_ALIAS_FILE, session.cwd);
32
- }
33
-
34
36
  /**
35
- * Resolve a write/edit target to its absolute filesystem path.
36
- *
37
- * In plan mode, transparently redirects `PLAN.md` aliases and targets whose
38
- * basename matches the plan file's basename to the canonical plan file
39
- * location at `state.planFilePath`. This lets `write` and `edit` accept the
40
- * habitual plan filename after approval even when the active artifact has a
41
- * titled path such as `local://APPROVED.md`.
42
- *
43
- * Outside plan mode (or when the basename does not match) this is a no-op.
37
+ * Plan mode keeps the working tree read-only while letting the agent draft its
38
+ * plan. Writes and edits to the `local://` artifact sandbox are allowed (that is
39
+ * where the plan and any scratch notes live); anything that would touch the
40
+ * working tree or rename/delete a file is rejected.
44
41
  */
45
- export function resolvePlanPath(session: ToolSession, targetPath: string): string {
46
- const resolved = resolveRawPath(session, targetPath);
47
-
48
- const state = session.getPlanModeState?.();
49
- if (!state?.enabled) return resolved;
50
-
51
- const planResolved = resolveRawPath(session, state.planFilePath);
52
- if (resolved === planResolved) return resolved;
53
- if (isPlanAliasTarget(session, targetPath, resolved)) return planResolved;
54
- if (path.basename(resolved) !== path.basename(planResolved)) return resolved;
55
-
56
- return planResolved;
57
- }
58
-
59
42
  export function enforcePlanModeWrite(
60
43
  session: ToolSession,
61
44
  targetPath: string,
@@ -64,9 +47,6 @@ export function enforcePlanModeWrite(
64
47
  const state = session.getPlanModeState?.();
65
48
  if (!state?.enabled) return;
66
49
 
67
- const resolvedTarget = resolvePlanPath(session, targetPath);
68
- const resolvedPlan = resolvePlanPath(session, state.planFilePath);
69
-
70
50
  if (options?.move) {
71
51
  throw new ToolError("Plan mode: renaming files is not allowed.");
72
52
  }
@@ -75,7 +55,9 @@ export function enforcePlanModeWrite(
75
55
  throw new ToolError("Plan mode: deleting files is not allowed.");
76
56
  }
77
57
 
78
- if (resolvedTarget !== resolvedPlan) {
79
- throw new ToolError(`Plan mode: only the plan file may be modified (${state.planFilePath}).`);
80
- }
58
+ if (targetsLocalSandbox(targetPath)) return;
59
+
60
+ throw new ToolError(
61
+ "Plan mode: the working tree is read-only. Write your plan to a local://<slug>-plan.md file instead.",
62
+ );
81
63
  }