@oh-my-pi/pi-coding-agent 8.10.11 → 8.10.12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "8.10.11",
3
+ "version": "8.10.12",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -83,18 +83,18 @@
83
83
  "test": "bun test"
84
84
  },
85
85
  "dependencies": {
86
- "@oh-my-pi/omp-stats": "8.10.11",
87
- "@oh-my-pi/pi-agent-core": "8.10.11",
88
- "@oh-my-pi/pi-ai": "8.10.11",
89
- "@oh-my-pi/pi-tui": "8.10.11",
90
- "@oh-my-pi/pi-utils": "8.10.11",
91
- "@openai/agents": "^0.4.3",
92
- "@sinclair/typebox": "^0.34.46",
86
+ "@oh-my-pi/omp-stats": "8.10.12",
87
+ "@oh-my-pi/pi-agent-core": "8.10.12",
88
+ "@oh-my-pi/pi-ai": "8.10.12",
89
+ "@oh-my-pi/pi-tui": "8.10.12",
90
+ "@oh-my-pi/pi-utils": "8.10.12",
91
+ "@openai/agents": "^0.4.4",
92
+ "@sinclair/typebox": "^0.34.48",
93
93
  "ajv": "^8.17.1",
94
- "chalk": "^5.5.0",
94
+ "chalk": "^5.6.2",
95
95
  "cli-highlight": "^2.1.11",
96
- "diff": "^8.0.2",
97
- "file-type": "^21.1.1",
96
+ "diff": "^8.0.3",
97
+ "file-type": "^21.3.0",
98
98
  "glob": "^13.0.0",
99
99
  "handlebars": "^4.7.8",
100
100
  "highlight.js": "^11.11.1",
@@ -103,7 +103,7 @@
103
103
  "node-html-parser": "^7.0.2",
104
104
  "smol-toml": "^1.6.0",
105
105
  "strip-ansi": "^7.1.2",
106
- "zod": "^4.3.5"
106
+ "zod": "^4.3.6"
107
107
  },
108
108
  "devDependencies": {
109
109
  "@types/diff": "^8.0.0",
@@ -107,6 +107,15 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
107
107
  // Start the dashboard server
108
108
  const { port } = await startServer(cmd.port);
109
109
  console.log(chalk.green(`Dashboard available at: http://localhost:${port}`));
110
+
111
+ // Open browser
112
+ const url = `http://localhost:${port}`;
113
+ const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
114
+ Bun.spawn(openCommand === "cmd" ? ["cmd", "/c", "start", url] : [openCommand, url], {
115
+ stdout: "ignore",
116
+ stderr: "ignore",
117
+ }).unref();
118
+
110
119
  console.log("Press Ctrl+C to stop\n");
111
120
 
112
121
  // Keep process running
@@ -28,6 +28,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
28
28
  mistral: "devstral-medium-latest",
29
29
  minimax: "MiniMax-M2",
30
30
  opencode: "claude-opus-4-5",
31
+ "kimi-code": "kimi-k2.5",
31
32
  };
32
33
 
33
34
  export interface ScopedModel {
@@ -1,8 +1,8 @@
1
1
  import * as path from "node:path";
2
- import { jtdToTypeScript } from "@oh-my-pi/pi-coding-agent/tools/jtd-to-typescript";
3
2
  import { logger } from "@oh-my-pi/pi-utils";
4
3
  import Handlebars from "handlebars";
5
4
  import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
5
+ import { jtdToTypeScript } from "../tools/jtd-to-typescript";
6
6
  import { parseFrontmatter } from "../utils/frontmatter";
7
7
 
8
8
  /**
@@ -135,6 +135,8 @@ export interface EditSettings {
135
135
  fuzzyThreshold?: number; // default: 0.95 (similarity threshold for fuzzy matching)
136
136
  patchMode?: boolean; // default: true (use codex-style apply-patch format instead of old_text/new_text)
137
137
  streamingAbort?: boolean; // default: false (abort streaming edit tool calls when patch preview fails)
138
+ /** Model-specific variant overrides. Keys are model pattern substrings (e.g., "kimi", "deepseek"). */
139
+ modelVariants?: Record<string, "patch" | "replace">;
138
140
  }
139
141
 
140
142
  export type { SymbolPreset };
@@ -1387,6 +1389,57 @@ export class SettingsManager {
1387
1389
  await this.save();
1388
1390
  }
1389
1391
 
1392
+ /**
1393
+ * Default model patterns that should use replace mode instead of patch mode.
1394
+ * These are models known to struggle with unified diff format.
1395
+ */
1396
+ static readonly DEFAULT_REPLACE_MODE_PATTERNS = ["kimi"];
1397
+
1398
+ /**
1399
+ * Get the edit variant for a specific model.
1400
+ * Returns "patch", "replace", or null (use global default).
1401
+ */
1402
+ getEditVariantForModel(model: string | undefined): "patch" | "replace" | null {
1403
+ if (!model) return null;
1404
+ const modelLower = model.toLowerCase();
1405
+
1406
+ const userVariants = this.settings.edit?.modelVariants;
1407
+ if (userVariants) {
1408
+ for (const [pattern, variant] of Object.entries(userVariants)) {
1409
+ if (modelLower.includes(pattern.toLowerCase())) {
1410
+ return variant;
1411
+ }
1412
+ }
1413
+ }
1414
+
1415
+ for (const pattern of SettingsManager.DEFAULT_REPLACE_MODE_PATTERNS) {
1416
+ if (modelLower.includes(pattern)) {
1417
+ return "replace";
1418
+ }
1419
+ }
1420
+
1421
+ return null;
1422
+ }
1423
+
1424
+ getEditModelVariants(): Record<string, "patch" | "replace"> {
1425
+ return this.settings.edit?.modelVariants ?? {};
1426
+ }
1427
+
1428
+ async setEditModelVariant(pattern: string, variant: "patch" | "replace" | null): Promise<void> {
1429
+ if (!this.globalSettings.edit) {
1430
+ this.globalSettings.edit = {};
1431
+ }
1432
+ if (!this.globalSettings.edit.modelVariants) {
1433
+ this.globalSettings.edit.modelVariants = {};
1434
+ }
1435
+ if (variant === null) {
1436
+ delete this.globalSettings.edit.modelVariants[pattern];
1437
+ } else {
1438
+ this.globalSettings.edit.modelVariants[pattern] = variant;
1439
+ }
1440
+ await this.save();
1441
+ }
1442
+
1390
1443
  getNormativeRewrite(): boolean {
1391
1444
  return this.settings.normativeRewrite ?? false;
1392
1445
  }
@@ -50,7 +50,15 @@ export class HookSelectorComponent extends Container {
50
50
  opts.timeout,
51
51
  opts.tui,
52
52
  s => this.titleText.setText(theme.fg("accent", `${this.baseTitle} (${s}s)`)),
53
- () => this.onCancelCallback(),
53
+ () => {
54
+ // Auto-select current option on timeout (typically the first/recommended option)
55
+ const selected = this.options[this.selectedIndex];
56
+ if (selected) {
57
+ this.onSelectCallback(selected);
58
+ } else {
59
+ this.onCancelCallback();
60
+ }
61
+ },
54
62
  );
55
63
  }
56
64
 
@@ -8,6 +8,7 @@ import type {
8
8
  ExtensionContextActions,
9
9
  ExtensionError,
10
10
  ExtensionUIContext,
11
+ ExtensionUIDialogOptions,
11
12
  } from "../../extensibility/extensions";
12
13
  import { HookEditorComponent } from "../../modes/components/hook-editor";
13
14
  import { HookInputComponent } from "../../modes/components/hook-input";
@@ -25,7 +26,7 @@ export class ExtensionUiController {
25
26
  async initHooksAndCustomTools(): Promise<void> {
26
27
  // Create and set hook & tool UI context
27
28
  const uiContext: ExtensionUIContext = {
28
- select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions?.initialIndex),
29
+ select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions),
29
30
  confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
30
31
  input: (title, placeholder, _dialogOptions) => this.showHookInput(title, placeholder),
31
32
  notify: (message, type) => this.showHookNotify(message, type),
@@ -484,7 +485,11 @@ export class ExtensionUiController {
484
485
  /**
485
486
  * Show a selector for hooks.
486
487
  */
487
- showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined> {
488
+ showHookSelector(
489
+ title: string,
490
+ options: string[],
491
+ dialogOptions?: ExtensionUIDialogOptions,
492
+ ): Promise<string | undefined> {
488
493
  const { promise, resolve } = Promise.withResolvers<string | undefined>();
489
494
  this.ctx.hookSelector = new HookSelectorComponent(
490
495
  title,
@@ -497,7 +502,7 @@ export class ExtensionUiController {
497
502
  this.hideHookSelector();
498
503
  resolve(undefined);
499
504
  },
500
- { initialIndex },
505
+ { initialIndex: dialogOptions?.initialIndex, timeout: dialogOptions?.timeout, tui: this.ctx.ui },
501
506
  );
502
507
 
503
508
  this.ctx.editorContainer.clear();
@@ -32,12 +32,12 @@ export class InputController {
32
32
  this.ctx.editor.setText("");
33
33
  this.ctx.isBashMode = false;
34
34
  this.ctx.updateEditorBorderColor();
35
+ } else if (this.ctx.session.isPythonRunning) {
36
+ this.ctx.session.abortPython();
35
37
  } else if (this.ctx.isPythonMode) {
36
38
  this.ctx.editor.setText("");
37
39
  this.ctx.isPythonMode = false;
38
40
  this.ctx.updateEditorBorderColor();
39
- } else if (this.ctx.session.isPythonRunning) {
40
- this.ctx.session.abortPython();
41
41
  } else if (!this.ctx.editor.getText().trim()) {
42
42
  // Double-escape with empty editor triggers /tree or /branch based on setting
43
43
  const now = Date.now();
@@ -20,7 +20,7 @@ import chalk from "chalk";
20
20
  import { KeybindingsManager } from "../config/keybindings";
21
21
  import { renderPromptTemplate } from "../config/prompt-templates";
22
22
  import type { SettingsManager } from "../config/settings-manager";
23
- import type { ExtensionUIContext } from "../extensibility/extensions";
23
+ import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
24
24
  import type { CompactOptions } from "../extensibility/extensions/types";
25
25
  import { loadSlashCommands } from "../extensibility/slash-commands";
26
26
  import { resolvePlanUrlToPath } from "../internal-urls";
@@ -1020,8 +1020,12 @@ export class InteractiveMode implements InteractiveModeContext {
1020
1020
  this.extensionUiController.setHookStatus(key, text);
1021
1021
  }
1022
1022
 
1023
- showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined> {
1024
- return this.extensionUiController.showHookSelector(title, options, initialIndex);
1023
+ showHookSelector(
1024
+ title: string,
1025
+ options: string[],
1026
+ dialogOptions?: ExtensionUIDialogOptions,
1027
+ ): Promise<string | undefined> {
1028
+ return this.extensionUiController.showHookSelector(title, options, dialogOptions);
1025
1029
  }
1026
1030
 
1027
1031
  hideHookSelector(): void {
@@ -3,7 +3,7 @@ import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-m
3
3
  import type { Component, Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
4
4
  import type { KeybindingsManager } from "../config/keybindings";
5
5
  import type { SettingsManager } from "../config/settings-manager";
6
- import type { ExtensionUIContext } from "../extensibility/extensions";
6
+ import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../extensibility/extensions";
7
7
  import type { CompactOptions } from "../extensibility/extensions/types";
8
8
  import type { MCPManager } from "../mcp";
9
9
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
@@ -189,7 +189,11 @@ export interface InteractiveModeContext {
189
189
  ): Promise<void>;
190
190
  setHookWidget(key: string, content: unknown): void;
191
191
  setHookStatus(key: string, text: string | undefined): void;
192
- showHookSelector(title: string, options: string[], initialIndex?: number): Promise<string | undefined>;
192
+ showHookSelector(
193
+ title: string,
194
+ options: string[],
195
+ dialogOptions?: ExtensionUIDialogOptions,
196
+ ): Promise<string | undefined>;
193
197
  hideHookSelector(): void;
194
198
  showHookInput(title: string, placeholder?: string): Promise<string | undefined>;
195
199
  hideHookInput(): void;
@@ -202,14 +202,12 @@ type TInput = typeof replaceEditSchema | typeof patchEditSchema;
202
202
  export class EditTool implements AgentTool<TInput> {
203
203
  public readonly name = "edit";
204
204
  public readonly label = "Edit";
205
- public readonly description: string;
206
- public readonly parameters: TInput;
207
205
 
208
206
  private readonly session: ToolSession;
209
- private readonly patchMode: boolean;
210
207
  private readonly allowFuzzy: boolean;
211
208
  private readonly fuzzyThreshold: number;
212
209
  private readonly writethrough: WritethroughCallback;
210
+ private readonly envEditVariant: string;
213
211
 
214
212
  constructor(session: ToolSession) {
215
213
  this.session = session;
@@ -217,22 +215,14 @@ export class EditTool implements AgentTool<TInput> {
217
215
  const {
218
216
  OMP_EDIT_FUZZY: editFuzzy = "auto",
219
217
  OMP_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
220
- OMP_EDIT_VARIANT: editVariant = "auto",
218
+ OMP_EDIT_VARIANT: envEditVariant = "auto",
221
219
  } = process.env;
220
+ this.envEditVariant = envEditVariant;
222
221
 
223
- switch (editVariant) {
224
- case "replace":
225
- this.patchMode = false;
226
- break;
227
- case "patch":
228
- this.patchMode = true;
229
- break;
230
- case "auto":
231
- this.patchMode = session.settings?.getEditPatchMode?.() ?? true;
232
- break;
233
- default:
234
- throw new Error(`Invalid OMP_EDIT_VARIANT: ${process.env.OMP_EDIT_VARIANT}`);
222
+ if (envEditVariant !== "replace" && envEditVariant !== "patch" && envEditVariant !== "auto") {
223
+ throw new Error(`Invalid OMP_EDIT_VARIANT: ${envEditVariant}`);
235
224
  }
225
+
236
226
  switch (editFuzzy) {
237
227
  case "true":
238
228
  case "1":
@@ -266,10 +256,37 @@ export class EditTool implements AgentTool<TInput> {
266
256
  this.writethrough = enableLsp
267
257
  ? createLspWritethrough(session.cwd, { enableFormat, enableDiagnostics })
268
258
  : writethroughNoop;
269
- this.description = this.patchMode
270
- ? renderPromptTemplate(patchDescription)
271
- : renderPromptTemplate(replaceDescription);
272
- this.parameters = this.patchMode ? patchEditSchema : replaceEditSchema;
259
+ }
260
+
261
+ /**
262
+ * Determine patch mode dynamically based on current model.
263
+ * This is re-evaluated on each access so tool definitions stay current when model changes.
264
+ */
265
+ private get patchMode(): boolean {
266
+ if (this.envEditVariant === "replace") return false;
267
+ if (this.envEditVariant === "patch") return true;
268
+
269
+ // Auto mode: check model-specific settings
270
+ const activeModel = this.session.getActiveModelString?.();
271
+ const modelVariant = this.session.settings?.getEditVariantForModel?.(activeModel);
272
+ if (modelVariant === "replace") return false;
273
+ if (modelVariant === "patch") return true;
274
+
275
+ return this.session.settings?.getEditPatchMode?.() ?? true;
276
+ }
277
+
278
+ /**
279
+ * Dynamic description based on current patch mode (which depends on current model).
280
+ */
281
+ public get description(): string {
282
+ return this.patchMode ? renderPromptTemplate(patchDescription) : renderPromptTemplate(replaceDescription);
283
+ }
284
+
285
+ /**
286
+ * Dynamic parameters schema based on current patch mode (which depends on current model).
287
+ */
288
+ public get parameters(): TInput {
289
+ return this.patchMode ? patchEditSchema : replaceEditSchema;
273
290
  }
274
291
 
275
292
  public async execute(
@@ -10,7 +10,7 @@ Ask the user a question when you need clarification or input during task executi
10
10
  </conditions>
11
11
 
12
12
  <instruction>
13
- - Place recommended option first with " (Recommended)" suffix
13
+ - Use `recommended: <index>` to mark the default option (0-indexed); " (Recommended)" suffix is added automatically
14
14
  - Use `questions` array for multiple related questions instead of asking one at a time
15
15
  - Set `multi: true` on a question to allow multiple selections
16
16
  </instruction>
@@ -37,12 +37,13 @@ If you can make a reasonable inference from the user's request, **do it**. Users
37
37
 
38
38
  <example name="single">
39
39
  question: "Which authentication method should this API use?"
40
- options: [{"label": "JWT (Recommended)"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
40
+ options: [{"label": "JWT"}, {"label": "OAuth2"}, {"label": "Session cookies"}]
41
+ recommended: 0
41
42
  </example>
42
43
 
43
44
  <example name="multi-part">
44
45
  questions: [
45
- {"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}]},
46
+ {"id": "auth", "question": "Which auth method?", "options": [{"label": "JWT"}, {"label": "OAuth2"}], "recommended": 0},
46
47
  {"id": "cache", "question": "Enable caching?", "options": [{"label": "Yes"}, {"label": "No"}]},
47
48
  {"id": "features", "question": "Which features to include?", "options": [{"label": "Logging"}, {"label": "Metrics"}, {"label": "Tracing"}], "multi": true}
48
49
  ]
package/src/sdk.ts CHANGED
@@ -753,7 +753,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
753
753
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
754
754
  getActiveModelString: () => {
755
755
  const activeModel = agent?.state.model;
756
- return activeModel ? formatModelString(activeModel) : undefined;
756
+ if (activeModel) return formatModelString(activeModel);
757
+ // Fall back to initial model during tool creation (before agent exists)
758
+ if (model) return formatModelString(model);
759
+ return undefined;
757
760
  },
758
761
  getPlanModeState: () => session.getPlanModeState(),
759
762
  settings: settingsManager,
@@ -11,11 +11,13 @@ import {
11
11
  getOAuthApiKey,
12
12
  githubCopilotUsageProvider,
13
13
  googleGeminiCliUsageProvider,
14
+ kimiUsageProvider,
14
15
  loginAnthropic,
15
16
  loginAntigravity,
16
17
  loginCursor,
17
18
  loginGeminiCli,
18
19
  loginGitHubCopilot,
20
+ loginKimi,
19
21
  loginOpenAICodex,
20
22
  type OAuthController,
21
23
  type OAuthCredentials,
@@ -85,6 +87,7 @@ export type AuthStorageOptions = {
85
87
 
86
88
  const DEFAULT_USAGE_PROVIDERS: UsageProvider[] = [
87
89
  openaiCodexUsageProvider,
90
+ kimiUsageProvider,
88
91
  antigravityUsageProvider,
89
92
  googleGeminiCliUsageProvider,
90
93
  claudeUsageProvider,
@@ -770,6 +773,9 @@ export class AuthStorage {
770
773
  case "openai-codex":
771
774
  credentials = await loginOpenAICodex(ctrl);
772
775
  break;
776
+ case "kimi-code":
777
+ credentials = await loginKimi(ctrl);
778
+ break;
773
779
  case "cursor":
774
780
  credentials = await loginCursor(
775
781
  url => ctrl.onAuth({ url }),
@@ -6,26 +6,26 @@
6
6
  import path from "node:path";
7
7
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
9
- import type { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
10
- import { parseModelPattern } from "@oh-my-pi/pi-coding-agent/config/model-resolver";
11
- import { type PromptTemplate, renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
12
- import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
13
- import type { CustomTool } from "@oh-my-pi/pi-coding-agent/extensibility/custom-tools/types";
14
- import type { Skill } from "@oh-my-pi/pi-coding-agent/extensibility/skills";
15
- import { callTool } from "@oh-my-pi/pi-coding-agent/mcp/client";
16
- import type { MCPManager } from "@oh-my-pi/pi-coding-agent/mcp/manager";
17
- import { createAgentSession, discoverAuthStorage, discoverModels } from "@oh-my-pi/pi-coding-agent/sdk";
18
- import type { AgentSession, AgentSessionEvent } from "@oh-my-pi/pi-coding-agent/session/agent-session";
19
- import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
20
- import { SessionManager } from "@oh-my-pi/pi-coding-agent/session/session-manager";
21
- import type { ContextFileEntry } from "@oh-my-pi/pi-coding-agent/tools";
22
- import { jtdToJsonSchema } from "@oh-my-pi/pi-coding-agent/tools/jtd-to-json-schema";
23
- import { ToolAbortError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors";
24
- import type { EventBus } from "@oh-my-pi/pi-coding-agent/utils/event-bus";
25
9
  import { logger, untilAborted } from "@oh-my-pi/pi-utils";
26
10
  import type { TSchema } from "@sinclair/typebox";
27
11
  import Ajv, { type ValidateFunction } from "ajv";
12
+ import type { ModelRegistry } from "../config/model-registry";
13
+ import { parseModelPattern } from "../config/model-resolver";
14
+ import { type PromptTemplate, renderPromptTemplate } from "../config/prompt-templates";
15
+ import { SettingsManager } from "../config/settings-manager";
16
+ import type { CustomTool } from "../extensibility/custom-tools/types";
17
+ import type { Skill } from "../extensibility/skills";
18
+ import { callTool } from "../mcp/client";
19
+ import type { MCPManager } from "../mcp/manager";
28
20
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
21
+ import { createAgentSession, discoverAuthStorage, discoverModels } from "../sdk";
22
+ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
23
+ import type { AuthStorage } from "../session/auth-storage";
24
+ import { SessionManager } from "../session/session-manager";
25
+ import type { ContextFileEntry } from "../tools";
26
+ import { jtdToJsonSchema } from "../tools/jtd-to-json-schema";
27
+ import { ToolAbortError } from "../tools/tool-errors";
28
+ import type { EventBus } from "../utils/event-bus";
29
29
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
30
30
  import {
31
31
  type AgentDefinition,
package/src/tools/ask.ts CHANGED
@@ -11,8 +11,8 @@
11
11
  * Usage notes:
12
12
  * - Users will always be able to select "Other" to provide custom text input
13
13
  * - Use multi: true to allow multiple answers to be selected for a question
14
- * - If you recommend a specific option, make that the first option in the list
15
- * and add "(Recommended)" at the end of the label
14
+ * - Use recommended: <index> to mark the default option; "(Recommended)" suffix is added automatically
15
+ * - Questions time out after 30 seconds and auto-select the recommended option
16
16
  */
17
17
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
18
18
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -39,12 +39,14 @@ const QuestionItem = Type.Object({
39
39
  question: Type.String({ description: "Question text" }),
40
40
  options: Type.Array(OptionItem, { description: "Available options" }),
41
41
  multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections" })),
42
+ recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed)" })),
42
43
  });
43
44
 
44
45
  const askSchema = Type.Object({
45
46
  question: Type.Optional(Type.String({ description: "Question to ask" })),
46
47
  options: Type.Optional(Type.Array(OptionItem, { description: "Available options" })),
47
48
  multi: Type.Optional(Type.Boolean({ description: "Allow multiple selections (default: false)" })),
49
+ recommended: Type.Optional(Type.Number({ description: "Index of recommended option (0-indexed, default: 0)" })),
48
50
  questions: Type.Optional(Type.Array(QuestionItem, { description: "Multiple questions in sequence" })),
49
51
  });
50
52
 
@@ -74,10 +76,30 @@ export interface AskToolDetails {
74
76
  // =============================================================================
75
77
 
76
78
  const OTHER_OPTION = "Other (type your own)";
79
+ const RECOMMENDED_SUFFIX = " (Recommended)";
80
+
77
81
  function getDoneOptionLabel(): string {
78
82
  return `${theme.status.success} Done selecting`;
79
83
  }
80
84
 
85
+ /** Add "(Recommended)" suffix to the option at the given index if not already present */
86
+ function addRecommendedSuffix(labels: string[], recommendedIndex?: number): string[] {
87
+ if (recommendedIndex === undefined || recommendedIndex < 0 || recommendedIndex >= labels.length) {
88
+ return labels;
89
+ }
90
+ return labels.map((label, i) => {
91
+ if (i === recommendedIndex && !label.endsWith(RECOMMENDED_SUFFIX)) {
92
+ return label + RECOMMENDED_SUFFIX;
93
+ }
94
+ return label;
95
+ });
96
+ }
97
+
98
+ /** Strip "(Recommended)" suffix from a label */
99
+ function stripRecommendedSuffix(label: string): string {
100
+ return label.endsWith(RECOMMENDED_SUFFIX) ? label.slice(0, -RECOMMENDED_SUFFIX.length) : label;
101
+ }
102
+
81
103
  // =============================================================================
82
104
  // Question Selection Logic
83
105
  // =============================================================================
@@ -88,7 +110,11 @@ interface SelectionResult {
88
110
  }
89
111
 
90
112
  interface UIContext {
91
- select(prompt: string, options: string[], options_?: { initialIndex?: number }): Promise<string | undefined>;
113
+ select(
114
+ prompt: string,
115
+ options: string[],
116
+ options_?: { initialIndex?: number; timeout?: number },
117
+ ): Promise<string | undefined>;
92
118
  input(prompt: string): Promise<string | undefined>;
93
119
  }
94
120
 
@@ -97,6 +123,7 @@ async function askSingleQuestion(
97
123
  question: string,
98
124
  optionLabels: string[],
99
125
  multi: boolean,
126
+ recommended?: number,
100
127
  ): Promise<SelectionResult> {
101
128
  const doneLabel = getDoneOptionLabel();
102
129
  let selectedOptions: string[] = [];
@@ -155,12 +182,16 @@ async function askSingleQuestion(
155
182
  }
156
183
  selectedOptions = Array.from(selected);
157
184
  } else {
158
- const choice = await ui.select(question, [...optionLabels, OTHER_OPTION]);
185
+ const displayLabels = addRecommendedSuffix(optionLabels, recommended);
186
+ const choice = await ui.select(question, [...displayLabels, OTHER_OPTION], {
187
+ timeout: 30000,
188
+ initialIndex: recommended,
189
+ });
159
190
  if (choice === OTHER_OPTION) {
160
191
  const input = await ui.input("Enter your response:");
161
192
  if (input) customInput = input;
162
193
  } else if (choice) {
163
- selectedOptions = [choice];
194
+ selectedOptions = [stripRecommendedSuffix(choice)];
164
195
  }
165
196
  }
166
197
 
@@ -187,11 +218,13 @@ interface AskParams {
187
218
  question?: string;
188
219
  options?: Array<{ label: string }>;
189
220
  multi?: boolean;
221
+ recommended?: number;
190
222
  questions?: Array<{
191
223
  id: string;
192
224
  question: string;
193
225
  options: Array<{ label: string }>;
194
226
  multi?: boolean;
227
+ recommended?: number;
195
228
  }>;
196
229
  }
197
230
 
@@ -243,6 +276,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
243
276
  q.question,
244
277
  optionLabels,
245
278
  q.multi ?? false,
279
+ q.recommended,
246
280
  );
247
281
 
248
282
  results.push({
@@ -275,7 +309,13 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
275
309
  };
276
310
  }
277
311
 
278
- const { selectedOptions, customInput } = await askSingleQuestion(ui, question, optionLabels, multi);
312
+ const { selectedOptions, customInput } = await askSingleQuestion(
313
+ ui,
314
+ question,
315
+ optionLabels,
316
+ multi,
317
+ params.recommended,
318
+ );
279
319
 
280
320
  const details: AskToolDetails = {
281
321
  question,
@@ -20,9 +20,11 @@ import { convertWithMarkitdown, fetchBinary } from "../web/scrapers/utils";
20
20
  import type { ToolSession } from ".";
21
21
  import { applyListLimit } from "./list-limit";
22
22
  import type { OutputMeta } from "./output-meta";
23
+ import { allocateOutputArtifact } from "./output-utils";
23
24
  import { formatExpandHint } from "./render-utils";
24
25
  import { ToolAbortError } from "./tool-errors";
25
26
  import { toolResult } from "./tool-result";
27
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, truncateHead } from "./truncate";
26
28
 
27
29
  // =============================================================================
28
30
  // Types and Constants
@@ -921,29 +923,46 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
921
923
 
922
924
  const scratchDir = this.session.getArtifactsDir?.() ?? this.session.cwd;
923
925
  const result = await renderUrl(url, effectiveTimeout, raw, scratchDir, signal);
926
+ const truncation = truncateHead(result.content, { maxBytes: DEFAULT_MAX_BYTES, maxLines: DEFAULT_MAX_LINES });
927
+ const needsArtifact = truncation.truncated;
928
+ let artifactId: string | undefined;
929
+
930
+ const buildOutput = (content: string): string => {
931
+ let output = "";
932
+ output += `URL: ${result.finalUrl}\n`;
933
+ output += `Content-Type: ${result.contentType}\n`;
934
+ output += `Method: ${result.method}\n`;
935
+ if (result.notes.length > 0) {
936
+ output += `Notes: ${result.notes.join("; ")}\n`;
937
+ }
938
+ output += `\n---\n\n`;
939
+ output += content;
940
+ return output;
941
+ };
924
942
 
925
- // Format output
926
- let output = "";
927
- output += `URL: ${result.finalUrl}\n`;
928
- output += `Content-Type: ${result.contentType}\n`;
929
- output += `Method: ${result.method}\n`;
930
- if (result.notes.length > 0) {
931
- output += `Notes: ${result.notes.join("; ")}\n`;
943
+ if (needsArtifact) {
944
+ const { artifactPath, artifactId: allocatedId } = await allocateOutputArtifact(this.session, "fetch");
945
+ if (artifactPath) {
946
+ await Bun.write(artifactPath, buildOutput(result.content));
947
+ artifactId = allocatedId;
948
+ }
932
949
  }
933
- output += `\n---\n\n`;
934
- output += result.content;
950
+
951
+ const output = buildOutput(needsArtifact ? truncation.content : result.content);
935
952
 
936
953
  const details: FetchToolDetails = {
937
954
  url: result.url,
938
955
  finalUrl: result.finalUrl,
939
956
  contentType: result.contentType,
940
957
  method: result.method,
941
- truncated: result.truncated,
958
+ truncated: result.truncated || needsArtifact,
942
959
  notes: result.notes,
943
960
  };
944
961
 
945
962
  const resultBuilder = toolResult(details).text(output).sourceUrl(result.finalUrl);
946
- if (result.truncated) {
963
+ if (needsArtifact) {
964
+ resultBuilder.truncation(truncation, { direction: "head", artifactId });
965
+ } else if (result.truncated) {
947
966
  const outputLines = result.content.split("\n").length;
948
967
  const outputBytes = Buffer.byteLength(result.content, "utf-8");
949
968
  const totalBytes = Math.max(outputBytes + 1, MAX_OUTPUT_CHARS + 1);
@@ -1,8 +1,7 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
- import type { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
3
2
  import { logger } from "@oh-my-pi/pi-utils";
4
3
  import type { PromptTemplate } from "../config/prompt-templates";
5
- import type { BashInterceptorRule } from "../config/settings-manager";
4
+ import type { BashInterceptorRule, SettingsManager } from "../config/settings-manager";
6
5
  import type { Skill } from "../extensibility/skills";
7
6
  import type { InternalUrlRouter } from "../internal-urls";
8
7
  import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
@@ -164,6 +163,7 @@ export interface ToolSession {
164
163
  getEditFuzzyMatch(): boolean;
165
164
  getEditFuzzyThreshold?(): number;
166
165
  getEditPatchMode?(): boolean;
166
+ getEditVariantForModel?(model: string | undefined): "patch" | "replace" | null;
167
167
  getBashInterceptorEnabled(): boolean;
168
168
  getBashInterceptorSimpleLsEnabled(): boolean;
169
169
  getBashInterceptorRules(): BashInterceptorRule[];