@oh-my-pi/pi-coding-agent 3.37.1 → 4.0.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 (70) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +193 -47
  9. package/src/core/auth-storage.ts +16 -3
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +1 -1
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -5,41 +5,43 @@
5
5
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
6
6
  import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
7
7
  import type { KeyId } from "@oh-my-pi/pi-tui";
8
- import { theme } from "../../modes/interactive/theme/theme";
8
+ import { type Theme, theme } from "../../modes/interactive/theme/theme";
9
+ import { logger } from "../logger";
9
10
  import type { ModelRegistry } from "../model-registry";
10
11
  import type { SessionManager } from "../session-manager";
11
12
  import type {
12
- AppendEntryHandler,
13
13
  BeforeAgentStartEvent,
14
14
  BeforeAgentStartEventResult,
15
15
  ContextEvent,
16
16
  ContextEventResult,
17
+ Extension,
18
+ ExtensionActions,
17
19
  ExtensionCommandContext,
20
+ ExtensionCommandContextActions,
18
21
  ExtensionContext,
22
+ ExtensionContextActions,
19
23
  ExtensionError,
20
24
  ExtensionEvent,
21
25
  ExtensionFlag,
26
+ ExtensionRuntime,
22
27
  ExtensionShortcut,
23
28
  ExtensionUIContext,
24
- GetActiveToolsHandler,
25
- GetAllToolsHandler,
26
- LoadedExtension,
27
29
  MessageRenderer,
28
30
  RegisteredCommand,
29
31
  RegisteredTool,
30
- SendMessageHandler,
31
32
  SessionBeforeCompactResult,
32
33
  SessionBeforeTreeResult,
33
- SetActiveToolsHandler,
34
34
  ToolCallEvent,
35
35
  ToolCallEventResult,
36
36
  ToolResultEventResult,
37
+ UserBashEvent,
38
+ UserBashEventResult,
37
39
  } from "./types";
38
40
 
39
41
  /** Combined result from all before_agent_start handlers */
40
42
  interface BeforeAgentStartCombinedResult {
41
43
  messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
42
- systemPromptAppend?: string;
44
+ systemPrompt?: string;
43
45
  }
44
46
 
45
47
  export type ExtensionErrorListener = (error: ExtensionError) => void;
@@ -56,27 +58,49 @@ export type NavigateTreeHandler = (
56
58
  options?: { summarize?: boolean },
57
59
  ) => Promise<{ cancelled: boolean }>;
58
60
 
61
+ export type ShutdownHandler = () => void;
62
+
63
+ /**
64
+ * Helper function to emit session_shutdown event to extensions.
65
+ * Returns true if the event was emitted, false if there were no handlers.
66
+ */
67
+ export async function emitSessionShutdownEvent(extensionRunner: ExtensionRunner | undefined): Promise<boolean> {
68
+ if (extensionRunner?.hasHandlers("session_shutdown")) {
69
+ await extensionRunner.emit({
70
+ type: "session_shutdown",
71
+ });
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+
59
77
  const noOpUIContext: ExtensionUIContext = {
60
- select: async () => undefined,
61
- confirm: async () => false,
62
- input: async () => undefined,
78
+ select: async (_title, _options, _dialogOptions) => undefined,
79
+ confirm: async (_title, _message, _dialogOptions) => false,
80
+ input: async (_title, _placeholder, _dialogOptions) => undefined,
63
81
  notify: () => {},
64
82
  setStatus: () => {},
65
83
  setWidget: () => {},
84
+ setFooter: () => {},
85
+ setHeader: () => {},
66
86
  setTitle: () => {},
67
87
  custom: async () => undefined as never,
68
88
  setEditorText: () => {},
69
89
  getEditorText: () => "",
70
90
  editor: async () => undefined,
91
+ setEditorComponent: () => {},
71
92
  get theme() {
72
93
  return theme;
73
94
  },
95
+ getAllThemes: () => [],
96
+ getTheme: () => undefined,
97
+ setTheme: (_theme: string | Theme) => ({ success: false, error: "UI not available" }),
74
98
  };
75
99
 
76
100
  export class ExtensionRunner {
77
- private extensions: LoadedExtension[];
101
+ private extensions: Extension[];
102
+ private runtime: ExtensionRuntime;
78
103
  private uiContext: ExtensionUIContext;
79
- private hasUI: boolean;
80
104
  private cwd: string;
81
105
  private sessionManager: SessionManager;
82
106
  private modelRegistry: ModelRegistry;
@@ -89,72 +113,64 @@ export class ExtensionRunner {
89
113
  private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
90
114
  private branchHandler: BranchHandler = async () => ({ cancelled: false });
91
115
  private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
116
+ private shutdownHandler: ShutdownHandler = () => {};
92
117
 
93
118
  constructor(
94
- extensions: LoadedExtension[],
119
+ extensions: Extension[],
120
+ runtime: ExtensionRuntime,
95
121
  cwd: string,
96
122
  sessionManager: SessionManager,
97
123
  modelRegistry: ModelRegistry,
98
124
  ) {
99
125
  this.extensions = extensions;
126
+ this.runtime = runtime;
100
127
  this.uiContext = noOpUIContext;
101
- this.hasUI = false;
102
128
  this.cwd = cwd;
103
129
  this.sessionManager = sessionManager;
104
130
  this.modelRegistry = modelRegistry;
105
131
  }
106
132
 
107
- initialize(options: {
108
- getModel: () => Model<any> | undefined;
109
- sendMessageHandler: SendMessageHandler;
110
- appendEntryHandler: AppendEntryHandler;
111
- getActiveToolsHandler: GetActiveToolsHandler;
112
- getAllToolsHandler: GetAllToolsHandler;
113
- setActiveToolsHandler: SetActiveToolsHandler;
114
- newSessionHandler?: NewSessionHandler;
115
- branchHandler?: BranchHandler;
116
- navigateTreeHandler?: NavigateTreeHandler;
117
- isIdle?: () => boolean;
118
- waitForIdle?: () => Promise<void>;
119
- abort?: () => void;
120
- hasPendingMessages?: () => boolean;
121
- uiContext?: ExtensionUIContext;
122
- hasUI?: boolean;
123
- }): void {
124
- this.getModel = options.getModel;
125
- this.isIdleFn = options.isIdle ?? (() => true);
126
- this.waitForIdleFn = options.waitForIdle ?? (async () => {});
127
- this.abortFn = options.abort ?? (() => {});
128
- this.hasPendingMessagesFn = options.hasPendingMessages ?? (() => false);
129
-
130
- if (options.newSessionHandler) {
131
- this.newSessionHandler = options.newSessionHandler;
132
- }
133
- if (options.branchHandler) {
134
- this.branchHandler = options.branchHandler;
135
- }
136
- if (options.navigateTreeHandler) {
137
- this.navigateTreeHandler = options.navigateTreeHandler;
138
- }
139
-
140
- for (const ext of this.extensions) {
141
- ext.setSendMessageHandler(options.sendMessageHandler);
142
- ext.setAppendEntryHandler(options.appendEntryHandler);
143
- ext.setGetActiveToolsHandler(options.getActiveToolsHandler);
144
- ext.setGetAllToolsHandler(options.getAllToolsHandler);
145
- ext.setSetActiveToolsHandler(options.setActiveToolsHandler);
133
+ initialize(
134
+ actions: ExtensionActions,
135
+ contextActions: ExtensionContextActions,
136
+ commandContextActions?: ExtensionCommandContextActions,
137
+ uiContext?: ExtensionUIContext,
138
+ ): void {
139
+ // Copy actions into the shared runtime (all extension APIs reference this)
140
+ this.runtime.sendMessage = actions.sendMessage;
141
+ this.runtime.sendUserMessage = actions.sendUserMessage;
142
+ this.runtime.appendEntry = actions.appendEntry;
143
+ this.runtime.getActiveTools = actions.getActiveTools;
144
+ this.runtime.getAllTools = actions.getAllTools;
145
+ this.runtime.setActiveTools = actions.setActiveTools;
146
+ this.runtime.setModel = actions.setModel;
147
+ this.runtime.getThinkingLevel = actions.getThinkingLevel;
148
+ this.runtime.setThinkingLevel = actions.setThinkingLevel;
149
+
150
+ // Context actions (required)
151
+ this.getModel = contextActions.getModel;
152
+ this.isIdleFn = contextActions.isIdle;
153
+ this.abortFn = contextActions.abort;
154
+ this.hasPendingMessagesFn = contextActions.hasPendingMessages;
155
+ this.shutdownHandler = contextActions.shutdown;
156
+
157
+ // Command context actions (optional, only for interactive mode)
158
+ if (commandContextActions) {
159
+ this.waitForIdleFn = commandContextActions.waitForIdle;
160
+ this.newSessionHandler = commandContextActions.newSession;
161
+ this.branchHandler = commandContextActions.branch;
162
+ this.navigateTreeHandler = commandContextActions.navigateTree;
146
163
  }
147
164
 
148
- this.uiContext = options.uiContext ?? noOpUIContext;
149
- this.hasUI = options.hasUI ?? false;
165
+ this.uiContext = uiContext ?? noOpUIContext;
150
166
  }
151
167
 
152
- getUIContext(): ExtensionUIContext | null {
168
+ getUIContext(): ExtensionUIContext {
153
169
  return this.uiContext;
154
170
  }
155
171
 
156
- getHasUI(): boolean {
157
- return this.hasUI;
172
+ hasUI(): boolean {
173
+ return this.uiContext !== noOpUIContext;
158
174
  }
159
175
 
160
176
  getExtensionPaths(): string[] {
@@ -183,11 +199,7 @@ export class ExtensionRunner {
183
199
  }
184
200
 
185
201
  setFlagValue(name: string, value: boolean | string): void {
186
- for (const ext of this.extensions) {
187
- if (ext.flags.has(name)) {
188
- ext.setFlagValue(name, value);
189
- }
190
- }
202
+ this.runtime.flagValues.set(name, value);
191
203
  }
192
204
 
193
205
  private static readonly RESERVED_SHORTCUTS = new Set([
@@ -214,17 +226,20 @@ export class ExtensionRunner {
214
226
  const normalizedKey = key.toLowerCase() as KeyId;
215
227
 
216
228
  if (ExtensionRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {
217
- console.warn(
218
- `Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`,
219
- );
229
+ logger.warn("Extension shortcut conflicts with built-in shortcut", {
230
+ key,
231
+ extensionPath: shortcut.extensionPath,
232
+ });
220
233
  continue;
221
234
  }
222
235
 
223
236
  const existing = allShortcuts.get(normalizedKey);
224
237
  if (existing) {
225
- console.warn(
226
- `Extension shortcut conflict: '${key}' registered by both ${existing.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
227
- );
238
+ logger.warn("Extension shortcut conflict", {
239
+ key,
240
+ extensionPath: shortcut.extensionPath,
241
+ existingExtensionPath: existing.extensionPath,
242
+ });
228
243
  }
229
244
  allShortcuts.set(normalizedKey, shortcut);
230
245
  }
@@ -283,10 +298,10 @@ export class ExtensionRunner {
283
298
  return undefined;
284
299
  }
285
300
 
286
- private createContext(): ExtensionContext {
301
+ createContext(): ExtensionContext {
287
302
  return {
288
303
  ui: this.uiContext,
289
- hasUI: this.hasUI,
304
+ hasUI: this.hasUI(),
290
305
  cwd: this.cwd,
291
306
  sessionManager: this.sessionManager,
292
307
  modelRegistry: this.modelRegistry,
@@ -294,10 +309,18 @@ export class ExtensionRunner {
294
309
  isIdle: () => this.isIdleFn(),
295
310
  abort: () => this.abortFn(),
296
311
  hasPendingMessages: () => this.hasPendingMessagesFn(),
312
+ shutdown: () => this.shutdownHandler(),
297
313
  hasQueuedMessages: () => this.hasPendingMessagesFn(),
298
314
  };
299
315
  }
300
316
 
317
+ /**
318
+ * Request a graceful shutdown. Called by extension tools and event handlers.
319
+ */
320
+ shutdown(): void {
321
+ this.shutdownHandler();
322
+ }
323
+
301
324
  createCommandContext(): ExtensionCommandContext {
302
325
  return {
303
326
  ...this.createContext(),
@@ -394,6 +417,35 @@ export class ExtensionRunner {
394
417
  return result;
395
418
  }
396
419
 
420
+ async emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {
421
+ const ctx = this.createContext();
422
+
423
+ for (const ext of this.extensions) {
424
+ const handlers = ext.handlers.get("user_bash");
425
+ if (!handlers || handlers.length === 0) continue;
426
+
427
+ for (const handler of handlers) {
428
+ try {
429
+ const handlerResult = await handler(event, ctx);
430
+ if (handlerResult) {
431
+ return handlerResult as UserBashEventResult;
432
+ }
433
+ } catch (err) {
434
+ const message = err instanceof Error ? err.message : String(err);
435
+ const stack = err instanceof Error ? err.stack : undefined;
436
+ this.emitError({
437
+ extensionPath: ext.path,
438
+ event: "user_bash",
439
+ error: message,
440
+ stack,
441
+ });
442
+ }
443
+ }
444
+ }
445
+
446
+ return undefined;
447
+ }
448
+
397
449
  async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
398
450
  const ctx = this.createContext();
399
451
  let currentMessages = structuredClone(messages);
@@ -428,11 +480,13 @@ export class ExtensionRunner {
428
480
 
429
481
  async emitBeforeAgentStart(
430
482
  prompt: string,
431
- images?: ImageContent[],
483
+ images: ImageContent[] | undefined,
484
+ systemPrompt: string,
432
485
  ): Promise<BeforeAgentStartCombinedResult | undefined> {
433
486
  const ctx = this.createContext();
434
487
  const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
435
- const systemPromptAppends: string[] = [];
488
+ let currentSystemPrompt = systemPrompt;
489
+ let systemPromptModified = false;
436
490
 
437
491
  for (const ext of this.extensions) {
438
492
  const handlers = ext.handlers.get("before_agent_start");
@@ -440,7 +494,12 @@ export class ExtensionRunner {
440
494
 
441
495
  for (const handler of handlers) {
442
496
  try {
443
- const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images };
497
+ const event: BeforeAgentStartEvent = {
498
+ type: "before_agent_start",
499
+ prompt,
500
+ images,
501
+ systemPrompt: currentSystemPrompt,
502
+ };
444
503
  const handlerResult = await handler(event, ctx);
445
504
 
446
505
  if (handlerResult) {
@@ -448,8 +507,9 @@ export class ExtensionRunner {
448
507
  if (result.message) {
449
508
  messages.push(result.message);
450
509
  }
451
- if (result.systemPromptAppend) {
452
- systemPromptAppends.push(result.systemPromptAppend);
510
+ if (result.systemPrompt !== undefined) {
511
+ currentSystemPrompt = result.systemPrompt;
512
+ systemPromptModified = true;
453
513
  }
454
514
  }
455
515
  } catch (err) {
@@ -465,10 +525,10 @@ export class ExtensionRunner {
465
525
  }
466
526
  }
467
527
 
468
- if (messages.length > 0 || systemPromptAppends.length > 0) {
528
+ if (messages.length > 0 || systemPromptModified) {
469
529
  return {
470
530
  messages: messages.length > 0 ? messages : undefined,
471
- systemPromptAppend: systemPromptAppends.length > 0 ? systemPromptAppends.join("\n\n") : undefined,
531
+ systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
472
532
  };
473
533
  }
474
534