@draht/coding-agent 2026.3.25 → 2026.4.5

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 (111) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +6 -2
  3. package/dist/core/agent-session-runtime.d.ts +136 -0
  4. package/dist/core/agent-session-runtime.d.ts.map +1 -0
  5. package/dist/core/agent-session-runtime.js +267 -0
  6. package/dist/core/agent-session-runtime.js.map +1 -0
  7. package/dist/core/agent-session.d.ts +22 -44
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +44 -248
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/auth-storage.d.ts +3 -1
  12. package/dist/core/auth-storage.d.ts.map +1 -1
  13. package/dist/core/auth-storage.js +5 -2
  14. package/dist/core/auth-storage.js.map +1 -1
  15. package/dist/core/compaction/branch-summarization.d.ts +2 -0
  16. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  17. package/dist/core/compaction/branch-summarization.js +2 -2
  18. package/dist/core/compaction/branch-summarization.js.map +1 -1
  19. package/dist/core/compaction/compaction.d.ts +2 -2
  20. package/dist/core/compaction/compaction.d.ts.map +1 -1
  21. package/dist/core/compaction/compaction.js +9 -9
  22. package/dist/core/compaction/compaction.js.map +1 -1
  23. package/dist/core/export-html/tool-renderer.d.ts +2 -0
  24. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  25. package/dist/core/export-html/tool-renderer.js +2 -2
  26. package/dist/core/export-html/tool-renderer.js.map +1 -1
  27. package/dist/core/extensions/index.d.ts +2 -2
  28. package/dist/core/extensions/index.d.ts.map +1 -1
  29. package/dist/core/extensions/index.js +1 -1
  30. package/dist/core/extensions/index.js.map +1 -1
  31. package/dist/core/extensions/types.d.ts +16 -16
  32. package/dist/core/extensions/types.d.ts.map +1 -1
  33. package/dist/core/extensions/types.js +10 -0
  34. package/dist/core/extensions/types.js.map +1 -1
  35. package/dist/core/footer-data-provider.d.ts +5 -1
  36. package/dist/core/footer-data-provider.d.ts.map +1 -1
  37. package/dist/core/footer-data-provider.js +70 -8
  38. package/dist/core/footer-data-provider.js.map +1 -1
  39. package/dist/core/index.d.ts +2 -1
  40. package/dist/core/index.d.ts.map +1 -1
  41. package/dist/core/index.js +2 -1
  42. package/dist/core/index.js.map +1 -1
  43. package/dist/core/model-registry.d.ts +21 -3
  44. package/dist/core/model-registry.d.ts.map +1 -1
  45. package/dist/core/model-registry.js +90 -70
  46. package/dist/core/model-registry.js.map +1 -1
  47. package/dist/core/model-resolver.d.ts.map +1 -1
  48. package/dist/core/model-resolver.js +4 -4
  49. package/dist/core/model-resolver.js.map +1 -1
  50. package/dist/core/resolve-config-value.d.ts +6 -0
  51. package/dist/core/resolve-config-value.d.ts.map +1 -1
  52. package/dist/core/resolve-config-value.js +37 -5
  53. package/dist/core/resolve-config-value.js.map +1 -1
  54. package/dist/core/resource-loader.d.ts +2 -0
  55. package/dist/core/resource-loader.d.ts.map +1 -1
  56. package/dist/core/resource-loader.js +5 -1
  57. package/dist/core/resource-loader.js.map +1 -1
  58. package/dist/core/sdk.d.ts +6 -3
  59. package/dist/core/sdk.d.ts.map +1 -1
  60. package/dist/core/sdk.js +17 -23
  61. package/dist/core/sdk.js.map +1 -1
  62. package/dist/index.d.ts +3 -3
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +3 -3
  65. package/dist/index.js.map +1 -1
  66. package/dist/main.d.ts.map +1 -1
  67. package/dist/main.js +49 -10
  68. package/dist/main.js.map +1 -1
  69. package/dist/modes/interactive/components/footer.d.ts +1 -0
  70. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  71. package/dist/modes/interactive/components/footer.js +4 -1
  72. package/dist/modes/interactive/components/footer.js.map +1 -1
  73. package/dist/modes/interactive/interactive-mode.d.ts +8 -4
  74. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  75. package/dist/modes/interactive/interactive-mode.js +90 -87
  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 +4 -4
  81. package/dist/modes/print-mode.d.ts.map +1 -1
  82. package/dist/modes/print-mode.js +87 -74
  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 +69 -49
  87. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  88. package/docs/development.md +2 -2
  89. package/docs/extensions.md +78 -22
  90. package/docs/models.md +6 -0
  91. package/docs/packages.md +3 -3
  92. package/docs/rpc.md +2 -2
  93. package/docs/sdk.md +170 -82
  94. package/docs/tree.md +1 -1
  95. package/examples/extensions/custom-compaction.ts +17 -4
  96. package/examples/extensions/handoff.ts +5 -2
  97. package/examples/extensions/hello.ts +18 -17
  98. package/examples/extensions/qna.ts +5 -2
  99. package/examples/extensions/rpc-demo.ts +3 -9
  100. package/examples/extensions/status-line.ts +0 -8
  101. package/examples/extensions/subagent/index.ts +1 -1
  102. package/examples/extensions/summarize.ts +15 -4
  103. package/examples/extensions/todo.ts +0 -2
  104. package/examples/extensions/tools.ts +0 -5
  105. package/examples/extensions/widget-placement.ts +4 -12
  106. package/examples/sdk/02-custom-model.ts +1 -1
  107. package/examples/sdk/09-api-keys-and-oauth.ts +3 -3
  108. package/examples/sdk/12-full-control.ts +1 -1
  109. package/examples/sdk/13-session-runtime.ts +49 -0
  110. package/examples/sdk/README.md +5 -4
  111. package/package.json +4 -4
@@ -54,7 +54,7 @@ function isExpandable(obj) {
54
54
  }
55
55
  export class InteractiveMode {
56
56
  options;
57
- session;
57
+ runtimeHost;
58
58
  ui;
59
59
  chatContainer;
60
60
  pendingMessagesContainer;
@@ -128,6 +128,9 @@ export class InteractiveMode {
128
128
  // Custom header from extension (undefined = use built-in header)
129
129
  customHeader = undefined;
130
130
  // Convenience accessors
131
+ get session() {
132
+ return this.runtimeHost.session;
133
+ }
131
134
  get agent() {
132
135
  return this.session.agent;
133
136
  }
@@ -137,9 +140,9 @@ export class InteractiveMode {
137
140
  get settingsManager() {
138
141
  return this.session.settingsManager;
139
142
  }
140
- constructor(session, options = {}) {
143
+ constructor(runtimeHost, options = {}) {
141
144
  this.options = options;
142
- this.session = session;
145
+ this.runtimeHost = runtimeHost;
143
146
  this.version = VERSION;
144
147
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
145
148
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
@@ -160,9 +163,9 @@ export class InteractiveMode {
160
163
  this.editor = this.defaultEditor;
161
164
  this.editorContainer = new Container();
162
165
  this.editorContainer.addChild(this.editor);
163
- this.footerDataProvider = new FooterDataProvider();
164
- this.footer = new FooterComponent(session, this.footerDataProvider);
165
- this.footer.setAutoCompactEnabled(session.autoCompactionEnabled);
166
+ this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
167
+ this.footer = new FooterComponent(this.session, this.footerDataProvider);
168
+ this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
166
169
  // Load hide thinking block setting
167
170
  this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
168
171
  // Register themes from resource loader and initialize
@@ -269,7 +272,7 @@ export class InteractiveMode {
269
272
  }
270
273
  }
271
274
  // Setup autocomplete
272
- this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), fdPath);
275
+ this.autocompleteProvider = new CombinedAutocompleteProvider([...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], this.sessionManager.getCwd(), fdPath);
273
276
  this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider);
274
277
  if (this.editor !== this.defaultEditor) {
275
278
  this.editor.setAutocompleteProvider?.(this.autocompleteProvider);
@@ -363,7 +366,7 @@ export class InteractiveMode {
363
366
  this.ui.start();
364
367
  this.isInitialized = true;
365
368
  // Initialize extensions first so resources are shown before messages
366
- await this.initExtensions();
369
+ await this.bindCurrentSessionExtensions();
367
370
  // Render initial messages AFTER showing loaded resources
368
371
  this.renderInitialMessages();
369
372
  // Set terminal title
@@ -387,7 +390,7 @@ export class InteractiveMode {
387
390
  * Update terminal title with session name and cwd.
388
391
  */
389
392
  updateTerminalTitle() {
390
- const cwdBasename = path.basename(process.cwd());
393
+ const cwdBasename = path.basename(this.sessionManager.getCwd());
391
394
  const sessionName = this.sessionManager.getSessionName();
392
395
  if (sessionName) {
393
396
  this.ui.terminal.setTitle(`D - ${sessionName} - ${cwdBasename}`);
@@ -494,7 +497,7 @@ export class InteractiveMode {
494
497
  }
495
498
  try {
496
499
  const packageManager = new DefaultPackageManager({
497
- cwd: process.cwd(),
500
+ cwd: this.sessionManager.getCwd(),
498
501
  agentDir: getAgentDir(),
499
502
  settingsManager: this.settingsManager,
500
503
  });
@@ -882,7 +885,7 @@ export class InteractiveMode {
882
885
  /**
883
886
  * Initialize the extension system with TUI-based UI context.
884
887
  */
885
- async initExtensions() {
888
+ async bindCurrentSessionExtensions() {
886
889
  const uiContext = this.createExtensionUIContext();
887
890
  await this.session.bindExtensions({
888
891
  uiContext,
@@ -894,33 +897,23 @@ export class InteractiveMode {
894
897
  this.loadingAnimation = undefined;
895
898
  }
896
899
  this.statusContainer.clear();
897
- // Delegate to AgentSession (handles setup + agent state sync)
898
- const success = await this.session.newSession(options);
899
- if (!success) {
900
- return { cancelled: true };
900
+ const result = await this.runtimeHost.newSession(options);
901
+ if (!result.cancelled) {
902
+ await this.handleRuntimeSessionChange();
903
+ this.renderCurrentSessionState();
904
+ this.ui.requestRender();
901
905
  }
902
- // Clear UI state
903
- this.chatContainer.clear();
904
- this.pendingMessagesContainer.clear();
905
- this.compactionQueuedMessages = [];
906
- this.streamingComponent = undefined;
907
- this.streamingMessage = undefined;
908
- this.pendingTools.clear();
909
- // Render any messages added via setup, or show empty session
910
- this.renderInitialMessages();
911
- this.ui.requestRender();
912
- return { cancelled: false };
906
+ return result;
913
907
  },
914
908
  fork: async (entryId) => {
915
- const result = await this.session.fork(entryId);
916
- if (result.cancelled) {
917
- return { cancelled: true };
909
+ const result = await this.runtimeHost.fork(entryId);
910
+ if (!result.cancelled) {
911
+ await this.handleRuntimeSessionChange();
912
+ this.renderCurrentSessionState();
913
+ this.editor.setText(result.selectedText ?? "");
914
+ this.showStatus("Forked to new session");
918
915
  }
919
- this.chatContainer.clear();
920
- this.renderInitialMessages();
921
- this.editor.setText(result.selectedText);
922
- this.showStatus("Forked to new session");
923
- return { cancelled: false };
916
+ return { cancelled: result.cancelled };
924
917
  },
925
918
  navigateTree: async (targetId, options) => {
926
919
  const result = await this.session.navigateTree(targetId, {
@@ -968,6 +961,42 @@ export class InteractiveMode {
968
961
  this.setupExtensionShortcuts(extensionRunner);
969
962
  this.showLoadedResources({ force: false });
970
963
  }
964
+ applyRuntimeSettings() {
965
+ this.footer.setSession(this.session);
966
+ this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
967
+ this.footerDataProvider.setCwd(this.sessionManager.getCwd());
968
+ this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock();
969
+ this.ui.setShowHardwareCursor(this.settingsManager.getShowHardwareCursor());
970
+ this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
971
+ const editorPaddingX = this.settingsManager.getEditorPaddingX();
972
+ const autocompleteMaxVisible = this.settingsManager.getAutocompleteMaxVisible();
973
+ this.defaultEditor.setPaddingX(editorPaddingX);
974
+ this.defaultEditor.setAutocompleteMaxVisible(autocompleteMaxVisible);
975
+ if (this.editor !== this.defaultEditor) {
976
+ this.editor.setPaddingX?.(editorPaddingX);
977
+ this.editor.setAutocompleteMaxVisible?.(autocompleteMaxVisible);
978
+ }
979
+ }
980
+ async handleRuntimeSessionChange() {
981
+ this.resetExtensionUI();
982
+ this.unsubscribe?.();
983
+ this.unsubscribe = undefined;
984
+ this.applyRuntimeSettings();
985
+ await this.bindCurrentSessionExtensions();
986
+ this.subscribeToAgent();
987
+ await this.updateAvailableProviderCount();
988
+ this.updateEditorBorderColor();
989
+ this.updateTerminalTitle();
990
+ }
991
+ renderCurrentSessionState() {
992
+ this.chatContainer.clear();
993
+ this.pendingMessagesContainer.clear();
994
+ this.compactionQueuedMessages = [];
995
+ this.streamingComponent = undefined;
996
+ this.streamingMessage = undefined;
997
+ this.pendingTools.clear();
998
+ this.renderInitialMessages();
999
+ }
971
1000
  /**
972
1001
  * Get a registered tool definition by name (for custom rendering).
973
1002
  */
@@ -985,7 +1014,7 @@ export class InteractiveMode {
985
1014
  const createContext = () => ({
986
1015
  ui: this.createExtensionUIContext(),
987
1016
  hasUI: true,
988
- cwd: process.cwd(),
1017
+ cwd: this.sessionManager.getCwd(),
989
1018
  sessionManager: this.sessionManager,
990
1019
  modelRegistry: this.session.modelRegistry,
991
1020
  model: this.session.model,
@@ -1864,7 +1893,7 @@ export class InteractiveMode {
1864
1893
  if (!this.pendingTools.has(content.id)) {
1865
1894
  const component = new ToolExecutionComponent(content.name, content.id, content.arguments, {
1866
1895
  showImages: this.settingsManager.getShowImages(),
1867
- }, this.getRegisteredToolDefinition(content.name), this.ui);
1896
+ }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
1868
1897
  component.setExpanded(this.toolOutputExpanded);
1869
1898
  this.chatContainer.addChild(component);
1870
1899
  this.pendingTools.set(content.id, component);
@@ -1924,7 +1953,7 @@ export class InteractiveMode {
1924
1953
  if (!component) {
1925
1954
  component = new ToolExecutionComponent(event.toolName, event.toolCallId, event.args, {
1926
1955
  showImages: this.settingsManager.getShowImages(),
1927
- }, this.getRegisteredToolDefinition(event.toolName), this.ui);
1956
+ }, this.getRegisteredToolDefinition(event.toolName), this.ui, this.sessionManager.getCwd());
1928
1957
  component.setExpanded(this.toolOutputExpanded);
1929
1958
  this.chatContainer.addChild(component);
1930
1959
  this.pendingTools.set(event.toolCallId, component);
@@ -2178,7 +2207,7 @@ export class InteractiveMode {
2178
2207
  // Render tool call components
2179
2208
  for (const content of message.content) {
2180
2209
  if (content.type === "toolCall") {
2181
- const component = new ToolExecutionComponent(content.name, content.id, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui);
2210
+ const component = new ToolExecutionComponent(content.name, content.id, content.arguments, { showImages: this.settingsManager.getShowImages() }, this.getRegisteredToolDefinition(content.name), this.ui, this.sessionManager.getCwd());
2182
2211
  component.setExpanded(this.toolOutputExpanded);
2183
2212
  this.chatContainer.addChild(component);
2184
2213
  if (message.stopReason === "aborted" || message.stopReason === "error") {
@@ -2271,13 +2300,7 @@ export class InteractiveMode {
2271
2300
  if (this.isShuttingDown)
2272
2301
  return;
2273
2302
  this.isShuttingDown = true;
2274
- // Emit shutdown event to extensions
2275
- const extensionRunner = this.session.extensionRunner;
2276
- if (extensionRunner?.hasHandlers("session_shutdown")) {
2277
- await extensionRunner.emit({
2278
- type: "session_shutdown",
2279
- });
2280
- }
2303
+ await this.runtimeHost.dispose();
2281
2304
  // Wait for any pending renders to complete
2282
2305
  // requestRender() uses process.nextTick(), so we wait one tick
2283
2306
  await new Promise((resolve) => process.nextTick(resolve));
@@ -3003,16 +3026,15 @@ export class InteractiveMode {
3003
3026
  }
3004
3027
  this.showSelector((done) => {
3005
3028
  const selector = new UserMessageSelectorComponent(userMessages.map((m) => ({ id: m.entryId, text: m.text })), async (entryId) => {
3006
- const result = await this.session.fork(entryId);
3029
+ const result = await this.runtimeHost.fork(entryId);
3007
3030
  if (result.cancelled) {
3008
- // Extension cancelled the fork
3009
3031
  done();
3010
3032
  this.ui.requestRender();
3011
3033
  return;
3012
3034
  }
3013
- this.chatContainer.clear();
3014
- this.renderInitialMessages();
3015
- this.editor.setText(result.selectedText);
3035
+ await this.handleRuntimeSessionChange();
3036
+ this.renderCurrentSessionState();
3037
+ this.editor.setText(result.selectedText ?? "");
3016
3038
  done();
3017
3039
  this.showStatus("Branched to new session");
3018
3040
  }, () => {
@@ -3148,23 +3170,17 @@ export class InteractiveMode {
3148
3170
  });
3149
3171
  }
3150
3172
  async handleResumeSession(sessionPath) {
3151
- // Stop loading animation
3152
3173
  if (this.loadingAnimation) {
3153
3174
  this.loadingAnimation.stop();
3154
3175
  this.loadingAnimation = undefined;
3155
3176
  }
3156
3177
  this.statusContainer.clear();
3157
- // Clear UI state
3158
- this.pendingMessagesContainer.clear();
3159
- this.compactionQueuedMessages = [];
3160
- this.streamingComponent = undefined;
3161
- this.streamingMessage = undefined;
3162
- this.pendingTools.clear();
3163
- // Switch session via AgentSession (emits extension session events)
3164
- await this.session.switchSession(sessionPath);
3165
- // Clear and re-render the chat
3166
- this.chatContainer.clear();
3167
- this.renderInitialMessages();
3178
+ const result = await this.runtimeHost.switchSession(sessionPath);
3179
+ if (result.cancelled) {
3180
+ return;
3181
+ }
3182
+ await this.handleRuntimeSessionChange();
3183
+ this.renderCurrentSessionState();
3168
3184
  this.showStatus("Resumed session");
3169
3185
  }
3170
3186
  async showOAuthSelector(mode) {
@@ -3383,26 +3399,18 @@ export class InteractiveMode {
3383
3399
  return;
3384
3400
  }
3385
3401
  try {
3386
- // Stop loading animation
3387
3402
  if (this.loadingAnimation) {
3388
3403
  this.loadingAnimation.stop();
3389
3404
  this.loadingAnimation = undefined;
3390
3405
  }
3391
3406
  this.statusContainer.clear();
3392
- // Clear UI state
3393
- this.pendingMessagesContainer.clear();
3394
- this.compactionQueuedMessages = [];
3395
- this.streamingComponent = undefined;
3396
- this.streamingMessage = undefined;
3397
- this.pendingTools.clear();
3398
- const success = await this.session.importFromJsonl(inputPath);
3399
- if (!success) {
3400
- this.showWarning("Import cancelled");
3407
+ const result = await this.runtimeHost.importFromJsonl(inputPath);
3408
+ if (result.cancelled) {
3409
+ this.showStatus("Import cancelled");
3401
3410
  return;
3402
3411
  }
3403
- // Clear and re-render the chat
3404
- this.chatContainer.clear();
3405
- this.renderInitialMessages();
3412
+ await this.handleRuntimeSessionChange();
3413
+ this.renderCurrentSessionState();
3406
3414
  this.showStatus(`Session imported from: ${inputPath}`);
3407
3415
  }
3408
3416
  catch (error) {
@@ -3716,22 +3724,17 @@ export class InteractiveMode {
3716
3724
  this.ui.requestRender();
3717
3725
  }
3718
3726
  async handleClearCommand() {
3719
- // Stop loading animation
3720
3727
  if (this.loadingAnimation) {
3721
3728
  this.loadingAnimation.stop();
3722
3729
  this.loadingAnimation = undefined;
3723
3730
  }
3724
3731
  this.statusContainer.clear();
3725
- // New session via session (emits extension session events)
3726
- await this.session.newSession();
3727
- // Clear UI state
3728
- this.headerContainer.clear();
3729
- this.chatContainer.clear();
3730
- this.pendingMessagesContainer.clear();
3731
- this.compactionQueuedMessages = [];
3732
- this.streamingComponent = undefined;
3733
- this.streamingMessage = undefined;
3734
- this.pendingTools.clear();
3732
+ const result = await this.runtimeHost.newSession();
3733
+ if (result.cancelled) {
3734
+ return;
3735
+ }
3736
+ await this.handleRuntimeSessionChange();
3737
+ this.renderCurrentSessionState();
3735
3738
  this.chatContainer.addChild(new Spacer(1));
3736
3739
  this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
3737
3740
  this.ui.requestRender();
@@ -3786,7 +3789,7 @@ export class InteractiveMode {
3786
3789
  type: "user_bash",
3787
3790
  command,
3788
3791
  excludeFromContext,
3789
- cwd: process.cwd(),
3792
+ cwd: this.sessionManager.getCwd(),
3790
3793
  })
3791
3794
  : undefined;
3792
3795
  // If extension returned a full result, use it directly