@oh-my-pi/pi-coding-agent 11.0.3 → 11.2.0

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 (143) hide show
  1. package/CHANGELOG.md +199 -49
  2. package/README.md +1 -1
  3. package/docs/config-usage.md +3 -4
  4. package/docs/sdk.md +6 -5
  5. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  6. package/examples/sdk/README.md +1 -1
  7. package/package.json +19 -11
  8. package/src/cli/args.ts +11 -94
  9. package/src/cli/config-cli.ts +1 -1
  10. package/src/cli/file-processor.ts +3 -3
  11. package/src/cli/oclif-help.ts +26 -0
  12. package/src/cli/web-search-cli.ts +148 -0
  13. package/src/cli.ts +8 -2
  14. package/src/commands/commit.ts +36 -0
  15. package/src/commands/config.ts +51 -0
  16. package/src/commands/grep.ts +41 -0
  17. package/src/commands/index/index.ts +136 -0
  18. package/src/commands/jupyter.ts +32 -0
  19. package/src/commands/plugin.ts +70 -0
  20. package/src/commands/setup.ts +39 -0
  21. package/src/commands/shell.ts +29 -0
  22. package/src/commands/stats.ts +29 -0
  23. package/src/commands/update.ts +21 -0
  24. package/src/commands/web-search.ts +50 -0
  25. package/src/commit/agentic/index.ts +3 -2
  26. package/src/commit/agentic/tools/analyze-file.ts +1 -3
  27. package/src/commit/git/errors.ts +4 -6
  28. package/src/commit/pipeline.ts +3 -2
  29. package/src/config/keybindings.ts +1 -3
  30. package/src/config/model-registry.ts +89 -162
  31. package/src/config/settings-schema.ts +10 -0
  32. package/src/config.ts +202 -132
  33. package/src/exa/mcp-client.ts +8 -41
  34. package/src/export/html/index.ts +1 -1
  35. package/src/extensibility/extensions/loader.ts +7 -10
  36. package/src/extensibility/extensions/runner.ts +5 -15
  37. package/src/extensibility/extensions/types.ts +1 -1
  38. package/src/extensibility/hooks/runner.ts +6 -9
  39. package/src/index.ts +0 -1
  40. package/src/ipy/kernel.ts +10 -22
  41. package/src/lsp/clients/biome-client.ts +4 -7
  42. package/src/lsp/clients/lsp-linter-client.ts +4 -6
  43. package/src/lsp/index.ts +5 -4
  44. package/src/lsp/utils.ts +18 -0
  45. package/src/main.ts +86 -181
  46. package/src/mcp/json-rpc.ts +2 -2
  47. package/src/mcp/transports/http.ts +12 -49
  48. package/src/modes/components/armin.ts +1 -3
  49. package/src/modes/components/assistant-message.ts +4 -4
  50. package/src/modes/components/bash-execution.ts +5 -3
  51. package/src/modes/components/branch-summary-message.ts +1 -3
  52. package/src/modes/components/compaction-summary-message.ts +1 -3
  53. package/src/modes/components/custom-message.ts +4 -5
  54. package/src/modes/components/extensions/extension-dashboard.ts +10 -16
  55. package/src/modes/components/extensions/extension-list.ts +5 -5
  56. package/src/modes/components/footer.ts +1 -4
  57. package/src/modes/components/hook-editor.ts +7 -32
  58. package/src/modes/components/hook-message.ts +4 -5
  59. package/src/modes/components/model-selector.ts +2 -2
  60. package/src/modes/components/plugin-settings.ts +16 -20
  61. package/src/modes/components/python-execution.ts +5 -5
  62. package/src/modes/components/session-selector.ts +6 -7
  63. package/src/modes/components/settings-defs.ts +49 -40
  64. package/src/modes/components/settings-selector.ts +8 -17
  65. package/src/modes/components/skill-message.ts +1 -3
  66. package/src/modes/components/status-line-segment-editor.ts +1 -3
  67. package/src/modes/components/status-line.ts +1 -3
  68. package/src/modes/components/todo-reminder.ts +5 -7
  69. package/src/modes/components/tree-selector.ts +10 -12
  70. package/src/modes/components/ttsr-notification.ts +1 -3
  71. package/src/modes/components/user-message-selector.ts +2 -4
  72. package/src/modes/components/welcome.ts +6 -18
  73. package/src/modes/controllers/event-controller.ts +1 -0
  74. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  75. package/src/modes/controllers/input-controller.ts +7 -34
  76. package/src/modes/controllers/selector-controller.ts +3 -3
  77. package/src/modes/interactive-mode.ts +27 -1
  78. package/src/modes/rpc/rpc-client.ts +2 -5
  79. package/src/modes/rpc/rpc-mode.ts +2 -2
  80. package/src/modes/theme/theme.ts +2 -6
  81. package/src/modes/types.ts +1 -0
  82. package/src/modes/utils/ui-helpers.ts +6 -1
  83. package/src/patch/index.ts +1 -4
  84. package/src/prompts/agents/explore.md +1 -0
  85. package/src/prompts/agents/frontmatter.md +2 -1
  86. package/src/prompts/agents/init.md +1 -0
  87. package/src/prompts/agents/plan.md +1 -0
  88. package/src/prompts/agents/reviewer.md +1 -0
  89. package/src/prompts/system/subagent-submit-reminder.md +2 -0
  90. package/src/prompts/system/subagent-system-prompt.md +2 -0
  91. package/src/prompts/system/subagent-user-prompt.md +8 -0
  92. package/src/prompts/system/system-prompt.md +5 -3
  93. package/src/prompts/system/web-search.md +6 -4
  94. package/src/prompts/tools/task.md +216 -163
  95. package/src/sdk.ts +11 -110
  96. package/src/session/agent-session.ts +117 -83
  97. package/src/session/auth-storage.ts +10 -51
  98. package/src/session/messages.ts +17 -3
  99. package/src/session/session-manager.ts +30 -30
  100. package/src/session/streaming-output.ts +1 -1
  101. package/src/ssh/ssh-executor.ts +6 -3
  102. package/src/task/agents.ts +2 -0
  103. package/src/task/discovery.ts +1 -1
  104. package/src/task/executor.ts +5 -10
  105. package/src/task/index.ts +43 -23
  106. package/src/task/render.ts +67 -64
  107. package/src/task/template.ts +17 -34
  108. package/src/task/types.ts +49 -22
  109. package/src/tools/ask.ts +1 -3
  110. package/src/tools/bash.ts +1 -4
  111. package/src/tools/browser.ts +5 -7
  112. package/src/tools/exit-plan-mode.ts +1 -4
  113. package/src/tools/fetch.ts +1 -3
  114. package/src/tools/find.ts +4 -3
  115. package/src/tools/gemini-image.ts +24 -55
  116. package/src/tools/grep.ts +4 -4
  117. package/src/tools/index.ts +12 -14
  118. package/src/tools/notebook.ts +1 -5
  119. package/src/tools/python.ts +4 -3
  120. package/src/tools/read.ts +2 -4
  121. package/src/tools/render-utils.ts +23 -0
  122. package/src/tools/ssh.ts +8 -12
  123. package/src/tools/todo-write.ts +1 -4
  124. package/src/tools/tool-errors.ts +1 -4
  125. package/src/tools/write.ts +1 -3
  126. package/src/utils/external-editor.ts +59 -0
  127. package/src/utils/file-mentions.ts +39 -1
  128. package/src/utils/image-convert.ts +1 -1
  129. package/src/utils/image-resize.ts +4 -4
  130. package/src/web/search/auth.ts +3 -33
  131. package/src/web/search/index.ts +73 -139
  132. package/src/web/search/provider.ts +58 -0
  133. package/src/web/search/providers/anthropic.ts +53 -14
  134. package/src/web/search/providers/base.ts +22 -0
  135. package/src/web/search/providers/codex.ts +38 -16
  136. package/src/web/search/providers/exa.ts +30 -6
  137. package/src/web/search/providers/gemini.ts +56 -20
  138. package/src/web/search/providers/jina.ts +28 -5
  139. package/src/web/search/providers/perplexity.ts +103 -36
  140. package/src/web/search/render.ts +84 -74
  141. package/src/web/search/types.ts +285 -59
  142. package/src/migrations.ts +0 -175
  143. package/src/session/storage-migration.ts +0 -173
@@ -5,14 +5,12 @@ import { getMarkdownTheme, theme } from "../../modes/theme/theme";
5
5
  import type { CustomMessage, SkillPromptDetails } from "../../session/messages";
6
6
 
7
7
  export class SkillMessageComponent extends Container {
8
- private message: CustomMessage<SkillPromptDetails>;
9
8
  private box: Box;
10
9
  private contentComponent?: Component;
11
10
  private _expanded = false;
12
11
 
13
- constructor(message: CustomMessage<SkillPromptDetails>) {
12
+ constructor(private readonly message: CustomMessage<SkillPromptDetails>) {
14
13
  super();
15
- this.message = message;
16
14
  this.addChild(new Spacer(1));
17
15
 
18
16
  this.box = new Box(1, 1, t => theme.bg("customMessageBg", t));
@@ -53,15 +53,13 @@ export class StatusLineSegmentEditorComponent extends Container {
53
53
  private segments: SegmentState[];
54
54
  private selectedIndex: number = 0;
55
55
  private focusColumn: "left" | "right" | "disabled" = "left";
56
- private callbacks: SegmentEditorCallbacks;
57
56
 
58
57
  constructor(
59
58
  currentLeft: StatusLineSegmentId[],
60
59
  currentRight: StatusLineSegmentId[],
61
- callbacks: SegmentEditorCallbacks,
60
+ private readonly callbacks: SegmentEditorCallbacks,
62
61
  ) {
63
62
  super();
64
- this.callbacks = callbacks;
65
63
 
66
64
  // Initialize segment states
67
65
  this.segments = [];
@@ -60,7 +60,6 @@ function findGitHeadPath(): string | null {
60
60
  // ═══════════════════════════════════════════════════════════════════════════
61
61
 
62
62
  export class StatusLineComponent implements Component {
63
- private session: AgentSession;
64
63
  private settings: StatusLineSettings = {};
65
64
  private cachedBranch: string | null | undefined = undefined;
66
65
  private gitWatcher: fs.FSWatcher | null = null;
@@ -75,8 +74,7 @@ export class StatusLineComponent implements Component {
75
74
  private cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
76
75
  private gitStatusLastFetch = 0;
77
76
 
78
- constructor(session: AgentSession) {
79
- this.session = session;
77
+ constructor(private readonly session: AgentSession) {
80
78
  this.settings = {
81
79
  preset: settings.get("statusLine.preset"),
82
80
  leftSegments: settings.get("statusLine.leftSegments"),
@@ -7,16 +7,14 @@ import type { TodoItem } from "../../tools/todo-write";
7
7
  * Shows when the agent stops with incomplete todos.
8
8
  */
9
9
  export class TodoReminderComponent extends Container {
10
- private todos: TodoItem[];
11
- private attempt: number;
12
- private maxAttempts: number;
13
10
  private box: Box;
14
11
 
15
- constructor(todos: TodoItem[], attempt: number, maxAttempts: number) {
12
+ constructor(
13
+ private readonly todos: TodoItem[],
14
+ private readonly attempt: number,
15
+ private readonly maxAttempts: number,
16
+ ) {
16
17
  super();
17
- this.todos = todos;
18
- this.attempt = attempt;
19
- this.maxAttempts = maxAttempts;
20
18
 
21
19
  this.addChild(new Spacer(1));
22
20
 
@@ -50,8 +50,6 @@ class TreeList implements Component {
50
50
  private flatNodes: FlatNode[] = [];
51
51
  private filteredNodes: FlatNode[] = [];
52
52
  private selectedIndex = 0;
53
- private currentLeafId: string | null;
54
- private maxVisibleLines: number;
55
53
  private filterMode: FilterMode = "default";
56
54
  private searchQuery = "";
57
55
  private toolCallMap: Map<string, ToolCallInfo> = new Map();
@@ -62,9 +60,11 @@ class TreeList implements Component {
62
60
  public onCancel?: () => void;
63
61
  public onLabelEdit?: (entryId: string, currentLabel: string | undefined) => void;
64
62
 
65
- constructor(tree: SessionTreeNode[], currentLeafId: string | null, maxVisibleLines: number) {
66
- this.currentLeafId = currentLeafId;
67
- this.maxVisibleLines = maxVisibleLines;
63
+ constructor(
64
+ tree: SessionTreeNode[],
65
+ private readonly currentLeafId: string | null,
66
+ private readonly maxVisibleLines: number,
67
+ ) {
68
68
  this.multipleRoots = tree.length > 1;
69
69
  this.flatNodes = this.flattenTree(tree);
70
70
  this.buildActivePath();
@@ -745,12 +745,13 @@ class SearchLine implements Component {
745
745
  /** Label input component shown when editing a label */
746
746
  class LabelInput implements Component {
747
747
  private input: Input;
748
- private entryId: string;
749
748
  public onSubmit?: (entryId: string, label: string | undefined) => void;
750
749
  public onCancel?: () => void;
751
750
 
752
- constructor(entryId: string, currentLabel: string | undefined) {
753
- this.entryId = entryId;
751
+ constructor(
752
+ private readonly entryId: string,
753
+ currentLabel: string | undefined,
754
+ ) {
754
755
  this.input = new Input();
755
756
  if (currentLabel) {
756
757
  this.input.setValue(currentLabel);
@@ -789,7 +790,6 @@ export class TreeSelectorComponent extends Container {
789
790
  private labelInput: LabelInput | null = null;
790
791
  private labelInputContainer: Container;
791
792
  private treeContainer: Container;
792
- private onLabelChangeCallback?: (entryId: string, label: string | undefined) => void;
793
793
 
794
794
  constructor(
795
795
  tree: SessionTreeNode[],
@@ -797,11 +797,9 @@ export class TreeSelectorComponent extends Container {
797
797
  terminalHeight: number,
798
798
  onSelect: (entryId: string) => void,
799
799
  onCancel: () => void,
800
- onLabelChange?: (entryId: string, label: string | undefined) => void,
800
+ private readonly onLabelChangeCallback?: (entryId: string, label: string | undefined) => void,
801
801
  ) {
802
802
  super();
803
-
804
- this.onLabelChangeCallback = onLabelChange;
805
803
  const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2));
806
804
 
807
805
  this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines);
@@ -7,13 +7,11 @@ import { theme } from "../../modes/theme/theme";
7
7
  * Shows when a rule violation is detected and the stream is being rewound.
8
8
  */
9
9
  export class TtsrNotificationComponent extends Container {
10
- private rules: Rule[];
11
10
  private box: Box;
12
11
  private _expanded = false;
13
12
 
14
- constructor(rules: Rule[]) {
13
+ constructor(private readonly rules: Rule[]) {
15
14
  super();
16
- this.rules = rules;
17
15
 
18
16
  this.addChild(new Spacer(1));
19
17
 
@@ -12,17 +12,15 @@ interface UserMessageItem {
12
12
  * Custom user message list component with selection
13
13
  */
14
14
  class UserMessageList implements Component {
15
- private messages: UserMessageItem[] = [];
16
15
  private selectedIndex: number = 0;
17
16
  public onSelect?: (entryId: string) => void;
18
17
  public onCancel?: () => void;
19
18
  private maxVisible: number = 10; // Max messages visible
20
19
 
21
- constructor(messages: UserMessageItem[]) {
20
+ constructor(private readonly messages: UserMessageItem[]) {
22
21
  // Store messages in chronological order (oldest to newest)
23
- this.messages = messages;
24
22
  // Start with the last (most recent) message selected
25
- this.selectedIndex = Math.max(0, messages.length - 1);
23
+ this.selectedIndex = Math.max(0, this.messages.length - 1);
26
24
  }
27
25
 
28
26
  invalidate(): void {
@@ -17,25 +17,13 @@ export interface LspServerInfo {
17
17
  * Premium welcome screen with block-based OMP logo and two-column layout.
18
18
  */
19
19
  export class WelcomeComponent implements Component {
20
- private version: string;
21
- private modelName: string;
22
- private providerName: string;
23
- private recentSessions: RecentSession[];
24
- private lspServers: LspServerInfo[];
25
-
26
20
  constructor(
27
- version: string,
28
- modelName: string,
29
- providerName: string,
30
- recentSessions: RecentSession[] = [],
31
- lspServers: LspServerInfo[] = [],
32
- ) {
33
- this.version = version;
34
- this.modelName = modelName;
35
- this.providerName = providerName;
36
- this.recentSessions = recentSessions;
37
- this.lspServers = lspServers;
38
- }
21
+ private readonly version: string,
22
+ private modelName: string,
23
+ private providerName: string,
24
+ private recentSessions: RecentSession[] = [],
25
+ private lspServers: LspServerInfo[] = [],
26
+ ) {}
39
27
 
40
28
  invalidate(): void {}
41
29
 
@@ -269,6 +269,7 @@ export class EventController {
269
269
  this.ctx.streamingComponent = undefined;
270
270
  this.ctx.streamingMessage = undefined;
271
271
  }
272
+ await this.ctx.flushPendingModelSwitch();
272
273
  this.ctx.pendingTools.clear();
273
274
  this.ctx.ui.requestRender();
274
275
  this.sendCompletionNotification();
@@ -282,7 +282,7 @@ export class ExtensionUiController {
282
282
  return true;
283
283
  },
284
284
  getThinkingLevel: () => this.ctx.session.thinkingLevel,
285
- setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
285
+ setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
286
286
  };
287
287
  const contextActions: ExtensionContextActions = {
288
288
  getModel: () => this.ctx.session.model,
@@ -1,16 +1,14 @@
1
- import { spawn } from "node:child_process";
2
1
  import * as fs from "node:fs/promises";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
2
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
3
  import { readImageFromClipboard } from "@oh-my-pi/pi-natives";
7
- import { $env, Snowflake } from "@oh-my-pi/pi-utils";
4
+ import { $env } from "@oh-my-pi/pi-utils";
8
5
  import type { SettingPath, SettingValue } from "../../config/settings";
9
6
  import { settings } from "../../config/settings";
10
7
  import { theme } from "../../modes/theme/theme";
11
8
  import type { InteractiveModeContext } from "../../modes/types";
12
9
  import type { AgentSessionEvent } from "../../session/agent-session";
13
10
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
11
+ import { getEditorCommand, openInEditor } from "../../utils/external-editor";
14
12
  import { resizeImage } from "../../utils/image-resize";
15
13
  import { generateSessionTitle, setTerminalTitle } from "../../utils/title-generator";
16
14
 
@@ -601,7 +599,7 @@ export class InputController {
601
599
  try {
602
600
  const image = await readImageFromClipboard();
603
601
  if (image) {
604
- const base64Data = Buffer.from(image.data).toString("base64");
602
+ const base64Data = image.data.toBase64();
605
603
  let imageData = { data: base64Data, mimeType: image.mimeType };
606
604
  if (settings.get("images.autoResize")) {
607
605
  try {
@@ -730,61 +728,36 @@ export class InputController {
730
728
  }
731
729
 
732
730
  async openExternalEditor(): Promise<void> {
733
- // Determine editor (respect $VISUAL, then $EDITOR)
734
- const editorCmd = $env.VISUAL || $env.EDITOR;
731
+ const editorCmd = getEditorCommand();
735
732
  if (!editorCmd) {
736
733
  this.ctx.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
737
734
  return;
738
735
  }
739
736
 
740
737
  const currentText = this.ctx.editor.getText();
741
- const tmpFile = path.join(os.tmpdir(), `omp-editor-${Snowflake.next()}.omp.md`);
742
738
 
743
739
  let ttyHandle: fs.FileHandle | null = null;
744
740
  try {
745
- // Write current content to temp file
746
- await Bun.write(tmpFile, currentText);
747
-
748
- // Stop TUI to release terminal
749
741
  ttyHandle = await this.openEditorTerminalHandle();
750
742
  this.ctx.ui.stop();
751
743
 
752
- // Split by space to support editor arguments (e.g., "code --wait")
753
- const [editor, ...editorArgs] = editorCmd.split(" ");
754
-
755
744
  const stdio: [number | "inherit", number | "inherit", number | "inherit"] = ttyHandle
756
745
  ? [ttyHandle.fd, ttyHandle.fd, ttyHandle.fd]
757
746
  : ["inherit", "inherit", "inherit"];
758
747
 
759
- const child = spawn(editor, [...editorArgs, tmpFile], { stdio });
760
- const exitCode = await new Promise<number>((resolve, reject) => {
761
- child.once("exit", (code, signal) => resolve(code ?? (signal ? -1 : 0)));
762
- child.once("error", error => reject(error));
763
- });
764
-
765
- // On successful exit (exitCode 0), replace editor content
766
- if (exitCode === 0) {
767
- const newContent = (await Bun.file(tmpFile).text()).replace(/\n$/, "");
768
- this.ctx.editor.setText(newContent);
748
+ const result = await openInEditor(editorCmd, currentText, { extension: ".omp.md", stdio });
749
+ if (result !== null) {
750
+ this.ctx.editor.setText(result);
769
751
  }
770
- // On non-zero exit, keep original text (no action needed)
771
752
  } catch (error) {
772
753
  this.ctx.showWarning(
773
754
  `Failed to open external editor: ${error instanceof Error ? error.message : String(error)}`,
774
755
  );
775
756
  } finally {
776
- // Clean up temp file
777
- try {
778
- await fs.rm(tmpFile, { force: true });
779
- } catch {
780
- // Ignore cleanup errors
781
- }
782
-
783
757
  if (ttyHandle) {
784
758
  await ttyHandle.close();
785
759
  }
786
760
 
787
- // Restart TUI
788
761
  this.ctx.ui.start();
789
762
  this.ctx.ui.requestRender();
790
763
  }
@@ -27,7 +27,7 @@ import {
27
27
  } from "../../modes/theme/theme";
28
28
  import type { InteractiveModeContext } from "../../modes/types";
29
29
  import { SessionManager } from "../../session/session-manager";
30
- import { setPreferredImageProvider, setPreferredWebSearchProvider } from "../../tools";
30
+ import { setPreferredImageProvider, setPreferredSearchProvider } from "../../tools";
31
31
 
32
32
  export class SelectorController {
33
33
  constructor(private ctx: InteractiveModeContext) {}
@@ -180,7 +180,7 @@ export class SelectorController {
180
180
  break;
181
181
  case "thinkingLevel":
182
182
  case "defaultThinkingLevel":
183
- this.ctx.session.setThinkingLevel(value as ThinkingLevel);
183
+ this.ctx.session.setThinkingLevel(value as ThinkingLevel, true);
184
184
  this.ctx.statusLine.invalidate();
185
185
  this.ctx.updateEditorBorderColor();
186
186
  break;
@@ -258,7 +258,7 @@ export class SelectorController {
258
258
 
259
259
  // Provider settings - update runtime preferences
260
260
  case "webSearchProvider":
261
- setPreferredWebSearchProvider(value as "auto" | "exa" | "perplexity" | "anthropic");
261
+ setPreferredSearchProvider(value as "auto" | "exa" | "perplexity" | "anthropic");
262
262
  break;
263
263
  case "imageProvider":
264
264
  setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
@@ -136,6 +136,7 @@ export class InteractiveMode implements InteractiveModeContext {
136
136
  private readonly changelogMarkdown: string | undefined;
137
137
  private planModePreviousTools: string[] | undefined;
138
138
  private planModePreviousModel: Model | undefined;
139
+ private pendingModelSwitch: Model | undefined;
139
140
  private planModeHasEntered = false;
140
141
  public readonly lspServers:
141
142
  | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
@@ -533,6 +534,10 @@ export class InteractiveMode implements InteractiveModeContext {
533
534
  return;
534
535
  }
535
536
  this.planModePreviousModel = currentModel;
537
+ if (this.session.isStreaming) {
538
+ this.pendingModelSwitch = planModel;
539
+ return;
540
+ }
536
541
  try {
537
542
  await this.session.setModelTemporary(planModel);
538
543
  } catch (error) {
@@ -542,6 +547,20 @@ export class InteractiveMode implements InteractiveModeContext {
542
547
  }
543
548
  }
544
549
 
550
+ /** Apply any deferred model switch after the current stream ends. */
551
+ async flushPendingModelSwitch(): Promise<void> {
552
+ const model = this.pendingModelSwitch;
553
+ if (!model) return;
554
+ this.pendingModelSwitch = undefined;
555
+ try {
556
+ await this.session.setModelTemporary(model);
557
+ } catch (error) {
558
+ this.showWarning(
559
+ `Failed to switch model after streaming: ${error instanceof Error ? error.message : String(error)}`,
560
+ );
561
+ }
562
+ }
563
+
545
564
  private async enterPlanMode(options?: {
546
565
  planFilePath?: string;
547
566
  workflow?: "parallel" | "iterative";
@@ -569,6 +588,9 @@ export class InteractiveMode implements InteractiveModeContext {
569
588
  workflow: options?.workflow ?? "parallel",
570
589
  reentry: this.planModeHasEntered,
571
590
  });
591
+ if (this.session.isStreaming) {
592
+ await this.session.sendPlanModeContext({ deliverAs: "steer" });
593
+ }
572
594
  this.planModeHasEntered = true;
573
595
  await this.applyPlanModeModel();
574
596
  this.updatePlanModeStatus();
@@ -585,7 +607,11 @@ export class InteractiveMode implements InteractiveModeContext {
585
607
  await this.session.setActiveToolsByName(previousTools);
586
608
  }
587
609
  if (this.planModePreviousModel) {
588
- await this.session.setModelTemporary(this.planModePreviousModel);
610
+ if (this.session.isStreaming) {
611
+ this.pendingModelSwitch = this.planModePreviousModel;
612
+ } else {
613
+ await this.session.setModelTemporary(this.planModePreviousModel);
614
+ }
589
615
  }
590
616
 
591
617
  this.session.setPlanModeState(undefined);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
7
  import type { ImageContent } from "@oh-my-pi/pi-ai";
8
- import { createSanitizerStream, createSplitterStream, createTextDecoderStream, ptree } from "@oh-my-pi/pi-utils";
8
+ import { createTextLineSplitter, ptree } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";
@@ -119,10 +119,7 @@ export class RpcClient {
119
119
  });
120
120
 
121
121
  // Process lines in background
122
- const lines = this.process.stdout
123
- .pipeThrough(createTextDecoderStream())
124
- .pipeThrough(createSanitizerStream())
125
- .pipeThrough(createSplitterStream("\n"));
122
+ const lines = this.process.stdout.pipeThrough(createTextLineSplitter(true));
126
123
  this.lineReader = lines;
127
124
  void (async () => {
128
125
  try {
@@ -10,7 +10,7 @@
10
10
  * - Events: AgentSessionEvent objects streamed as they occur
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
- import { readLines, Snowflake } from "@oh-my-pi/pi-utils";
13
+ import { createTextLineSplitter, Snowflake } from "@oh-my-pi/pi-utils";
14
14
  import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
15
15
  import { type Theme, theme } from "../../modes/theme/theme";
16
16
  import type { AgentSession } from "../../session/agent-session";
@@ -633,7 +633,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
633
633
  }
634
634
 
635
635
  // Listen for JSON input using Bun's stdin
636
- for await (const line of readLines(Bun.stdin.stream())) {
636
+ for await (const line of Bun.stdin.stream().pipeThrough(createTextLineSplitter())) {
637
637
  if (!line.trim()) continue;
638
638
 
639
639
  const result = Bun.JSONL.parseChunk(`${line}\n`);
@@ -1221,19 +1221,15 @@ const langMap: Record<string, SymbolKey> = {
1221
1221
  export class Theme {
1222
1222
  private fgColors: Record<ThemeColor, string>;
1223
1223
  private bgColors: Record<ThemeBg, string>;
1224
- private mode: ColorMode;
1225
1224
  private symbols: SymbolMap;
1226
- private symbolPreset: SymbolPreset;
1227
1225
 
1228
1226
  constructor(
1229
1227
  fgColors: Record<ThemeColor, string | number>,
1230
1228
  bgColors: Record<ThemeBg, string | number>,
1231
- mode: ColorMode,
1232
- symbolPreset: SymbolPreset,
1229
+ private readonly mode: ColorMode,
1230
+ private readonly symbolPreset: SymbolPreset,
1233
1231
  symbolOverrides: Partial<Record<SymbolKey, string>>,
1234
1232
  ) {
1235
- this.mode = mode;
1236
- this.symbolPreset = symbolPreset;
1237
1233
  this.fgColors = {} as Record<ThemeColor, string>;
1238
1234
  for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
1239
1235
  this.fgColors[key] = fgAnsi(value, mode);
@@ -114,6 +114,7 @@ export interface InteractiveModeContext {
114
114
  queueCompactionMessage(text: string, mode: "steer" | "followUp"): void;
115
115
  flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void>;
116
116
  flushPendingBashComponents(): void;
117
+ flushPendingModelSwitch(): Promise<void>;
117
118
  setWorkingMessage(message?: string): void;
118
119
  applyPendingWorkingMessage(): void;
119
120
  isKnownSlashCommand(text: string): boolean;
@@ -127,10 +127,15 @@ export class UiHelpers {
127
127
  case "fileMention": {
128
128
  // Render compact file mention display
129
129
  for (const file of message.files) {
130
+ const suffix = file.image
131
+ ? "(image)"
132
+ : file.lineCount === undefined
133
+ ? "(unknown lines)"
134
+ : `(${file.lineCount} lines)`;
130
135
  const text = `${theme.fg("dim", `${theme.tree.last} `)}${theme.fg("muted", "Read")} ${theme.fg(
131
136
  "accent",
132
137
  file.path,
133
- )} ${theme.fg("dim", `(${file.lineCount} lines)`)}`;
138
+ )} ${theme.fg("dim", suffix)}`;
134
139
  this.ctx.chatContainer.addChild(new Text(text, 0, 0));
135
140
  }
136
141
  break;
@@ -205,15 +205,12 @@ export class EditTool implements AgentTool<TInput> {
205
205
  public readonly nonAbortable = true;
206
206
  public readonly concurrency = "exclusive";
207
207
 
208
- private readonly session: ToolSession;
209
208
  private readonly allowFuzzy: boolean;
210
209
  private readonly fuzzyThreshold: number;
211
210
  private readonly writethrough: WritethroughCallback;
212
211
  private readonly envEditVariant: string;
213
212
 
214
- constructor(session: ToolSession) {
215
- this.session = session;
216
-
213
+ constructor(private readonly session: ToolSession) {
217
214
  const {
218
215
  PI_EDIT_FUZZY: editFuzzy = "auto",
219
216
  PI_EDIT_FUZZY_THRESHOLD: editFuzzyThreshold = "auto",
@@ -3,6 +3,7 @@ name: explore
3
3
  description: Fast read-only codebase scout returning compressed context for handoff
4
4
  tools: read, grep, find, bash
5
5
  model: pi/smol, haiku-4.5, haiku-4-5, gemini-flash-latest, gemini-3-flash, zai-glm-4.7, glm-4.7-flash, glm-4.5-flash, gpt-5.1-codex-mini, haiku, flash, mini
6
+ thinking-level: minimal
6
7
  output:
7
8
  properties:
8
9
  query:
@@ -1,8 +1,9 @@
1
1
  ---
2
+
2
3
  name: {{jsonStringify name}}
3
4
  description: {{jsonStringify description}}
4
5
  {{#if spawns}}spawns: {{jsonStringify spawns}}
5
6
  {{/if}}{{#if model}}model: {{jsonStringify model}}
6
- {{/if}}{{#if thinkingLevel}}thinkingLevel: {{jsonStringify thinkingLevel}}
7
+ {{/if}}{{#if thinkingLevel}}thinking-level: {{jsonStringify thinkingLevel}}
7
8
  {{/if}}---
8
9
  {{body}}
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: init
3
3
  description: Generate AGENTS.md for current codebase
4
+ thinking-level: medium
4
5
  ---
5
6
 
6
7
  <task>
@@ -4,6 +4,7 @@ description: Software architect for complex multi-file architectural decisions.
4
4
  tools: read, grep, find, bash
5
5
  spawns: explore
6
6
  model: pi/plan, pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt, opus-4.5, opus-4-5, gemini-3-pro
7
+ thinking-level: high
7
8
  ---
8
9
 
9
10
  <critical>
@@ -4,6 +4,7 @@ description: "Code review specialist for quality/security analysis"
4
4
  tools: read, grep, find, bash, report_finding
5
5
  spawns: explore, task
6
6
  model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
7
+ thinking-level: high
7
8
  output:
8
9
  properties:
9
10
  overall_correctness:
@@ -5,5 +5,7 @@ Your only available action now is to call submit_result. Choose one:
5
5
  - If task is complete: call submit_result with your result data
6
6
  - If task failed or was interrupted: call submit_result with status="aborted" and describe what happened
7
7
 
8
+ Do NOT choose aborted if you can still complete the task through exploration (using available tools or repo context). If you must abort, include what you tried and the exact blocker.
9
+
8
10
  Do NOT output text without a tool call. You must call submit_result to finish.
9
11
  </system-reminder>
@@ -27,5 +27,7 @@ For additional parent conversation context, check {{contextFile}} (`tail -100` o
27
27
  ```
28
28
  {{/if}}
29
29
  - If cannot complete, call `submit_result` exactly once with result indicating failure/abort status (use failure/notes field if available). Do not claim success.
30
+ - Do NOT abort due to uncertainty or missing info that can be obtained via tools or repo context. Use `find`/`grep`/`read` first, then proceed with reasonable defaults if multiple options are acceptable.
31
+ - Aborting is only acceptable when truly blocked after exhausting tools and reasonable attempts. If you abort, include what you tried and the exact blocker in the result.
30
32
  - Keep going until request is fully fulfilled. This matters.
31
33
  </critical>
@@ -0,0 +1,8 @@
1
+ {{#if context}}
2
+ <swarm_context>{{context}}</swarm_context>
3
+
4
+ # Your Assignment
5
+ {{assignment}}
6
+ {{else}}
7
+ {{assignment}}
8
+ {{/if}}
@@ -37,10 +37,11 @@ State assumptions before non-trivial work. Format:
37
37
  ASSUMPTIONS:
38
38
  1. [assumption]
39
39
  2. [assumption]
40
- → Then proceed. User will interrupt if wrong.
41
40
  ```
41
+ Proceed without confirmation. User can interrupt if wrong.
42
42
 
43
43
  Do NOT use ask tool to confirm assumptions. State them, then act. Asking for confirmation wastes a round-trip on questions where "yes, proceed" is the obvious answer.
44
+ Do NOT ask for file paths the user implies or you can resolve from repo context. If a file is referenced, locate and read it.
44
45
 
45
46
  Before finishing (within requested scope):
46
47
  - Can this be simpler?
@@ -154,7 +155,7 @@ Continue non-destructively; someone's work may live there.
154
155
 
155
156
  <procedure>
156
157
  ## Before action
157
- 0. **CHECKPOINT** — multi-step/multi-file/ambiguous tasks: pause before acting:
158
+ 0. **CHECKPOINT** — multi-step/multi-file/ambiguous tasks: do a brief internal checkpoint, then continue in the same response (do not wait for user input):
158
159
  - Distinct work streams? Dependencies?
159
160
  {{#has tools "task"}}
160
161
  - Parallel via Task tool, or sequential?
@@ -169,7 +170,7 @@ Continue non-destructively; someone's work may live there.
169
170
  1. Plan if task has weight: 3–7 bullets, no more.
170
171
  2. Before each tool call: state intent in one sentence.
171
172
  3. After each tool call: interpret, decide, move; no echo.
172
- 4. Requirements conflict/unclear: stop, name confusion, ask resolution before proceeding.
173
+ 4. Requirements conflict/unclear: if genuinely blocked **ONLY AFTER** exhausting your exploration with tools/context/files, ask.
173
174
  5. If requested change includes refactor: remove now-unused elements; note removals.
174
175
 
175
176
  ## Verification
@@ -328,6 +329,7 @@ Keep going until finished.
328
329
  - Blocked: show evidence, what tried, ask minimum question.
329
330
  - Quote only needed; rest noise.
330
331
  - Don't claim unverified correctness.
332
+ - Do not ask when it may be obtained from available tools or repo context/files.
331
333
  - CHECKPOINT step 0 not optional.
332
334
  - Touch only requested; no incidental refactors/cleanup.
333
335
  {{#has tools "ask"}}- If files differ from expectations: ask before discarding uncommitted work.{{/has}}