@codex-infinity/pi-infinity 0.63.3 → 0.64.2

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 (112) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +7 -3
  3. package/dist/core/agent-session-runtime.d.ts +134 -0
  4. package/dist/core/agent-session-runtime.d.ts.map +1 -0
  5. package/dist/core/agent-session-runtime.js +262 -0
  6. package/dist/core/agent-session-runtime.js.map +1 -0
  7. package/dist/core/agent-session.d.ts +10 -42
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +56 -236
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/export-html/tool-renderer.d.ts +2 -0
  12. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  13. package/dist/core/export-html/tool-renderer.js +2 -2
  14. package/dist/core/export-html/tool-renderer.js.map +1 -1
  15. package/dist/core/extensions/index.d.ts +1 -1
  16. package/dist/core/extensions/index.d.ts.map +1 -1
  17. package/dist/core/extensions/index.js.map +1 -1
  18. package/dist/core/extensions/runner.d.ts.map +1 -1
  19. package/dist/core/extensions/runner.js +1 -0
  20. package/dist/core/extensions/runner.js.map +1 -1
  21. package/dist/core/extensions/types.d.ts +11 -16
  22. package/dist/core/extensions/types.d.ts.map +1 -1
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/core/footer-data-provider.d.ts +5 -1
  25. package/dist/core/footer-data-provider.d.ts.map +1 -1
  26. package/dist/core/footer-data-provider.js +69 -8
  27. package/dist/core/footer-data-provider.js.map +1 -1
  28. package/dist/core/index.d.ts +2 -1
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js +1 -0
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/keybindings.d.ts +10 -0
  33. package/dist/core/keybindings.d.ts.map +1 -1
  34. package/dist/core/keybindings.js +10 -0
  35. package/dist/core/keybindings.js.map +1 -1
  36. package/dist/core/model-registry.d.ts +3 -1
  37. package/dist/core/model-registry.d.ts.map +1 -1
  38. package/dist/core/model-registry.js +7 -1
  39. package/dist/core/model-registry.js.map +1 -1
  40. package/dist/core/sdk.d.ts +5 -2
  41. package/dist/core/sdk.d.ts.map +1 -1
  42. package/dist/core/sdk.js +5 -2
  43. package/dist/core/sdk.js.map +1 -1
  44. package/dist/core/session-manager.d.ts +3 -0
  45. package/dist/core/session-manager.d.ts.map +1 -1
  46. package/dist/core/session-manager.js +13 -6
  47. package/dist/core/session-manager.js.map +1 -1
  48. package/dist/core/tools/edit.d.ts.map +1 -1
  49. package/dist/core/tools/edit.js +15 -4
  50. package/dist/core/tools/edit.js.map +1 -1
  51. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  52. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  53. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +2 -2
  57. package/dist/index.js.map +1 -1
  58. package/dist/main.d.ts.map +1 -1
  59. package/dist/main.js +42 -10
  60. package/dist/main.js.map +1 -1
  61. package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  62. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  63. package/dist/modes/interactive/components/assistant-message.js +13 -3
  64. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  65. package/dist/modes/interactive/components/footer.d.ts +1 -0
  66. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/footer.js +4 -1
  68. package/dist/modes/interactive/components/footer.js.map +1 -1
  69. package/dist/modes/interactive/components/tree-selector.d.ts +4 -2
  70. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/tree-selector.js +48 -15
  72. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.d.ts +11 -4
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +112 -89
  76. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  77. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  78. package/dist/modes/interactive/theme/theme.js +6 -11
  79. package/dist/modes/interactive/theme/theme.js.map +1 -1
  80. package/dist/modes/print-mode.d.ts +2 -2
  81. package/dist/modes/print-mode.d.ts.map +1 -1
  82. package/dist/modes/print-mode.js +37 -36
  83. package/dist/modes/print-mode.js.map +1 -1
  84. package/dist/modes/rpc/rpc-mode.d.ts +2 -2
  85. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  86. package/dist/modes/rpc/rpc-mode.js +72 -49
  87. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  88. package/docs/extensions.md +104 -19
  89. package/docs/json.md +5 -2
  90. package/docs/keybindings.md +2 -0
  91. package/docs/rpc.md +21 -7
  92. package/docs/sdk.md +168 -75
  93. package/docs/tree.md +6 -3
  94. package/examples/extensions/README.md +1 -0
  95. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  96. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  97. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  98. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  99. package/examples/extensions/hidden-thinking-label.ts +53 -0
  100. package/examples/extensions/rpc-demo.ts +3 -9
  101. package/examples/extensions/status-line.ts +0 -8
  102. package/examples/extensions/todo.ts +0 -2
  103. package/examples/extensions/tools.ts +0 -5
  104. package/examples/extensions/widget-placement.ts +4 -12
  105. package/examples/extensions/with-deps/package-lock.json +2 -2
  106. package/examples/extensions/with-deps/package.json +1 -1
  107. package/examples/sdk/02-custom-model.ts +1 -1
  108. package/examples/sdk/09-api-keys-and-oauth.ts +3 -3
  109. package/examples/sdk/12-full-control.ts +1 -1
  110. package/examples/sdk/13-session-runtime.ts +49 -0
  111. package/examples/sdk/README.md +5 -4
  112. package/package.json +4 -4
@@ -12,14 +12,14 @@
12
12
  *
13
13
  * Modes use this class and add their own I/O layer on top.
14
14
  */
15
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { basename, dirname, join, resolve } from "node:path";
17
17
  import { isContextOverflow, modelsAreEqual, resetApiProviders, supportsXhigh } from "@mariozechner/pi-ai";
18
18
  import { getDocsPath } from "../config.js";
19
19
  import { theme } from "../modes/interactive/theme/theme.js";
20
20
  import { stripFrontmatter } from "../utils/frontmatter.js";
21
21
  import { sleep } from "../utils/sleep.js";
22
- import { executeBash as executeBashCommand, executeBashWithOperations } from "./bash-executor.js";
22
+ import { executeBashWithOperations } from "./bash-executor.js";
23
23
  import { calculateContextTokens, collectEntriesForBranchSummary, compact, estimateContextTokens, generateBranchSummary, prepareCompaction, shouldCompact, } from "./compaction/index.js";
24
24
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
25
25
  import { exportSessionToHtml } from "./export-html/index.js";
@@ -29,6 +29,7 @@ import { expandPromptTemplate } from "./prompt-templates.js";
29
29
  import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-manager.js";
30
30
  import { createSyntheticSourceInfo } from "./source-info.js";
31
31
  import { buildSystemPrompt } from "./system-prompt.js";
32
+ import { createLocalBashOperations } from "./tools/bash.js";
32
33
  import { createAllToolDefinitions } from "./tools/index.js";
33
34
  import { createToolDefinitionFromAgentTool, wrapToolDefinition } from "./tools/tool-definition-wrapper.js";
34
35
  /**
@@ -121,6 +122,7 @@ export class AgentSession {
121
122
  this._baseToolsOverride = config.baseToolsOverride;
122
123
  this._autoNextSteps = config.autoNextSteps ?? false;
123
124
  this._autoNextIdea = config.autoNextIdea ?? false;
125
+ this._sessionStartEvent = config.sessionStartEvent ?? { type: "session_start", reason: "startup" };
124
126
  // Always subscribe to agent events for internal handling
125
127
  // (session persistence, extensions, auto-compaction, retry logic)
126
128
  this._unsubscribeAgent = this.agent.subscribe(this._handleAgentEvent);
@@ -160,7 +162,7 @@ export class AgentSession {
160
162
  * happens here instead of in wrappers.
161
163
  */
162
164
  _installAgentToolHooks() {
163
- this.agent.setBeforeToolCall(async ({ toolCall, args }) => {
165
+ this.agent.beforeToolCall = async ({ toolCall, args }) => {
164
166
  const runner = this._extensionRunner;
165
167
  if (!runner?.hasHandlers("tool_call")) {
166
168
  return undefined;
@@ -180,8 +182,8 @@ export class AgentSession {
180
182
  }
181
183
  throw new Error(`Extension failed, blocking execution: ${String(err)}`);
182
184
  }
183
- });
184
- this.agent.setAfterToolCall(async ({ toolCall, args, result, isError }) => {
185
+ };
186
+ this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => {
185
187
  const runner = this._extensionRunner;
186
188
  if (!runner?.hasHandlers("tool_result")) {
187
189
  return undefined;
@@ -202,7 +204,7 @@ export class AgentSession {
202
204
  content: hookResult.content,
203
205
  details: hookResult.details,
204
206
  };
205
- });
207
+ };
206
208
  }
207
209
  // =========================================================================
208
210
  // Event Subscription
@@ -213,6 +215,13 @@ export class AgentSession {
213
215
  l(event);
214
216
  }
215
217
  }
218
+ _emitQueueUpdate() {
219
+ this._emit({
220
+ type: "queue_update",
221
+ steering: [...this._steeringMessages],
222
+ followUp: [...this._followUpMessages],
223
+ });
224
+ }
216
225
  _createRetryPromiseForAgentEnd(event) {
217
226
  if (event.type !== "agent_end" || this._retryPromise) {
218
227
  return;
@@ -249,12 +258,14 @@ export class AgentSession {
249
258
  const steeringIndex = this._steeringMessages.indexOf(messageText);
250
259
  if (steeringIndex !== -1) {
251
260
  this._steeringMessages.splice(steeringIndex, 1);
261
+ this._emitQueueUpdate();
252
262
  }
253
263
  else {
254
264
  // Check follow-up queue
255
265
  const followUpIndex = this._followUpMessages.indexOf(messageText);
256
266
  if (followUpIndex !== -1) {
257
267
  this._followUpMessages.splice(followUpIndex, 1);
268
+ this._emitQueueUpdate();
258
269
  }
259
270
  }
260
271
  }
@@ -529,10 +540,10 @@ export class AgentSession {
529
540
  validToolNames.push(name);
530
541
  }
531
542
  }
532
- this.agent.setTools(tools);
543
+ this.agent.state.tools = tools;
533
544
  // Rebuild base system prompt with new tool set
534
545
  this._baseSystemPrompt = this._rebuildSystemPrompt(validToolNames);
535
- this.agent.setSystemPrompt(this._baseSystemPrompt);
546
+ this.agent.state.systemPrompt = this._baseSystemPrompt;
536
547
  }
537
548
  /** Whether compaction or branch summarization is currently running */
538
549
  get isCompacting() {
@@ -546,11 +557,11 @@ export class AgentSession {
546
557
  }
547
558
  /** Current steering mode */
548
559
  get steeringMode() {
549
- return this.agent.getSteeringMode();
560
+ return this.agent.steeringMode;
550
561
  }
551
562
  /** Current follow-up mode */
552
563
  get followUpMode() {
553
- return this.agent.getFollowUpMode();
564
+ return this.agent.followUpMode;
554
565
  }
555
566
  /** Current session file path, or undefined if sessions are disabled */
556
567
  get sessionFile() {
@@ -741,11 +752,11 @@ export class AgentSession {
741
752
  }
742
753
  // Apply extension-modified system prompt, or reset to base
743
754
  if (result?.systemPrompt) {
744
- this.agent.setSystemPrompt(result.systemPrompt);
755
+ this.agent.state.systemPrompt = result.systemPrompt;
745
756
  }
746
757
  else {
747
758
  // Ensure we're using the base prompt (in case previous turn had modifications)
748
- this.agent.setSystemPrompt(this._baseSystemPrompt);
759
+ this.agent.state.systemPrompt = this._baseSystemPrompt;
749
760
  }
750
761
  }
751
762
  await this.agent.prompt(messages);
@@ -850,6 +861,7 @@ export class AgentSession {
850
861
  */
851
862
  async _queueSteer(text, images) {
852
863
  this._steeringMessages.push(text);
864
+ this._emitQueueUpdate();
853
865
  const content = [{ type: "text", text }];
854
866
  if (images) {
855
867
  content.push(...images);
@@ -865,6 +877,7 @@ export class AgentSession {
865
877
  */
866
878
  async _queueFollowUp(text, images) {
867
879
  this._followUpMessages.push(text);
880
+ this._emitQueueUpdate();
868
881
  const content = [{ type: "text", text }];
869
882
  if (images) {
870
883
  content.push(...images);
@@ -924,7 +937,7 @@ export class AgentSession {
924
937
  await this.agent.prompt(appMessage);
925
938
  }
926
939
  else {
927
- this.agent.appendMessage(appMessage);
940
+ this.agent.state.messages.push(appMessage);
928
941
  this.sessionManager.appendCustomMessageEntry(message.customType, message.content, message.display, message.details);
929
942
  this._emit({ type: "message_start", message: appMessage });
930
943
  this._emit({ type: "message_end", message: appMessage });
@@ -978,6 +991,7 @@ export class AgentSession {
978
991
  this._steeringMessages = [];
979
992
  this._followUpMessages = [];
980
993
  this.agent.clearAllQueues();
994
+ this._emitQueueUpdate();
981
995
  return { steering, followUp };
982
996
  }
983
997
  /** Number of pending messages (includes both steering and follow-up) */
@@ -1003,54 +1017,6 @@ export class AgentSession {
1003
1017
  this.agent.abort();
1004
1018
  await this.agent.waitForIdle();
1005
1019
  }
1006
- /**
1007
- * Start a new session, optionally with initial messages and parent tracking.
1008
- * Clears all messages and starts a new session.
1009
- * Listeners are preserved and will continue receiving events.
1010
- * @param options.parentSession - Optional parent session path for tracking
1011
- * @param options.setup - Optional callback to initialize session (e.g., append messages)
1012
- * @returns true if completed, false if cancelled by extension
1013
- */
1014
- async newSession(options) {
1015
- const previousSessionFile = this.sessionFile;
1016
- // Emit session_before_switch event with reason "new" (can be cancelled)
1017
- if (this._extensionRunner?.hasHandlers("session_before_switch")) {
1018
- const result = (await this._extensionRunner.emit({
1019
- type: "session_before_switch",
1020
- reason: "new",
1021
- }));
1022
- if (result?.cancel) {
1023
- return false;
1024
- }
1025
- }
1026
- this._disconnectFromAgent();
1027
- await this.abort();
1028
- this.agent.reset();
1029
- this.sessionManager.newSession({ parentSession: options?.parentSession });
1030
- this.agent.sessionId = this.sessionManager.getSessionId();
1031
- this._steeringMessages = [];
1032
- this._followUpMessages = [];
1033
- this._pendingNextTurnMessages = [];
1034
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
1035
- // Run setup callback if provided (e.g., to append initial messages)
1036
- if (options?.setup) {
1037
- await options.setup(this.sessionManager);
1038
- // Sync agent state with session manager after setup
1039
- const sessionContext = this.sessionManager.buildSessionContext();
1040
- this.agent.replaceMessages(sessionContext.messages);
1041
- }
1042
- this._reconnectToAgent();
1043
- // Emit session_switch event with reason "new" to extensions
1044
- if (this._extensionRunner) {
1045
- await this._extensionRunner.emit({
1046
- type: "session_switch",
1047
- reason: "new",
1048
- previousSessionFile,
1049
- });
1050
- }
1051
- // Emit session event to custom tools
1052
- return true;
1053
- }
1054
1020
  // =========================================================================
1055
1021
  // Model Management
1056
1022
  // =========================================================================
@@ -1077,7 +1043,7 @@ export class AgentSession {
1077
1043
  }
1078
1044
  const previousModel = this.model;
1079
1045
  const thinkingLevel = this._getThinkingLevelForModelSwitch();
1080
- this.agent.setModel(model);
1046
+ this.agent.state.model = model;
1081
1047
  this.sessionManager.appendModelChange(model.provider, model.id);
1082
1048
  this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);
1083
1049
  // Re-clamp thinking level for new model's capabilities
@@ -1096,11 +1062,8 @@ export class AgentSession {
1096
1062
  }
1097
1063
  return this._cycleAvailableModel(direction);
1098
1064
  }
1099
- _getScopedModelsWithAuth() {
1100
- return this._scopedModels.filter((scoped) => this._modelRegistry.hasConfiguredAuth(scoped.model));
1101
- }
1102
1065
  async _cycleScopedModel(direction) {
1103
- const scopedModels = this._getScopedModelsWithAuth();
1066
+ const scopedModels = this._scopedModels.filter((scoped) => this._modelRegistry.hasConfiguredAuth(scoped.model));
1104
1067
  if (scopedModels.length <= 1)
1105
1068
  return undefined;
1106
1069
  const currentModel = this.model;
@@ -1112,7 +1075,7 @@ export class AgentSession {
1112
1075
  const next = scopedModels[nextIndex];
1113
1076
  const thinkingLevel = this._getThinkingLevelForModelSwitch(next.thinkingLevel);
1114
1077
  // Apply model
1115
- this.agent.setModel(next.model);
1078
+ this.agent.state.model = next.model;
1116
1079
  this.sessionManager.appendModelChange(next.model.provider, next.model.id);
1117
1080
  this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id);
1118
1081
  // Apply thinking level.
@@ -1135,7 +1098,7 @@ export class AgentSession {
1135
1098
  const nextIndex = direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len;
1136
1099
  const nextModel = availableModels[nextIndex];
1137
1100
  const thinkingLevel = this._getThinkingLevelForModelSwitch();
1138
- this.agent.setModel(nextModel);
1101
+ this.agent.state.model = nextModel;
1139
1102
  this.sessionManager.appendModelChange(nextModel.provider, nextModel.id);
1140
1103
  this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
1141
1104
  // Re-clamp thinking level for new model's capabilities
@@ -1156,7 +1119,7 @@ export class AgentSession {
1156
1119
  const effectiveLevel = availableLevels.includes(level) ? level : this._clampThinkingLevel(level, availableLevels);
1157
1120
  // Only persist if actually changing
1158
1121
  const isChanging = effectiveLevel !== this.agent.state.thinkingLevel;
1159
- this.agent.setThinkingLevel(effectiveLevel);
1122
+ this.agent.state.thinkingLevel = effectiveLevel;
1160
1123
  if (isChanging) {
1161
1124
  this.sessionManager.appendThinkingLevelChange(effectiveLevel);
1162
1125
  if (this.supportsThinking() || effectiveLevel !== "off") {
@@ -1235,7 +1198,7 @@ export class AgentSession {
1235
1198
  * Saves to settings.
1236
1199
  */
1237
1200
  setSteeringMode(mode) {
1238
- this.agent.setSteeringMode(mode);
1201
+ this.agent.steeringMode = mode;
1239
1202
  this.settingsManager.setSteeringMode(mode);
1240
1203
  }
1241
1204
  /**
@@ -1243,7 +1206,7 @@ export class AgentSession {
1243
1206
  * Saves to settings.
1244
1207
  */
1245
1208
  setFollowUpMode(mode) {
1246
- this.agent.setFollowUpMode(mode);
1209
+ this.agent.followUpMode = mode;
1247
1210
  this.settingsManager.setFollowUpMode(mode);
1248
1211
  }
1249
1212
  // =========================================================================
@@ -1318,7 +1281,7 @@ export class AgentSession {
1318
1281
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1319
1282
  const newEntries = this.sessionManager.getEntries();
1320
1283
  const sessionContext = this.sessionManager.buildSessionContext();
1321
- this.agent.replaceMessages(sessionContext.messages);
1284
+ this.agent.state.messages = sessionContext.messages;
1322
1285
  // Get the saved compaction entry for the extension event
1323
1286
  const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1324
1287
  if (this._extensionRunner && savedCompactionEntry) {
@@ -1424,7 +1387,7 @@ export class AgentSession {
1424
1387
  // but we don't want it in context for the retry)
1425
1388
  const messages = this.agent.state.messages;
1426
1389
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
1427
- this.agent.replaceMessages(messages.slice(0, -1));
1390
+ this.agent.state.messages = messages.slice(0, -1);
1428
1391
  }
1429
1392
  await this._runAutoCompaction("overflow", true);
1430
1393
  return;
@@ -1555,7 +1518,7 @@ export class AgentSession {
1555
1518
  this.sessionManager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromExtension);
1556
1519
  const newEntries = this.sessionManager.getEntries();
1557
1520
  const sessionContext = this.sessionManager.buildSessionContext();
1558
- this.agent.replaceMessages(sessionContext.messages);
1521
+ this.agent.state.messages = sessionContext.messages;
1559
1522
  // Get the saved compaction entry for the extension event
1560
1523
  const savedCompactionEntry = newEntries.find((e) => e.type === "compaction" && e.summary === summary);
1561
1524
  if (this._extensionRunner && savedCompactionEntry) {
@@ -1576,7 +1539,7 @@ export class AgentSession {
1576
1539
  const messages = this.agent.state.messages;
1577
1540
  const lastMsg = messages[messages.length - 1];
1578
1541
  if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") {
1579
- this.agent.replaceMessages(messages.slice(0, -1));
1542
+ this.agent.state.messages = messages.slice(0, -1);
1580
1543
  }
1581
1544
  setTimeout(() => {
1582
1545
  this.agent.continue().catch(() => { });
@@ -1632,8 +1595,8 @@ export class AgentSession {
1632
1595
  }
1633
1596
  if (this._extensionRunner) {
1634
1597
  this._applyExtensionBindings(this._extensionRunner);
1635
- await this._extensionRunner.emit({ type: "session_start" });
1636
- await this.extendResourcesFromExtensions("startup");
1598
+ await this._extensionRunner.emit(this._sessionStartEvent);
1599
+ await this.extendResourcesFromExtensions(this._sessionStartEvent.reason === "reload" ? "reload" : "startup");
1637
1600
  }
1638
1601
  }
1639
1602
  async extendResourcesFromExtensions(reason) {
@@ -1651,7 +1614,7 @@ export class AgentSession {
1651
1614
  };
1652
1615
  this._resourceLoader.extendResources(extensionPaths);
1653
1616
  this._baseSystemPrompt = this._rebuildSystemPrompt(this.getActiveToolNames());
1654
- this.agent.setSystemPrompt(this._baseSystemPrompt);
1617
+ this.agent.state.systemPrompt = this._baseSystemPrompt;
1655
1618
  }
1656
1619
  buildExtensionResourcePaths(entries) {
1657
1620
  return entries.map((entry) => {
@@ -1693,7 +1656,7 @@ export class AgentSession {
1693
1656
  if (!refreshedModel || refreshedModel === currentModel) {
1694
1657
  return;
1695
1658
  }
1696
- this.agent.setModel(refreshedModel);
1659
+ this.agent.state.model = refreshedModel;
1697
1660
  }
1698
1661
  _bindExtensionCore(runner) {
1699
1662
  const getCommands = () => {
@@ -1917,7 +1880,7 @@ export class AgentSession {
1917
1880
  this._extensionShutdownHandler ||
1918
1881
  this._extensionErrorListener;
1919
1882
  if (this._extensionRunner && hasBindings) {
1920
- await this._extensionRunner.emit({ type: "session_start" });
1883
+ await this._extensionRunner.emit({ type: "session_start", reason: "reload" });
1921
1884
  await this.extendResourcesFromExtensions("reload");
1922
1885
  }
1923
1886
  }
@@ -2018,7 +1981,7 @@ export class AgentSession {
2018
1981
  // Remove error message from agent state (keep in session for history)
2019
1982
  const messages = this.agent.state.messages;
2020
1983
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
2021
- this.agent.replaceMessages(messages.slice(0, -1));
1984
+ this.agent.state.messages = messages.slice(0, -1);
2022
1985
  }
2023
1986
  // Wait with exponential backoff (abortable)
2024
1987
  this._retryAbortController = new AbortController();
@@ -2061,9 +2024,11 @@ export class AgentSession {
2061
2024
  * Returns immediately if no retry is in progress.
2062
2025
  */
2063
2026
  async waitForRetry() {
2064
- if (this._retryPromise) {
2065
- await this._retryPromise;
2027
+ if (!this._retryPromise) {
2028
+ return;
2066
2029
  }
2030
+ await this._retryPromise;
2031
+ await this.agent.waitForIdle();
2067
2032
  }
2068
2033
  /** Whether auto-retry is currently in progress */
2069
2034
  get isRetrying() {
@@ -2096,15 +2061,10 @@ export class AgentSession {
2096
2061
  const prefix = this.settingsManager.getShellCommandPrefix();
2097
2062
  const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
2098
2063
  try {
2099
- const result = options?.operations
2100
- ? await executeBashWithOperations(resolvedCommand, process.cwd(), options.operations, {
2101
- onChunk,
2102
- signal: this._bashAbortController.signal,
2103
- })
2104
- : await executeBashCommand(resolvedCommand, {
2105
- onChunk,
2106
- signal: this._bashAbortController.signal,
2107
- });
2064
+ const result = await executeBashWithOperations(resolvedCommand, this.sessionManager.getCwd(), options?.operations ?? createLocalBashOperations(), {
2065
+ onChunk,
2066
+ signal: this._bashAbortController.signal,
2067
+ });
2108
2068
  this.recordBashResult(command, result, options);
2109
2069
  return result;
2110
2070
  }
@@ -2135,7 +2095,7 @@ export class AgentSession {
2135
2095
  }
2136
2096
  else {
2137
2097
  // Add to agent state immediately
2138
- this.agent.appendMessage(bashMessage);
2098
+ this.agent.state.messages.push(bashMessage);
2139
2099
  // Save to session
2140
2100
  this.sessionManager.appendMessage(bashMessage);
2141
2101
  }
@@ -2163,7 +2123,7 @@ export class AgentSession {
2163
2123
  return;
2164
2124
  for (const bashMessage of this._pendingBashMessages) {
2165
2125
  // Add to agent state
2166
- this.agent.appendMessage(bashMessage);
2126
+ this.agent.state.messages.push(bashMessage);
2167
2127
  // Save to session
2168
2128
  this.sessionManager.appendMessage(bashMessage);
2169
2129
  }
@@ -2172,130 +2132,12 @@ export class AgentSession {
2172
2132
  // =========================================================================
2173
2133
  // Session Management
2174
2134
  // =========================================================================
2175
- /**
2176
- * Switch to a different session file.
2177
- * Aborts current operation, loads messages, restores model/thinking.
2178
- * Listeners are preserved and will continue receiving events.
2179
- * @returns true if switch completed, false if cancelled by extension
2180
- */
2181
- async switchSession(sessionPath) {
2182
- const previousSessionFile = this.sessionManager.getSessionFile();
2183
- // Emit session_before_switch event (can be cancelled)
2184
- if (this._extensionRunner?.hasHandlers("session_before_switch")) {
2185
- const result = (await this._extensionRunner.emit({
2186
- type: "session_before_switch",
2187
- reason: "resume",
2188
- targetSessionFile: sessionPath,
2189
- }));
2190
- if (result?.cancel) {
2191
- return false;
2192
- }
2193
- }
2194
- this._disconnectFromAgent();
2195
- await this.abort();
2196
- this._steeringMessages = [];
2197
- this._followUpMessages = [];
2198
- this._pendingNextTurnMessages = [];
2199
- // Set new session
2200
- this.sessionManager.setSessionFile(sessionPath);
2201
- this.agent.sessionId = this.sessionManager.getSessionId();
2202
- // Reload messages
2203
- const sessionContext = this.sessionManager.buildSessionContext();
2204
- // Emit session_switch event to extensions
2205
- if (this._extensionRunner) {
2206
- await this._extensionRunner.emit({
2207
- type: "session_switch",
2208
- reason: "resume",
2209
- previousSessionFile,
2210
- });
2211
- }
2212
- // Emit session event to custom tools
2213
- this.agent.replaceMessages(sessionContext.messages);
2214
- // Restore model if saved
2215
- if (sessionContext.model) {
2216
- const previousModel = this.model;
2217
- const availableModels = await this._modelRegistry.getAvailable();
2218
- const match = availableModels.find((m) => m.provider === sessionContext.model.provider && m.id === sessionContext.model.modelId);
2219
- if (match) {
2220
- this.agent.setModel(match);
2221
- await this._emitModelSelect(match, previousModel, "restore");
2222
- }
2223
- }
2224
- const hasThinkingEntry = this.sessionManager.getBranch().some((entry) => entry.type === "thinking_level_change");
2225
- const defaultThinkingLevel = this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;
2226
- if (hasThinkingEntry) {
2227
- // Restore thinking level if saved (setThinkingLevel clamps to model capabilities)
2228
- this.setThinkingLevel(sessionContext.thinkingLevel);
2229
- }
2230
- else {
2231
- const availableLevels = this.getAvailableThinkingLevels();
2232
- const effectiveLevel = availableLevels.includes(defaultThinkingLevel)
2233
- ? defaultThinkingLevel
2234
- : this._clampThinkingLevel(defaultThinkingLevel, availableLevels);
2235
- this.agent.setThinkingLevel(effectiveLevel);
2236
- this.sessionManager.appendThinkingLevelChange(effectiveLevel);
2237
- }
2238
- this._reconnectToAgent();
2239
- return true;
2240
- }
2241
2135
  /**
2242
2136
  * Set a display name for the current session.
2243
2137
  */
2244
2138
  setSessionName(name) {
2245
2139
  this.sessionManager.appendSessionInfo(name);
2246
2140
  }
2247
- /**
2248
- * Create a fork from a specific entry.
2249
- * Emits before_fork/fork session events to extensions.
2250
- *
2251
- * @param entryId ID of the entry to fork from
2252
- * @returns Object with:
2253
- * - selectedText: The text of the selected user message (for editor pre-fill)
2254
- * - cancelled: True if an extension cancelled the fork
2255
- */
2256
- async fork(entryId) {
2257
- const previousSessionFile = this.sessionFile;
2258
- const selectedEntry = this.sessionManager.getEntry(entryId);
2259
- if (!selectedEntry || selectedEntry.type !== "message" || selectedEntry.message.role !== "user") {
2260
- throw new Error("Invalid entry ID for forking");
2261
- }
2262
- const selectedText = this._extractUserMessageText(selectedEntry.message.content);
2263
- let skipConversationRestore = false;
2264
- // Emit session_before_fork event (can be cancelled)
2265
- if (this._extensionRunner?.hasHandlers("session_before_fork")) {
2266
- const result = (await this._extensionRunner.emit({
2267
- type: "session_before_fork",
2268
- entryId,
2269
- }));
2270
- if (result?.cancel) {
2271
- return { selectedText, cancelled: true };
2272
- }
2273
- skipConversationRestore = result?.skipConversationRestore ?? false;
2274
- }
2275
- // Clear pending messages (bound to old session state)
2276
- this._pendingNextTurnMessages = [];
2277
- if (!selectedEntry.parentId) {
2278
- this.sessionManager.newSession({ parentSession: previousSessionFile });
2279
- }
2280
- else {
2281
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
2282
- }
2283
- this.agent.sessionId = this.sessionManager.getSessionId();
2284
- // Reload messages from entries (works for both file and in-memory mode)
2285
- const sessionContext = this.sessionManager.buildSessionContext();
2286
- // Emit session_fork event to extensions (after fork completes)
2287
- if (this._extensionRunner) {
2288
- await this._extensionRunner.emit({
2289
- type: "session_fork",
2290
- previousSessionFile,
2291
- });
2292
- }
2293
- // Emit session event to custom tools (with reason "fork")
2294
- if (!skipConversationRestore) {
2295
- this.agent.replaceMessages(sessionContext.messages);
2296
- }
2297
- return { selectedText, cancelled: false };
2298
- }
2299
2141
  // =========================================================================
2300
2142
  // Tree Navigation
2301
2143
  // =========================================================================
@@ -2451,7 +2293,7 @@ export class AgentSession {
2451
2293
  }
2452
2294
  // Update agent state
2453
2295
  const sessionContext = this.sessionManager.buildSessionContext();
2454
- this.agent.replaceMessages(sessionContext.messages);
2296
+ this.agent.state.messages = sessionContext.messages;
2455
2297
  // Emit session_tree event
2456
2298
  if (this._extensionRunner) {
2457
2299
  await this._extensionRunner.emit({
@@ -2591,6 +2433,7 @@ export class AgentSession {
2591
2433
  const toolRenderer = createToolHtmlRenderer({
2592
2434
  getToolDefinition: (name) => this.getToolDefinition(name),
2593
2435
  theme,
2436
+ cwd: this.sessionManager.getCwd(),
2594
2437
  });
2595
2438
  return await exportSessionToHtml(this.sessionManager, this.state, {
2596
2439
  outputPath,
@@ -2629,29 +2472,6 @@ export class AgentSession {
2629
2472
  writeFileSync(filePath, `${lines.join("\n")}\n`);
2630
2473
  return filePath;
2631
2474
  }
2632
- /**
2633
- * Import a JSONL session file.
2634
- * Copies the file into the session directory and switches to it (like /resume).
2635
- * @param inputPath Path to the JSONL file to import.
2636
- * @returns true if the session was switched successfully.
2637
- */
2638
- async importFromJsonl(inputPath) {
2639
- const resolved = resolve(inputPath);
2640
- if (!existsSync(resolved)) {
2641
- throw new Error(`File not found: ${resolved}`);
2642
- }
2643
- // Copy into the session directory so we don't modify the original
2644
- const sessionDir = this.sessionManager.getSessionDir();
2645
- if (!existsSync(sessionDir)) {
2646
- mkdirSync(sessionDir, { recursive: true });
2647
- }
2648
- const destPath = join(sessionDir, basename(resolved));
2649
- // Avoid overwriting if source and destination are the same file
2650
- if (resolve(destPath) !== resolved) {
2651
- copyFileSync(resolved, destPath);
2652
- }
2653
- return this.switchSession(destPath);
2654
- }
2655
2475
  // =========================================================================
2656
2476
  // Utilities
2657
2477
  // =========================================================================