@oh-my-pi/pi-coding-agent 11.8.2 → 11.8.3

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 (122) hide show
  1. package/docs/tui.md +9 -9
  2. package/package.json +7 -7
  3. package/src/cli/file-processor.ts +8 -13
  4. package/src/cli/oclif-help.ts +1 -1
  5. package/src/cli.ts +14 -0
  6. package/src/commit/git/index.ts +16 -16
  7. package/src/config/keybindings.ts +11 -11
  8. package/src/config/model-registry.ts +31 -66
  9. package/src/config/settings.ts +88 -95
  10. package/src/config.ts +2 -2
  11. package/src/cursor.ts +4 -4
  12. package/src/debug/index.ts +28 -28
  13. package/src/discovery/codex.ts +5 -13
  14. package/src/discovery/cursor.ts +2 -7
  15. package/src/exa/mcp-client.ts +2 -2
  16. package/src/exa/websets.ts +2 -2
  17. package/src/export/html/index.ts +3 -3
  18. package/src/export/ttsr.ts +27 -27
  19. package/src/extensibility/custom-tools/loader.ts +9 -9
  20. package/src/extensibility/extensions/runner.ts +64 -64
  21. package/src/extensibility/hooks/runner.ts +46 -46
  22. package/src/extensibility/plugins/manager.ts +49 -49
  23. package/src/index.ts +0 -1
  24. package/src/internal-urls/router.ts +5 -5
  25. package/src/ipy/kernel.ts +61 -57
  26. package/src/lsp/client.ts +1 -1
  27. package/src/lsp/clients/biome-client.ts +2 -2
  28. package/src/lsp/clients/lsp-linter-client.ts +7 -7
  29. package/src/lsp/index.ts +9 -9
  30. package/src/mcp/manager.ts +47 -47
  31. package/src/mcp/tool-bridge.ts +12 -12
  32. package/src/mcp/transports/http.ts +34 -34
  33. package/src/mcp/transports/stdio.ts +47 -47
  34. package/src/modes/components/assistant-message.ts +25 -25
  35. package/src/modes/components/bash-execution.ts +51 -51
  36. package/src/modes/components/bordered-loader.ts +7 -7
  37. package/src/modes/components/branch-summary-message.ts +7 -7
  38. package/src/modes/components/compaction-summary-message.ts +7 -7
  39. package/src/modes/components/countdown-timer.ts +15 -15
  40. package/src/modes/components/custom-editor.ts +22 -22
  41. package/src/modes/components/custom-message.ts +21 -21
  42. package/src/modes/components/dynamic-border.ts +3 -3
  43. package/src/modes/components/extensions/extension-dashboard.ts +72 -72
  44. package/src/modes/components/extensions/extension-list.ts +99 -97
  45. package/src/modes/components/extensions/inspector-panel.ts +26 -26
  46. package/src/modes/components/footer.ts +36 -36
  47. package/src/modes/components/history-search.ts +52 -52
  48. package/src/modes/components/hook-editor.ts +20 -20
  49. package/src/modes/components/hook-input.ts +20 -20
  50. package/src/modes/components/hook-message.ts +22 -22
  51. package/src/modes/components/hook-selector.ts +52 -52
  52. package/src/modes/components/index.ts +0 -1
  53. package/src/modes/components/login-dialog.ts +57 -57
  54. package/src/modes/components/model-selector.ts +173 -173
  55. package/src/modes/components/oauth-selector.ts +45 -45
  56. package/src/modes/components/plugin-settings.ts +52 -52
  57. package/src/modes/components/python-execution.ts +53 -53
  58. package/src/modes/components/queue-mode-selector.ts +7 -7
  59. package/src/modes/components/read-tool-group.ts +23 -23
  60. package/src/modes/components/session-selector.ts +40 -37
  61. package/src/modes/components/settings-selector.ts +80 -80
  62. package/src/modes/components/show-images-selector.ts +7 -7
  63. package/src/modes/components/skill-message.ts +27 -27
  64. package/src/modes/components/status-line-segment-editor.ts +81 -81
  65. package/src/modes/components/status-line.ts +73 -73
  66. package/src/modes/components/theme-selector.ts +11 -11
  67. package/src/modes/components/thinking-selector.ts +7 -7
  68. package/src/modes/components/todo-display.ts +19 -19
  69. package/src/modes/components/todo-reminder.ts +9 -9
  70. package/src/modes/components/tool-execution.ts +204 -196
  71. package/src/modes/components/tree-selector.ts +144 -144
  72. package/src/modes/components/ttsr-notification.ts +17 -17
  73. package/src/modes/components/user-message-selector.ts +18 -18
  74. package/src/modes/components/welcome.ts +10 -10
  75. package/src/modes/controllers/command-controller.ts +0 -7
  76. package/src/modes/controllers/event-controller.ts +23 -23
  77. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  78. package/src/modes/controllers/input-controller.ts +4 -9
  79. package/src/modes/interactive-mode.ts +234 -241
  80. package/src/modes/rpc/rpc-client.ts +77 -77
  81. package/src/modes/rpc/rpc-mode.ts +5 -5
  82. package/src/modes/theme/theme.ts +113 -113
  83. package/src/modes/types.ts +0 -1
  84. package/src/patch/index.ts +45 -45
  85. package/src/prompts/tools/task.md +22 -2
  86. package/src/session/agent-session.ts +463 -476
  87. package/src/session/agent-storage.ts +72 -75
  88. package/src/session/auth-storage.ts +186 -252
  89. package/src/session/history-storage.ts +36 -38
  90. package/src/session/session-manager.ts +300 -299
  91. package/src/session/session-storage.ts +65 -90
  92. package/src/ssh/connection-manager.ts +9 -9
  93. package/src/task/agents.ts +1 -1
  94. package/src/task/executor.ts +2 -2
  95. package/src/task/index.ts +13 -12
  96. package/src/task/subprocess-tool-registry.ts +5 -5
  97. package/src/tools/ask.ts +7 -7
  98. package/src/tools/bash.ts +8 -7
  99. package/src/tools/browser.ts +123 -123
  100. package/src/tools/calculator.ts +46 -46
  101. package/src/tools/context.ts +9 -9
  102. package/src/tools/exit-plan-mode.ts +5 -5
  103. package/src/tools/fetch.ts +5 -5
  104. package/src/tools/find.ts +16 -16
  105. package/src/tools/grep.ts +10 -10
  106. package/src/tools/notebook.ts +6 -6
  107. package/src/tools/output-meta.ts +10 -2
  108. package/src/tools/python.ts +12 -11
  109. package/src/tools/read.ts +17 -17
  110. package/src/tools/ssh.ts +9 -9
  111. package/src/tools/submit-result.ts +13 -13
  112. package/src/tools/todo-write.ts +6 -6
  113. package/src/tools/write.ts +10 -10
  114. package/src/tui/output-block.ts +6 -6
  115. package/src/tui/utils.ts +9 -9
  116. package/src/utils/event-bus.ts +10 -10
  117. package/src/utils/frontmatter.ts +1 -1
  118. package/src/utils/ignore-files.ts +1 -1
  119. package/src/web/search/index.ts +5 -5
  120. package/src/web/search/providers/anthropic.ts +7 -2
  121. package/examples/hooks/snake.ts +0 -342
  122. package/src/modes/components/armin.ts +0 -379
@@ -28,7 +28,7 @@ import type { SlashCommand } from "../capability/slash-command";
28
28
  import { slashCommandCapability } from "../capability/slash-command";
29
29
  import type { CustomTool } from "../capability/tool";
30
30
  import { toolCapability } from "../capability/tool";
31
- import type { LoadContext, LoadResult } from "../capability/types";
31
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
32
32
  import { parseFrontmatter } from "../utils/frontmatter";
33
33
  import {
34
34
  createSourceMeta,
@@ -276,8 +276,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
276
276
  const projectCommandsDir = path.join(codexDir, "commands");
277
277
 
278
278
  const transformCommand =
279
- (level: "user" | "project") =>
280
- (name: string, content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
279
+ (level: "user" | "project") => (name: string, content: string, path: string, source: SourceMeta) => {
281
280
  const { frontmatter, body } = parseFrontmatter(content, { source: path });
282
281
  const commandName = frontmatter.name || name.replace(/\.md$/, "");
283
282
  return {
@@ -315,12 +314,7 @@ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
315
314
  const codexDir = getProjectCodexDir(ctx);
316
315
  const projectPromptsDir = path.join(codexDir, "prompts");
317
316
 
318
- const transformPrompt = (
319
- name: string,
320
- content: string,
321
- path: string,
322
- source: ReturnType<typeof createSourceMeta>,
323
- ) => {
317
+ const transformPrompt = (name: string, content: string, path: string, source: SourceMeta) => {
324
318
  const { frontmatter, body } = parseFrontmatter(content, { source: path });
325
319
  const promptName = frontmatter.name || name.replace(/\.md$/, "");
326
320
  return {
@@ -359,8 +353,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
359
353
  const projectHooksDir = path.join(codexDir, "hooks");
360
354
 
361
355
  const transformHook =
362
- (level: "user" | "project") =>
363
- (name: string, _content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
356
+ (level: "user" | "project") => (name: string, _content: string, path: string, source: SourceMeta) => {
364
357
  const baseName = name.replace(/\.(ts|js)$/, "");
365
358
  const match = baseName.match(/^(pre|post)-(.+)$/);
366
359
  const hookType = (match?.[1] as "pre" | "post") || "pre";
@@ -402,8 +395,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
402
395
  const projectToolsDir = path.join(codexDir, "tools");
403
396
 
404
397
  const transformTool =
405
- (level: "user" | "project") =>
406
- (name: string, _content: string, path: string, source: ReturnType<typeof createSourceMeta>) => {
398
+ (level: "user" | "project") => (name: string, _content: string, path: string, source: SourceMeta) => {
407
399
  const toolName = name.replace(/\.(ts|js)$/, "");
408
400
  return {
409
401
  name: toolName,
@@ -20,7 +20,7 @@ import type { Rule } from "../capability/rule";
20
20
  import { ruleCapability } from "../capability/rule";
21
21
  import type { Settings } from "../capability/settings";
22
22
  import { settingsCapability } from "../capability/settings";
23
- import type { LoadContext, LoadResult } from "../capability/types";
23
+ import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
24
24
  import { parseFrontmatter } from "../utils/frontmatter";
25
25
  import {
26
26
  createSourceMeta,
@@ -136,12 +136,7 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
136
136
  return { items, warnings };
137
137
  }
138
138
 
139
- function transformMDCRule(
140
- name: string,
141
- content: string,
142
- path: string,
143
- source: ReturnType<typeof createSourceMeta>,
144
- ): Rule {
139
+ function transformMDCRule(name: string, content: string, path: string, source: SourceMeta): Rule {
145
140
  const { frontmatter, body } = parseFrontmatter(content, { source: path });
146
141
 
147
142
  // Extract frontmatter fields
@@ -217,8 +217,8 @@ export async function fetchMCPToolSchema(
217
217
  * reducing drift when MCP servers add new parameters.
218
218
  */
219
219
  export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
220
- public readonly name: string;
221
- public readonly label: string;
220
+ readonly name: string;
221
+ readonly label: string;
222
222
 
223
223
  constructor(
224
224
  private readonly config: MCPToolWrapperConfig,
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * CRUD operations for websets, items, searches, enrichments, and monitoring.
5
5
  */
6
- import { Type } from "@sinclair/typebox";
6
+ import { type TObject, type TProperties, Type } from "@sinclair/typebox";
7
7
  import type { CustomTool } from "../extensibility/custom-tools/types";
8
8
  import { callWebsetsTool, findApiKey } from "./mcp-client";
9
9
  import type { ExaRenderDetails } from "./types";
@@ -13,7 +13,7 @@ function createWebsetTool(
13
13
  name: string,
14
14
  label: string,
15
15
  description: string,
16
- parameters: ReturnType<typeof Type.Object>,
16
+ parameters: TObject<TProperties>,
17
17
  mcpToolName: string,
18
18
  ): CustomTool<any, ExaRenderDetails> {
19
19
  return {
@@ -3,7 +3,7 @@ import type { AgentState } from "@oh-my-pi/pi-agent-core";
3
3
  import { isEnoent } from "@oh-my-pi/pi-utils";
4
4
  import { APP_NAME } from "../../config";
5
5
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
6
- import { SessionManager } from "../../session/session-manager";
6
+ import { type SessionEntry, type SessionHeader, SessionManager } from "../../session/session-manager";
7
7
  // Pre-generated template (created by scripts/generate-template.ts at publish time)
8
8
  import { TEMPLATE } from "./template.generated";
9
9
 
@@ -92,8 +92,8 @@ async function generateThemeVars(themeName?: string): Promise<string> {
92
92
  }
93
93
 
94
94
  interface SessionData {
95
- header: ReturnType<SessionManager["getHeader"]>;
96
- entries: ReturnType<SessionManager["getEntries"]>;
95
+ header: SessionHeader | null;
96
+ entries: SessionEntry[];
97
97
  leafId: string | null;
98
98
  systemPrompt?: string;
99
99
  tools?: { name: string; description: string }[];
@@ -28,29 +28,29 @@ const DEFAULT_SETTINGS: Required<TtsrSettings> = {
28
28
  };
29
29
 
30
30
  export class TtsrManager {
31
- private readonly settings: Required<TtsrSettings>;
32
- private readonly rules = new Map<string, TtsrEntry>();
33
- private readonly injectionRecords = new Map<string, InjectionRecord>();
34
- private buffer = "";
35
- private messageCount = 0;
31
+ readonly #settings: Required<TtsrSettings>;
32
+ readonly #rules = new Map<string, TtsrEntry>();
33
+ readonly #injectionRecords = new Map<string, InjectionRecord>();
34
+ #buffer = "";
35
+ #messageCount = 0;
36
36
 
37
37
  constructor(settings?: TtsrSettings) {
38
- this.settings = { ...DEFAULT_SETTINGS, ...settings };
38
+ this.#settings = { ...DEFAULT_SETTINGS, ...settings };
39
39
  }
40
40
 
41
41
  /** Check if a rule can be triggered based on repeat settings */
42
- private canTrigger(ruleName: string): boolean {
43
- const record = this.injectionRecords.get(ruleName);
42
+ #canTrigger(ruleName: string): boolean {
43
+ const record = this.#injectionRecords.get(ruleName);
44
44
  if (!record) {
45
45
  return true;
46
46
  }
47
47
 
48
- if (this.settings.repeatMode === "once") {
48
+ if (this.#settings.repeatMode === "once") {
49
49
  return false;
50
50
  }
51
51
 
52
- const gap = this.messageCount - record.lastInjectedAt;
53
- return gap >= this.settings.repeatGap;
52
+ const gap = this.#messageCount - record.lastInjectedAt;
53
+ return gap >= this.#settings.repeatGap;
54
54
  }
55
55
 
56
56
  /** Add a TTSR rule to be monitored */
@@ -59,13 +59,13 @@ export class TtsrManager {
59
59
  return;
60
60
  }
61
61
 
62
- if (this.rules.has(rule.name)) {
62
+ if (this.#rules.has(rule.name)) {
63
63
  return;
64
64
  }
65
65
 
66
66
  try {
67
67
  const regex = new RegExp(rule.ttsrTrigger);
68
- this.rules.set(rule.name, { rule, regex });
68
+ this.#rules.set(rule.name, { rule, regex });
69
69
  logger.debug("TTSR rule registered", {
70
70
  ruleName: rule.name,
71
71
  pattern: rule.ttsrTrigger,
@@ -83,8 +83,8 @@ export class TtsrManager {
83
83
  check(streamBuffer: string): Rule[] {
84
84
  const matches: Rule[] = [];
85
85
 
86
- for (const [name, entry] of this.rules) {
87
- if (!this.canTrigger(name)) {
86
+ for (const [name, entry] of this.#rules) {
87
+ if (!this.#canTrigger(name)) {
88
88
  continue;
89
89
  }
90
90
 
@@ -103,24 +103,24 @@ export class TtsrManager {
103
103
  /** Mark rules as injected (won't trigger again until conditions allow) */
104
104
  markInjected(rulesToMark: Rule[]): void {
105
105
  for (const rule of rulesToMark) {
106
- this.injectionRecords.set(rule.name, { lastInjectedAt: this.messageCount });
106
+ this.#injectionRecords.set(rule.name, { lastInjectedAt: this.#messageCount });
107
107
  logger.debug("TTSR rule marked as injected", {
108
108
  ruleName: rule.name,
109
- messageCount: this.messageCount,
110
- repeatMode: this.settings.repeatMode,
109
+ messageCount: this.#messageCount,
110
+ repeatMode: this.#settings.repeatMode,
111
111
  });
112
112
  }
113
113
  }
114
114
 
115
115
  /** Get names of all injected rules (for persistence) */
116
116
  getInjectedRuleNames(): string[] {
117
- return Array.from(this.injectionRecords.keys());
117
+ return Array.from(this.#injectionRecords.keys());
118
118
  }
119
119
 
120
120
  /** Restore injected state from a list of rule names */
121
121
  restoreInjected(ruleNames: string[]): void {
122
122
  for (const name of ruleNames) {
123
- this.injectionRecords.set(name, { lastInjectedAt: 0 });
123
+ this.#injectionRecords.set(name, { lastInjectedAt: 0 });
124
124
  }
125
125
  if (ruleNames.length > 0) {
126
126
  logger.debug("TTSR injected state restored", { ruleNames });
@@ -129,36 +129,36 @@ export class TtsrManager {
129
129
 
130
130
  /** Reset stream buffer (called on new turn) */
131
131
  resetBuffer(): void {
132
- this.buffer = "";
132
+ this.#buffer = "";
133
133
  }
134
134
 
135
135
  /** Get current stream buffer */
136
136
  getBuffer(): string {
137
- return this.buffer;
137
+ return this.#buffer;
138
138
  }
139
139
 
140
140
  /** Append to stream buffer */
141
141
  appendToBuffer(text: string): void {
142
- this.buffer += text;
142
+ this.#buffer += text;
143
143
  }
144
144
 
145
145
  /** Check if any TTSRs are registered */
146
146
  hasRules(): boolean {
147
- return this.rules.size > 0;
147
+ return this.#rules.size > 0;
148
148
  }
149
149
 
150
150
  /** Increment message counter (call after each turn) */
151
151
  incrementMessageCount(): void {
152
- this.messageCount++;
152
+ this.#messageCount++;
153
153
  }
154
154
 
155
155
  /** Get current message count */
156
156
  getMessageCount(): number {
157
- return this.messageCount;
157
+ return this.#messageCount;
158
158
  }
159
159
 
160
160
  /** Get settings */
161
161
  getSettings(): Required<TtsrSettings> {
162
- return this.settings;
162
+ return this.#settings;
163
163
  }
164
164
  }
@@ -119,11 +119,11 @@ interface ToolPathWithSource {
119
119
  export class CustomToolLoader {
120
120
  tools: LoadedCustomTool[] = [];
121
121
  errors: ToolLoadError[] = [];
122
- private sharedApi: CustomToolAPI;
123
- private seenNames: Set<string>;
122
+ #sharedApi: CustomToolAPI;
123
+ #seenNames: Set<string>;
124
124
 
125
125
  constructor(cwd: string, builtInToolNames: string[]) {
126
- this.sharedApi = {
126
+ this.#sharedApi = {
127
127
  cwd,
128
128
  exec: (command: string, args: string[], options?: ExecOptions) =>
129
129
  execCommand(command, args, options?.cwd ?? cwd, options),
@@ -133,12 +133,12 @@ export class CustomToolLoader {
133
133
  typebox,
134
134
  pi: piCodingAgent,
135
135
  };
136
- this.seenNames = new Set<string>(builtInToolNames);
136
+ this.#seenNames = new Set<string>(builtInToolNames);
137
137
  }
138
138
 
139
139
  async load(pathsWithSources: ToolPathWithSource[]): Promise<void> {
140
140
  for (const { path: toolPath, source } of pathsWithSources) {
141
- const { tools: loadedTools, error } = await loadTool(toolPath, this.sharedApi.cwd, this.sharedApi, source);
141
+ const { tools: loadedTools, error } = await loadTool(toolPath, this.#sharedApi.cwd, this.#sharedApi, source);
142
142
 
143
143
  if (error) {
144
144
  this.errors.push(error);
@@ -148,7 +148,7 @@ export class CustomToolLoader {
148
148
  if (loadedTools) {
149
149
  for (const loadedTool of loadedTools) {
150
150
  // Check for name conflicts
151
- if (this.seenNames.has(loadedTool.tool.name)) {
151
+ if (this.#seenNames.has(loadedTool.tool.name)) {
152
152
  this.errors.push({
153
153
  path: toolPath,
154
154
  error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
@@ -157,7 +157,7 @@ export class CustomToolLoader {
157
157
  continue;
158
158
  }
159
159
 
160
- this.seenNames.add(loadedTool.tool.name);
160
+ this.#seenNames.add(loadedTool.tool.name);
161
161
  this.tools.push(loadedTool);
162
162
  }
163
163
  }
@@ -165,8 +165,8 @@ export class CustomToolLoader {
165
165
  }
166
166
 
167
167
  setUIContext(uiContext: HookUIContext, hasUI: boolean): void {
168
- this.sharedApi.ui = uiContext;
169
- this.sharedApi.hasUI = hasUI;
168
+ this.#sharedApi.ui = uiContext;
169
+ this.#sharedApi.hasUI = hasUI;
170
170
  }
171
171
  }
172
172
 
@@ -153,23 +153,23 @@ const noOpUIContext: ExtensionUIContext = {
153
153
  };
154
154
 
155
155
  export class ExtensionRunner {
156
- private uiContext: ExtensionUIContext;
157
- private errorListeners: Set<ExtensionErrorListener> = new Set();
158
- private getModel: () => Model | undefined = () => undefined;
159
- private isIdleFn: () => boolean = () => true;
160
- private waitForIdleFn: () => Promise<void> = async () => {};
161
- private abortFn: () => void = () => {};
162
- private hasPendingMessagesFn: () => boolean = () => false;
163
- private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
164
- private compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
165
- private getSystemPromptFn: () => string = () => "";
166
- private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
167
- private branchHandler: BranchHandler = async () => ({ cancelled: false });
168
- private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
169
- private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
170
- private reloadHandler: () => Promise<void> = async () => {};
171
- private shutdownHandler: ShutdownHandler = () => {};
172
- private commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
156
+ #uiContext: ExtensionUIContext;
157
+ #errorListeners: Set<ExtensionErrorListener> = new Set();
158
+ #getModel: () => Model | undefined = () => undefined;
159
+ #isIdleFn: () => boolean = () => true;
160
+ #waitForIdleFn: () => Promise<void> = async () => {};
161
+ #abortFn: () => void = () => {};
162
+ #hasPendingMessagesFn: () => boolean = () => false;
163
+ #getContextUsageFn: () => ContextUsage | undefined = () => undefined;
164
+ #compactFn: (instructionsOrOptions?: string | CompactOptions) => Promise<void> = async () => {};
165
+ #getSystemPromptFn: () => string = () => "";
166
+ #newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
167
+ #branchHandler: BranchHandler = async () => ({ cancelled: false });
168
+ #navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
169
+ #switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
170
+ #reloadHandler: () => Promise<void> = async () => {};
171
+ #shutdownHandler: ShutdownHandler = () => {};
172
+ #commandDiagnostics: Array<{ type: string; message: string; path: string }> = [];
173
173
 
174
174
  constructor(
175
175
  private readonly extensions: Extension[],
@@ -178,7 +178,7 @@ export class ExtensionRunner {
178
178
  private readonly sessionManager: SessionManager,
179
179
  private readonly modelRegistry: ModelRegistry,
180
180
  ) {
181
- this.uiContext = noOpUIContext;
181
+ this.#uiContext = noOpUIContext;
182
182
  }
183
183
 
184
184
  initialize(
@@ -200,34 +200,34 @@ export class ExtensionRunner {
200
200
  this.runtime.setThinkingLevel = actions.setThinkingLevel;
201
201
 
202
202
  // Context actions (required)
203
- this.getModel = contextActions.getModel;
204
- this.isIdleFn = contextActions.isIdle;
205
- this.abortFn = contextActions.abort;
206
- this.hasPendingMessagesFn = contextActions.hasPendingMessages;
207
- this.shutdownHandler = contextActions.shutdown;
208
- this.getSystemPromptFn = contextActions.getSystemPrompt;
203
+ this.#getModel = contextActions.getModel;
204
+ this.#isIdleFn = contextActions.isIdle;
205
+ this.#abortFn = contextActions.abort;
206
+ this.#hasPendingMessagesFn = contextActions.hasPendingMessages;
207
+ this.#shutdownHandler = contextActions.shutdown;
208
+ this.#getSystemPromptFn = contextActions.getSystemPrompt;
209
209
 
210
210
  // Command context actions (optional, only for interactive mode)
211
211
  if (commandContextActions) {
212
- this.waitForIdleFn = commandContextActions.waitForIdle;
213
- this.newSessionHandler = commandContextActions.newSession;
214
- this.branchHandler = commandContextActions.branch;
215
- this.navigateTreeHandler = commandContextActions.navigateTree;
216
- this.switchSessionHandler = commandContextActions.switchSession;
217
- this.reloadHandler = commandContextActions.reload;
218
- this.getContextUsageFn = commandContextActions.getContextUsage;
219
- this.compactFn = commandContextActions.compact;
212
+ this.#waitForIdleFn = commandContextActions.waitForIdle;
213
+ this.#newSessionHandler = commandContextActions.newSession;
214
+ this.#branchHandler = commandContextActions.branch;
215
+ this.#navigateTreeHandler = commandContextActions.navigateTree;
216
+ this.#switchSessionHandler = commandContextActions.switchSession;
217
+ this.#reloadHandler = commandContextActions.reload;
218
+ this.#getContextUsageFn = commandContextActions.getContextUsage;
219
+ this.#compactFn = commandContextActions.compact;
220
220
  }
221
221
 
222
- this.uiContext = uiContext ?? noOpUIContext;
222
+ this.#uiContext = uiContext ?? noOpUIContext;
223
223
  }
224
224
 
225
225
  getUIContext(): ExtensionUIContext {
226
- return this.uiContext;
226
+ return this.#uiContext;
227
227
  }
228
228
 
229
229
  hasUI(): boolean {
230
- return this.uiContext !== noOpUIContext;
230
+ return this.#uiContext !== noOpUIContext;
231
231
  }
232
232
 
233
233
  getExtensionPaths(): string[] {
@@ -259,7 +259,7 @@ export class ExtensionRunner {
259
259
  this.runtime.flagValues.set(name, value);
260
260
  }
261
261
 
262
- private static readonly RESERVED_SHORTCUTS = new Set([
262
+ static readonly #RESERVED_SHORTCUTS = new Set([
263
263
  "ctrl+c",
264
264
  "ctrl+d",
265
265
  "ctrl+z",
@@ -282,7 +282,7 @@ export class ExtensionRunner {
282
282
  for (const [key, shortcut] of ext.shortcuts) {
283
283
  const normalizedKey = key.toLowerCase() as KeyId;
284
284
 
285
- if (ExtensionRunner.RESERVED_SHORTCUTS.has(normalizedKey)) {
285
+ if (ExtensionRunner.#RESERVED_SHORTCUTS.has(normalizedKey)) {
286
286
  logger.warn("Extension shortcut conflicts with built-in shortcut", {
287
287
  key,
288
288
  extensionPath: shortcut.extensionPath,
@@ -305,12 +305,12 @@ export class ExtensionRunner {
305
305
  }
306
306
 
307
307
  onError(listener: ExtensionErrorListener): () => void {
308
- this.errorListeners.add(listener);
309
- return () => this.errorListeners.delete(listener);
308
+ this.#errorListeners.add(listener);
309
+ return () => this.#errorListeners.delete(listener);
310
310
  }
311
311
 
312
312
  emitError(error: ExtensionError): void {
313
- for (const listener of this.errorListeners) {
313
+ for (const listener of this.#errorListeners) {
314
314
  listener(error);
315
315
  }
316
316
  }
@@ -336,14 +336,14 @@ export class ExtensionRunner {
336
336
  }
337
337
 
338
338
  getRegisteredCommands(reserved?: Set<string>): RegisteredCommand[] {
339
- this.commandDiagnostics = [];
339
+ this.#commandDiagnostics = [];
340
340
 
341
341
  const commands: RegisteredCommand[] = [];
342
342
  for (const ext of this.extensions) {
343
343
  for (const command of ext.commands.values()) {
344
344
  if (reserved?.has(command.name)) {
345
345
  const message = `Extension command '${command.name}' from ${ext.path} conflicts with built-in commands. Skipping.`;
346
- this.commandDiagnostics.push({ type: "warning", message, path: ext.path });
346
+ this.#commandDiagnostics.push({ type: "warning", message, path: ext.path });
347
347
  if (!this.hasUI()) {
348
348
  logger.warn(message);
349
349
  }
@@ -357,7 +357,7 @@ export class ExtensionRunner {
357
357
  }
358
358
 
359
359
  getCommandDiagnostics(): Array<{ type: string; message: string; path: string }> {
360
- return this.commandDiagnostics;
360
+ return this.#commandDiagnostics;
361
361
  }
362
362
 
363
363
  getCommand(name: string): RegisteredCommand | undefined {
@@ -371,11 +371,11 @@ export class ExtensionRunner {
371
371
  }
372
372
 
373
373
  createContext(): ExtensionContext {
374
- const getModel = this.getModel;
374
+ const getModel = this.#getModel;
375
375
  return {
376
- ui: this.uiContext,
377
- getContextUsage: () => this.getContextUsageFn(),
378
- compact: instructionsOrOptions => this.compactFn(instructionsOrOptions),
376
+ ui: this.#uiContext,
377
+ getContextUsage: () => this.#getContextUsageFn(),
378
+ compact: instructionsOrOptions => this.#compactFn(instructionsOrOptions),
379
379
  hasUI: this.hasUI(),
380
380
  cwd: this.cwd,
381
381
  sessionManager: this.sessionManager,
@@ -383,12 +383,12 @@ export class ExtensionRunner {
383
383
  get model() {
384
384
  return getModel();
385
385
  },
386
- isIdle: () => this.isIdleFn(),
387
- abort: () => this.abortFn(),
388
- hasPendingMessages: () => this.hasPendingMessagesFn(),
389
- shutdown: () => this.shutdownHandler(),
390
- getSystemPrompt: () => this.getSystemPromptFn(),
391
- hasQueuedMessages: () => this.hasPendingMessagesFn(), // deprecated alias
386
+ isIdle: () => this.#isIdleFn(),
387
+ abort: () => this.#abortFn(),
388
+ hasPendingMessages: () => this.#hasPendingMessagesFn(),
389
+ shutdown: () => this.#shutdownHandler(),
390
+ getSystemPrompt: () => this.#getSystemPromptFn(),
391
+ hasQueuedMessages: () => this.#hasPendingMessagesFn(), // deprecated alias
392
392
  };
393
393
  }
394
394
 
@@ -396,24 +396,24 @@ export class ExtensionRunner {
396
396
  * Request a graceful shutdown. Called by extension tools and event handlers.
397
397
  */
398
398
  shutdown(): void {
399
- this.shutdownHandler();
399
+ this.#shutdownHandler();
400
400
  }
401
401
 
402
402
  createCommandContext(): ExtensionCommandContext {
403
403
  return {
404
404
  ...this.createContext(),
405
- getContextUsage: () => this.getContextUsageFn(),
406
- waitForIdle: () => this.waitForIdleFn(),
407
- newSession: options => this.newSessionHandler(options),
408
- branch: entryId => this.branchHandler(entryId),
409
- navigateTree: (targetId, options) => this.navigateTreeHandler(targetId, options),
410
- switchSession: sessionPath => this.switchSessionHandler(sessionPath),
411
- reload: () => this.reloadHandler(),
412
- compact: instructionsOrOptions => this.compactFn(instructionsOrOptions),
405
+ getContextUsage: () => this.#getContextUsageFn(),
406
+ waitForIdle: () => this.#waitForIdleFn(),
407
+ newSession: options => this.#newSessionHandler(options),
408
+ branch: entryId => this.#branchHandler(entryId),
409
+ navigateTree: (targetId, options) => this.#navigateTreeHandler(targetId, options),
410
+ switchSession: sessionPath => this.#switchSessionHandler(sessionPath),
411
+ reload: () => this.#reloadHandler(),
412
+ compact: instructionsOrOptions => this.#compactFn(instructionsOrOptions),
413
413
  };
414
414
  }
415
415
 
416
- private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
416
+ #isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
417
417
  return (
418
418
  event.type === "session_before_switch" ||
419
419
  event.type === "session_before_branch" ||
@@ -434,7 +434,7 @@ export class ExtensionRunner {
434
434
  try {
435
435
  const handlerResult = await handler(event, ctx);
436
436
 
437
- if (this.isSessionBeforeEvent(event) && handlerResult) {
437
+ if (this.#isSessionBeforeEvent(event) && handlerResult) {
438
438
  result = handlerResult as SessionBeforeEventResult;
439
439
  if (result.cancel) {
440
440
  return result as RunnerEmitResult<TEvent>;