@oh-my-pi/pi-coding-agent 13.9.15 → 13.9.16

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.
@@ -54,7 +54,7 @@ import {
54
54
  onThemeChange,
55
55
  theme,
56
56
  } from "./theme/theme";
57
- import type { CompactionQueuedMessage, InteractiveModeContext, TodoItem, TodoPhase } from "./types";
57
+ import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInput, TodoItem, TodoPhase } from "./types";
58
58
  import { UiHelpers } from "./utils/ui-helpers";
59
59
 
60
60
  const EDITOR_MAX_HEIGHT_MIN = 6;
@@ -121,8 +121,9 @@ export class InteractiveMode implements InteractiveModeContext {
121
121
  autoCompactionEscapeHandler?: () => void;
122
122
  retryEscapeHandler?: () => void;
123
123
  unsubscribe?: () => void;
124
- onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
124
+ onInputCallback?: (input: SubmittedUserInput) => void;
125
125
  optimisticUserMessageSignature: string | undefined = undefined;
126
+ #pendingSubmittedInput: SubmittedUserInput | undefined;
126
127
  lastSigintTime = 0;
127
128
  lastEscapeTime = 0;
128
129
  shutdownRequested = false;
@@ -397,8 +398,8 @@ export class InteractiveMode implements InteractiveModeContext {
397
398
  this.session.setSlashCommands(fileCommands);
398
399
  }
399
400
 
400
- async getUserInput(): Promise<{ text: string; images?: ImageContent[] }> {
401
- const { promise, resolve } = Promise.withResolvers<{ text: string; images?: ImageContent[] }>();
401
+ async getUserInput(): Promise<SubmittedUserInput> {
402
+ const { promise, resolve } = Promise.withResolvers<SubmittedUserInput>();
402
403
  this.onInputCallback = input => {
403
404
  this.onInputCallback = undefined;
404
405
  resolve(input);
@@ -406,6 +407,64 @@ export class InteractiveMode implements InteractiveModeContext {
406
407
  return promise;
407
408
  }
408
409
 
410
+ startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
411
+ const submission: SubmittedUserInput = {
412
+ text: input.text,
413
+ images: input.images,
414
+ cancelled: false,
415
+ started: false,
416
+ };
417
+ this.#pendingSubmittedInput = submission;
418
+ this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
419
+ this.addMessageToChat({
420
+ role: "user",
421
+ content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
422
+ attribution: "user",
423
+ timestamp: Date.now(),
424
+ });
425
+ this.editor.setText("");
426
+ this.ensureLoadingAnimation();
427
+ this.ui.requestRender();
428
+ return submission;
429
+ }
430
+
431
+ cancelPendingSubmission(): boolean {
432
+ const submission = this.#pendingSubmittedInput;
433
+ if (!submission || submission.started) {
434
+ return false;
435
+ }
436
+
437
+ submission.cancelled = true;
438
+ this.#pendingSubmittedInput = undefined;
439
+ this.optimisticUserMessageSignature = undefined;
440
+ this.#pendingWorkingMessage = undefined;
441
+ if (this.loadingAnimation) {
442
+ this.loadingAnimation.stop();
443
+ this.loadingAnimation = undefined;
444
+ this.statusContainer.clear();
445
+ }
446
+ this.pendingImages = submission.images ? [...submission.images] : [];
447
+ this.rebuildChatFromMessages();
448
+ this.editor.setText(submission.text);
449
+ this.updateEditorBorderColor();
450
+ this.ui.requestRender();
451
+ return true;
452
+ }
453
+
454
+ markPendingSubmissionStarted(input: SubmittedUserInput): boolean {
455
+ if (this.#pendingSubmittedInput !== input || input.cancelled) {
456
+ return false;
457
+ }
458
+ input.started = true;
459
+ return true;
460
+ }
461
+
462
+ finishPendingSubmission(input: SubmittedUserInput): void {
463
+ if (this.#pendingSubmittedInput === input) {
464
+ this.#pendingSubmittedInput = undefined;
465
+ }
466
+ }
467
+
409
468
  #computeEditorMaxHeight(): number {
410
469
  const rows = this.ui.terminal.rows;
411
470
  const terminalRows = Number.isFinite(rows) && rows > 0 ? rows : EDITOR_FALLBACK_ROWS;
@@ -713,8 +772,8 @@ export class InteractiveMode implements InteractiveModeContext {
713
772
  return;
714
773
  }
715
774
  await this.#enterPlanMode();
716
- if (initialPrompt) {
717
- this.onInputCallback?.({ text: initialPrompt });
775
+ if (initialPrompt && this.onInputCallback) {
776
+ this.onInputCallback(this.startPendingSubmission({ text: initialPrompt }));
718
777
  }
719
778
  }
720
779
 
@@ -855,6 +914,7 @@ export class InteractiveMode implements InteractiveModeContext {
855
914
  }
856
915
 
857
916
  showError(message: string): void {
917
+ this.#pendingSubmittedInput = undefined;
858
918
  this.optimisticUserMessageSignature = undefined;
859
919
  this.#pendingWorkingMessage = undefined;
860
920
  if (this.loadingAnimation) {
@@ -27,6 +27,13 @@ export type CompactionQueuedMessage = {
27
27
  mode: "steer" | "followUp";
28
28
  };
29
29
 
30
+ export type SubmittedUserInput = {
31
+ text: string;
32
+ images?: ImageContent[];
33
+ cancelled: boolean;
34
+ started: boolean;
35
+ };
36
+
30
37
  export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
31
38
 
32
39
  export type TodoItem = {
@@ -87,7 +94,7 @@ export interface InteractiveModeContext {
87
94
  autoCompactionEscapeHandler?: () => void;
88
95
  retryEscapeHandler?: () => void;
89
96
  unsubscribe?: () => void;
90
- onInputCallback?: (input: { text: string; images?: ImageContent[] }) => void;
97
+ onInputCallback?: (input: SubmittedUserInput) => void;
91
98
  optimisticUserMessageSignature: string | undefined;
92
99
  lastSigintTime: number;
93
100
  lastEscapeTime: number;
@@ -129,6 +136,10 @@ export interface InteractiveModeContext {
129
136
  setWorkingMessage(message?: string): void;
130
137
  applyPendingWorkingMessage(): void;
131
138
  ensureLoadingAnimation(): void;
139
+ startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput;
140
+ cancelPendingSubmission(): boolean;
141
+ markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
142
+ finishPendingSubmission(input: SubmittedUserInput): void;
132
143
  isKnownSlashCommand(text: string): boolean;
133
144
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
134
145
  renderSessionContext(
@@ -79,6 +79,7 @@ You generate code inside-out: starting at the function body, working outward. Th
79
79
  - **Time:** You do not feel the cost of duplicating a pattern across six files, of a resource operation with no upper bound, of an escape hatch that bypasses the type system. Name these costs before you choose the easy path. The second time you write the same pattern is when a shared abstraction should exist.
80
80
  - When writing a function in a pipeline, ask "what does the next consumer need?" — not just "what do I need right now?"
81
81
  - **DRY at 2.** When you write the same pattern a second time, stop and extract a shared helper. Two copies is a maintenance fork. Three copies is a bug.
82
+ - Write maintainable code. Add brief comments when they clarify non-obvious intent, invariants, edge cases, or tradeoffs. Prefer explaining why over restating what the code already does.
82
83
  - **Earn every line.** A 12-line switch for a 3-way mapping is a lookup table. A one-liner wrapper that exists only for test access is a design smell.
83
84
  </code-integrity>
84
85
 
@@ -230,7 +231,7 @@ Don't open a file hoping. Hope is not a strategy.
230
231
  {{#has tools "grep"}}- `grep` to locate target{{/has}}
231
232
  {{#has tools "find"}}- `find` to map it{{/has}}
232
233
  {{#has tools "read"}}- `read` with offset/limit, not whole file{{/has}}
233
- {{#has tools "task"}}- `task` to gather context if needed via explore agent{{/has}}
234
+ {{#has tools "task"}}- `task` for investigate+edit in one pass prefer this over a separate explore→task chain{{/has}}
234
235
  {{/ifAny}}
235
236
 
236
237
  {{#if (includes tools "inspect_image")}}
@@ -6,7 +6,7 @@ Kernel persists across calls and cells; **imports, variables, and functions surv
6
6
  - You **SHOULD** use one logical step per cell (imports, define function, test it, use it)
7
7
  - You **SHOULD** pass multiple small cells in one call
8
8
  - You **SHOULD** define small functions you can reuse and debug individually
9
- - You **MUST** put explanations in assistant message or cell title, **MUST NOT** put them in code
9
+ - You **MUST** put workflow explanations in assistant message or cell title
10
10
  **When something fails:**
11
11
  - Errors tell you which cell failed (e.g., "Cell 3 failed")
12
12
  - You **SHOULD** resubmit only the fixed cell (or fixed cell + remaining cells)
@@ -22,7 +22,7 @@ Subagents lack your conversation history. Every decision, file content, and user
22
22
  - **MUST NOT** duplicate shared constraints across assignments — put them in `context` once.
23
23
  - **MUST NOT** tell tasks to run project-wide build/test/lint. Parallel agents share the working tree; each task edits, stops. Caller verifies after all complete.
24
24
  - For large payloads (traces, JSON blobs), write to `local://<path>` and pass the path in context.
25
- - If scope is unclear, run a **Discovery task** first to enumerate files and callsites, then fan out.
25
+ - Prefer `task` agents that investigate **and** edit in one pass. Only launch a dedicated read-only discovery step when the affected files are genuinely unknown and cannot be inferred from the task description.
26
26
  </critical>
27
27
 
28
28
  <scope>
package/src/sdk.ts CHANGED
@@ -612,9 +612,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
612
612
  const { authStorage, modelRegistry } = await logger.timeAsync("discoverModels", async () => {
613
613
  const authStorage = options.authStorage ?? (await discoverAuthStorage(agentDir));
614
614
  const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
615
- if (!options.modelRegistry) {
616
- await modelRegistry.refresh();
617
- }
618
615
  return { authStorage, modelRegistry };
619
616
  });
620
617
 
@@ -623,6 +620,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
623
620
  async () => options.settings ?? (await Settings.init({ cwd, agentDir })),
624
621
  );
625
622
  logger.time("initializeWithSettings", initializeWithSettings, settings);
623
+ if (!options.modelRegistry) {
624
+ modelRegistry.refreshInBackground();
625
+ }
626
626
  const skillsSettings = settings.getGroup("skills") as SkillsSettings;
627
627
  const discoveredSkillsPromise =
628
628
  options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
@@ -1358,6 +1358,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1358
1358
  tools: initialTools,
1359
1359
  },
1360
1360
  convertToLlm: convertToLlmFinal,
1361
+ onPayload: extensionRunner
1362
+ ? async (payload, _model) => {
1363
+ return extensionRunner.emitBeforeProviderRequest(payload);
1364
+ }
1365
+ : undefined,
1361
1366
  sessionId: sessionManager.getSessionId(),
1362
1367
  transformContext: extensionRunner
1363
1368
  ? async messages => {
@@ -4113,8 +4113,8 @@ export class AgentSession {
4113
4113
  }
4114
4114
 
4115
4115
  #isRetryableErrorMessage(errorMessage: string): boolean {
4116
- // Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed, retry delay exceeded
4117
- return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|unable to connect|fetch failed|retry delay/i.test(
4116
+ // Match: overloaded_error, rate limit, usage limit, 429, 500, 502, 503, 504, service unavailable, connection error, fetch failed, retry delay exceeded, stream stall
4117
+ return /overloaded|rate.?limit|usage.?limit|too many requests|429|500|502|503|504|service.?unavailable|server error|internal error|connection.?error|unable to connect|fetch failed|retry delay|stream stall/i.test(
4118
4118
  errorMessage,
4119
4119
  );
4120
4120
  }
@@ -104,6 +104,19 @@ export function upsertFileOperations(summary: string, readFiles: string[], modif
104
104
  // Message Serialization
105
105
  // ============================================================================
106
106
 
107
+ /** Maximum characters for a tool result in serialized summaries. */
108
+ const TOOL_RESULT_MAX_CHARS = 2000;
109
+
110
+ /**
111
+ * Truncate text to a maximum character length for summarization.
112
+ * Keeps the beginning and appends a truncation marker.
113
+ */
114
+ function truncateForSummary(text: string, maxChars: number): string {
115
+ if (text.length <= maxChars) return text;
116
+ const truncatedChars = text.length - maxChars;
117
+ return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
118
+ }
119
+
107
120
  /**
108
121
  * Serialize LLM messages to text for summarization.
109
122
  * This prevents the model from treating it as a conversation to continue.
@@ -156,7 +169,7 @@ export function serializeConversation(messages: Message[]): string {
156
169
  .map(c => c.text)
157
170
  .join("");
158
171
  if (content) {
159
- parts.push(`[Tool result]: ${content}`);
172
+ parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
160
173
  }
161
174
  }
162
175
  }
@@ -152,9 +152,13 @@ function formatMetadataLine(lineCount: number | null, language: string | undefin
152
152
  return uiTheme.fg("dim", `${icon}`);
153
153
  }
154
154
 
155
+ function normalizeDisplayText(text: string): string {
156
+ return text.replace(/\r/g, "");
157
+ }
158
+
155
159
  function formatStreamingContent(content: string, uiTheme: Theme): string {
156
160
  if (!content) return "";
157
- const lines = content.split("\n");
161
+ const lines = normalizeDisplayText(content).split("\n");
158
162
  const displayLines = lines.slice(-WRITE_STREAMING_PREVIEW_LINES);
159
163
  const hidden = lines.length - displayLines.length;
160
164
 
@@ -171,7 +175,7 @@ function formatStreamingContent(content: string, uiTheme: Theme): string {
171
175
 
172
176
  function renderContentPreview(content: string, expanded: boolean, uiTheme: Theme): string {
173
177
  if (!content) return "";
174
- const lines = content.split("\n");
178
+ const lines = normalizeDisplayText(content).split("\n");
175
179
  const maxLines = expanded ? lines.length : Math.min(lines.length, WRITE_PREVIEW_LINES);
176
180
  const displayLines = expanded ? lines : lines.slice(-maxLines);
177
181
  const hidden = lines.length - displayLines.length;
@@ -39,7 +39,7 @@ export async function openInEditor(
39
39
  const [editor, ...editorArgs] = editorCmd.split(" ");
40
40
  const stdio = options?.stdio ?? ["inherit", "inherit", "inherit"];
41
41
 
42
- const child = spawn(editor, [...editorArgs, tmpFile], { stdio });
42
+ const child = spawn(editor, [...editorArgs, tmpFile], { stdio, shell: process.platform === "win32" });
43
43
  const exitCode = await new Promise<number>((resolve, reject) => {
44
44
  child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
45
45
  child.once("error", error => reject(error));
@@ -26,34 +26,12 @@ import type { ToolSession } from "../../tools";
26
26
  import { formatAge } from "../../tools/render-utils";
27
27
  import { getSearchProvider, resolveProviderChain, type SearchProvider } from "./provider";
28
28
  import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
29
- import type { SearchResponse } from "./types";
29
+ import type { SearchProviderId, SearchResponse } from "./types";
30
30
  import { SearchProviderError } from "./types";
31
31
 
32
- /** Web search parameters schema */
32
+ /** Web search tool parameters schema */
33
33
  export const webSearchSchema = Type.Object({
34
34
  query: Type.String({ description: "Search query" }),
35
- provider: Type.Optional(
36
- StringEnum(
37
- [
38
- "auto",
39
- "exa",
40
- "brave",
41
- "jina",
42
- "kimi",
43
- "zai",
44
- "anthropic",
45
- "perplexity",
46
- "gemini",
47
- "codex",
48
- "tavily",
49
- "kagi",
50
- "synthetic",
51
- ],
52
- {
53
- description: "Search provider (default: auto)",
54
- },
55
- ),
56
- ),
57
35
  recency: Type.Optional(
58
36
  StringEnum(["day", "week", "month", "year"], {
59
37
  description: "Recency filter (Brave, Perplexity)",
@@ -65,22 +43,8 @@ export const webSearchSchema = Type.Object({
65
43
  num_search_results: Type.Optional(Type.Number({ description: "Number of search results to retrieve" })),
66
44
  });
67
45
 
68
- export type SearchParams = {
46
+ export type SearchToolParams = {
69
47
  query: string;
70
- provider?:
71
- | "auto"
72
- | "exa"
73
- | "brave"
74
- | "jina"
75
- | "kimi"
76
- | "zai"
77
- | "anthropic"
78
- | "perplexity"
79
- | "gemini"
80
- | "codex"
81
- | "tavily"
82
- | "kagi"
83
- | "synthetic";
84
48
  recency?: "day" | "week" | "month" | "year";
85
49
  limit?: number;
86
50
  /** Maximum output tokens. Defaults to 4096. */
@@ -89,10 +53,12 @@ export type SearchParams = {
89
53
  temperature?: number;
90
54
  /** Number of search results to retrieve. Defaults to 10. */
91
55
  num_search_results?: number;
92
- /** Deprecated CLI flag; explicit provider fallback now happens only when provider is unavailable. */
93
- no_fallback?: boolean;
94
56
  };
95
57
 
58
+ export interface SearchQueryParams extends SearchToolParams {
59
+ provider?: SearchProviderId | "auto";
60
+ }
61
+
96
62
  function formatProviderList(providers: SearchProvider[]): string {
97
63
  return providers.map(provider => provider.label).join(", ");
98
64
  }
@@ -178,14 +144,14 @@ function formatForLLM(response: SearchResponse): string {
178
144
  /** Execute web search */
179
145
  async function executeSearch(
180
146
  _toolCallId: string,
181
- params: SearchParams,
147
+ params: SearchQueryParams,
182
148
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
183
149
  const providers =
184
150
  params.provider && params.provider !== "auto"
185
151
  ? (await getSearchProvider(params.provider).isAvailable())
186
152
  ? [getSearchProvider(params.provider)]
187
153
  : await resolveProviderChain("auto")
188
- : await resolveProviderChain(params.provider);
154
+ : await resolveProviderChain();
189
155
  if (providers.length === 0) {
190
156
  const message = "No web search provider configured.";
191
157
  return {
@@ -237,7 +203,7 @@ async function executeSearch(
237
203
  * Execute a web search query for CLI/testing workflows.
238
204
  */
239
205
  export async function runSearchQuery(
240
- params: SearchParams,
206
+ params: SearchQueryParams,
241
207
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
242
208
  return executeSearch("cli-web-search", params);
243
209
  }
@@ -261,7 +227,7 @@ export class SearchTool implements AgentTool<typeof webSearchSchema, SearchRende
261
227
 
262
228
  async execute(
263
229
  _toolCallId: string,
264
- params: SearchParams,
230
+ params: SearchToolParams,
265
231
  _signal?: AbortSignal,
266
232
  _onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
267
233
  _context?: AgentToolContext,
@@ -277,11 +243,17 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
277
243
  description: renderPromptTemplate(webSearchDescription),
278
244
  parameters: webSearchSchema,
279
245
 
280
- async execute(toolCallId: string, params: SearchParams, _onUpdate, _ctx: CustomToolContext, _signal?: AbortSignal) {
246
+ async execute(
247
+ toolCallId: string,
248
+ params: SearchToolParams,
249
+ _onUpdate,
250
+ _ctx: CustomToolContext,
251
+ _signal?: AbortSignal,
252
+ ) {
281
253
  return executeSearch(toolCallId, params);
282
254
  },
283
255
 
284
- renderCall(args: SearchParams, options: RenderResultOptions, theme: Theme) {
256
+ renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
285
257
  return renderSearchCall(args, options, theme);
286
258
  },
287
259
 
@@ -75,7 +75,6 @@ export function renderSearchResult(
75
75
  theme: Theme,
76
76
  args?: {
77
77
  query?: string;
78
- provider?: string;
79
78
  allowLongAnswer?: boolean;
80
79
  maxAnswerLines?: number;
81
80
  },
@@ -282,13 +281,12 @@ export function renderSearchResult(
282
281
 
283
282
  /** Render web search call (query preview) */
284
283
  export function renderSearchCall(
285
- args: { query?: string; provider?: string; [key: string]: unknown },
284
+ args: { query?: string; [key: string]: unknown },
286
285
  _options: RenderResultOptions,
287
286
  theme: Theme,
288
287
  ): Component {
289
- const provider = args.provider ?? "auto";
290
288
  const query = truncateToWidth(args.query ?? "", 80);
291
- const text = renderStatusLine({ icon: "pending", title: "Web Search", description: query, meta: [provider] }, theme);
289
+ const text = renderStatusLine({ icon: "pending", title: "Web Search", description: query }, theme);
292
290
  return new Text(text, 0, 0);
293
291
  }
294
292