@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Tool wrappers for extensions.
3
+ */
4
+
5
+ import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
6
+ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
+ import type { Theme } from "../../modes/interactive/theme/theme";
8
+ import type { ExtensionRunner } from "./runner";
9
+ import type { ExtensionContext, RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
10
+
11
+ /**
12
+ * Wrap a RegisteredTool into an AgentTool.
13
+ */
14
+ export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: () => ExtensionContext): AgentTool {
15
+ const { definition } = registeredTool;
16
+ return {
17
+ name: definition.name,
18
+ label: definition.label,
19
+ description: definition.description,
20
+ parameters: definition.parameters,
21
+ execute: (toolCallId, params, signal, onUpdate) =>
22
+ definition.execute(toolCallId, params, onUpdate, getContext(), signal),
23
+ renderCall: definition.renderCall ? (args, theme) => definition.renderCall?.(args, theme as Theme) : undefined,
24
+ renderResult: definition.renderResult
25
+ ? (result, options, theme) =>
26
+ definition.renderResult?.(
27
+ result,
28
+ { expanded: options.expanded, isPartial: options.isPartial, spinnerFrame: options.spinnerFrame },
29
+ theme as Theme,
30
+ )
31
+ : undefined,
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Wrap all registered tools into AgentTools.
37
+ */
38
+ export function wrapRegisteredTools(
39
+ registeredTools: RegisteredTool[],
40
+ getContext: () => ExtensionContext,
41
+ ): AgentTool[] {
42
+ return registeredTools.map((rt) => wrapRegisteredTool(rt, getContext));
43
+ }
44
+
45
+ /**
46
+ * Wrap a tool with extension callbacks for interception.
47
+ * - Emits tool_call event before execution (can block)
48
+ * - Emits tool_result event after execution (can modify result)
49
+ */
50
+ export function wrapToolWithExtensions<T>(tool: AgentTool<any, T>, runner: ExtensionRunner): AgentTool<any, T> {
51
+ return {
52
+ ...tool,
53
+ execute: async (
54
+ toolCallId: string,
55
+ params: Record<string, unknown>,
56
+ signal?: AbortSignal,
57
+ onUpdate?: AgentToolUpdateCallback<T>,
58
+ context?: AgentToolContext,
59
+ ) => {
60
+ // Emit tool_call event - extensions can block execution
61
+ if (runner.hasHandlers("tool_call")) {
62
+ try {
63
+ const callResult = (await runner.emitToolCall({
64
+ type: "tool_call",
65
+ toolName: tool.name,
66
+ toolCallId,
67
+ input: params,
68
+ })) as ToolCallEventResult | undefined;
69
+
70
+ if (callResult?.block) {
71
+ const reason = callResult.reason || "Tool execution was blocked by an extension";
72
+ throw new Error(reason);
73
+ }
74
+ } catch (err) {
75
+ if (err instanceof Error) {
76
+ throw err;
77
+ }
78
+ throw new Error(`Extension failed, blocking execution: ${String(err)}`);
79
+ }
80
+ }
81
+
82
+ // Execute the actual tool
83
+ let result: { content: any; details: T };
84
+ let executionError: Error | undefined;
85
+
86
+ try {
87
+ result = await tool.execute(toolCallId, params, signal, onUpdate, context);
88
+ } catch (err) {
89
+ executionError = err instanceof Error ? err : new Error(String(err));
90
+ result = {
91
+ content: [{ type: "text", text: executionError.message }],
92
+ details: undefined as T,
93
+ };
94
+ }
95
+
96
+ // Emit tool_result event - extensions can modify the result and error status
97
+ if (runner.hasHandlers("tool_result")) {
98
+ const resultResult = (await runner.emit({
99
+ type: "tool_result",
100
+ toolName: tool.name,
101
+ toolCallId,
102
+ input: params,
103
+ content: result.content,
104
+ details: result.details,
105
+ isError: !!executionError,
106
+ })) as ToolResultEventResult | undefined;
107
+
108
+ if (resultResult) {
109
+ const modifiedContent: (TextContent | ImageContent)[] = resultResult.content ?? result.content;
110
+ const modifiedDetails = (resultResult.details ?? result.details) as T;
111
+
112
+ // Extension can override error status
113
+ if (resultResult.isError === true && !executionError) {
114
+ // Extension marks a successful result as error
115
+ const textBlocks = (modifiedContent ?? []).filter((c): c is TextContent => c.type === "text");
116
+ const errorText =
117
+ textBlocks.map((t) => t.text).join("\n") || "Tool result marked as error by extension";
118
+ throw new Error(errorText);
119
+ }
120
+ if (resultResult.isError === false && executionError) {
121
+ // Extension clears the error - return success
122
+ return { content: modifiedContent, details: modifiedDetails };
123
+ }
124
+
125
+ // Error status unchanged, but content/details may be modified
126
+ if (executionError) {
127
+ throw executionError;
128
+ }
129
+ return { content: modifiedContent, details: modifiedDetails };
130
+ }
131
+ }
132
+
133
+ // No extension modification
134
+ if (executionError) {
135
+ throw executionError;
136
+ }
137
+ return result;
138
+ },
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Wrap all tools with extension callbacks.
144
+ */
145
+ export function wrapToolsWithExtensions<T>(tools: AgentTool<any, T>[], runner: ExtensionRunner): AgentTool<any, T>[] {
146
+ return tools.map((tool) => wrapToolWithExtensions(tool, runner));
147
+ }
@@ -268,7 +268,7 @@ export interface SessionCompactEvent {
268
268
  type: "session_compact";
269
269
  compactionEntry: CompactionEntry;
270
270
  /** Whether the compaction entry was provided by a hook */
271
- fromHook: boolean;
271
+ fromExtension: boolean;
272
272
  }
273
273
 
274
274
  /** Fired on process exit (SIGINT/SIGTERM) */
@@ -309,7 +309,7 @@ export interface SessionTreeEvent {
309
309
  /** Branch summary entry if one was created */
310
310
  summaryEntry?: BranchSummaryEntry;
311
311
  /** Whether summary came from hook */
312
- fromHook?: boolean;
312
+ fromExtension?: boolean;
313
313
  }
314
314
 
315
315
  /** Union of all session event types */
package/src/core/index.ts CHANGED
@@ -14,27 +14,16 @@ export {
14
14
  export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor";
15
15
  export type { CompactionResult } from "./compaction/index";
16
16
  export {
17
- type CustomTool,
18
- type CustomToolAPI,
19
- type CustomToolFactory,
20
- type CustomToolsLoadResult,
21
- type CustomToolUIContext,
22
- discoverAndLoadCustomTools,
23
- type ExecResult,
24
- type LoadedCustomTool,
25
- loadCustomTools,
26
- type RenderResultOptions,
27
- } from "./custom-tools/index";
28
- export {
29
- type HookAPI,
30
- type HookContext,
31
- type HookError,
32
- type HookEvent,
33
- type HookFactory,
34
- HookRunner,
35
- type HookUIContext,
36
- loadHooks,
37
- } from "./hooks/index";
17
+ discoverAndLoadExtensions,
18
+ type ExtensionAPI,
19
+ type ExtensionCommandContext,
20
+ type ExtensionContext,
21
+ type ExtensionFactory,
22
+ ExtensionRunner,
23
+ type ExtensionUIContext,
24
+ loadExtensionFromFactory,
25
+ type ToolDefinition,
26
+ } from "./extensions/index";
38
27
  export {
39
28
  createMCPManager,
40
29
  discoverAndLoadMCPTools,
@@ -0,0 +1,199 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ DEFAULT_EDITOR_KEYBINDINGS,
5
+ type EditorAction,
6
+ type EditorKeybindingsConfig,
7
+ EditorKeybindingsManager,
8
+ type KeyId,
9
+ matchesKey,
10
+ setEditorKeybindings,
11
+ } from "@oh-my-pi/pi-tui";
12
+ import { getAgentDir } from "../config";
13
+
14
+ /**
15
+ * Application-level actions (coding agent specific).
16
+ */
17
+ export type AppAction =
18
+ | "interrupt"
19
+ | "clear"
20
+ | "exit"
21
+ | "suspend"
22
+ | "cycleThinkingLevel"
23
+ | "cycleModelForward"
24
+ | "cycleModelBackward"
25
+ | "selectModel"
26
+ | "expandTools"
27
+ | "toggleThinking"
28
+ | "externalEditor"
29
+ | "followUp";
30
+
31
+ /**
32
+ * All configurable actions.
33
+ */
34
+ export type KeyAction = AppAction | EditorAction;
35
+
36
+ /**
37
+ * Full keybindings configuration (app + editor actions).
38
+ */
39
+ export type KeybindingsConfig = {
40
+ [K in KeyAction]?: KeyId | KeyId[];
41
+ };
42
+
43
+ /**
44
+ * Default application keybindings.
45
+ */
46
+ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
47
+ interrupt: "escape",
48
+ clear: "ctrl+c",
49
+ exit: "ctrl+d",
50
+ suspend: "ctrl+z",
51
+ cycleThinkingLevel: "shift+tab",
52
+ cycleModelForward: "ctrl+p",
53
+ cycleModelBackward: "shift+ctrl+p",
54
+ selectModel: "ctrl+l",
55
+ expandTools: "ctrl+o",
56
+ toggleThinking: "ctrl+t",
57
+ externalEditor: "ctrl+g",
58
+ followUp: "alt+enter",
59
+ };
60
+
61
+ /**
62
+ * All default keybindings (app + editor).
63
+ */
64
+ export const DEFAULT_KEYBINDINGS: Required<KeybindingsConfig> = {
65
+ ...DEFAULT_EDITOR_KEYBINDINGS,
66
+ ...DEFAULT_APP_KEYBINDINGS,
67
+ };
68
+
69
+ // App actions list for type checking
70
+ const APP_ACTIONS: AppAction[] = [
71
+ "interrupt",
72
+ "clear",
73
+ "exit",
74
+ "suspend",
75
+ "cycleThinkingLevel",
76
+ "cycleModelForward",
77
+ "cycleModelBackward",
78
+ "selectModel",
79
+ "expandTools",
80
+ "toggleThinking",
81
+ "externalEditor",
82
+ "followUp",
83
+ ];
84
+
85
+ function isAppAction(action: string): action is AppAction {
86
+ return APP_ACTIONS.includes(action as AppAction);
87
+ }
88
+
89
+ /**
90
+ * Manages all keybindings (app + editor).
91
+ */
92
+ export class KeybindingsManager {
93
+ private config: KeybindingsConfig;
94
+ private appActionToKeys: Map<AppAction, KeyId[]>;
95
+
96
+ private constructor(config: KeybindingsConfig) {
97
+ this.config = config;
98
+ this.appActionToKeys = new Map();
99
+ this.buildMaps();
100
+ }
101
+
102
+ /**
103
+ * Create from config file and set up editor keybindings.
104
+ */
105
+ static async create(agentDir: string = getAgentDir()): Promise<KeybindingsManager> {
106
+ const configPath = join(agentDir, "keybindings.json");
107
+ const config = await KeybindingsManager.loadFromFile(configPath);
108
+ const manager = new KeybindingsManager(config);
109
+
110
+ // Set up editor keybindings globally
111
+ const editorConfig: EditorKeybindingsConfig = {};
112
+ for (const [action, keys] of Object.entries(config)) {
113
+ if (!isAppAction(action)) {
114
+ editorConfig[action as EditorAction] = keys;
115
+ }
116
+ }
117
+ setEditorKeybindings(new EditorKeybindingsManager(editorConfig));
118
+
119
+ return manager;
120
+ }
121
+
122
+ /**
123
+ * Create in-memory.
124
+ */
125
+ static inMemory(config: KeybindingsConfig = {}): KeybindingsManager {
126
+ return new KeybindingsManager(config);
127
+ }
128
+
129
+ private static async loadFromFile(path: string): Promise<KeybindingsConfig> {
130
+ if (!existsSync(path)) return {};
131
+ try {
132
+ return JSON.parse(readFileSync(path, "utf-8"));
133
+ } catch {
134
+ return {};
135
+ }
136
+ }
137
+
138
+ private buildMaps(): void {
139
+ this.appActionToKeys.clear();
140
+
141
+ // Set defaults for app actions
142
+ for (const [action, keys] of Object.entries(DEFAULT_APP_KEYBINDINGS)) {
143
+ const keyArray = Array.isArray(keys) ? keys : [keys];
144
+ this.appActionToKeys.set(action as AppAction, [...keyArray]);
145
+ }
146
+
147
+ // Override with user config (app actions only)
148
+ for (const [action, keys] of Object.entries(this.config)) {
149
+ if (keys === undefined || !isAppAction(action)) continue;
150
+ const keyArray = Array.isArray(keys) ? keys : [keys];
151
+ this.appActionToKeys.set(action, keyArray);
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Check if input matches an app action.
157
+ */
158
+ matches(data: string, action: AppAction): boolean {
159
+ const keys = this.appActionToKeys.get(action);
160
+ if (!keys) return false;
161
+ for (const key of keys) {
162
+ if (matchesKey(data, key)) return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ /**
168
+ * Get keys bound to an app action.
169
+ */
170
+ getKeys(action: AppAction): KeyId[] {
171
+ return this.appActionToKeys.get(action) ?? [];
172
+ }
173
+
174
+ /**
175
+ * Get display string for an action.
176
+ */
177
+ getDisplayString(action: AppAction): string {
178
+ const keys = this.getKeys(action);
179
+ if (keys.length === 0) return "";
180
+ if (keys.length === 1) return keys[0]!;
181
+ return keys.join("/");
182
+ }
183
+
184
+ /**
185
+ * Get the full effective config.
186
+ */
187
+ getEffectiveConfig(): Required<KeybindingsConfig> {
188
+ const result = { ...DEFAULT_KEYBINDINGS };
189
+ for (const [action, keys] of Object.entries(this.config)) {
190
+ if (keys !== undefined) {
191
+ (result as KeybindingsConfig)[action as KeyAction] = keys;
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+ }
197
+
198
+ // Re-export for convenience
199
+ export type { EditorAction, KeyId };
@@ -35,11 +35,24 @@ export interface BashExecutionMessage {
35
35
  truncated: boolean;
36
36
  fullOutputPath?: string;
37
37
  timestamp: number;
38
+ /** If true, this message is excluded from LLM context (!! prefix) */
39
+ excludeFromContext?: boolean;
38
40
  }
39
41
 
40
42
  /**
41
- * Message type for hook-injected messages via sendMessage().
42
- * These are custom messages that hooks can inject into the conversation.
43
+ * Message type for extension-injected messages via sendMessage().
44
+ */
45
+ export interface CustomMessage<T = unknown> {
46
+ role: "custom";
47
+ customType: string;
48
+ content: string | (TextContent | ImageContent)[];
49
+ display: boolean;
50
+ details?: T;
51
+ timestamp: number;
52
+ }
53
+
54
+ /**
55
+ * Legacy hook message type (pre-extensions). Kept for session migration.
43
56
  */
44
57
  export interface HookMessage<T = unknown> {
45
58
  role: "hookMessage";
@@ -78,9 +91,11 @@ export interface FileMentionMessage {
78
91
  }
79
92
 
80
93
  // Extend CustomAgentMessages via declaration merging
94
+ // Legacy hookMessage is kept for migration; new code should use custom.
81
95
  declare module "@oh-my-pi/pi-agent-core" {
82
96
  interface CustomAgentMessages {
83
97
  bashExecution: BashExecutionMessage;
98
+ custom: CustomMessage;
84
99
  hookMessage: HookMessage;
85
100
  branchSummary: BranchSummaryMessage;
86
101
  compactionSummary: CompactionSummaryMessage;
@@ -125,22 +140,22 @@ export function createCompactionSummaryMessage(
125
140
  ): CompactionSummaryMessage {
126
141
  return {
127
142
  role: "compactionSummary",
128
- summary: summary,
143
+ summary,
129
144
  tokensBefore,
130
145
  timestamp: new Date(timestamp).getTime(),
131
146
  };
132
147
  }
133
148
 
134
149
  /** Convert CustomMessageEntry to AgentMessage format */
135
- export function createHookMessage(
150
+ export function createCustomMessage(
136
151
  customType: string,
137
152
  content: string | (TextContent | ImageContent)[],
138
153
  display: boolean,
139
154
  details: unknown | undefined,
140
155
  timestamp: string,
141
- ): HookMessage {
156
+ ): CustomMessage {
142
157
  return {
143
- role: "hookMessage",
158
+ role: "custom",
144
159
  customType,
145
160
  content,
146
161
  display,
@@ -155,18 +170,22 @@ export function createHookMessage(
155
170
  * This is used by:
156
171
  * - Agent's transormToLlm option (for prompt calls and queued messages)
157
172
  * - Compaction's generateSummary (for summarization)
158
- * - Custom hooks and tools
173
+ * - Custom extensions and tools
159
174
  */
160
175
  export function convertToLlm(messages: AgentMessage[]): Message[] {
161
176
  return messages
162
177
  .map((m): Message | undefined => {
163
178
  switch (m.role) {
164
179
  case "bashExecution":
180
+ if (m.excludeFromContext) {
181
+ return undefined;
182
+ }
165
183
  return {
166
184
  role: "user",
167
185
  content: [{ type: "text", text: bashExecutionToText(m) }],
168
186
  timestamp: m.timestamp,
169
187
  };
188
+ case "custom":
170
189
  case "hookMessage": {
171
190
  const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
172
191
  return {