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

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 (109) hide show
  1. package/CHANGELOG.md +68 -2
  2. package/dist/types/cli/classify-install-target.d.ts +5 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +104 -0
  4. package/dist/types/commands/dry-balance.d.ts +31 -0
  5. package/dist/types/config/model-registry.d.ts +2 -0
  6. package/dist/types/config/models-config-schema.d.ts +3 -0
  7. package/dist/types/config/settings-schema.d.ts +13 -4
  8. package/dist/types/config/settings.d.ts +11 -0
  9. package/dist/types/discovery/helpers.d.ts +1 -0
  10. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
  11. package/dist/types/hindsight/bank.d.ts +17 -9
  12. package/dist/types/hindsight/mental-models.d.ts +1 -1
  13. package/dist/types/hindsight/state.d.ts +9 -3
  14. package/dist/types/mcp/manager.d.ts +1 -1
  15. package/dist/types/modes/components/assistant-message.d.ts +11 -0
  16. package/dist/types/modes/components/custom-editor.d.ts +3 -1
  17. package/dist/types/modes/components/error-banner.d.ts +11 -0
  18. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  19. package/dist/types/modes/components/transcript-container.d.ts +4 -2
  20. package/dist/types/modes/components/user-message.d.ts +1 -1
  21. package/dist/types/modes/image-references.d.ts +17 -0
  22. package/dist/types/modes/interactive-mode.d.ts +7 -0
  23. package/dist/types/modes/types.d.ts +7 -0
  24. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  25. package/dist/types/session/agent-session.d.ts +9 -0
  26. package/dist/types/session/auth-storage.d.ts +2 -2
  27. package/dist/types/session/blob-store.d.ts +12 -11
  28. package/dist/types/session/session-manager.d.ts +5 -3
  29. package/dist/types/system-prompt.d.ts +2 -0
  30. package/dist/types/task/types.d.ts +2 -0
  31. package/dist/types/tiny/title-client.d.ts +16 -1
  32. package/dist/types/tool-discovery/mode.d.ts +8 -0
  33. package/dist/types/tools/archive-reader.d.ts +5 -1
  34. package/dist/types/tools/index.d.ts +16 -0
  35. package/dist/types/tools/path-utils.d.ts +11 -0
  36. package/dist/types/tui/hyperlink.d.ts +12 -0
  37. package/dist/types/web/search/render.d.ts +1 -2
  38. package/package.json +9 -9
  39. package/src/cli/classify-install-target.ts +31 -5
  40. package/src/cli/dry-balance-cli.ts +823 -0
  41. package/src/cli/plugin-cli.ts +45 -0
  42. package/src/cli/web-search-cli.ts +0 -1
  43. package/src/cli-commands.ts +1 -0
  44. package/src/commands/dry-balance.ts +43 -0
  45. package/src/config/model-registry.ts +60 -4
  46. package/src/config/models-config-schema.ts +2 -0
  47. package/src/config/settings-schema.ts +14 -4
  48. package/src/config/settings.ts +38 -0
  49. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
  50. package/src/discovery/github.ts +37 -1
  51. package/src/discovery/helpers.ts +3 -1
  52. package/src/eval/__tests__/agent-bridge.test.ts +72 -0
  53. package/src/eval/py/tool-bridge.ts +43 -5
  54. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
  55. package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
  56. package/src/hindsight/backend.ts +184 -35
  57. package/src/hindsight/bank.ts +32 -22
  58. package/src/hindsight/mental-models.ts +1 -1
  59. package/src/hindsight/state.ts +21 -7
  60. package/src/internal-urls/docs-index.generated.ts +6 -6
  61. package/src/internal-urls/omp-protocol.ts +8 -2
  62. package/src/main.ts +7 -1
  63. package/src/mcp/manager.ts +40 -21
  64. package/src/modes/components/assistant-message.ts +22 -0
  65. package/src/modes/components/custom-editor.ts +14 -2
  66. package/src/modes/components/error-banner.ts +33 -0
  67. package/src/modes/components/tool-execution.ts +44 -0
  68. package/src/modes/components/transcript-container.ts +102 -30
  69. package/src/modes/components/tree-selector.ts +29 -2
  70. package/src/modes/components/user-message.ts +9 -2
  71. package/src/modes/controllers/event-controller.ts +42 -3
  72. package/src/modes/controllers/input-controller.ts +41 -3
  73. package/src/modes/image-references.ts +111 -0
  74. package/src/modes/interactive-mode.ts +48 -13
  75. package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
  76. package/src/modes/types.ts +10 -1
  77. package/src/modes/utils/ui-helpers.ts +23 -2
  78. package/src/prompts/agents/explore.md +1 -0
  79. package/src/prompts/agents/librarian.md +1 -0
  80. package/src/prompts/ci-green-request.md +5 -3
  81. package/src/prompts/dry-balance-bench.md +8 -0
  82. package/src/prompts/system/project-prompt.md +1 -0
  83. package/src/sdk.ts +99 -18
  84. package/src/session/agent-session.ts +103 -19
  85. package/src/session/auth-storage.ts +4 -0
  86. package/src/session/blob-store.ts +96 -9
  87. package/src/session/session-manager.ts +19 -10
  88. package/src/system-prompt.ts +4 -0
  89. package/src/task/executor.ts +6 -2
  90. package/src/task/index.ts +8 -7
  91. package/src/task/types.ts +2 -0
  92. package/src/tiny/title-client.ts +7 -1
  93. package/src/tool-discovery/mode.ts +24 -0
  94. package/src/tools/archive-reader.ts +339 -31
  95. package/src/tools/bash.ts +3 -4
  96. package/src/tools/fetch.ts +29 -9
  97. package/src/tools/gh.ts +65 -11
  98. package/src/tools/index.ts +22 -8
  99. package/src/tools/job.ts +3 -3
  100. package/src/tools/memory-reflect.ts +2 -2
  101. package/src/tools/path-utils.ts +21 -0
  102. package/src/tools/read.ts +58 -12
  103. package/src/tools/search-tool-bm25.ts +4 -6
  104. package/src/tools/search.ts +78 -12
  105. package/src/tui/hyperlink.ts +42 -7
  106. package/src/utils/file-mentions.ts +7 -107
  107. package/src/utils/title-generator.ts +58 -37
  108. package/src/web/search/index.ts +2 -2
  109. package/src/web/search/render.ts +20 -52
@@ -149,7 +149,10 @@ export async function generateSessionTitle(
149
149
  // tiny title model can't reliably decline trivial input, so this happens
150
150
  // deterministically before any model is invoked; the caller retries on the
151
151
  // next user message while the session stays unnamed.
152
- if (isLowSignalTitleInput(firstMessage)) return null;
152
+ if (isLowSignalTitleInput(firstMessage)) {
153
+ logger.debug("title-generator: skipped low-signal input", { sessionId, reason: "low-signal" });
154
+ return null;
155
+ }
153
156
 
154
157
  const tinyModel = settings.get("providers.tinyModel");
155
158
  if (tinyModel === ONLINE_TINY_TITLE_MODEL_KEY) {
@@ -159,7 +162,14 @@ export async function generateSessionTitle(
159
162
  const onlineAbortController = new AbortController();
160
163
  const localTitle = tinyTitleClient.generate(tinyModel, firstMessage).then(
161
164
  title => title || null,
162
- () => null,
165
+ err => {
166
+ logger.warn("title-generator: local model error", {
167
+ sessionId,
168
+ model: tinyModel,
169
+ error: err instanceof Error ? err.message : String(err),
170
+ });
171
+ return null;
172
+ },
163
173
  );
164
174
  const startOnline = (): Promise<string | null> =>
165
175
  generateTitleOnline(
@@ -188,49 +198,48 @@ export async function generateTitleOnline(
188
198
  ): Promise<string | null> {
189
199
  const model = getTitleModel(registry, settings, currentModel);
190
200
  if (!model) {
191
- logger.debug("title-generator: no title model found");
201
+ logger.warn("title-generator: no title model found", { sessionId, reason: "no-title-model" });
192
202
  return null;
193
203
  }
194
204
 
195
205
  const userMessage = formatTitleUserMessage(firstMessage);
196
-
197
- const apiKey = await registry.getApiKey(model, sessionId);
198
- if (!apiKey) {
199
- logger.debug("title-generator: no API key for smol model", {
200
- provider: model.provider,
201
- id: model.id,
202
- });
203
- return null;
204
- }
205
- // Resolve metadata after getApiKey so the session-sticky credential for this
206
- // request is already recorded; metadataResolver can then return the correct
207
- // account_uuid rather than the snapshot-at-call-site value.
208
- const metadata = metadataResolver?.(model.provider);
209
-
210
- // Title generation is a 3-6 word task, but some reasoning backends ignore
211
- // disableReasoning. Keep the normal cheap budget for non-reasoning models
212
- // while reserving enough output room for reasoning models to still emit
213
- // the forced tool call after any unavoidable thinking tokens.
214
- const maxTokens = model.reasoning ? Math.max(TITLE_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : TITLE_MAX_TOKENS;
215
- const request = {
216
- model: `${model.provider}/${model.id}`,
217
- systemPrompt: TITLE_SYSTEM_PROMPT,
218
- userMessage,
219
- maxTokens,
206
+ const modelName = `${model.provider}/${model.id}`;
207
+ const modelContext = {
208
+ sessionId,
209
+ provider: model.provider,
210
+ id: model.id,
211
+ model: modelName,
220
212
  };
221
- logger.debug("title-generator: request", request);
213
+ logger.debug("title-generator: start", modelContext);
222
214
 
223
215
  try {
216
+ const apiKey = await registry.getApiKey(model, sessionId);
217
+ if (!apiKey) {
218
+ logger.warn("title-generator: no API key", { ...modelContext, reason: "missing-api-key" });
219
+ return null;
220
+ }
221
+ // Resolve metadata after getApiKey so the session-sticky credential for this
222
+ // request is already recorded; metadataResolver can then return the correct
223
+ // account_uuid rather than the snapshot-at-call-site value.
224
+ const metadata = metadataResolver?.(model.provider);
225
+
226
+ // Title generation is a 3-6 word task, but some reasoning backends ignore
227
+ // disableReasoning. Keep the normal cheap budget for non-reasoning models
228
+ // while reserving enough output room for reasoning models to still emit
229
+ // the forced tool call after any unavoidable thinking tokens.
230
+ const maxTokens = model.reasoning ? Math.max(TITLE_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : TITLE_MAX_TOKENS;
231
+ logger.debug("title-generator: request", { ...modelContext, maxTokens });
232
+
224
233
  const response = await completeSimple(
225
234
  model,
226
235
  {
227
- systemPrompt: [request.systemPrompt],
228
- messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
236
+ systemPrompt: [TITLE_SYSTEM_PROMPT],
237
+ messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
229
238
  tools: [setTitleTool],
230
239
  },
231
240
  {
232
241
  apiKey,
233
- maxTokens: request.maxTokens,
242
+ maxTokens,
234
243
  disableReasoning: true,
235
244
  toolChoice: { type: "tool", name: SET_TITLE_TOOL_NAME },
236
245
  metadata,
@@ -239,8 +248,9 @@ export async function generateTitleOnline(
239
248
  );
240
249
 
241
250
  if (response.stopReason === "error") {
242
- logger.debug("title-generator: response error", {
243
- model: request.model,
251
+ logger.warn("title-generator: response error", {
252
+ ...modelContext,
253
+ reason: "provider-response-error",
244
254
  stopReason: response.stopReason,
245
255
  errorMessage: response.errorMessage,
246
256
  });
@@ -249,8 +259,18 @@ export async function generateTitleOnline(
249
259
 
250
260
  const title = normalizeGeneratedTitle(extractGeneratedTitle(response.content));
251
261
 
252
- logger.debug("title-generator: response", {
253
- model: request.model,
262
+ if (!title) {
263
+ logger.debug("title-generator: no title returned", {
264
+ ...modelContext,
265
+ reason: "model-returned-none",
266
+ usage: response.usage,
267
+ stopReason: response.stopReason,
268
+ });
269
+ return null;
270
+ }
271
+
272
+ logger.debug("title-generator: success", {
273
+ ...modelContext,
254
274
  title,
255
275
  usage: response.usage,
256
276
  stopReason: response.stopReason,
@@ -258,8 +278,9 @@ export async function generateTitleOnline(
258
278
 
259
279
  return title;
260
280
  } catch (err) {
261
- logger.debug("title-generator: error", {
262
- model: request.model,
281
+ logger.warn("title-generator: error", {
282
+ ...modelContext,
283
+ reason: "exception",
263
284
  error: err instanceof Error ? err.message : String(err),
264
285
  });
265
286
  return null;
@@ -278,8 +278,8 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
278
278
  return renderSearchCall(args, options, theme);
279
279
  },
280
280
 
281
- renderResult(result, options: RenderResultOptions, theme: Theme) {
282
- return renderSearchResult(result, options, theme);
281
+ renderResult(result, options: RenderResultOptions, theme: Theme, args) {
282
+ return renderSearchResult(result, options, theme, args);
283
283
  },
284
284
  };
285
285
 
@@ -5,9 +5,9 @@
5
5
  */
6
6
 
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
- import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
+ import { Markdown, Text } from "@oh-my-pi/pi-tui";
9
9
  import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
10
- import type { Theme } from "../../modes/theme/theme";
10
+ import { getMarkdownTheme, type Theme } from "../../modes/theme/theme";
11
11
  import {
12
12
  formatAge,
13
13
  formatCount,
@@ -26,8 +26,6 @@ import { getSearchProviderLabel } from "./provider";
26
26
  import type { SearchResponse } from "./types";
27
27
 
28
28
  const MAX_COLLAPSED_ANSWER_LINES = PREVIEW_LIMITS.COLLAPSED_LINES;
29
- const MAX_EXPANDED_ANSWER_LINES = PREVIEW_LIMITS.EXPANDED_LINES;
30
- const MAX_ANSWER_LINE_LEN = TRUNCATE_LENGTHS.LINE;
31
29
  const MAX_SNIPPET_LINES = 2;
32
30
  const MAX_SNIPPET_LINE_LEN = TRUNCATE_LENGTHS.LINE;
33
31
  const MAX_COLLAPSED_ITEMS = PREVIEW_LIMITS.COLLAPSED_ITEMS;
@@ -75,7 +73,6 @@ export function renderSearchResult(
75
73
  theme: Theme,
76
74
  args?: {
77
75
  query?: string;
78
- allowLongAnswer?: boolean;
79
76
  maxAnswerLines?: number;
80
77
  },
81
78
  ): Component {
@@ -104,13 +101,6 @@ export function renderSearchResult(
104
101
  // Get answer text
105
102
  const answerText = typeof response.answer === "string" ? response.answer.trim() : "";
106
103
  const contentText = answerText || rawText;
107
- const answerLines = contentText
108
- ? contentText
109
- .split("\n")
110
- .filter(l => l.trim())
111
- .map(l => l.trim())
112
- : [];
113
- const totalAnswerLines = answerLines.length;
114
104
 
115
105
  const providerLabel = provider !== "none" ? getSearchProviderLabel(provider) : "None";
116
106
  const queryPreview = args?.query
@@ -159,6 +149,7 @@ export function renderSearchResult(
159
149
  metaLines.push(`${theme.fg("muted", "Queries:")} ${theme.fg("text", queryList.join("; "))}${suffix}`);
160
150
  }
161
151
 
152
+ const answerMarkdown = contentText ? new Markdown(contentText, 0, 0, getMarkdownTheme()) : undefined;
162
153
  const outputBlock = new CachedOutputBlock();
163
154
 
164
155
  return {
@@ -166,14 +157,22 @@ export function renderSearchResult(
166
157
  // Read mutable state at render time
167
158
  const { expanded } = options;
168
159
 
169
- // Expanded-dependent computations
170
- const answerLimit = expanded ? MAX_EXPANDED_ANSWER_LINES : MAX_COLLAPSED_ANSWER_LINES;
171
- const answerPreview = contentText
172
- ? args?.allowLongAnswer
173
- ? answerLines.slice(0, args.maxAnswerLines ?? answerLines.length)
174
- : getPreviewLines(contentText, answerLimit, MAX_ANSWER_LINE_LEN)
175
- : [];
176
- const remainingAnswer = totalAnswerLines - answerPreview.length;
160
+ // Answer lines: full markdown when expanded, capped markdown preview when collapsed.
161
+ const answerWidth = Math.max(20, width - 3);
162
+ const renderedAnswer = answerMarkdown ? answerMarkdown.render(answerWidth) : [];
163
+ let answerLines: string[];
164
+ if (renderedAnswer.length === 0) {
165
+ answerLines = [theme.fg("muted", "No answer text returned")];
166
+ } else if (expanded) {
167
+ answerLines = renderedAnswer;
168
+ } else {
169
+ const collapsedCap = args?.maxAnswerLines ?? MAX_COLLAPSED_ANSWER_LINES;
170
+ answerLines = renderedAnswer.slice(0, collapsedCap);
171
+ const remaining = renderedAnswer.length - answerLines.length;
172
+ if (remaining > 0) {
173
+ answerLines.push(theme.fg("muted", formatMoreItems(remaining, "line")));
174
+ }
175
+ }
177
176
 
178
177
  const sourceTree = renderTreeList(
179
178
  {
@@ -217,37 +216,6 @@ export function renderSearchResult(
217
216
  theme,
218
217
  );
219
218
 
220
- // Build answer section
221
- const answerState = sourceCount > 0 ? "success" : "warning";
222
- const borderColor: "warning" | "dim" = answerState === "warning" ? "warning" : "dim";
223
- const border = (t: string) => theme.fg(borderColor, t);
224
- const contentPrefix = border(`${theme.boxSharp.vertical} `);
225
- const contentSuffix = border(theme.boxSharp.vertical);
226
- const contentWidth = Math.max(0, width - visibleWidth(contentPrefix) - visibleWidth(contentSuffix));
227
- const answerTreeLines = answerPreview.length > 0 ? answerPreview : ["No answer text returned"];
228
- const answerTree = renderTreeList(
229
- {
230
- items: answerTreeLines,
231
- expanded: true,
232
- maxCollapsed: answerTreeLines.length,
233
- itemType: "line",
234
- renderItem: (line, context) => {
235
- const coloredLine =
236
- line === "No answer text returned" ? theme.fg("muted", line) : theme.fg("dim", line);
237
- if (!args?.allowLongAnswer) {
238
- return coloredLine;
239
- }
240
- const prefixWidth = visibleWidth(context.continuePrefix);
241
- const wrapWidth = Math.max(10, contentWidth - prefixWidth);
242
- return wrapTextWithAnsi(coloredLine, wrapWidth);
243
- },
244
- },
245
- theme,
246
- );
247
- if (remainingAnswer > 0) {
248
- answerTree.push(theme.fg("muted", formatMoreItems(remainingAnswer, "line")));
249
- }
250
-
251
219
  return outputBlock.render(
252
220
  {
253
221
  header,
@@ -262,7 +230,7 @@ export function renderSearchResult(
262
230
  : []),
263
231
  {
264
232
  label: theme.fg("toolTitle", "Answer"),
265
- lines: answerTree,
233
+ lines: answerLines,
266
234
  },
267
235
  {
268
236
  label: theme.fg("toolTitle", "Sources"),