@oh-my-pi/pi-coding-agent 14.4.1 → 14.4.4

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 (71) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +7 -7
  3. package/src/cli.ts +0 -1
  4. package/src/config/prompt-templates.ts +0 -30
  5. package/src/config/settings-schema.ts +68 -36
  6. package/src/config/settings.ts +1 -1
  7. package/src/edit/index.ts +1 -53
  8. package/src/edit/line-hash.ts +0 -53
  9. package/src/edit/modes/atom.ts +82 -47
  10. package/src/edit/modes/hashline.ts +6 -8
  11. package/src/edit/renderer.ts +6 -8
  12. package/src/edit/streaming.ts +90 -114
  13. package/src/export/html/template.generated.ts +1 -1
  14. package/src/export/html/template.js +10 -15
  15. package/src/internal-urls/docs-index.generated.ts +1 -2
  16. package/src/modes/components/session-observer-overlay.ts +635 -295
  17. package/src/modes/components/settings-defs.ts +1 -5
  18. package/src/modes/components/tool-execution.ts +2 -5
  19. package/src/modes/controllers/btw-controller.ts +17 -105
  20. package/src/modes/controllers/command-controller.ts +16 -5
  21. package/src/modes/controllers/selector-controller.ts +32 -19
  22. package/src/modes/controllers/todo-command-controller.ts +537 -0
  23. package/src/modes/interactive-mode.ts +45 -10
  24. package/src/modes/types.ts +3 -0
  25. package/src/modes/utils/ui-helpers.ts +17 -0
  26. package/src/prompts/system/irc-incoming.md +8 -0
  27. package/src/prompts/system/subagent-system-prompt.md +8 -0
  28. package/src/prompts/tools/ast-grep.md +1 -1
  29. package/src/prompts/tools/atom.md +37 -26
  30. package/src/prompts/tools/bash.md +2 -2
  31. package/src/prompts/tools/grep.md +2 -5
  32. package/src/prompts/tools/irc.md +49 -0
  33. package/src/prompts/tools/job.md +11 -0
  34. package/src/prompts/tools/read.md +12 -13
  35. package/src/prompts/tools/task.md +1 -1
  36. package/src/prompts/tools/todo-write.md +14 -5
  37. package/src/registry/agent-registry.ts +139 -0
  38. package/src/sdk.ts +35 -0
  39. package/src/session/agent-session.ts +226 -6
  40. package/src/session/session-manager.ts +13 -0
  41. package/src/session/session-storage.ts +4 -0
  42. package/src/session/streaming-output.ts +1 -1
  43. package/src/slash-commands/builtin-registry.ts +32 -0
  44. package/src/task/executor.ts +14 -0
  45. package/src/tools/bash.ts +1 -1
  46. package/src/tools/fetch.ts +18 -6
  47. package/src/tools/fs-cache-invalidation.ts +0 -5
  48. package/src/tools/grep.ts +4 -124
  49. package/src/tools/index.ts +12 -6
  50. package/src/tools/irc.ts +258 -0
  51. package/src/tools/job.ts +489 -0
  52. package/src/tools/match-line-format.ts +7 -6
  53. package/src/tools/output-meta.ts +1 -1
  54. package/src/tools/read.ts +36 -126
  55. package/src/tools/renderers.ts +2 -0
  56. package/src/tools/todo-write.ts +243 -12
  57. package/src/utils/edit-mode.ts +1 -2
  58. package/src/utils/file-display-mode.ts +0 -3
  59. package/src/web/search/index.ts +2 -2
  60. package/src/web/search/provider.ts +3 -0
  61. package/src/web/search/providers/searxng.ts +238 -0
  62. package/src/web/search/types.ts +3 -1
  63. package/src/cli/read-cli.ts +0 -67
  64. package/src/commands/read.ts +0 -33
  65. package/src/edit/modes/chunk.ts +0 -832
  66. package/src/prompts/tools/cancel-job.md +0 -5
  67. package/src/prompts/tools/chunk-edit.md +0 -158
  68. package/src/prompts/tools/poll.md +0 -5
  69. package/src/prompts/tools/read-chunk.md +0 -73
  70. package/src/tools/cancel-job.ts +0 -95
  71. package/src/tools/poll-tool.ts +0 -173
@@ -298,11 +298,6 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
298
298
  { value: "1000", label: "1000 lines" },
299
299
  { value: "5000", label: "5000 lines" },
300
300
  ],
301
- "read.anchorstyle": [
302
- { value: "full", label: "Full", description: "Show the kind prefix and identifier" },
303
- { value: "kind", label: "Kind", description: "Show only the kind prefix plus checksum" },
304
- { value: "bare", label: "Bare", description: "Show only the checksum" },
305
- ],
306
301
  // Todo auto-clear delay
307
302
  "tasks.todoClearDelay": [
308
303
  { value: "0", label: "Instant" },
@@ -353,6 +348,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
353
348
  { value: "kagi", label: "Kagi", description: "Requires KAGI_API_KEY and Kagi Search API beta access" },
354
349
  { value: "synthetic", label: "Synthetic", description: "Requires SYNTHETIC_API_KEY" },
355
350
  { value: "parallel", label: "Parallel", description: "Requires PARALLEL_API_KEY" },
351
+ { value: "searxng", label: "SearXNG", description: "Self-hosted metasearch; set searxng.endpoint" },
356
352
  ],
357
353
  "providers.image": [
358
354
  {
@@ -109,7 +109,7 @@ export class ToolExecutionComponent extends Container {
109
109
  isError?: boolean;
110
110
  details?: any;
111
111
  };
112
- // Edit preview state (single-file for legacy modes, multi-file for chunk)
112
+ // Edit preview state
113
113
  #editMode?: EditMode;
114
114
  #editDiffPreview?: PerFileDiffPreview[];
115
115
  #editDiffScheduleTimer?: NodeJS.Timeout;
@@ -639,10 +639,7 @@ export class ToolExecutionComponent extends Container {
639
639
  return this.#args;
640
640
  }
641
641
  // Single-file previews feed the existing `previewDiff` channel consumed
642
- // by `formatStreamingDiff` in the renderer. Multi-file previews are
643
- // piped via `renderContext.perFileDiffPreview`, so the args we hand to
644
- // `renderCall` only need the first file's diff to preserve prior
645
- // single-file behavior.
642
+ // by `formatStreamingDiff` in the renderer.
646
643
  const first = previews[0];
647
644
  if (!first?.diff) {
648
645
  return this.#args;
@@ -1,8 +1,5 @@
1
- import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
- import { type AssistantMessage, type Context, streamSimple } from "@oh-my-pi/pi-ai";
3
1
  import { prompt } from "@oh-my-pi/pi-utils";
4
2
  import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
5
- import { toReasoningEffort } from "../../thinking";
6
3
  import { BtwPanelComponent } from "../components/btw-panel";
7
4
  import type { InteractiveModeContext } from "../types";
8
5
 
@@ -14,14 +11,8 @@ interface BtwRequest {
14
11
 
15
12
  export class BtwController {
16
13
  #activeRequest: BtwRequest | undefined;
17
- readonly #streamFn: typeof streamSimple;
18
14
 
19
- constructor(
20
- private readonly ctx: InteractiveModeContext,
21
- options?: { streamFn?: typeof streamSimple },
22
- ) {
23
- this.#streamFn = options?.streamFn ?? streamSimple;
24
- }
15
+ constructor(private readonly ctx: InteractiveModeContext) {}
25
16
 
26
17
  hasActiveRequest(): boolean {
27
18
  return this.#activeRequest !== undefined;
@@ -61,64 +52,29 @@ export class BtwController {
61
52
  this.ctx.btwContainer.addChild(request.component);
62
53
  this.ctx.ui.requestRender();
63
54
  this.#activeRequest = request;
64
- void this.#runRequest(request, model);
55
+ void this.#runRequest(request);
65
56
  }
66
57
 
67
- async #runRequest(
68
- request: BtwRequest,
69
- model: NonNullable<InteractiveModeContext["session"]["model"]>,
70
- ): Promise<void> {
58
+ async #runRequest(request: BtwRequest): Promise<void> {
71
59
  try {
72
- const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
73
- if (!apiKey) {
74
- throw new Error(`No API key for provider: ${model.provider}`);
75
- }
76
-
77
- const llmMessages = await this.ctx.session.convertMessagesToLlm(
78
- [...this.#buildMessageSnapshot(), this.#buildQuestionMessage(request.question)],
79
- request.abortController.signal,
80
- );
81
- const context: Context = {
82
- systemPrompt: this.ctx.session.systemPrompt,
83
- messages: llmMessages,
84
- };
85
- const options = this.ctx.session.prepareSimpleStreamOptions({
86
- apiKey,
87
- sessionId: this.ctx.session.sessionId,
88
- reasoning: toReasoningEffort(this.ctx.session.thinkingLevel),
89
- serviceTier: this.ctx.session.serviceTier,
60
+ const promptText = prompt.render(btwUserPrompt, { question: request.question });
61
+ const { replyText } = await this.ctx.session.runEphemeralTurn({
62
+ promptText,
63
+ onTextDelta: delta => {
64
+ if (this.#isActiveRequest(request)) {
65
+ request.component.appendText(delta);
66
+ }
67
+ },
90
68
  signal: request.abortController.signal,
91
- toolChoice: "none",
92
69
  });
93
- const stream = this.#streamFn(model, context, options);
94
70
 
95
- for await (const event of stream) {
96
- if (!this.#isActiveRequest(request)) {
97
- return;
98
- }
99
- if (event.type === "text_delta") {
100
- request.component.appendText(event.delta);
101
- continue;
102
- }
103
- if (event.type === "done") {
104
- const finalText = this.#assistantText(event.message);
105
- if (finalText) {
106
- request.component.setAnswer(finalText);
107
- }
108
- request.component.markComplete();
109
- return;
110
- }
111
- if (event.type === "error") {
112
- if (event.reason === "aborted" || request.abortController.signal.aborted) {
113
- request.component.markAborted();
114
- } else {
115
- request.component.markError(
116
- this.#assistantText(event.error) || event.error.errorMessage || "BTW request failed.",
117
- );
118
- }
119
- return;
120
- }
71
+ if (!this.#isActiveRequest(request)) {
72
+ return;
121
73
  }
74
+ if (replyText) {
75
+ request.component.setAnswer(replyText);
76
+ }
77
+ request.component.markComplete();
122
78
  } catch (error) {
123
79
  if (!this.#isActiveRequest(request)) {
124
80
  return;
@@ -131,50 +87,6 @@ export class BtwController {
131
87
  }
132
88
  }
133
89
 
134
- #buildQuestionMessage(question: string): AgentMessage {
135
- return {
136
- role: "user",
137
- content: [
138
- {
139
- type: "text",
140
- text: prompt.render(btwUserPrompt, { question }),
141
- },
142
- ],
143
- attribution: "user",
144
- timestamp: Date.now(),
145
- };
146
- }
147
-
148
- #buildMessageSnapshot(): AgentMessage[] {
149
- const messages = this.ctx.session.messages.slice();
150
- if (!this.ctx.session.isStreaming || !this.ctx.streamingMessage) {
151
- return messages;
152
- }
153
- const streamingText = this.ctx.extractAssistantText(this.ctx.streamingMessage);
154
- const lastMessage = messages.at(-1);
155
- if (!streamingText) {
156
- return lastMessage?.role === "assistant" ? messages.slice(0, -1) : messages;
157
- }
158
- const normalizedStreamingMessage: AssistantMessage = {
159
- ...this.ctx.streamingMessage,
160
- content: [{ type: "text", text: streamingText }],
161
- };
162
- if (lastMessage?.role === "assistant") {
163
- return [...messages.slice(0, -1), normalizedStreamingMessage];
164
- }
165
- return [...messages, normalizedStreamingMessage];
166
- }
167
-
168
- #assistantText(message: AssistantMessage): string {
169
- let text = "";
170
- for (const content of message.content) {
171
- if (content.type === "text") {
172
- text += content.text;
173
- }
174
- }
175
- return text.trim();
176
- }
177
-
178
90
  #closeActiveRequest(options: { abort: boolean }): void {
179
91
  const request = this.#activeRequest;
180
92
  if (!request) return;
@@ -28,6 +28,7 @@ import { buildHotkeysMarkdown } from "../../modes/utils/hotkeys-markdown";
28
28
  import { buildToolsMarkdown } from "../../modes/utils/tools-markdown";
29
29
  import type { AsyncJobSnapshotItem } from "../../session/agent-session";
30
30
  import type { AuthStorage } from "../../session/auth-storage";
31
+ import type { NewSessionOptions } from "../../session/session-manager";
31
32
  import { outputMeta } from "../../tools/output-meta";
32
33
  import { resolveToCwd, stripOuterDoubleQuotes } from "../../tools/path-utils";
33
34
  import { replaceTabs } from "../../tools/render-utils";
@@ -573,7 +574,7 @@ export class CommandController {
573
574
  this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
574
575
  }
575
576
 
576
- async handleClearCommand(): Promise<void> {
577
+ async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
577
578
  if (this.ctx.loadingAnimation) {
578
579
  this.ctx.loadingAnimation.stop();
579
580
  this.ctx.loadingAnimation = undefined;
@@ -586,7 +587,7 @@ export class CommandController {
586
587
  await Bun.sleep(10);
587
588
  }
588
589
  }
589
- await this.ctx.session.newSession();
590
+ if (!(await this.ctx.session.newSession(options))) return;
590
591
  this.ctx.resetObserverRegistry();
591
592
  setSessionTerminalTitle(
592
593
  this.ctx.sessionManager.getSessionName(),
@@ -608,13 +609,23 @@ export class CommandController {
608
609
  this.ctx.pendingTools.clear();
609
610
 
610
611
  this.ctx.chatContainer.addChild(new Spacer(1));
611
- this.ctx.chatContainer.addChild(
612
- new Text(`${theme.fg("accent", `${theme.status.success} New session started`)}`, 1, 1),
613
- );
612
+ this.ctx.chatContainer.addChild(new Text(`${theme.fg("accent", `${theme.status.success} ${label}`)}`, 1, 1));
614
613
  await this.ctx.reloadTodos();
615
614
  this.ctx.ui.requestRender();
616
615
  }
617
616
 
617
+ async handleClearCommand(): Promise<void> {
618
+ await this.#runNewSessionFlow();
619
+ }
620
+
621
+ async handleDropCommand(): Promise<void> {
622
+ if (!this.ctx.sessionManager.getSessionFile()) {
623
+ this.ctx.showError("Nothing to drop (in-memory session)");
624
+ return;
625
+ }
626
+ await this.#runNewSessionFlow({ drop: true }, "Session dropped");
627
+ }
628
+
618
629
  async handleForkCommand(): Promise<void> {
619
630
  if (this.ctx.session.isStreaming) {
620
631
  this.ctx.showWarning("Wait for the current response to finish or abort it before forking.");
@@ -1,14 +1,17 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
1
3
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
4
  import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
3
- import type { Component } from "@oh-my-pi/pi-tui";
5
+ import type { Component, OverlayHandle } from "@oh-my-pi/pi-tui";
4
6
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
- import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
7
+ import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
8
+ import { invalidate as invalidateFsCache } from "../../capability/fs";
6
9
  import { getRoleInfo } from "../../config/model-registry";
7
10
  import { formatModelSelectorValue } from "../../config/model-resolver";
8
11
  import { settings } from "../../config/settings";
9
12
  import { DebugSelectorComponent } from "../../debug";
10
13
  import { disableProvider, enableProvider } from "../../discovery";
11
- import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
14
+ import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
12
15
  import {
13
16
  getInstalledPluginsRegistryPath,
14
17
  getMarketplacesCacheDir,
@@ -440,7 +443,13 @@ export class SelectorController {
440
443
  projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
441
444
  marketplacesCacheDir: getMarketplacesCacheDir(),
442
445
  pluginsCacheDir: getPluginsCacheDir(),
443
- clearPluginRootsCache: clearPluginRootsAndCaches,
446
+ clearPluginRootsCache: (extraPaths?: readonly string[]) => {
447
+ const home = os.homedir();
448
+ invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
449
+ invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
450
+ for (const p of extraPaths ?? []) invalidateFsCache(p);
451
+ clearClaudePluginRootsCache();
452
+ },
444
453
  });
445
454
 
446
455
  const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
@@ -969,25 +978,29 @@ export class SelectorController {
969
978
 
970
979
  showSessionObserver(registry: SessionObserverRegistry): void {
971
980
  const observeKeys = this.ctx.keybindings.getKeys("app.session.observe");
981
+ let cleanup: (() => void) | undefined;
982
+ let overlayHandle: OverlayHandle | undefined;
972
983
 
973
- this.showSelector(done => {
974
- let cleanup: (() => void) | undefined;
984
+ const done = () => {
985
+ cleanup?.();
986
+ overlayHandle?.hide();
987
+ this.ctx.ui.requestRender();
988
+ };
975
989
 
976
- const selector = new SessionObserverOverlayComponent(
977
- registry,
978
- () => {
979
- cleanup?.();
980
- done();
981
- },
982
- observeKeys,
983
- );
990
+ const selector = new SessionObserverOverlayComponent(registry, done, observeKeys);
984
991
 
985
- cleanup = registry.onChange(() => {
986
- selector.refreshFromRegistry();
987
- this.ctx.ui.requestRender();
988
- });
992
+ cleanup = registry.onChange(() => {
993
+ selector.refreshFromRegistry();
994
+ this.ctx.ui.requestRender();
995
+ });
989
996
 
990
- return { component: selector, focus: selector };
997
+ overlayHandle = this.ctx.ui.showOverlay(selector, {
998
+ anchor: "bottom-center",
999
+ width: "100%",
1000
+ maxHeight: "100%",
1001
+ margin: 0,
991
1002
  });
1003
+ this.ctx.ui.setFocus(selector);
1004
+ this.ctx.ui.requestRender();
992
1005
  }
993
1006
  }