@dreb/coding-agent 2.34.3 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +3 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +30 -2
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +172 -130
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/export-html/template.js +1 -1
  12. package/dist/core/footer-data-provider.d.ts.map +1 -1
  13. package/dist/core/footer-data-provider.js +5 -102
  14. package/dist/core/footer-data-provider.js.map +1 -1
  15. package/dist/core/git-branch.d.ts +19 -0
  16. package/dist/core/git-branch.d.ts.map +1 -0
  17. package/dist/core/git-branch.js +109 -0
  18. package/dist/core/git-branch.js.map +1 -0
  19. package/dist/core/sdk.d.ts.map +1 -1
  20. package/dist/core/sdk.js +1 -1
  21. package/dist/core/sdk.js.map +1 -1
  22. package/dist/core/session-manager.d.ts +21 -0
  23. package/dist/core/session-manager.d.ts.map +1 -1
  24. package/dist/core/session-manager.js +117 -42
  25. package/dist/core/session-manager.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +6 -0
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js +8 -0
  29. package/dist/core/settings-manager.js.map +1 -1
  30. package/dist/{modes/interactive → core}/tab-title.d.ts +12 -3
  31. package/dist/core/tab-title.d.ts.map +1 -0
  32. package/dist/{modes/interactive → core}/tab-title.js +58 -16
  33. package/dist/core/tab-title.js.map +1 -0
  34. package/dist/core/tools/search.d.ts.map +1 -1
  35. package/dist/core/tools/search.js +4 -1
  36. package/dist/core/tools/search.js.map +1 -1
  37. package/dist/core/tools/subagent.d.ts +63 -2
  38. package/dist/core/tools/subagent.d.ts.map +1 -1
  39. package/dist/core/tools/subagent.js +459 -73
  40. package/dist/core/tools/subagent.js.map +1 -1
  41. package/dist/core/tools/web.d.ts +1 -1
  42. package/dist/core/tools/web.d.ts.map +1 -1
  43. package/dist/core/tools/web.js +60 -16
  44. package/dist/core/tools/web.js.map +1 -1
  45. package/dist/main.d.ts +10 -0
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +54 -4
  48. package/dist/main.js.map +1 -1
  49. package/dist/modes/index.d.ts +2 -2
  50. package/dist/modes/index.d.ts.map +1 -1
  51. package/dist/modes/index.js +1 -1
  52. package/dist/modes/index.js.map +1 -1
  53. package/dist/modes/interactive/components/session-selector.d.ts +1 -1
  54. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/session-selector.js +4 -40
  56. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.js +2 -2
  59. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/dist/modes/rpc/index.d.ts +2 -2
  61. package/dist/modes/rpc/index.d.ts.map +1 -1
  62. package/dist/modes/rpc/index.js.map +1 -1
  63. package/dist/modes/rpc/rpc-client.d.ts +114 -6
  64. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-client.js +140 -2
  66. package/dist/modes/rpc/rpc-client.js.map +1 -1
  67. package/dist/modes/rpc/rpc-mode.d.ts +97 -0
  68. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  69. package/dist/modes/rpc/rpc-mode.js +656 -27
  70. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  71. package/dist/modes/rpc/rpc-types.d.ts +312 -1
  72. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  73. package/dist/modes/rpc/rpc-types.js.map +1 -1
  74. package/docs/dashboard.md +152 -0
  75. package/docs/development.md +8 -5
  76. package/docs/rpc.md +652 -5
  77. package/examples/sdk/12-full-control.ts +1 -1
  78. package/package.json +1 -1
  79. package/dist/modes/interactive/tab-title.d.ts.map +0 -1
  80. package/dist/modes/interactive/tab-title.js.map +0 -1
@@ -581,6 +581,7 @@ export class AgentSession {
581
581
  agentId,
582
582
  agentType: result.agent,
583
583
  success: result.exitCode === 0,
584
+ sessionFile: result.sessionFile,
584
585
  });
585
586
  }
586
587
  catch (emitErr) {
@@ -716,13 +717,13 @@ export class AgentSession {
716
717
  const messageText = this._getUserMessageText(event.message);
717
718
  if (messageText) {
718
719
  // Check steering queue first
719
- const steeringIndex = this._steeringMessages.indexOf(messageText);
720
+ const steeringIndex = this._steeringMessages.findIndex((message) => message.text === messageText);
720
721
  if (steeringIndex !== -1) {
721
722
  this._steeringMessages.splice(steeringIndex, 1);
722
723
  }
723
724
  else {
724
725
  // Check follow-up queue
725
- const followUpIndex = this._followUpMessages.indexOf(messageText);
726
+ const followUpIndex = this._followUpMessages.findIndex((message) => message.text === messageText);
726
727
  if (followUpIndex !== -1) {
727
728
  this._followUpMessages.splice(followUpIndex, 1);
728
729
  }
@@ -1389,7 +1390,7 @@ export class AgentSession {
1389
1390
  * Internal: Queue a steering message (already expanded, no extension command check).
1390
1391
  */
1391
1392
  async _queueSteer(text, images) {
1392
- this._steeringMessages.push(text);
1393
+ this._steeringMessages.push({ text, images: images ? [...images] : undefined });
1393
1394
  const content = [{ type: "text", text }];
1394
1395
  if (images) {
1395
1396
  content.push(...images);
@@ -1404,7 +1405,7 @@ export class AgentSession {
1404
1405
  * Internal: Queue a follow-up message (already expanded, no extension command check).
1405
1406
  */
1406
1407
  async _queueFollowUp(text, images) {
1407
- this._followUpMessages.push(text);
1408
+ this._followUpMessages.push({ text, images: images ? [...images] : undefined });
1408
1409
  const content = [{ type: "text", text }];
1409
1410
  if (images) {
1410
1411
  content.push(...images);
@@ -1513,23 +1514,39 @@ export class AgentSession {
1513
1514
  * @returns Object with steering and followUp arrays
1514
1515
  */
1515
1516
  clearQueue() {
1516
- const steering = [...this._steeringMessages];
1517
- const followUp = [...this._followUpMessages];
1517
+ const steeringMessages = this._steeringMessages.map((message) => ({
1518
+ text: message.text,
1519
+ images: message.images ? [...message.images] : undefined,
1520
+ }));
1521
+ const followUpMessages = this._followUpMessages.map((message) => ({
1522
+ text: message.text,
1523
+ images: message.images ? [...message.images] : undefined,
1524
+ }));
1525
+ const steering = steeringMessages.map((message) => message.text);
1526
+ const followUp = followUpMessages.map((message) => message.text);
1518
1527
  this._steeringMessages = [];
1519
1528
  this._followUpMessages = [];
1520
1529
  this.agent.clearAllQueues();
1521
- return { steering, followUp };
1530
+ return { steering, followUp, steeringMessages, followUpMessages };
1522
1531
  }
1523
1532
  /** Number of pending messages (includes both steering and follow-up) */
1524
1533
  get pendingMessageCount() {
1525
1534
  return this._steeringMessages.length + this._followUpMessages.length;
1526
1535
  }
1527
- /** Get pending steering messages (read-only) */
1536
+ /** Get pending steering message text (read-only compatibility view). */
1528
1537
  getSteeringMessages() {
1529
- return this._steeringMessages;
1538
+ return this._steeringMessages.map((message) => message.text);
1530
1539
  }
1531
- /** Get pending follow-up messages (read-only) */
1540
+ /** Get pending follow-up message text (read-only compatibility view). */
1532
1541
  getFollowUpMessages() {
1542
+ return this._followUpMessages.map((message) => message.text);
1543
+ }
1544
+ /** Get full pending steering payloads, including inline image attachments. */
1545
+ getSteeringMessagePayloads() {
1546
+ return this._steeringMessages;
1547
+ }
1548
+ /** Get full pending follow-up payloads, including inline image attachments. */
1549
+ getFollowUpMessagePayloads() {
1533
1550
  return this._followUpMessages;
1534
1551
  }
1535
1552
  get resourceLoader() {
@@ -2274,7 +2291,7 @@ export class AgentSession {
2274
2291
  this.sessionManager.appendCustomEntry(customType, data);
2275
2292
  },
2276
2293
  setSessionName: (name) => {
2277
- this.sessionManager.appendSessionInfo(name);
2294
+ this.setSessionName(name);
2278
2295
  },
2279
2296
  getSessionName: () => {
2280
2297
  return this.sessionManager.getSessionName();
@@ -2446,12 +2463,15 @@ export class AgentSession {
2446
2463
  parentSessionFile: () => this.sessionFile,
2447
2464
  modelRegistry: this._modelRegistry,
2448
2465
  getAgentModelsForAgent: (name) => this.settingsManager?.getAgentModelsForAgent(name),
2449
- onBackgroundStart: (agentId, agentType, taskSummary) => {
2450
- this._emit({ type: "background_agent_start", agentId, agentType, taskSummary });
2466
+ onBackgroundStart: (agentId, agentType, taskSummary, sessionDir) => {
2467
+ this._emit({ type: "background_agent_start", agentId, agentType, taskSummary, sessionDir });
2451
2468
  },
2452
2469
  onBackgroundComplete: (agentId, result, cancelled) => {
2453
2470
  this._handleBackgroundComplete(agentId, result, cancelled);
2454
2471
  },
2472
+ onBackgroundEvent: (agentId, event) => {
2473
+ this._emit({ type: "background_agent_event", agentId, event });
2474
+ },
2455
2475
  },
2456
2476
  });
2457
2477
  this._baseToolDefinitions = new Map(Object.entries(baseToolDefinitions).map(([name, tool]) => [name, tool]));
@@ -2811,6 +2831,7 @@ export class AgentSession {
2811
2831
  */
2812
2832
  setSessionName(name) {
2813
2833
  this.sessionManager.appendSessionInfo(name);
2834
+ this._emit({ type: "session_name_changed", name: this.sessionName ?? "" });
2814
2835
  }
2815
2836
  /**
2816
2837
  * Create a fork from a specific entry.
@@ -2879,6 +2900,16 @@ export class AgentSession {
2879
2900
  * @returns Result with editorText (if user message) and cancelled status
2880
2901
  */
2881
2902
  async navigateTree(targetId, options = {}) {
2903
+ // Navigating mid-stream would replace agent messages during an active run,
2904
+ // corrupting the conversation state. Fail loudly instead.
2905
+ if (this.isStreaming) {
2906
+ throw new Error("Cannot navigate the session tree while the agent is streaming. Abort or wait for idle first.");
2907
+ }
2908
+ // A second concurrent navigation (or a compaction) would clobber the shared branch-summary
2909
+ // abort controller and interleave tree mutations. Fail loudly instead.
2910
+ if (this.isCompacting) {
2911
+ throw new Error("Cannot navigate the session tree while summarization or compaction is in progress. Wait for idle first.");
2912
+ }
2882
2913
  const oldLeafId = this.sessionManager.getLeafId();
2883
2914
  // No-op if already at target
2884
2915
  if (targetId === oldLeafId) {
@@ -2908,133 +2939,144 @@ export class AgentSession {
2908
2939
  replaceInstructions,
2909
2940
  label,
2910
2941
  };
2911
- // Set up abort controller for summarization
2942
+ // Set up abort controller for summarization. Cleared in the finally below on every
2943
+ // exit path (throws, cancel returns, summary abort, success) so isCompacting cannot
2944
+ // be left wedged true after an early exit.
2912
2945
  this._branchSummaryAbortController = new AbortController();
2913
- let extensionSummary;
2914
- let fromExtension = false;
2915
- // Emit session_before_tree event
2916
- if (this._extensionRunner?.hasHandlers("session_before_tree")) {
2917
- const result = (await this._extensionRunner.emit({
2918
- type: "session_before_tree",
2919
- preparation,
2920
- signal: this._branchSummaryAbortController.signal,
2921
- }));
2922
- if (result?.cancel) {
2923
- return { cancelled: true };
2924
- }
2925
- if (result?.summary && options.summarize) {
2926
- extensionSummary = result.summary;
2927
- fromExtension = true;
2946
+ try {
2947
+ let extensionSummary;
2948
+ let fromExtension = false;
2949
+ // Emit session_before_tree event
2950
+ if (this._extensionRunner?.hasHandlers("session_before_tree")) {
2951
+ const result = (await this._extensionRunner.emit({
2952
+ type: "session_before_tree",
2953
+ preparation,
2954
+ signal: this._branchSummaryAbortController.signal,
2955
+ }));
2956
+ if (result?.cancel) {
2957
+ return { cancelled: true };
2958
+ }
2959
+ if (result?.summary && options.summarize) {
2960
+ extensionSummary = result.summary;
2961
+ fromExtension = true;
2962
+ }
2963
+ // Allow extensions to override instructions and label
2964
+ if (result?.customInstructions !== undefined) {
2965
+ customInstructions = result.customInstructions;
2966
+ }
2967
+ if (result?.replaceInstructions !== undefined) {
2968
+ replaceInstructions = result.replaceInstructions;
2969
+ }
2970
+ if (result?.label !== undefined) {
2971
+ label = result.label;
2972
+ }
2928
2973
  }
2929
- // Allow extensions to override instructions and label
2930
- if (result?.customInstructions !== undefined) {
2931
- customInstructions = result.customInstructions;
2974
+ // Run default summarizer if needed
2975
+ let summaryText;
2976
+ let summaryDetails;
2977
+ if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
2978
+ const model = this.model;
2979
+ const apiKey = await this._modelRegistry.getApiKey(model);
2980
+ if (!apiKey) {
2981
+ throw new Error(`No API key for ${model.provider}`);
2982
+ }
2983
+ const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
2984
+ const result = await generateBranchSummary(entriesToSummarize, {
2985
+ model,
2986
+ apiKey,
2987
+ signal: this._branchSummaryAbortController.signal,
2988
+ customInstructions,
2989
+ replaceInstructions,
2990
+ reserveTokens: branchSummarySettings.reserveTokens,
2991
+ });
2992
+ if (result.aborted) {
2993
+ return { cancelled: true, aborted: true };
2994
+ }
2995
+ if (result.error) {
2996
+ throw new Error(result.error);
2997
+ }
2998
+ summaryText = result.summary;
2999
+ summaryDetails = {
3000
+ readFiles: result.readFiles || [],
3001
+ modifiedFiles: result.modifiedFiles || [],
3002
+ };
3003
+ }
3004
+ else if (extensionSummary) {
3005
+ summaryText = extensionSummary.summary;
3006
+ summaryDetails = extensionSummary.details;
3007
+ }
3008
+ // Re-check after extension/summarization awaits: prompt dispatch can start streaming
3009
+ // while we yielded (same race class guarded in the prompt path), and the mutation
3010
+ // block below must not replace messages during an active run.
3011
+ if (this.isStreaming) {
3012
+ throw new Error("Cannot navigate the session tree while the agent is streaming. Abort or wait for idle first.");
3013
+ }
3014
+ // Determine the new leaf position based on target type
3015
+ let newLeafId;
3016
+ let editorText;
3017
+ if (targetEntry.type === "message" && targetEntry.message.role === "user") {
3018
+ // User message: leaf = parent (null if root), text goes to editor
3019
+ newLeafId = targetEntry.parentId;
3020
+ editorText = this._extractUserMessageText(targetEntry.message.content);
3021
+ }
3022
+ else if (targetEntry.type === "custom_message") {
3023
+ // Custom message: leaf = parent (null if root), text goes to editor
3024
+ newLeafId = targetEntry.parentId;
3025
+ editorText =
3026
+ typeof targetEntry.content === "string"
3027
+ ? targetEntry.content
3028
+ : targetEntry.content
3029
+ .filter((c) => c.type === "text")
3030
+ .map((c) => c.text)
3031
+ .join("");
2932
3032
  }
2933
- if (result?.replaceInstructions !== undefined) {
2934
- replaceInstructions = result.replaceInstructions;
3033
+ else {
3034
+ // Non-user message: leaf = selected node
3035
+ newLeafId = targetId;
3036
+ }
3037
+ // Switch leaf (with or without summary)
3038
+ // Summary is attached at the navigation target position (newLeafId), not the old branch
3039
+ let summaryEntry;
3040
+ if (summaryText) {
3041
+ // Create summary at target position (can be null for root)
3042
+ const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
3043
+ summaryEntry = this.sessionManager.getEntry(summaryId);
3044
+ // Attach label to the summary entry
3045
+ if (label) {
3046
+ this.sessionManager.appendLabelChange(summaryId, label);
3047
+ }
2935
3048
  }
2936
- if (result?.label !== undefined) {
2937
- label = result.label;
3049
+ else if (newLeafId === null) {
3050
+ // No summary, navigating to root - reset leaf
3051
+ this.sessionManager.resetLeaf();
2938
3052
  }
2939
- }
2940
- // Run default summarizer if needed
2941
- let summaryText;
2942
- let summaryDetails;
2943
- if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) {
2944
- const model = this.model;
2945
- const apiKey = await this._modelRegistry.getApiKey(model);
2946
- if (!apiKey) {
2947
- throw new Error(`No API key for ${model.provider}`);
2948
- }
2949
- const branchSummarySettings = this.settingsManager.getBranchSummarySettings();
2950
- const result = await generateBranchSummary(entriesToSummarize, {
2951
- model,
2952
- apiKey,
2953
- signal: this._branchSummaryAbortController.signal,
2954
- customInstructions,
2955
- replaceInstructions,
2956
- reserveTokens: branchSummarySettings.reserveTokens,
2957
- });
2958
- this._branchSummaryAbortController = undefined;
2959
- if (result.aborted) {
2960
- return { cancelled: true, aborted: true };
3053
+ else {
3054
+ // No summary, navigating to non-root
3055
+ this.sessionManager.branch(newLeafId);
2961
3056
  }
2962
- if (result.error) {
2963
- throw new Error(result.error);
3057
+ // Attach label to target entry when not summarizing (no summary entry to label)
3058
+ if (label && !summaryText) {
3059
+ this.sessionManager.appendLabelChange(targetId, label);
2964
3060
  }
2965
- summaryText = result.summary;
2966
- summaryDetails = {
2967
- readFiles: result.readFiles || [],
2968
- modifiedFiles: result.modifiedFiles || [],
2969
- };
2970
- }
2971
- else if (extensionSummary) {
2972
- summaryText = extensionSummary.summary;
2973
- summaryDetails = extensionSummary.details;
2974
- }
2975
- // Determine the new leaf position based on target type
2976
- let newLeafId;
2977
- let editorText;
2978
- if (targetEntry.type === "message" && targetEntry.message.role === "user") {
2979
- // User message: leaf = parent (null if root), text goes to editor
2980
- newLeafId = targetEntry.parentId;
2981
- editorText = this._extractUserMessageText(targetEntry.message.content);
2982
- }
2983
- else if (targetEntry.type === "custom_message") {
2984
- // Custom message: leaf = parent (null if root), text goes to editor
2985
- newLeafId = targetEntry.parentId;
2986
- editorText =
2987
- typeof targetEntry.content === "string"
2988
- ? targetEntry.content
2989
- : targetEntry.content
2990
- .filter((c) => c.type === "text")
2991
- .map((c) => c.text)
2992
- .join("");
2993
- }
2994
- else {
2995
- // Non-user message: leaf = selected node
2996
- newLeafId = targetId;
2997
- }
2998
- // Switch leaf (with or without summary)
2999
- // Summary is attached at the navigation target position (newLeafId), not the old branch
3000
- let summaryEntry;
3001
- if (summaryText) {
3002
- // Create summary at target position (can be null for root)
3003
- const summaryId = this.sessionManager.branchWithSummary(newLeafId, summaryText, summaryDetails, fromExtension);
3004
- summaryEntry = this.sessionManager.getEntry(summaryId);
3005
- // Attach label to the summary entry
3006
- if (label) {
3007
- this.sessionManager.appendLabelChange(summaryId, label);
3061
+ // Update agent state
3062
+ const sessionContext = this.sessionManager.buildSessionContext();
3063
+ this.agent.replaceMessages(sessionContext.messages);
3064
+ // Emit session_tree event
3065
+ if (this._extensionRunner) {
3066
+ await this._extensionRunner.emit({
3067
+ type: "session_tree",
3068
+ newLeafId: this.sessionManager.getLeafId(),
3069
+ oldLeafId,
3070
+ summaryEntry,
3071
+ fromExtension: summaryText ? fromExtension : undefined,
3072
+ });
3008
3073
  }
3074
+ // Emit to custom tools
3075
+ return { editorText, cancelled: false, summaryEntry };
3009
3076
  }
3010
- else if (newLeafId === null) {
3011
- // No summary, navigating to root - reset leaf
3012
- this.sessionManager.resetLeaf();
3013
- }
3014
- else {
3015
- // No summary, navigating to non-root
3016
- this.sessionManager.branch(newLeafId);
3017
- }
3018
- // Attach label to target entry when not summarizing (no summary entry to label)
3019
- if (label && !summaryText) {
3020
- this.sessionManager.appendLabelChange(targetId, label);
3021
- }
3022
- // Update agent state
3023
- const sessionContext = this.sessionManager.buildSessionContext();
3024
- this.agent.replaceMessages(sessionContext.messages);
3025
- // Emit session_tree event
3026
- if (this._extensionRunner) {
3027
- await this._extensionRunner.emit({
3028
- type: "session_tree",
3029
- newLeafId: this.sessionManager.getLeafId(),
3030
- oldLeafId,
3031
- summaryEntry,
3032
- fromExtension: summaryText ? fromExtension : undefined,
3033
- });
3077
+ finally {
3078
+ this._branchSummaryAbortController = undefined;
3034
3079
  }
3035
- // Emit to custom tools
3036
- this._branchSummaryAbortController = undefined;
3037
- return { editorText, cancelled: false, summaryEntry };
3038
3080
  }
3039
3081
  /**
3040
3082
  * Get all user messages from session for fork selector.