@dreb/coding-agent 2.46.0 → 2.47.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 (48) hide show
  1. package/README.md +4 -2
  2. package/dist/core/agent-session.d.ts.map +1 -1
  3. package/dist/core/agent-session.js +19 -8
  4. package/dist/core/agent-session.js.map +1 -1
  5. package/dist/core/extensions/index.d.ts +1 -1
  6. package/dist/core/extensions/index.d.ts.map +1 -1
  7. package/dist/core/extensions/index.js.map +1 -1
  8. package/dist/core/extensions/runner.d.ts +7 -0
  9. package/dist/core/extensions/runner.d.ts.map +1 -1
  10. package/dist/core/extensions/runner.js +10 -0
  11. package/dist/core/extensions/runner.js.map +1 -1
  12. package/dist/core/extensions/types.d.ts +35 -0
  13. package/dist/core/extensions/types.d.ts.map +1 -1
  14. package/dist/core/extensions/types.js.map +1 -1
  15. package/dist/core/sdk.d.ts.map +1 -1
  16. package/dist/core/sdk.js +1 -0
  17. package/dist/core/sdk.js.map +1 -1
  18. package/dist/core/session-manager.d.ts.map +1 -1
  19. package/dist/core/session-manager.js +38 -2
  20. package/dist/core/session-manager.js.map +1 -1
  21. package/dist/core/tools/ask-user.d.ts +45 -0
  22. package/dist/core/tools/ask-user.d.ts.map +1 -0
  23. package/dist/core/tools/ask-user.js +210 -0
  24. package/dist/core/tools/ask-user.js.map +1 -0
  25. package/dist/core/tools/index.d.ts +11 -0
  26. package/dist/core/tools/index.d.ts.map +1 -1
  27. package/dist/core/tools/index.js +8 -0
  28. package/dist/core/tools/index.js.map +1 -1
  29. package/dist/modes/interactive/components/ask-user.d.ts +59 -0
  30. package/dist/modes/interactive/components/ask-user.d.ts.map +1 -0
  31. package/dist/modes/interactive/components/ask-user.js +240 -0
  32. package/dist/modes/interactive/components/ask-user.js.map +1 -0
  33. package/dist/modes/interactive/interactive-mode.d.ts +12 -0
  34. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  35. package/dist/modes/interactive/interactive-mode.js +92 -5
  36. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  37. package/dist/modes/rpc/rpc-mode.d.ts +16 -1
  38. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  39. package/dist/modes/rpc/rpc-mode.js +112 -59
  40. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  41. package/dist/modes/rpc/rpc-types.d.ts +24 -0
  42. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  43. package/dist/modes/rpc/rpc-types.js.map +1 -1
  44. package/docs/dashboard.md +1 -1
  45. package/docs/extensions.md +33 -3
  46. package/docs/rpc.md +45 -6
  47. package/docs/session.md +1 -1
  48. package/package.json +1 -1
@@ -855,77 +855,105 @@ export async function navigateTreeForRpc(session, targetId, options) {
855
855
  ...(result.editorText !== undefined ? { editorText: result.editorText } : {}),
856
856
  };
857
857
  }
858
- /**
859
- * Run in RPC mode.
860
- * Listens for JSON commands on stdin, outputs events and responses on stdout.
861
- */
862
- export async function runRpcMode(session, modelFallbackMessage) {
863
- takeOverStdout();
864
- const output = (obj) => {
865
- writeRawStdout(serializeJsonLine(obj));
866
- };
867
- const success = (id, command, data) => {
868
- if (data === undefined) {
869
- return { id, type: "response", command, success: true };
870
- }
871
- return { id, type: "response", command, success: true, data };
872
- };
873
- const error = (id, command, message) => {
874
- return { id, type: "response", command, success: false, error: message };
875
- };
876
- if (session.sessionFile && session.messages.length > 0) {
877
- const rehydratedCount = rehydrateBackgroundAgentsFromDisk(session.sessionFile);
878
- if (rehydratedCount > 0) {
879
- console.error(`[rpc] Rehydrated ${rehydratedCount} background subagent${rehydratedCount === 1 ? "" : "s"} from disk`);
880
- }
881
- }
882
- // Pending extension UI requests waiting for response
883
- const pendingExtensionRequests = new Map();
884
- // Shutdown request flag
885
- let shutdownRequested = false;
886
- let dailyCostTracker;
887
- let dailyCostTrackerPrimed = false;
858
+ export function createRpcExtensionUIContext(output, pendingExtensionRequests, onAskStop = () => { }) {
888
859
  /** Helper for dialog methods with signal/timeout support */
889
- function createDialogPromise(opts, defaultValue, request, parseResponse) {
860
+ function createDialogPromise(opts, defaultValue, request, parseResponse, onTimeout) {
890
861
  if (opts?.signal?.aborted)
891
862
  return Promise.resolve(defaultValue);
892
863
  const id = crypto.randomUUID();
893
864
  return new Promise((resolve, reject) => {
894
865
  let timeoutId;
866
+ let settled = false;
895
867
  const cleanup = () => {
868
+ if (settled)
869
+ return false;
870
+ settled = true;
896
871
  if (timeoutId)
897
872
  clearTimeout(timeoutId);
898
873
  opts?.signal?.removeEventListener("abort", onAbort);
899
874
  pendingExtensionRequests.delete(id);
875
+ output({ type: "extension_ui_response_handled", id });
876
+ return true;
900
877
  };
901
878
  const onAbort = () => {
902
- cleanup();
879
+ if (!cleanup())
880
+ return;
903
881
  resolve(defaultValue);
904
882
  };
905
883
  opts?.signal?.addEventListener("abort", onAbort, { once: true });
906
884
  if (opts?.timeout) {
907
885
  timeoutId = setTimeout(() => {
908
- cleanup();
886
+ if (!cleanup())
887
+ return;
888
+ onTimeout?.();
909
889
  resolve(defaultValue);
910
890
  }, opts.timeout);
911
891
  }
892
+ const rpcRequest = {
893
+ type: "extension_ui_request",
894
+ id,
895
+ ...request,
896
+ };
912
897
  pendingExtensionRequests.set(id, {
898
+ request: rpcRequest,
913
899
  resolve: (response) => {
914
- cleanup();
915
- resolve(parseResponse(response));
900
+ if (!cleanup())
901
+ return;
902
+ try {
903
+ resolve(parseResponse(response));
904
+ }
905
+ catch (error) {
906
+ reject(error);
907
+ }
908
+ },
909
+ reject: (error) => {
910
+ if (!cleanup())
911
+ return;
912
+ reject(error);
916
913
  },
917
- reject,
918
914
  });
919
- output({ type: "extension_ui_request", id, ...request });
915
+ output(rpcRequest);
920
916
  });
921
917
  }
922
918
  /**
923
919
  * Create an extension UI context that uses the RPC protocol.
924
920
  */
925
- const createExtensionUIContext = () => ({
921
+ return {
926
922
  select: (title, options, opts) => createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
927
923
  confirm: (title, message, opts) => createDialogPromise(opts, false, { method: "confirm", title, message, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? false : "confirmed" in r ? r.confirmed : false),
928
924
  input: (title, placeholder, opts) => createDialogPromise(opts, undefined, { method: "input", title, placeholder, timeout: opts?.timeout }, (r) => "cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined),
925
+ ask: (request, opts) => createDialogPromise(opts, undefined, {
926
+ method: "ask",
927
+ title: request.title ?? "Question",
928
+ question: request.question,
929
+ options: request.options,
930
+ allowFreeText: request.allowFreeText,
931
+ multiSelect: request.multiSelect,
932
+ multiline: request.multiline,
933
+ timeout: opts?.timeout,
934
+ // Preserve the authoritative deadline across Dashboard reload,
935
+ // resync, and drill-in hydration instead of restarting the full
936
+ // duration whenever the question component remounts.
937
+ expiresAt: opts?.timeout ? Date.now() + opts.timeout : undefined,
938
+ }, (response) => {
939
+ if ("cancelled" in response && response.cancelled) {
940
+ onAskStop();
941
+ return undefined;
942
+ }
943
+ const selected = response.selected;
944
+ const customText = response.customText;
945
+ if (!Array.isArray(selected) || !selected.every((value) => typeof value === "string")) {
946
+ throw new Error("Invalid RPC ask response: selected must be an array of strings");
947
+ }
948
+ if (customText !== undefined && typeof customText !== "string") {
949
+ throw new Error("Invalid RPC ask response: customText must be a string");
950
+ }
951
+ if (selected.length === 0 && !customText?.trim()) {
952
+ onAskStop();
953
+ return undefined;
954
+ }
955
+ return { selected, customText };
956
+ }, onAskStop),
929
957
  notify(message, type) {
930
958
  // Fire and forget - no response needed
931
959
  output({
@@ -1004,26 +1032,11 @@ export async function runRpcMode(session, modelFallbackMessage) {
1004
1032
  // Host should track editor state locally if needed
1005
1033
  return "";
1006
1034
  },
1007
- async editor(title, prefill) {
1008
- const id = crypto.randomUUID();
1009
- return new Promise((resolve, reject) => {
1010
- pendingExtensionRequests.set(id, {
1011
- resolve: (response) => {
1012
- if ("cancelled" in response && response.cancelled) {
1013
- resolve(undefined);
1014
- }
1015
- else if ("value" in response) {
1016
- resolve(response.value);
1017
- }
1018
- else {
1019
- resolve(undefined);
1020
- }
1021
- },
1022
- reject,
1023
- });
1024
- output({ type: "extension_ui_request", id, method: "editor", title, prefill });
1025
- });
1026
- },
1035
+ editor: (title, prefill) => createDialogPromise(undefined, undefined, { method: "editor", title, prefill }, (response) => "cancelled" in response && response.cancelled
1036
+ ? undefined
1037
+ : "value" in response
1038
+ ? response.value
1039
+ : undefined),
1027
1040
  setEditorComponent() {
1028
1041
  // Custom editor components not supported in RPC mode
1029
1042
  },
@@ -1047,7 +1060,45 @@ export async function runRpcMode(session, modelFallbackMessage) {
1047
1060
  setToolsExpanded(_expanded) {
1048
1061
  // Tool expansion not supported in RPC mode - no TUI
1049
1062
  },
1050
- });
1063
+ };
1064
+ }
1065
+ /** Cancel every pending dialog during RPC host teardown. */
1066
+ export function cancelPendingRpcExtensionRequests(pendingExtensionRequests) {
1067
+ for (const [id, pending] of [...pendingExtensionRequests]) {
1068
+ pending.resolve({ type: "extension_ui_response", id, cancelled: true });
1069
+ }
1070
+ }
1071
+ export async function runRpcMode(session, modelFallbackMessage) {
1072
+ takeOverStdout();
1073
+ const output = (obj) => {
1074
+ writeRawStdout(serializeJsonLine(obj));
1075
+ };
1076
+ const success = (id, command, data) => {
1077
+ if (data === undefined) {
1078
+ return { id, type: "response", command, success: true };
1079
+ }
1080
+ return { id, type: "response", command, success: true, data };
1081
+ };
1082
+ const error = (id, command, message) => {
1083
+ return { id, type: "response", command, success: false, error: message };
1084
+ };
1085
+ if (session.sessionFile && session.messages.length > 0) {
1086
+ const rehydratedCount = rehydrateBackgroundAgentsFromDisk(session.sessionFile);
1087
+ if (rehydratedCount > 0) {
1088
+ console.error(`[rpc] Rehydrated ${rehydratedCount} background subagent${rehydratedCount === 1 ? "" : "s"} from disk`);
1089
+ }
1090
+ }
1091
+ // Pending extension UI requests waiting for response. Keep the emitted
1092
+ // payload alongside its callbacks so recovery snapshots can reconstruct the
1093
+ // answer UI after a Dashboard reload or replay gap.
1094
+ const pendingExtensionRequests = new Map();
1095
+ // Shutdown request flag
1096
+ let shutdownRequested = false;
1097
+ let dailyCostTracker;
1098
+ let dailyCostTrackerPrimed = false;
1099
+ // Extension UI context uses the RPC protocol; built by a module-scope
1100
+ // factory so the dialog round trip is unit-testable (see createRpcExtensionUIContext).
1101
+ const createExtensionUIContext = () => createRpcExtensionUIContext(output, pendingExtensionRequests, () => void session.abort());
1051
1102
  // Set up extensions with RPC-based UI context
1052
1103
  await session.bindExtensions({
1053
1104
  uiContext: createExtensionUIContext(),
@@ -1186,6 +1237,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1186
1237
  state: getStateForRpc(session, modelFallbackMessage),
1187
1238
  messages: getDashboardMessagesForRpc(session),
1188
1239
  backgroundAgents: getBackgroundAgents().map(toRpcBackgroundAgentInfo),
1240
+ pendingExtensionUiRequests: [...pendingExtensionRequests.values()].map(({ request }) => request),
1189
1241
  };
1190
1242
  return success(id, "get_dashboard_snapshot", data);
1191
1243
  }
@@ -1515,6 +1567,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
1515
1567
  */
1516
1568
  let detachInput = () => { };
1517
1569
  async function shutdown() {
1570
+ cancelPendingRpcExtensionRequests(pendingExtensionRequests);
1518
1571
  const currentRunner = session.extensionRunner;
1519
1572
  if (currentRunner?.hasHandlers("session_shutdown")) {
1520
1573
  await currentRunner.emit({ type: "session_shutdown" });