@selesai/code 0.13.26 → 0.13.27

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 (119) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli/args.js +1 -0
  3. package/dist/cli/session-picker.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/core/agent-session-auto-handoff.test.js +2 -1
  6. package/dist/core/agent-session-length-continuation.test.js +2 -1
  7. package/dist/core/agent-session.d.ts +58 -7
  8. package/dist/core/agent-session.js +334 -167
  9. package/dist/core/bug-report-upload.d.ts +11 -0
  10. package/dist/core/bug-report-upload.js +21 -0
  11. package/dist/core/bug-report.d.ts +175 -0
  12. package/dist/core/bug-report.js +291 -0
  13. package/dist/core/cache-stats.js +12 -1
  14. package/dist/core/cache-warmer.d.ts +101 -0
  15. package/dist/core/cache-warmer.js +341 -0
  16. package/dist/core/compaction/branch-summarization.js +2 -2
  17. package/dist/core/compaction/compaction.d.ts +6 -5
  18. package/dist/core/compaction/compaction.js +49 -42
  19. package/dist/core/crash-log.d.ts +21 -0
  20. package/dist/core/crash-log.js +70 -0
  21. package/dist/core/experimental.d.ts +0 -4
  22. package/dist/core/experimental.js +0 -4
  23. package/dist/core/extensions/index.d.ts +1 -1
  24. package/dist/core/extensions/jiti-loader.d.ts +1 -0
  25. package/dist/core/extensions/jiti-loader.js +3 -0
  26. package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
  27. package/dist/core/extensions/jiti-static-loader.js +3 -0
  28. package/dist/core/extensions/loader.js +39 -49
  29. package/dist/core/extensions/runner.d.ts +10 -7
  30. package/dist/core/extensions/runner.js +85 -67
  31. package/dist/core/extensions/types.d.ts +66 -52
  32. package/dist/core/extensions/virtual-modules.d.ts +2 -0
  33. package/dist/core/extensions/virtual-modules.js +39 -0
  34. package/dist/core/extensions/wrapper.js +1 -20
  35. package/dist/core/index.d.ts +2 -1
  36. package/dist/core/keybindings.d.ts +1 -1
  37. package/dist/core/keybindings.js +1 -1
  38. package/dist/core/messages.js +1 -0
  39. package/dist/core/model-config.d.ts +101 -20
  40. package/dist/core/model-config.js +25 -18
  41. package/dist/core/model-resolver.js +2 -1
  42. package/dist/core/model-runtime.js +5 -3
  43. package/dist/core/provider-composer.d.ts +7 -2
  44. package/dist/core/provider-composer.js +10 -5
  45. package/dist/core/radius.d.ts +3 -0
  46. package/dist/core/radius.js +6 -0
  47. package/dist/core/sdk.js +60 -38
  48. package/dist/core/session-export.d.ts +6 -2
  49. package/dist/core/session-export.js +14 -13
  50. package/dist/core/session-format.test.d.ts +1 -0
  51. package/dist/core/session-format.test.js +96 -0
  52. package/dist/core/session-manager.d.ts +29 -6
  53. package/dist/core/session-manager.js +159 -96
  54. package/dist/core/settings-manager.d.ts +22 -5
  55. package/dist/core/settings-manager.js +56 -23
  56. package/dist/core/slash-commands.js +1 -0
  57. package/dist/core/system-prompt.d.ts +49 -4
  58. package/dist/core/system-prompt.js +155 -102
  59. package/dist/core/system-prompt.test.js +61 -23
  60. package/dist/core/tools/bash.d.ts +2 -1
  61. package/dist/core/tools/bash.js +10 -4
  62. package/dist/core/tools/edit.js +1 -2
  63. package/dist/core/tools/read.js +1 -2
  64. package/dist/core/tools/renderers/bash.js +9 -1
  65. package/dist/core/tools/write.js +1 -2
  66. package/dist/core/usage-totals.d.ts +1 -1
  67. package/dist/core/usage-totals.js +5 -1
  68. package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
  69. package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
  70. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
  71. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
  72. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
  73. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
  74. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
  75. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
  76. package/dist/extensions/pi-web-agent/src/types.ts +4 -0
  77. package/dist/index.d.ts +3 -2
  78. package/dist/main.js +13 -9
  79. package/dist/migrations.js +2 -2
  80. package/dist/modes/interactive/bug-report.d.ts +15 -0
  81. package/dist/modes/interactive/bug-report.js +205 -0
  82. package/dist/modes/interactive/chat-viewport.js +1 -1
  83. package/dist/modes/interactive/components/branch-summary-message.js +12 -5
  84. package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
  85. package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
  86. package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
  87. package/dist/modes/interactive/components/extension-editor.js +7 -2
  88. package/dist/modes/interactive/components/extension-input.d.ts +2 -0
  89. package/dist/modes/interactive/components/extension-input.js +6 -0
  90. package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
  91. package/dist/modes/interactive/components/extension-selector.js +4 -0
  92. package/dist/modes/interactive/components/footer.js +4 -1
  93. package/dist/modes/interactive/components/session-selector.d.ts +5 -5
  94. package/dist/modes/interactive/components/session-selector.js +74 -48
  95. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  96. package/dist/modes/interactive/components/settings-selector.js +11 -0
  97. package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
  98. package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
  99. package/dist/modes/interactive/components/status-indicator.js +7 -7
  100. package/dist/modes/interactive/components/tool-execution.js +18 -9
  101. package/dist/modes/interactive/components/tree-selector.js +2 -0
  102. package/dist/modes/interactive/interactive-mode.d.ts +15 -2
  103. package/dist/modes/interactive/interactive-mode.js +257 -81
  104. package/dist/modes/interactive/session-share.d.ts +2 -0
  105. package/dist/modes/interactive/session-share.js +8 -4
  106. package/dist/modes/interactive/tui-renderer.js +2 -2
  107. package/dist/modes/rpc/rpc-mode.js +2 -2
  108. package/dist/utils/clipboard-command.d.ts +6 -0
  109. package/dist/utils/clipboard-command.js +44 -0
  110. package/dist/utils/clipboard-image.js +55 -89
  111. package/dist/utils/clipboard.d.ts +1 -1
  112. package/dist/utils/clipboard.js +118 -108
  113. package/dist/utils/wsl.d.ts +2 -0
  114. package/dist/utils/wsl.js +14 -0
  115. package/dist/utils/zip.d.ts +6 -0
  116. package/dist/utils/zip.js +59 -0
  117. package/package.json +9 -9
  118. package/dist/utils/clipboard-native.d.ts +0 -10
  119. package/dist/utils/clipboard-native.js +0 -19
@@ -0,0 +1,70 @@
1
+ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { getAgentDir, VERSION } from "../config.js";
4
+ const MAX_CRASH_RECORDS = 5;
5
+ const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
6
+ function crashLogPath(agentDir = getAgentDir()) {
7
+ return join(agentDir, "crashes.json");
8
+ }
9
+ export function readCrashLog(path = crashLogPath()) {
10
+ try {
11
+ const records = JSON.parse(readFileSync(path, "utf8"));
12
+ return Array.isArray(records)
13
+ ? records.filter((record) => typeof record === "object" &&
14
+ record !== null &&
15
+ typeof record.timestamp === "string" &&
16
+ typeof record.message === "string")
17
+ : [];
18
+ }
19
+ catch {
20
+ return [];
21
+ }
22
+ }
23
+ function writeCrashLog(records, path) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(path, `${JSON.stringify(records, null, 2)}\n`);
26
+ }
27
+ /** Best-effort persistence for callers that are already crashing. */
28
+ export function recordCrash(crash, path = crashLogPath()) {
29
+ try {
30
+ const { error } = crash;
31
+ const record = {
32
+ timestamp: new Date().toISOString(),
33
+ version: VERSION,
34
+ kind: crash.kind,
35
+ message: error instanceof Error ? error.message || error.name : String(error),
36
+ stack: error instanceof Error && error.stack ? error.stack : null,
37
+ sessionFile: crash.sessionFile ?? null,
38
+ cwd: crash.cwd,
39
+ };
40
+ writeCrashLog([...readCrashLog(path), record].slice(-MAX_CRASH_RECORDS), path);
41
+ return record;
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** Return the newest recent crash, marking pending records as announced. */
48
+ export function takeUnnotifiedCrash(path = crashLogPath(), now = Date.now()) {
49
+ const records = readCrashLog(path);
50
+ const crash = [...records]
51
+ .reverse()
52
+ .find((record) => !record.notified && now - Date.parse(record.timestamp) <= MAX_AGE);
53
+ if (!crash)
54
+ return undefined;
55
+ try {
56
+ writeCrashLog(records.map((record) => (record.notified ? record : { ...record, notified: true })), path);
57
+ }
58
+ catch {
59
+ // Showing the notice again is harmless.
60
+ }
61
+ return crash;
62
+ }
63
+ export function clearCrashLog(path = crashLogPath()) {
64
+ try {
65
+ rmSync(path, { force: true });
66
+ }
67
+ catch {
68
+ // The records can be attached again if cleanup fails.
69
+ }
70
+ }
@@ -1,5 +1 @@
1
1
  export declare function areExperimentalFeaturesEnabled(): boolean;
2
- export declare function getExperimentalToolSampling(): {
3
- readonly type: "json_schema";
4
- readonly strict: "prefer";
5
- } | undefined;
@@ -1,7 +1,3 @@
1
- const PREFER_STRICT_TOOL_SAMPLING = { type: "json_schema", strict: "prefer" };
2
1
  export function areExperimentalFeaturesEnabled() {
3
2
  return process.env.PI_EXPERIMENTAL === "1";
4
3
  }
5
- export function getExperimentalToolSampling() {
6
- return areExperimentalFeaturesEnabled() ? PREFER_STRICT_TOOL_SAMPLING : undefined;
7
- }
@@ -6,6 +6,6 @@ export type { SourceInfo } from "../source-info.ts";
6
6
  export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.ts";
7
7
  export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.ts";
8
8
  export { ExtensionRunner } from "./runner.ts";
9
- export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResolvedSkillInfo, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
9
+ export type { AfterProviderResponseEvent, AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CacheWarmingDecisionEvent, CacheWarmingDecisionEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditorFactory, EditToolCallEvent, EditToolResultEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageEndEventResult, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, MarkdownTransformContext, MarkdownTransformer, NormalizedBuildSystemPromptOptions, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResolvedSkillInfo, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionCompactFailedEvent, SessionEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ThinkingLevelSelectEvent, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, WriteToolResultEvent, } from "./types.ts";
10
10
  export { defineTool, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.ts";
11
11
  export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
@@ -0,0 +1 @@
1
+ export { createJiti } from "jiti";
@@ -0,0 +1,3 @@
1
+ // Normal Node runtimes use jiti's lazy transform so Babel is loaded only when
2
+ // native loading fails and an extension needs transformation.
3
+ export { createJiti } from "jiti";
@@ -0,0 +1 @@
1
+ export { createJiti } from "jiti/static";
@@ -0,0 +1,3 @@
1
+ // Compiled binaries need jiti's static entry so Bun and SEA bundlers embed the
2
+ // Babel transform. The module itself remains lazy until an extension is loaded.
3
+ export { createJiti } from "jiti/static";
@@ -6,58 +6,27 @@ import * as fs from "node:fs";
6
6
  import { createRequire } from "node:module";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
10
- import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat";
11
- import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
12
- import * as _bundledPiAiProviders from "@earendil-works/pi-ai/providers/all";
13
- import * as _bundledPiTui from "@earendil-works/pi-tui";
14
- import { createJiti } from "jiti/static";
15
- // Static imports of packages that extensions may use.
16
- // These MUST be static so Bun bundles them into the compiled binary.
17
- // The virtualModules option then makes them available to extensions.
18
- import * as _bundledTypebox from "typebox";
19
- import * as _bundledTypeboxCompile from "typebox/compile";
20
- import * as _bundledTypeboxValue from "typebox/value";
21
- import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
22
- // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
23
- // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
24
- import * as _bundledPiCodingAgent from "../../index.js";
9
+ import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.js";
25
10
  import { resolvePath } from "../../utils/paths.js";
26
11
  import { createEventBus } from "../event-bus.js";
27
12
  import { execCommand } from "../exec.js";
28
13
  import { createSyntheticSourceInfo } from "../source-info.js";
29
14
  import { time } from "../timings.js";
30
- /** Modules available to extensions via virtualModules (for compiled Bun binary) */
31
- const VIRTUAL_MODULES = {
32
- typebox: _bundledTypebox,
33
- "typebox/compile": _bundledTypeboxCompile,
34
- "typebox/value": _bundledTypeboxValue,
35
- "@sinclair/typebox": _bundledTypebox,
36
- "@sinclair/typebox/compile": _bundledTypeboxCompile,
37
- "@sinclair/typebox/value": _bundledTypeboxValue,
38
- "@earendil-works/pi-agent-core": _bundledPiAgentCore,
39
- "@earendil-works/pi-tui": _bundledPiTui,
40
- // Extensions resolve the pi-ai root to the compat entrypoint (a strict
41
- // superset of the core entrypoint): existing extensions using the old
42
- // global API keep working at runtime until compat is removed.
43
- "@earendil-works/pi-ai": _bundledPiAiCompat,
44
- "@earendil-works/pi-ai/compat": _bundledPiAiCompat,
45
- "@earendil-works/pi-ai/oauth": _bundledPiAiOauth,
46
- "@earendil-works/pi-ai/providers/all": _bundledPiAiProviders,
47
- "@earendil-works/pi-coding-agent": _bundledPiCodingAgent,
48
- "@selesai/code": _bundledPiCodingAgent,
49
- "@mariozechner/pi-agent-core": _bundledPiAgentCore,
50
- "@mariozechner/pi-tui": _bundledPiTui,
51
- "@mariozechner/pi-ai": _bundledPiAiCompat,
52
- "@mariozechner/pi-ai/compat": _bundledPiAiCompat,
53
- "@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
54
- "@mariozechner/pi-ai/providers/all": _bundledPiAiProviders,
55
- "@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
56
- };
57
15
  const require = createRequire(import.meta.url);
58
16
  const isNodeSeaBinary = ("sea" in process.features && process.features.sea === true) ||
59
17
  process.getBuiltinModule("node:sea")?.isSea() === true;
60
18
  const isTypeScriptSourceRuntime = !isBunBinary && path.extname(fileURLToPath(import.meta.url)) === ".ts";
19
+ const usesEmbeddedModules = isBunBinary || isNodeSeaBinary || isBundledNode;
20
+ let createJitiPromise;
21
+ function getCreateJiti() {
22
+ createJitiPromise ??= (usesEmbeddedModules ? import("./jiti-static-loader.js") : import("./jiti-loader.js")).then((module) => module.createJiti);
23
+ return createJitiPromise;
24
+ }
25
+ let virtualModulesPromise;
26
+ function getVirtualModules() {
27
+ virtualModulesPromise ??= import("./virtual-modules.js").then((module) => module.VIRTUAL_MODULES);
28
+ return virtualModulesPromise;
29
+ }
61
30
  /**
62
31
  * Get aliases for jiti (used in Node.js/development mode).
63
32
  * In Bun binary mode, virtualModules is used instead.
@@ -210,12 +179,27 @@ function createExtensionAPI(extension, runtime, cwd, eventBus) {
210
179
  // Registration methods - write to extension
211
180
  on(event, handler) {
212
181
  assertActive();
182
+ const registeredHandler = (...args) => handler(...args);
213
183
  const list = extension.handlers.get(event) ?? [];
214
- list.push(handler);
184
+ list.push(registeredHandler);
215
185
  extension.handlers.set(event, list);
186
+ return () => {
187
+ const handlers = extension.handlers.get(event);
188
+ if (!handlers)
189
+ return;
190
+ const handlerIndex = handlers.indexOf(registeredHandler);
191
+ if (handlerIndex === -1)
192
+ return;
193
+ handlers.splice(handlerIndex, 1);
194
+ if (handlers.length === 0)
195
+ extension.handlers.delete(event);
196
+ };
216
197
  },
217
198
  registerTool(tool) {
218
199
  assertActive();
200
+ if (typeof tool.parameters !== "object" || tool.parameters === null || Array.isArray(tool.parameters)) {
201
+ throw new Error(`Tool "${tool.name}" registered by extension "${extension.path}" must define an object parameter schema.`);
202
+ }
219
203
  extension.tools.set(tool.name, {
220
204
  definition: tool,
221
205
  sourceInfo: extension.sourceInfo,
@@ -396,12 +380,18 @@ async function loadExtensionModule(extensionPath, cacheToken) {
396
380
  return cachedFactory;
397
381
  }
398
382
  }
399
- const jiti = createJiti(import.meta.url, {
383
+ const createJitiImpl = await getCreateJiti();
384
+ // Compiled binaries and the bundled Node distribution use embedded modules.
385
+ // Source TypeScript reuses host modules and root tsconfig paths. Unbundled
386
+ // Node builds use dist aliases and do not need the bundled virtual modules.
387
+ const resolutionOptions = usesEmbeddedModules
388
+ ? { virtualModules: await getVirtualModules(), tryNative: false }
389
+ : isTypeScriptSourceRuntime
390
+ ? { virtualModules: await getVirtualModules(), tsconfigPaths: true }
391
+ : { alias: getAliases() };
392
+ const jiti = createJitiImpl(import.meta.url, {
400
393
  moduleCache: false,
401
- // In Bun binary: use virtualModules for bundled packages (no filesystem resolution)
402
- // Also disable tryNative so jiti handles ALL imports (not just the entry point)
403
- // In Node.js/dev: use aliases to resolve to node_modules paths
404
- ...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),
394
+ ...resolutionOptions,
405
395
  });
406
396
  const module = await jiti.import(extensionPath, { default: true });
407
397
  const factory = module;
@@ -4,22 +4,23 @@
4
4
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
5
5
  import type { ImageContent, Provider, ProviderHeaders } from "@earendil-works/pi-ai";
6
6
  import type { KeyId } from "@earendil-works/pi-tui";
7
+ import type { CacheWarmingAction } from "../cache-warmer.ts";
7
8
  import type { ResourceDiagnostic } from "../diagnostics.ts";
8
9
  import type { KeybindingsConfig } from "../keybindings.ts";
9
10
  import type { ModelRegistry } from "../model-registry.ts";
10
11
  import type { SessionManager } from "../session-manager.ts";
11
- import type { BuildSystemPromptOptions } from "../system-prompt.ts";
12
- import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, ContextEvent, EntryRenderer, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, LoadExtensionsResult, MessageEndEvent, MessageRenderer, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventResult, ProviderConfig, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, SessionShutdownEvent, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.ts";
13
- /** Combined result from all before_agent_start handlers */
12
+ import { type BuildSystemPromptOptions, type NormalizedBuildSystemPromptOptions } from "../system-prompt.ts";
13
+ import type { BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, CacheWarmingDecisionEvent, ContextEvent, EntryRenderer, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, InputSource, LoadExtensionsResult, MessageEndEvent, MessageRenderer, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventResult, ProviderConfig, RegisteredTool, ReplacedSessionContext, ResolvedCommand, ResourcesDiscoverEvent, SessionBeforeCompactResult, SessionBeforeForkResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, SessionShutdownEvent, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult } from "./types.ts";
14
+ /** Combined result from all before_agent_start handlers. */
14
15
  interface BeforeAgentStartCombinedResult {
15
- messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
16
- systemPrompt?: string;
16
+ messages: NonNullable<BeforeAgentStartEventResult["message"]>[];
17
+ systemPromptOptions: NormalizedBuildSystemPromptOptions;
17
18
  }
18
19
  /**
19
20
  * Events handled by the generic emit() method.
20
21
  * Events with dedicated emitXxx() methods are excluded for stronger type safety.
21
22
  */
22
- type RunnerEmitEvent = Exclude<ExtensionEvent, ToolCallEvent | ProjectTrustEvent | ToolResultEvent | UserBashEvent | ContextEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent | BeforeAgentStartEvent | MessageEndEvent | ResourcesDiscoverEvent | InputEvent>;
23
+ type RunnerEmitEvent = Exclude<ExtensionEvent, ToolCallEvent | ProjectTrustEvent | ToolResultEvent | UserBashEvent | ContextEvent | CacheWarmingDecisionEvent | BeforeProviderRequestEvent | BeforeProviderHeadersEvent | BeforeAgentStartEvent | MessageEndEvent | ResourcesDiscoverEvent | InputEvent>;
23
24
  type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends {
24
25
  type: "session_before_switch";
25
26
  } ? SessionBeforeSwitchResult | undefined : TEvent extends {
@@ -148,6 +149,8 @@ export declare class ExtensionRunner {
148
149
  createCommandContext(): ExtensionCommandContext;
149
150
  private isSessionBeforeEvent;
150
151
  emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>>;
152
+ /** Returns the event's own action unless a handler overrides it; the last override wins. */
153
+ emitCacheWarmingDecision(event: CacheWarmingDecisionEvent): Promise<CacheWarmingAction>;
151
154
  emitMessageEnd(event: MessageEndEvent): Promise<AgentMessage | undefined>;
152
155
  emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined>;
153
156
  emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined>;
@@ -155,7 +158,7 @@ export declare class ExtensionRunner {
155
158
  emitContext(messages: AgentMessage[]): Promise<AgentMessage[]>;
156
159
  emitBeforeProviderRequest(payload: unknown): Promise<unknown>;
157
160
  emitBeforeProviderHeaders(headers: ProviderHeaders): Promise<ProviderHeaders>;
158
- emitBeforeAgentStart(prompt: string, images: ImageContent[] | undefined, systemPrompt: string, systemPromptOptions: BuildSystemPromptOptions): Promise<BeforeAgentStartCombinedResult | undefined>;
161
+ emitBeforeAgentStart(prompt: string, images: ImageContent[] | undefined, systemPromptOptions: BuildSystemPromptOptions): Promise<BeforeAgentStartCombinedResult>;
159
162
  emitResourcesDiscover(cwd: string, reason: ResourcesDiscoverEvent["reason"]): Promise<{
160
163
  skillPaths: Array<{
161
164
  path: string;
@@ -2,6 +2,7 @@
2
2
  * Extension runner - executes extensions and manages their lifecycle.
3
3
  */
4
4
  import { theme } from "../../modes/interactive/theme/theme.js";
5
+ import { buildSystemPrompt, normalizeBuildSystemPromptOptions, } from "../system-prompt.js";
5
6
  // Extension shortcuts compete with canonical keybinding ids from keybindings.json.
6
7
  // Only editor-global shortcuts are reserved here. Picker-specific bindings are not.
7
8
  const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [
@@ -46,6 +47,31 @@ const buildBuiltinKeybindings = (resolvedKeybindings) => {
46
47
  }
47
48
  return builtinKeybindings;
48
49
  };
50
+ function isUserBashEventResult(value) {
51
+ if (typeof value !== "object" || value === null)
52
+ return false;
53
+ const candidate = value;
54
+ const hasOperations = candidate.operations !== undefined;
55
+ const hasResult = candidate.result !== undefined;
56
+ if (hasOperations === hasResult)
57
+ return false;
58
+ if (hasOperations) {
59
+ const operations = candidate.operations;
60
+ if (typeof operations !== "object" || operations === null)
61
+ return false;
62
+ return typeof operations.exec === "function";
63
+ }
64
+ const result = candidate.result;
65
+ if (typeof result !== "object" || result === null)
66
+ return false;
67
+ const resultRecord = result;
68
+ return (typeof resultRecord.output === "string" &&
69
+ "exitCode" in resultRecord &&
70
+ (resultRecord.exitCode === undefined || typeof resultRecord.exitCode === "number") &&
71
+ typeof resultRecord.cancelled === "boolean" &&
72
+ typeof resultRecord.truncated === "boolean" &&
73
+ (resultRecord.fullOutputPath === undefined || typeof resultRecord.fullOutputPath === "string"));
74
+ }
49
75
  /**
50
76
  * Helper function to emit session_shutdown event to extensions.
51
77
  * Returns true if the event was emitted, false if there were no handlers.
@@ -57,14 +83,14 @@ export async function emitSessionShutdownEvent(extensionRunner, event) {
57
83
  }
58
84
  return false;
59
85
  }
86
+ function snapshotEventHandlers(extensions, event) {
87
+ return extensions.map((ext) => ({ ext, handlers: ext.handlers.get(event)?.slice() ?? [] }));
88
+ }
60
89
  export async function emitProjectTrustEvent(extensionsResult, event, ctx) {
61
90
  const errors = [];
62
- for (const ext of extensionsResult.extensions) {
91
+ for (const { ext, handlers } of snapshotEventHandlers(extensionsResult.extensions, "project_trust")) {
63
92
  // A single extension may register multiple handlers for the same event.
64
93
  // The first project_trust handler that returns yes/no wins; undecided falls through.
65
- const handlers = ext.handlers.get("project_trust");
66
- if (!handlers || handlers.length === 0)
67
- continue;
68
94
  for (const handler of handlers) {
69
95
  try {
70
96
  const handlerResult = (await handler(event, ctx));
@@ -139,7 +165,7 @@ export class ExtensionRunner {
139
165
  getContextUsageFn = () => undefined;
140
166
  compactFn = () => { };
141
167
  getSystemPromptFn = () => "";
142
- getSystemPromptOptionsFn = () => ({ cwd: this.cwd });
168
+ getSystemPromptOptionsFn = () => normalizeBuildSystemPromptOptions({ cwd: this.cwd });
143
169
  newSessionHandler = async () => ({ cancelled: false });
144
170
  forkHandler = async () => ({ cancelled: false });
145
171
  navigateTreeHandler = async () => ({ cancelled: false });
@@ -187,7 +213,8 @@ export class ExtensionRunner {
187
213
  this.getContextUsageFn = contextActions.getContextUsage;
188
214
  this.compactFn = contextActions.compact;
189
215
  this.getSystemPromptFn = contextActions.getSystemPrompt;
190
- this.getSystemPromptOptionsFn = contextActions.getSystemPromptOptions ?? (() => ({ cwd: this.cwd }));
216
+ this.getSystemPromptOptionsFn =
217
+ contextActions.getSystemPromptOptions ?? (() => normalizeBuildSystemPromptOptions({ cwd: this.cwd }));
191
218
  // Flush provider registrations queued during extension loading
192
219
  for (const { name, config, extensionPath } of this.runtime.pendingProviderRegistrations) {
193
220
  try {
@@ -625,10 +652,7 @@ export class ExtensionRunner {
625
652
  async emit(event) {
626
653
  const ctx = this.createContext();
627
654
  let result;
628
- for (const ext of this.extensions) {
629
- const handlers = ext.handlers.get(event.type);
630
- if (!handlers || handlers.length === 0)
631
- continue;
655
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, event.type)) {
632
656
  for (const handler of handlers) {
633
657
  try {
634
658
  const handlerResult = await handler(event, ctx);
@@ -653,14 +677,34 @@ export class ExtensionRunner {
653
677
  }
654
678
  return result;
655
679
  }
680
+ /** Returns the event's own action unless a handler overrides it; the last override wins. */
681
+ async emitCacheWarmingDecision(event) {
682
+ const ctx = this.createContext();
683
+ let action = event.action;
684
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, event.type)) {
685
+ for (const handler of handlers) {
686
+ try {
687
+ const result = (await handler(event, ctx));
688
+ if (result?.action !== undefined)
689
+ action = result.action;
690
+ }
691
+ catch (err) {
692
+ this.emitError({
693
+ extensionPath: ext.path,
694
+ event: event.type,
695
+ error: err instanceof Error ? err.message : String(err),
696
+ stack: err instanceof Error ? err.stack : undefined,
697
+ });
698
+ }
699
+ }
700
+ }
701
+ return action;
702
+ }
656
703
  async emitMessageEnd(event) {
657
704
  const ctx = this.createContext();
658
705
  let currentMessage = event.message;
659
706
  let modified = false;
660
- for (const ext of this.extensions) {
661
- const handlers = ext.handlers.get("message_end");
662
- if (!handlers || handlers.length === 0)
663
- continue;
707
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "message_end")) {
664
708
  for (const handler of handlers) {
665
709
  try {
666
710
  const currentEvent = { ...event, message: currentMessage };
@@ -697,10 +741,7 @@ export class ExtensionRunner {
697
741
  const currentEvent = { ...event };
698
742
  let modified = false;
699
743
  let terminate;
700
- for (const ext of this.extensions) {
701
- const handlers = ext.handlers.get("tool_result");
702
- if (!handlers || handlers.length === 0)
703
- continue;
744
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "tool_result")) {
704
745
  for (const handler of handlers) {
705
746
  try {
706
747
  const handlerResult = (await handler(currentEvent, ctx));
@@ -753,10 +794,7 @@ export class ExtensionRunner {
753
794
  async emitToolCall(event) {
754
795
  const ctx = this.createContext();
755
796
  let result;
756
- for (const ext of this.extensions) {
757
- const handlers = ext.handlers.get("tool_call");
758
- if (!handlers || handlers.length === 0)
759
- continue;
797
+ for (const { handlers } of snapshotEventHandlers(this.extensions, "tool_call")) {
760
798
  for (const handler of handlers) {
761
799
  const handlerResult = await handler(event, ctx);
762
800
  if (handlerResult) {
@@ -771,16 +809,16 @@ export class ExtensionRunner {
771
809
  }
772
810
  async emitUserBash(event) {
773
811
  const ctx = this.createContext();
774
- for (const ext of this.extensions) {
775
- const handlers = ext.handlers.get("user_bash");
776
- if (!handlers || handlers.length === 0)
777
- continue;
812
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "user_bash")) {
778
813
  for (const handler of handlers) {
779
814
  try {
780
815
  const handlerResult = await handler(event, ctx);
781
- if (handlerResult) {
782
- return handlerResult;
816
+ if (handlerResult === undefined)
817
+ continue;
818
+ if (!isUserBashEventResult(handlerResult)) {
819
+ throw new Error("Invalid user_bash handler result: return undefined for local execution or exactly one valid { operations } or { result } object");
783
820
  }
821
+ return handlerResult;
784
822
  }
785
823
  catch (err) {
786
824
  const message = err instanceof Error ? err.message : String(err);
@@ -791,6 +829,7 @@ export class ExtensionRunner {
791
829
  error: message,
792
830
  stack,
793
831
  });
832
+ throw err;
794
833
  }
795
834
  }
796
835
  }
@@ -799,10 +838,7 @@ export class ExtensionRunner {
799
838
  async emitContext(messages) {
800
839
  const ctx = this.createContext();
801
840
  let currentMessages = structuredClone(messages);
802
- for (const ext of this.extensions) {
803
- const handlers = ext.handlers.get("context");
804
- if (!handlers || handlers.length === 0)
805
- continue;
841
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "context")) {
806
842
  for (const handler of handlers) {
807
843
  try {
808
844
  const event = { type: "context", messages: currentMessages };
@@ -828,10 +864,7 @@ export class ExtensionRunner {
828
864
  async emitBeforeProviderRequest(payload) {
829
865
  const ctx = this.createContext();
830
866
  let currentPayload = payload;
831
- for (const ext of this.extensions) {
832
- const handlers = ext.handlers.get("before_provider_request");
833
- if (!handlers || handlers.length === 0)
834
- continue;
867
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "before_provider_request")) {
835
868
  for (const handler of handlers) {
836
869
  try {
837
870
  const event = {
@@ -859,10 +892,7 @@ export class ExtensionRunner {
859
892
  }
860
893
  async emitBeforeProviderHeaders(headers) {
861
894
  const ctx = this.createContext();
862
- for (const ext of this.extensions) {
863
- const handlers = ext.handlers.get("before_provider_headers");
864
- if (!handlers || handlers.length === 0)
865
- continue;
895
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "before_provider_headers")) {
866
896
  for (const handler of handlers) {
867
897
  try {
868
898
  // Handlers mutate `headers` in place; the return value is ignored.
@@ -886,37 +916,34 @@ export class ExtensionRunner {
886
916
  }
887
917
  return headers;
888
918
  }
889
- async emitBeforeAgentStart(prompt, images, systemPrompt, systemPromptOptions) {
890
- let currentSystemPrompt = systemPrompt;
919
+ async emitBeforeAgentStart(prompt, images, systemPromptOptions) {
920
+ const currentOptions = normalizeBuildSystemPromptOptions(systemPromptOptions);
921
+ const renderCurrentSystemPrompt = () => buildSystemPrompt(currentOptions);
891
922
  const ctx = Object.defineProperties({}, Object.getOwnPropertyDescriptors(this.createContext()));
892
923
  ctx.getSystemPrompt = () => {
893
924
  this.assertActive();
894
- return currentSystemPrompt;
925
+ return renderCurrentSystemPrompt();
895
926
  };
896
927
  const messages = [];
897
- let systemPromptModified = false;
898
- for (const ext of this.extensions) {
899
- const handlers = ext.handlers.get("before_agent_start");
900
- if (!handlers || handlers.length === 0)
901
- continue;
928
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "before_agent_start")) {
902
929
  for (const handler of handlers) {
903
930
  try {
904
931
  const event = {
905
932
  type: "before_agent_start",
906
933
  prompt,
907
934
  images,
908
- systemPrompt: currentSystemPrompt,
909
- systemPromptOptions,
935
+ get systemPrompt() {
936
+ return renderCurrentSystemPrompt();
937
+ },
938
+ systemPromptOptions: currentOptions,
910
939
  };
911
940
  const handlerResult = await handler(event, ctx);
912
941
  if (handlerResult) {
913
942
  const result = handlerResult;
914
- if (result.message) {
943
+ if (result.message)
915
944
  messages.push(result.message);
916
- }
917
945
  if (result.systemPrompt !== undefined) {
918
- currentSystemPrompt = result.systemPrompt;
919
- systemPromptModified = true;
946
+ currentOptions.forceSystemPrompt = result.systemPrompt;
920
947
  }
921
948
  }
922
949
  }
@@ -932,23 +959,14 @@ export class ExtensionRunner {
932
959
  }
933
960
  }
934
961
  }
935
- if (messages.length > 0 || systemPromptModified) {
936
- return {
937
- messages: messages.length > 0 ? messages : undefined,
938
- systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
939
- };
940
- }
941
- return undefined;
962
+ return { messages, systemPromptOptions: currentOptions };
942
963
  }
943
964
  async emitResourcesDiscover(cwd, reason) {
944
965
  const ctx = this.createContext();
945
966
  const skillPaths = [];
946
967
  const promptPaths = [];
947
968
  const themePaths = [];
948
- for (const ext of this.extensions) {
949
- const handlers = ext.handlers.get("resources_discover");
950
- if (!handlers || handlers.length === 0)
951
- continue;
969
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "resources_discover")) {
952
970
  for (const handler of handlers) {
953
971
  try {
954
972
  const event = { type: "resources_discover", cwd, reason };
@@ -983,8 +1001,8 @@ export class ExtensionRunner {
983
1001
  const ctx = this.createContext();
984
1002
  let currentText = text;
985
1003
  let currentImages = images;
986
- for (const ext of this.extensions) {
987
- for (const handler of ext.handlers.get("input") ?? []) {
1004
+ for (const { ext, handlers } of snapshotEventHandlers(this.extensions, "input")) {
1005
+ for (const handler of handlers) {
988
1006
  try {
989
1007
  const event = {
990
1008
  type: "input",