@oh-my-pi/pi-coding-agent 4.1.0 → 4.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 (81) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-storage.ts +450 -0
  7. package/src/core/auth-storage.ts +102 -183
  8. package/src/core/compaction/branch-summarization.ts +5 -4
  9. package/src/core/compaction/compaction.ts +7 -6
  10. package/src/core/compaction/utils.ts +6 -11
  11. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  12. package/src/core/custom-share.ts +66 -0
  13. package/src/core/history-storage.ts +15 -7
  14. package/src/core/prompt-templates.ts +271 -1
  15. package/src/core/sdk.ts +14 -3
  16. package/src/core/settings-manager.ts +100 -34
  17. package/src/core/slash-commands.ts +4 -1
  18. package/src/core/storage-migration.ts +215 -0
  19. package/src/core/system-prompt.ts +87 -289
  20. package/src/core/title-generator.ts +3 -2
  21. package/src/core/tools/ask.ts +2 -2
  22. package/src/core/tools/bash.ts +2 -1
  23. package/src/core/tools/calculator.ts +2 -1
  24. package/src/core/tools/edit.ts +2 -1
  25. package/src/core/tools/find.ts +2 -1
  26. package/src/core/tools/gemini-image.ts +2 -1
  27. package/src/core/tools/git.ts +2 -2
  28. package/src/core/tools/grep.ts +2 -1
  29. package/src/core/tools/index.test.ts +0 -28
  30. package/src/core/tools/index.ts +0 -6
  31. package/src/core/tools/lsp/index.ts +2 -1
  32. package/src/core/tools/output.ts +2 -1
  33. package/src/core/tools/read.ts +4 -1
  34. package/src/core/tools/ssh.ts +4 -2
  35. package/src/core/tools/task/agents.ts +56 -30
  36. package/src/core/tools/task/commands.ts +9 -8
  37. package/src/core/tools/task/index.ts +7 -15
  38. package/src/core/tools/web-fetch.ts +2 -1
  39. package/src/core/tools/web-search/auth.ts +106 -16
  40. package/src/core/tools/web-search/index.ts +3 -2
  41. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  42. package/src/core/tools/write.ts +2 -1
  43. package/src/core/voice.ts +3 -1
  44. package/src/main.ts +1 -1
  45. package/src/migrations.ts +20 -20
  46. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  47. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  48. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  49. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  50. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  51. package/src/modes/interactive/interactive-mode.ts +364 -3143
  52. package/src/modes/interactive/theme/theme.ts +5 -5
  53. package/src/modes/interactive/types.ts +189 -0
  54. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  55. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  56. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  57. package/src/prompts/agents/frontmatter.md +7 -0
  58. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  59. package/src/prompts/{task.md → agents/task.md} +1 -1
  60. package/src/prompts/review-request.md +44 -8
  61. package/src/prompts/system/custom-system-prompt.md +80 -0
  62. package/src/prompts/system/file-operations.md +12 -0
  63. package/src/prompts/system/system-prompt.md +232 -0
  64. package/src/prompts/system/title-system.md +2 -0
  65. package/src/prompts/tools/bash.md +1 -1
  66. package/src/prompts/tools/read.md +1 -1
  67. package/src/prompts/tools/task.md +9 -3
  68. package/src/core/tools/rulebook.ts +0 -132
  69. package/src/prompts/system-prompt.md +0 -43
  70. package/src/prompts/title-system.md +0 -8
  71. /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
  72. /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
  73. /package/src/prompts/{implement.md → agents/implement.md} +0 -0
  74. /package/src/prompts/{init.md → agents/init.md} +0 -0
  75. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  76. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  77. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  78. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  79. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  80. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  81. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -0,0 +1,340 @@
1
+ import { Loader, Text } from "@oh-my-pi/pi-tui";
2
+ import type { AgentSessionEvent } from "../../../core/agent-session";
3
+ import { detectNotificationProtocol, isNotificationSuppressed, sendNotification } from "../../../core/terminal-notify";
4
+ import { AssistantMessageComponent } from "../components/assistant-message";
5
+ import { ToolExecutionComponent } from "../components/tool-execution";
6
+ import { TtsrNotificationComponent } from "../components/ttsr-notification";
7
+ import { getSymbolTheme, theme } from "../theme/theme";
8
+ import type { InteractiveModeContext } from "../types";
9
+
10
+ export class EventController {
11
+ constructor(private ctx: InteractiveModeContext) {}
12
+
13
+ subscribeToAgent(): void {
14
+ this.ctx.unsubscribe = this.ctx.session.subscribe(async (event: AgentSessionEvent) => {
15
+ await this.handleEvent(event);
16
+ });
17
+ }
18
+
19
+ async handleEvent(event: AgentSessionEvent): Promise<void> {
20
+ if (!this.ctx.isInitialized) {
21
+ await this.ctx.init();
22
+ }
23
+
24
+ this.ctx.statusLine.invalidate();
25
+ this.ctx.updateEditorTopBorder();
26
+
27
+ switch (event.type) {
28
+ case "agent_start":
29
+ if (this.ctx.retryEscapeHandler) {
30
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
31
+ this.ctx.retryEscapeHandler = undefined;
32
+ }
33
+ if (this.ctx.retryLoader) {
34
+ this.ctx.retryLoader.stop();
35
+ this.ctx.retryLoader = undefined;
36
+ this.ctx.statusContainer.clear();
37
+ }
38
+ if (this.ctx.loadingAnimation) {
39
+ this.ctx.loadingAnimation.stop();
40
+ }
41
+ this.ctx.statusContainer.clear();
42
+ this.ctx.loadingAnimation = new Loader(
43
+ this.ctx.ui,
44
+ (spinner) => theme.fg("accent", spinner),
45
+ (text) => theme.fg("muted", text),
46
+ `Working${theme.format.ellipsis} (esc to interrupt)`,
47
+ getSymbolTheme().spinnerFrames,
48
+ );
49
+ this.ctx.statusContainer.addChild(this.ctx.loadingAnimation);
50
+ this.ctx.startVoiceProgressTimer();
51
+ this.ctx.ui.requestRender();
52
+ break;
53
+
54
+ case "message_start":
55
+ if (event.message.role === "hookMessage" || event.message.role === "custom") {
56
+ this.ctx.addMessageToChat(event.message);
57
+ this.ctx.ui.requestRender();
58
+ } else if (event.message.role === "user") {
59
+ this.ctx.addMessageToChat(event.message);
60
+ this.ctx.editor.setText("");
61
+ this.ctx.updatePendingMessagesDisplay();
62
+ this.ctx.ui.requestRender();
63
+ } else if (event.message.role === "fileMention") {
64
+ this.ctx.addMessageToChat(event.message);
65
+ this.ctx.ui.requestRender();
66
+ } else if (event.message.role === "assistant") {
67
+ this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock);
68
+ this.ctx.streamingMessage = event.message;
69
+ this.ctx.chatContainer.addChild(this.ctx.streamingComponent);
70
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
71
+ this.ctx.ui.requestRender();
72
+ }
73
+ break;
74
+
75
+ case "message_update":
76
+ if (this.ctx.streamingComponent && event.message.role === "assistant") {
77
+ this.ctx.streamingMessage = event.message;
78
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
79
+
80
+ for (const content of this.ctx.streamingMessage.content) {
81
+ if (content.type === "toolCall") {
82
+ if (!this.ctx.pendingTools.has(content.id)) {
83
+ this.ctx.chatContainer.addChild(new Text("", 0, 0));
84
+ const tool = this.ctx.session.getToolByName(content.name);
85
+ const component = new ToolExecutionComponent(
86
+ content.name,
87
+ content.arguments,
88
+ {
89
+ showImages: this.ctx.settingsManager.getShowImages(),
90
+ },
91
+ tool,
92
+ this.ctx.ui,
93
+ this.ctx.sessionManager.getCwd(),
94
+ );
95
+ component.setExpanded(this.ctx.toolOutputExpanded);
96
+ this.ctx.chatContainer.addChild(component);
97
+ this.ctx.pendingTools.set(content.id, component);
98
+ } else {
99
+ const component = this.ctx.pendingTools.get(content.id);
100
+ if (component) {
101
+ component.updateArgs(content.arguments);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ this.ctx.ui.requestRender();
107
+ }
108
+ break;
109
+
110
+ case "message_end":
111
+ if (event.message.role === "user") break;
112
+ if (this.ctx.streamingComponent && event.message.role === "assistant") {
113
+ this.ctx.streamingMessage = event.message;
114
+ if (this.ctx.session.isTtsrAbortPending && this.ctx.streamingMessage.stopReason === "aborted") {
115
+ const msgWithoutAbort = { ...this.ctx.streamingMessage, stopReason: "stop" as const };
116
+ this.ctx.streamingComponent.updateContent(msgWithoutAbort);
117
+ } else {
118
+ this.ctx.streamingComponent.updateContent(this.ctx.streamingMessage);
119
+ }
120
+
121
+ if (
122
+ this.ctx.streamingMessage.stopReason === "aborted" ||
123
+ this.ctx.streamingMessage.stopReason === "error"
124
+ ) {
125
+ if (!this.ctx.session.isTtsrAbortPending) {
126
+ let errorMessage: string;
127
+ if (this.ctx.streamingMessage.stopReason === "aborted") {
128
+ const retryAttempt = this.ctx.session.retryAttempt;
129
+ errorMessage =
130
+ retryAttempt > 0
131
+ ? `Aborted after ${retryAttempt} retry attempt${retryAttempt > 1 ? "s" : ""}`
132
+ : "Operation aborted";
133
+ } else {
134
+ errorMessage = this.ctx.streamingMessage.errorMessage || "Error";
135
+ }
136
+ for (const [, component] of this.ctx.pendingTools.entries()) {
137
+ component.updateResult({
138
+ content: [{ type: "text", text: errorMessage }],
139
+ isError: true,
140
+ });
141
+ }
142
+ }
143
+ this.ctx.pendingTools.clear();
144
+ } else {
145
+ for (const [, component] of this.ctx.pendingTools.entries()) {
146
+ component.setArgsComplete();
147
+ }
148
+ }
149
+ this.ctx.streamingComponent = undefined;
150
+ this.ctx.streamingMessage = undefined;
151
+ this.ctx.statusLine.invalidate();
152
+ this.ctx.updateEditorTopBorder();
153
+ }
154
+ this.ctx.ui.requestRender();
155
+ break;
156
+
157
+ case "tool_execution_start": {
158
+ if (!this.ctx.pendingTools.has(event.toolCallId)) {
159
+ const tool = this.ctx.session.getToolByName(event.toolName);
160
+ const component = new ToolExecutionComponent(
161
+ event.toolName,
162
+ event.args,
163
+ {
164
+ showImages: this.ctx.settingsManager.getShowImages(),
165
+ },
166
+ tool,
167
+ this.ctx.ui,
168
+ this.ctx.sessionManager.getCwd(),
169
+ );
170
+ component.setExpanded(this.ctx.toolOutputExpanded);
171
+ this.ctx.chatContainer.addChild(component);
172
+ this.ctx.pendingTools.set(event.toolCallId, component);
173
+ this.ctx.ui.requestRender();
174
+ }
175
+ break;
176
+ }
177
+
178
+ case "tool_execution_update": {
179
+ const component = this.ctx.pendingTools.get(event.toolCallId);
180
+ if (component) {
181
+ component.updateResult({ ...event.partialResult, isError: false }, true);
182
+ this.ctx.ui.requestRender();
183
+ }
184
+ break;
185
+ }
186
+
187
+ case "tool_execution_end": {
188
+ const component = this.ctx.pendingTools.get(event.toolCallId);
189
+ if (component) {
190
+ component.updateResult({ ...event.result, isError: event.isError });
191
+ this.ctx.pendingTools.delete(event.toolCallId);
192
+ this.ctx.ui.requestRender();
193
+ }
194
+ break;
195
+ }
196
+
197
+ case "agent_end":
198
+ this.ctx.stopVoiceProgressTimer();
199
+ if (this.ctx.loadingAnimation) {
200
+ this.ctx.loadingAnimation.stop();
201
+ this.ctx.loadingAnimation = undefined;
202
+ this.ctx.statusContainer.clear();
203
+ }
204
+ if (this.ctx.streamingComponent) {
205
+ this.ctx.chatContainer.removeChild(this.ctx.streamingComponent);
206
+ this.ctx.streamingComponent = undefined;
207
+ this.ctx.streamingMessage = undefined;
208
+ }
209
+ this.ctx.pendingTools.clear();
210
+ if (this.ctx.settingsManager.getVoiceEnabled() && this.ctx.voiceAutoModeEnabled) {
211
+ const lastAssistant = this.ctx.findLastAssistantMessage();
212
+ if (lastAssistant && lastAssistant.stopReason !== "aborted" && lastAssistant.stopReason !== "error") {
213
+ const text = this.ctx.extractAssistantText(lastAssistant);
214
+ if (text) {
215
+ this.ctx.voiceSupervisor.notifyResult(text);
216
+ }
217
+ }
218
+ }
219
+ this.ctx.ui.requestRender();
220
+ this.sendCompletionNotification();
221
+ break;
222
+
223
+ case "auto_compaction_start": {
224
+ this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
225
+ this.ctx.editor.onEscape = () => {
226
+ this.ctx.session.abortCompaction();
227
+ };
228
+ this.ctx.statusContainer.clear();
229
+ const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
230
+ this.ctx.autoCompactionLoader = new Loader(
231
+ this.ctx.ui,
232
+ (spinner) => theme.fg("accent", spinner),
233
+ (text) => theme.fg("muted", text),
234
+ `${reasonText}Auto-compacting${theme.format.ellipsis} (esc to cancel)`,
235
+ getSymbolTheme().spinnerFrames,
236
+ );
237
+ this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
238
+ this.ctx.ui.requestRender();
239
+ break;
240
+ }
241
+
242
+ case "auto_compaction_end": {
243
+ if (this.ctx.autoCompactionEscapeHandler) {
244
+ this.ctx.editor.onEscape = this.ctx.autoCompactionEscapeHandler;
245
+ this.ctx.autoCompactionEscapeHandler = undefined;
246
+ }
247
+ if (this.ctx.autoCompactionLoader) {
248
+ this.ctx.autoCompactionLoader.stop();
249
+ this.ctx.autoCompactionLoader = undefined;
250
+ this.ctx.statusContainer.clear();
251
+ }
252
+ if (event.aborted) {
253
+ this.ctx.showStatus("Auto-compaction cancelled");
254
+ } else if (event.result) {
255
+ this.ctx.chatContainer.clear();
256
+ this.ctx.rebuildChatFromMessages();
257
+ this.ctx.addMessageToChat({
258
+ role: "compactionSummary",
259
+ tokensBefore: event.result.tokensBefore,
260
+ summary: event.result.summary,
261
+ timestamp: Date.now(),
262
+ });
263
+ this.ctx.statusLine.invalidate();
264
+ this.ctx.updateEditorTopBorder();
265
+ }
266
+ await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
267
+ this.ctx.ui.requestRender();
268
+ break;
269
+ }
270
+
271
+ case "auto_retry_start": {
272
+ this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
273
+ this.ctx.editor.onEscape = () => {
274
+ this.ctx.session.abortRetry();
275
+ };
276
+ this.ctx.statusContainer.clear();
277
+ const delaySeconds = Math.round(event.delayMs / 1000);
278
+ this.ctx.retryLoader = new Loader(
279
+ this.ctx.ui,
280
+ (spinner) => theme.fg("warning", spinner),
281
+ (text) => theme.fg("muted", text),
282
+ `Retrying (${event.attempt}/${event.maxAttempts}) in ${delaySeconds}s${theme.format.ellipsis} (esc to cancel)`,
283
+ getSymbolTheme().spinnerFrames,
284
+ );
285
+ this.ctx.statusContainer.addChild(this.ctx.retryLoader);
286
+ this.ctx.ui.requestRender();
287
+ break;
288
+ }
289
+
290
+ case "auto_retry_end": {
291
+ if (this.ctx.retryEscapeHandler) {
292
+ this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
293
+ this.ctx.retryEscapeHandler = undefined;
294
+ }
295
+ if (this.ctx.retryLoader) {
296
+ this.ctx.retryLoader.stop();
297
+ this.ctx.retryLoader = undefined;
298
+ this.ctx.statusContainer.clear();
299
+ }
300
+ if (!event.success) {
301
+ this.ctx.showError(
302
+ `Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`,
303
+ );
304
+ }
305
+ this.ctx.ui.requestRender();
306
+ break;
307
+ }
308
+
309
+ case "ttsr_triggered": {
310
+ const component = new TtsrNotificationComponent(event.rules);
311
+ component.setExpanded(this.ctx.toolOutputExpanded);
312
+ this.ctx.chatContainer.addChild(component);
313
+ this.ctx.ui.requestRender();
314
+ break;
315
+ }
316
+ }
317
+ }
318
+
319
+ sendCompletionNotification(): void {
320
+ if (this.ctx.isBackgrounded === false) return;
321
+ if (isNotificationSuppressed()) return;
322
+ const method = this.ctx.settingsManager.getNotificationOnComplete();
323
+ if (method === "off") return;
324
+ const protocol = method === "auto" ? detectNotificationProtocol() : method;
325
+ const title = this.ctx.sessionManager.getSessionTitle();
326
+ const message = title ? `${title}: Complete` : "Complete";
327
+ sendNotification(protocol, message);
328
+ }
329
+
330
+ async handleBackgroundEvent(event: AgentSessionEvent): Promise<void> {
331
+ if (event.type !== "agent_end") {
332
+ return;
333
+ }
334
+ if (this.ctx.session.queuedMessageCount > 0 || this.ctx.session.isStreaming) {
335
+ return;
336
+ }
337
+ this.sendCompletionNotification();
338
+ await this.ctx.shutdown();
339
+ }
340
+ }