@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.2

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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. package/src/utils/prompt-format.ts +0 -170
@@ -8,6 +8,10 @@ import type {
8
8
  import type { AgentSessionEvent } from "../../session/agent-session";
9
9
  import type { TodoStatus } from "../../tools/todo-write";
10
10
 
11
+ interface AcpEventMapperOptions {
12
+ getMessageId?: (message: unknown) => string | undefined;
13
+ }
14
+
11
15
  interface ContentArrayContainer {
12
16
  content?: unknown;
13
17
  }
@@ -118,10 +122,11 @@ export function mapToolKind(toolName: string): ToolKind {
118
122
  export function mapAgentSessionEventToAcpSessionUpdates(
119
123
  event: AgentSessionEvent,
120
124
  sessionId: string,
125
+ options: AcpEventMapperOptions = {},
121
126
  ): SessionNotification[] {
122
127
  switch (event.type) {
123
128
  case "message_update":
124
- return mapAssistantMessageUpdate(event, sessionId);
129
+ return mapAssistantMessageUpdate(event, sessionId, options);
125
130
  case "tool_execution_start": {
126
131
  const update: SessionUpdate = {
127
132
  sessionUpdate: "tool_call",
@@ -181,6 +186,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
181
186
  function mapAssistantMessageUpdate(
182
187
  event: Extract<AgentSessionEvent, { type: "message_update" }>,
183
188
  sessionId: string,
189
+ options: AcpEventMapperOptions,
184
190
  ): SessionNotification[] {
185
191
  if (!isAssistantMessage(event.message)) {
186
192
  return [];
@@ -208,10 +214,12 @@ function mapAssistantMessageUpdate(
208
214
  return [];
209
215
  }
210
216
 
217
+ const messageId = options.getMessageId?.(event.message);
211
218
  return [
212
219
  toSessionNotification(sessionId, {
213
220
  sessionUpdate,
214
221
  content: { type: "text", text },
222
+ messageId,
215
223
  }),
216
224
  ];
217
225
  }
@@ -3,11 +3,13 @@ import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
3
3
  import type { AgentSession } from "../../session/agent-session";
4
4
  import { AcpAgent } from "./acp-agent";
5
5
 
6
- export async function runAcpMode(session: AgentSession): Promise<never> {
6
+ export type AcpSessionFactory = (cwd: string) => Promise<AgentSession>;
7
+
8
+ export async function runAcpMode(session: AgentSession, createSession: AcpSessionFactory): Promise<never> {
7
9
  const input = stream.Writable.toWeb(process.stdout);
8
10
  const output = stream.Readable.toWeb(process.stdin);
9
11
  const transport = ndJsonStream(input, output);
10
- const connection = new AgentSideConnection(conn => new AcpAgent(conn, session), transport);
12
+ const connection = new AgentSideConnection(conn => new AcpAgent(conn, session, createSession), transport);
11
13
  await connection.closed;
12
14
  process.exit(0);
13
15
  }
@@ -31,7 +31,7 @@ import {
31
31
  visibleWidth,
32
32
  wrapTextWithAnsi,
33
33
  } from "@oh-my-pi/pi-tui";
34
- import { isEnoent } from "@oh-my-pi/pi-utils";
34
+ import { isEnoent, prompt } from "@oh-my-pi/pi-utils";
35
35
  import { YAML } from "bun";
36
36
  import { getConfigDirs } from "../../config";
37
37
  import type { ModelRegistry } from "../../config/model-registry";
@@ -41,7 +41,6 @@ import {
41
41
  resolveConfiguredModelPatterns,
42
42
  resolveModelOverride,
43
43
  } from "../../config/model-resolver";
44
- import { renderPromptTemplate } from "../../config/prompt-templates";
45
44
  import { Settings } from "../../config/settings";
46
45
  import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
47
46
  import agentCreationUserPrompt from "../../prompts/system/agent-creation-user.md" with { type: "text" };
@@ -627,8 +626,8 @@ export class AgentDashboard extends Container {
627
626
  throw new Error("No available model to generate agent specification.");
628
627
  }
629
628
 
630
- const systemPrompt = renderPromptTemplate(agentCreationArchitectPrompt, { TASK_TOOL_NAME: "task" });
631
- const userPrompt = renderPromptTemplate(agentCreationUserPrompt, { request: description });
629
+ const systemPrompt = prompt.render(agentCreationArchitectPrompt, { TASK_TOOL_NAME: "task" });
630
+ const userPrompt = prompt.render(agentCreationUserPrompt, { request: description });
632
631
 
633
632
  const { session } = await createAgentSession({
634
633
  cwd: this.cwd,
@@ -1,4 +1,4 @@
1
- import { getIndentation } from "@oh-my-pi/pi-utils";
1
+ import { getIndentation } from "@oh-my-pi/pi-natives";
2
2
  import * as Diff from "diff";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
  import { replaceTabs } from "../../tools/render-utils";
@@ -15,11 +15,10 @@ const DIM_OFF = "\x1b[22m";
15
15
  */
16
16
  function visualizeIndent(text: string, filePath?: string): string {
17
17
  const match = text.match(/^([ \t]+)/);
18
- if (!match) return replaceTabs(text, filePath);
18
+ if (!match) return replaceTabs(text);
19
19
  const indent = match[1];
20
20
  const rest = text.slice(indent.length);
21
- const indentation = getIndentation(filePath);
22
- const tabWidth = indentation.length;
21
+ const tabWidth = getIndentation(filePath);
23
22
  const leftPadding = Math.floor(tabWidth / 2);
24
23
  const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
25
24
  const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
@@ -31,7 +30,7 @@ function visualizeIndent(text: string, filePath?: string): string {
31
30
  visible += `${DIM}·${DIM_OFF}`;
32
31
  }
33
32
  }
34
- return `${visible}${replaceTabs(rest, filePath)}`;
33
+ return `${visible}${replaceTabs(rest)}`;
35
34
  }
36
35
 
37
36
  /**
@@ -154,8 +153,8 @@ export function renderDiff(diffText: string, options: RenderDiffOptions = {}): s
154
153
  const added = addedLines[0];
155
154
 
156
155
  const { removedLine, addedLine } = renderIntraLineDiff(
157
- replaceTabs(removed.content, options.filePath),
158
- replaceTabs(added.content, options.filePath),
156
+ replaceTabs(removed.content),
157
+ replaceTabs(added.content),
159
158
  );
160
159
 
161
160
  result.push(
@@ -7,8 +7,7 @@ import type { ToolExecutionHandle } from "./tool-execution";
7
7
  type ReadRenderArgs = {
8
8
  path?: string;
9
9
  file_path?: string;
10
- offset?: number;
11
- limit?: number;
10
+ sel?: string;
12
11
  };
13
12
 
14
13
  type ReadToolSuffixResolution = {
@@ -33,8 +32,7 @@ function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadTo
33
32
  type ReadEntry = {
34
33
  toolCallId: string;
35
34
  path: string;
36
- offset?: number;
37
- limit?: number;
35
+ sel?: string;
38
36
  status: "pending" | "success" | "warning" | "error";
39
37
  correctedFrom?: string;
40
38
  };
@@ -56,13 +54,11 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
56
54
  const entry: ReadEntry = this.#entries.get(toolCallId) ?? {
57
55
  toolCallId,
58
56
  path: rawPath,
59
- offset: args.offset,
60
- limit: args.limit,
57
+ sel: args.sel,
61
58
  status: "pending",
62
59
  };
63
60
  entry.path = rawPath;
64
- entry.offset = args.offset;
65
- entry.limit = args.limit;
61
+ entry.sel = args.sel;
66
62
  this.#entries.set(toolCallId, entry);
67
63
  this.#updateDisplay();
68
64
  }
@@ -132,10 +128,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
132
128
  #formatPath(entry: ReadEntry): string {
133
129
  const filePath = shortenPath(entry.path);
134
130
  let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", "…");
135
- if (entry.offset !== undefined || entry.limit !== undefined) {
136
- const startLine = entry.offset ?? 1;
137
- const endLine = entry.limit !== undefined ? startLine + entry.limit - 1 : "";
138
- pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
131
+ if (entry.sel) {
132
+ pathDisplay += theme.fg("warning", `:${entry.sel}`);
139
133
  }
140
134
  if (entry.correctedFrom) {
141
135
  pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(entry.correctedFrom)})`);
@@ -280,6 +280,11 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
280
280
  { value: "1000", label: "1000 lines" },
281
281
  { value: "5000", label: "5000 lines" },
282
282
  ],
283
+ "read.anchorstyle": [
284
+ { value: "full", label: "Full", description: "Show the kind prefix and identifier" },
285
+ { value: "kind", label: "Kind", description: "Show only the kind prefix plus checksum" },
286
+ { value: "bare", label: "Bare", description: "Show only the checksum" },
287
+ ],
283
288
  // Todo auto-clear delay
284
289
  "tasks.todoClearDelay": [
285
290
  { value: "0", label: "Instant" },
@@ -14,9 +14,9 @@ import {
14
14
  type TUI,
15
15
  } from "@oh-my-pi/pi-tui";
16
16
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
17
+ import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../edit";
17
18
  import type { Theme } from "../../modes/theme/theme";
18
19
  import { theme } from "../../modes/theme/theme";
19
- import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../patch";
20
20
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
21
21
  import {
22
22
  formatArgsInline,
@@ -111,7 +111,7 @@ export class WelcomeComponent implements Component {
111
111
  server.status === "ready"
112
112
  ? theme.styledSymbol("status.success", "success")
113
113
  : server.status === "connecting"
114
- ? theme.styledSymbol("status.disabled", "warning")
114
+ ? theme.styledSymbol("status.pending", "muted")
115
115
  : theme.styledSymbol("status.error", "error");
116
116
  const exts = server.fileTypes.slice(0, 3).join(" ");
117
117
  lspLines.push(` ${icon} ${theme.fg("muted", server.name)} ${theme.fg("dim", exts)}`);
@@ -1,6 +1,6 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { type AssistantMessage, type Context, streamSimple } from "@oh-my-pi/pi-ai";
3
- import { renderPromptTemplate } from "../../config/prompt-templates";
3
+ import { prompt } from "@oh-my-pi/pi-utils";
4
4
  import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
5
5
  import { toReasoningEffort } from "../../thinking";
6
6
  import { BtwPanelComponent } from "../components/btw-panel";
@@ -137,7 +137,7 @@ export class BtwController {
137
137
  content: [
138
138
  {
139
139
  type: "text",
140
- text: renderPromptTemplate(btwUserPrompt, { question }),
140
+ text: prompt.render(btwUserPrompt, { question }),
141
141
  },
142
142
  ],
143
143
  attribution: "user",
@@ -9,7 +9,6 @@ import {
9
9
  type UsageLimit,
10
10
  type UsageReport,
11
11
  } from "@oh-my-pi/pi-ai";
12
- import { copyToClipboard } from "@oh-my-pi/pi-natives";
13
12
  import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@oh-my-pi/pi-tui";
14
13
  import { formatDuration, Snowflake, setProjectDir } from "@oh-my-pi/pi-utils";
15
14
  import { $ } from "bun";
@@ -33,6 +32,7 @@ import { outputMeta } from "../../tools/output-meta";
33
32
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
34
33
  import { replaceTabs } from "../../tools/render-utils";
35
34
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
35
+ import { copyToClipboard } from "../../utils/clipboard";
36
36
  import { openPath } from "../../utils/open";
37
37
  import { setSessionTerminalTitle } from "../../utils/title-generator";
38
38
 
@@ -396,7 +396,8 @@ export class CommandController {
396
396
  if (this.ctx.lspServers && this.ctx.lspServers.length > 0) {
397
397
  info += `\n${theme.bold("LSP Servers")}\n`;
398
398
  for (const server of this.ctx.lspServers) {
399
- const statusColor = server.status === "ready" ? "success" : "error";
399
+ const statusColor =
400
+ server.status === "ready" ? "success" : server.status === "connecting" ? "warning" : "error";
400
401
  const statusText =
401
402
  server.status === "error" && server.error ? `${server.status}: ${server.error}` : server.status;
402
403
  info += `${theme.fg("dim", `${server.name}:`)} ${theme.fg(statusColor, statusText)} ${theme.fg("dim", `(${server.fileTypes.join(", ")})`)}\n`;
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import { type AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
- import { copyToClipboard, readImageFromClipboard, sanitizeText } from "@oh-my-pi/pi-natives";
3
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
4
4
  import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
5
5
  import { $env } from "@oh-my-pi/pi-utils";
6
6
  import { settings } from "../../config/settings";
@@ -10,8 +10,9 @@ import type { InteractiveModeContext } from "../../modes/types";
10
10
  import type { AgentSessionEvent } from "../../session/agent-session";
11
11
  import { SKILL_PROMPT_MESSAGE_TYPE, type SkillPromptDetails } from "../../session/messages";
12
12
  import { executeBuiltinSlashCommand } from "../../slash-commands/builtin-registry";
13
+ import { copyToClipboard, readImageFromClipboard } from "../../utils/clipboard";
13
14
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
14
- import { ensureSupportedImageInput } from "../../utils/image-input";
15
+ import { ensureSupportedImageInput } from "../../utils/image-loading";
15
16
  import { resizeImage } from "../../utils/image-resize";
16
17
  import { generateSessionTitle, setSessionTerminalTitle } from "../../utils/title-generator";
17
18
 
@@ -218,14 +219,17 @@ export class InputController {
218
219
  if (!text) return;
219
220
 
220
221
  // Handle built-in slash commands
221
- if (
222
- await executeBuiltinSlashCommand(text, {
223
- ctx: this.ctx,
224
- handleBackgroundCommand: () => this.handleBackgroundCommand(),
225
- })
226
- ) {
222
+ const slashResult = await executeBuiltinSlashCommand(text, {
223
+ ctx: this.ctx,
224
+ handleBackgroundCommand: () => this.handleBackgroundCommand(),
225
+ });
226
+ if (slashResult === true) {
227
227
  return;
228
228
  }
229
+ if (typeof slashResult === "string") {
230
+ // Command handled but returned remaining text to use as prompt
231
+ text = slashResult;
232
+ }
229
233
 
230
234
  // Handle skill commands (/skill:name [args])
231
235
  if (text.startsWith("/skill:")) {
@@ -7,9 +7,27 @@ import { postmortem } from "@oh-my-pi/pi-utils";
7
7
  export { runAcpMode } from "./acp";
8
8
  export { InteractiveMode, type InteractiveModeOptions } from "./interactive-mode";
9
9
  export { type PrintModeOptions, runPrintMode } from "./print-mode";
10
- export { type ModelInfo, RpcClient, type RpcClientOptions, type RpcEventListener } from "./rpc/rpc-client";
10
+ export {
11
+ defineRpcClientTool,
12
+ type ModelInfo,
13
+ RpcClient,
14
+ type RpcClientCustomTool,
15
+ type RpcClientOptions,
16
+ type RpcClientToolContext,
17
+ type RpcClientToolResult,
18
+ type RpcEventListener,
19
+ } from "./rpc/rpc-client";
11
20
  export { runRpcMode } from "./rpc/rpc-mode";
12
- export type { RpcCommand, RpcResponse, RpcSessionState } from "./rpc/rpc-types";
21
+ export type {
22
+ RpcCommand,
23
+ RpcHostToolCallRequest,
24
+ RpcHostToolCancelRequest,
25
+ RpcHostToolDefinition,
26
+ RpcHostToolResult,
27
+ RpcHostToolUpdate,
28
+ RpcResponse,
29
+ RpcSessionState,
30
+ } from "./rpc/rpc-types";
13
31
 
14
32
  postmortem.register("terminal-restore", () => {
15
33
  emergencyTerminalRestore();
@@ -15,10 +15,9 @@ import {
15
15
  } from "@oh-my-pi/pi-ai";
16
16
  import type { Component, SlashCommand } from "@oh-my-pi/pi-tui";
17
17
  import { Container, Loader, Markdown, ProcessTerminal, Spacer, Text, TUI, visibleWidth } from "@oh-my-pi/pi-tui";
18
- import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem } from "@oh-my-pi/pi-utils";
18
+ import { APP_NAME, getProjectDir, hsvToRgb, isEnoent, logger, postmortem, prompt } from "@oh-my-pi/pi-utils";
19
19
  import chalk from "chalk";
20
20
  import { KeybindingsManager } from "../config/keybindings";
21
- import { renderPromptTemplate } from "../config/prompt-templates";
22
21
  import { type Settings, settings } from "../config/settings";
23
22
  import type {
24
23
  ExtensionUIContext,
@@ -29,6 +28,7 @@ import type {
29
28
  import type { CompactOptions } from "../extensibility/extensions/types";
30
29
  import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
31
30
  import { resolveLocalUrlToPath } from "../internal-urls";
31
+ import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
32
32
  import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
33
33
  import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
34
34
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
@@ -36,7 +36,7 @@ import { HistoryStorage } from "../session/history-storage";
36
36
  import type { SessionContext, SessionManager } from "../session/session-manager";
37
37
  import { getRecentSessions } from "../session/session-manager";
38
38
  import { STTController, type SttState } from "../stt";
39
- import type { ExitPlanModeDetails } from "../tools";
39
+ import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
40
40
  import type { EventBus } from "../utils/event-bus";
41
41
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
42
42
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
@@ -50,7 +50,7 @@ import type { HookSelectorComponent } from "./components/hook-selector";
50
50
  import type { PythonExecutionComponent } from "./components/python-execution";
51
51
  import { StatusLineComponent } from "./components/status-line";
52
52
  import type { ToolExecutionHandle } from "./components/tool-execution";
53
- import { WelcomeComponent } from "./components/welcome";
53
+ import { WelcomeComponent, type LspServerInfo as WelcomeLspServerInfo } from "./components/welcome";
54
54
  import { BtwController } from "./controllers/btw-controller";
55
55
  import { CommandController } from "./controllers/command-controller";
56
56
  import { EventController } from "./controllers/event-controller";
@@ -166,9 +166,7 @@ export class InteractiveMode implements InteractiveModeContext {
166
166
  #pendingModelSwitch: { model: Model; thinkingLevel?: ThinkingLevel } | undefined;
167
167
  #planModeHasEntered = false;
168
168
  #planReviewContainer: Container | undefined;
169
- readonly lspServers:
170
- | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
171
- | undefined = undefined;
169
+ readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
172
170
  mcpManager?: import("../mcp").MCPManager;
173
171
  readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
174
172
 
@@ -187,15 +185,15 @@ export class InteractiveMode implements InteractiveModeContext {
187
185
  #resizeHandler?: () => void;
188
186
  #observerRegistry: SessionObserverRegistry;
189
187
  #eventBus?: EventBus;
188
+ #eventBusUnsubscribers: Array<() => void> = [];
189
+ #welcomeComponent?: WelcomeComponent;
190
190
 
191
191
  constructor(
192
192
  session: AgentSession,
193
193
  version: string,
194
194
  changelogMarkdown: string | undefined = undefined,
195
195
  setToolUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void = () => {},
196
- lspServers:
197
- | Array<{ name: string; status: "ready" | "error"; fileTypes: string[]; error?: string }>
198
- | undefined = undefined,
196
+ lspServers: LspStartupServerInfo[] | undefined = undefined,
199
197
  mcpManager?: import("../mcp").MCPManager,
200
198
  eventBus?: EventBus,
201
199
  ) {
@@ -210,6 +208,13 @@ export class InteractiveMode implements InteractiveModeContext {
210
208
  this.lspServers = lspServers;
211
209
  this.mcpManager = mcpManager;
212
210
  this.#eventBus = eventBus;
211
+ if (eventBus) {
212
+ this.#eventBusUnsubscribers.push(
213
+ eventBus.on(LSP_STARTUP_EVENT_CHANNEL, data => {
214
+ this.#handleLspStartupEvent(data as LspStartupEvent);
215
+ }),
216
+ );
217
+ }
213
218
 
214
219
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
215
220
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
@@ -290,13 +295,16 @@ export class InteractiveMode implements InteractiveModeContext {
290
295
  async init(): Promise<void> {
291
296
  if (this.isInitialized) return;
292
297
 
293
- this.keybindings = logger.time("InteractiveMode.init:keybindings", () => KeybindingsManager.create());
298
+ logger.time("InteractiveMode.init:keybindings");
299
+ this.keybindings = KeybindingsManager.create();
294
300
 
295
301
  // Register session manager flush for signal handlers (SIGINT, SIGTERM, SIGHUP)
296
302
  this.#cleanupUnsubscribe = postmortem.register("session-manager-flush", () => this.sessionManager.flush());
297
303
 
298
- await logger.timeAsync("InteractiveMode.init:slashCommands", () =>
299
- this.refreshSlashCommandState(getProjectDir()),
304
+ await logger.time(
305
+ "InteractiveMode.init:slashCommands",
306
+ this.refreshSlashCommandState.bind(this),
307
+ getProjectDir(),
300
308
  );
301
309
 
302
310
  // Get current model info for welcome screen
@@ -304,7 +312,7 @@ export class InteractiveMode implements InteractiveModeContext {
304
312
  const providerName = this.session.model?.provider ?? "Unknown";
305
313
 
306
314
  // Get recent sessions
307
- const recentSessions = await logger.timeAsync("InteractiveMode.init:recentSessions", () =>
315
+ const recentSessions = await logger.time("InteractiveMode.init:recentSessions", () =>
308
316
  getRecentSessions(this.sessionManager.getSessionDir()).then(sessions =>
309
317
  sessions.map(s => ({
310
318
  name: s.name,
@@ -313,15 +321,8 @@ export class InteractiveMode implements InteractiveModeContext {
313
321
  ),
314
322
  );
315
323
 
316
- // Convert LSP servers to welcome format
317
- const lspServerInfo =
318
- this.lspServers?.map(s => ({
319
- name: s.name,
320
- status: s.status as "ready" | "error" | "connecting",
321
- fileTypes: s.fileTypes,
322
- })) ?? [];
323
-
324
324
  const startupQuiet = settings.get("startup.quiet");
325
+ this.#welcomeComponent = undefined;
325
326
 
326
327
  for (const warning of this.session.configWarnings) {
327
328
  this.ui.addChild(new Text(theme.fg("warning", `Warning: ${warning}`), 1, 0));
@@ -330,11 +331,17 @@ export class InteractiveMode implements InteractiveModeContext {
330
331
 
331
332
  if (!startupQuiet) {
332
333
  // Add welcome header
333
- const welcome = new WelcomeComponent(this.#version, modelName, providerName, recentSessions, lspServerInfo);
334
+ this.#welcomeComponent = new WelcomeComponent(
335
+ this.#version,
336
+ modelName,
337
+ providerName,
338
+ recentSessions,
339
+ this.#getWelcomeLspServers(),
340
+ );
334
341
 
335
342
  // Setup UI layout
336
343
  this.ui.addChild(new Spacer(1));
337
- this.ui.addChild(welcome);
344
+ this.ui.addChild(this.#welcomeComponent);
338
345
  this.ui.addChild(new Spacer(1));
339
346
 
340
347
  // Add changelog if provided
@@ -779,17 +786,21 @@ export class InteractiveMode implements InteractiveModeContext {
779
786
  }
780
787
 
781
788
  #renderPlanPreview(planContent: string): void {
782
- if (!this.#planReviewContainer) {
783
- this.#planReviewContainer = new Container();
784
- this.chatContainer.addChild(this.#planReviewContainer);
785
- }
786
- this.#planReviewContainer.clear();
787
- this.#planReviewContainer.addChild(new Spacer(1));
788
- this.#planReviewContainer.addChild(new DynamicBorder());
789
- this.#planReviewContainer.addChild(new Text(theme.bold(theme.fg("accent", "Plan Review")), 1, 1));
790
- this.#planReviewContainer.addChild(new Spacer(1));
791
- this.#planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
792
- this.#planReviewContainer.addChild(new DynamicBorder());
789
+ const planReviewContainer = this.#planReviewContainer ?? new Container();
790
+ if (this.#planReviewContainer) {
791
+ // Re-append the preview so repeated plan-review refreshes stay adjacent to the
792
+ // active selector instead of updating an older off-screen preview in place.
793
+ this.chatContainer.removeChild(this.#planReviewContainer);
794
+ }
795
+ planReviewContainer.clear();
796
+ planReviewContainer.addChild(new Spacer(1));
797
+ planReviewContainer.addChild(new DynamicBorder());
798
+ planReviewContainer.addChild(new Text(theme.bold(theme.fg("accent", "Plan Review")), 1, 1));
799
+ planReviewContainer.addChild(new Spacer(1));
800
+ planReviewContainer.addChild(new Markdown(planContent, 1, 1, getMarkdownTheme()));
801
+ planReviewContainer.addChild(new DynamicBorder());
802
+ this.chatContainer.addChild(planReviewContainer);
803
+ this.#planReviewContainer = planReviewContainer;
793
804
  this.ui.requestRender();
794
805
  }
795
806
 
@@ -895,11 +906,11 @@ export class InteractiveMode implements InteractiveModeContext {
895
906
  }
896
907
  this.session.setPlanReferencePath(options.finalPlanFilePath);
897
908
  this.session.markPlanReferenceSent();
898
- const prompt = renderPromptTemplate(planModeApprovedPrompt, {
909
+ const planModePrompt = prompt.render(planModeApprovedPrompt, {
899
910
  planContent,
900
911
  finalPlanFilePath: options.finalPlanFilePath,
901
912
  });
902
- await this.session.prompt(prompt, { synthetic: true });
913
+ await this.session.prompt(planModePrompt, { synthetic: true });
903
914
  }
904
915
 
905
916
  async handlePlanModeCommand(initialPrompt?: string): Promise<void> {
@@ -988,6 +999,10 @@ export class InteractiveMode implements InteractiveModeContext {
988
999
  }
989
1000
  this.#extensionUiController.clearExtensionTerminalInputListeners();
990
1001
  this.#extensionUiController.clearHookWidgets();
1002
+ for (const unsubscribe of this.#eventBusUnsubscribers) {
1003
+ unsubscribe();
1004
+ }
1005
+ this.#eventBusUnsubscribers = [];
991
1006
  this.#observerRegistry.dispose();
992
1007
  this.#eventController.dispose();
993
1008
  this.statusLine.dispose();
@@ -1086,6 +1101,48 @@ export class InteractiveMode implements InteractiveModeContext {
1086
1101
  this.#uiHelpers.showWarning(message);
1087
1102
  }
1088
1103
 
1104
+ #handleLspStartupEvent(event: LspStartupEvent): void {
1105
+ this.#updateWelcomeLspServers();
1106
+
1107
+ if (event.type === "failed") {
1108
+ this.showWarning(`LSP startup failed: ${event.error}. It will retry lazily on write.`);
1109
+ return;
1110
+ }
1111
+
1112
+ const failedServers = event.servers.filter(server => server.status === "error");
1113
+
1114
+ if (failedServers.length === 1) {
1115
+ const failedServer = failedServers[0];
1116
+ const detail = failedServer.error ? `: ${failedServer.error}` : "";
1117
+ this.showWarning(`LSP startup failed for ${failedServer.name}${detail}. It will retry lazily on write.`);
1118
+ return;
1119
+ }
1120
+
1121
+ if (failedServers.length > 1) {
1122
+ const failedNames = failedServers.map(server => server.name).join(", ");
1123
+ this.showWarning(`LSP startup failed for ${failedNames}. It will retry lazily on write.`);
1124
+ }
1125
+ }
1126
+
1127
+ #getWelcomeLspServers(): WelcomeLspServerInfo[] {
1128
+ return (
1129
+ this.lspServers?.map(server => ({
1130
+ name: server.name,
1131
+ status: server.status,
1132
+ fileTypes: server.fileTypes,
1133
+ })) ?? []
1134
+ );
1135
+ }
1136
+
1137
+ #updateWelcomeLspServers(): void {
1138
+ if (!this.#welcomeComponent) {
1139
+ return;
1140
+ }
1141
+
1142
+ this.#welcomeComponent.setLspServers(this.#getWelcomeLspServers());
1143
+ this.ui.requestRender();
1144
+ }
1145
+
1089
1146
  ensureLoadingAnimation(): void {
1090
1147
  if (!this.loadingAnimation) {
1091
1148
  this.statusContainer.clear();