@di-code/coding-agent 0.2.1 → 0.2.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 (88) hide show
  1. package/README.md +5 -0
  2. package/dist/core/session/session-manager.d.ts +18 -1
  3. package/dist/core/session/session-manager.d.ts.map +1 -1
  4. package/dist/core/session/session-manager.js +90 -1
  5. package/dist/core/session/session-manager.js.map +1 -1
  6. package/dist/core/session/session-storage.d.ts +2 -0
  7. package/dist/core/session/session-storage.d.ts.map +1 -1
  8. package/dist/core/session/session-storage.js +22 -1
  9. package/dist/core/session/session-storage.js.map +1 -1
  10. package/dist/core/session/types.d.ts +16 -1
  11. package/dist/core/session/types.d.ts.map +1 -1
  12. package/dist/core/session/types.js.map +1 -1
  13. package/dist/core/session.d.ts +27 -2
  14. package/dist/core/session.d.ts.map +1 -1
  15. package/dist/core/session.js +59 -0
  16. package/dist/core/session.js.map +1 -1
  17. package/dist/entry.js +1 -1
  18. package/dist/entry.js.map +1 -1
  19. package/dist/i18n.js +4 -4
  20. package/dist/i18n.js.map +1 -1
  21. package/dist/interactive-host-entry.d.ts.map +1 -1
  22. package/dist/interactive-host-entry.js +107 -12
  23. package/dist/interactive-host-entry.js.map +1 -1
  24. package/dist/main.d.ts.map +1 -1
  25. package/dist/main.js +3 -0
  26. package/dist/main.js.map +1 -1
  27. package/dist/modes/interactive-components.d.ts +8 -0
  28. package/dist/modes/interactive-components.d.ts.map +1 -1
  29. package/dist/modes/interactive-components.js +7 -2
  30. package/dist/modes/interactive-components.js.map +1 -1
  31. package/dist/modes/interactive.d.ts +4 -0
  32. package/dist/modes/interactive.d.ts.map +1 -1
  33. package/dist/modes/interactive.js +69 -0
  34. package/dist/modes/interactive.js.map +1 -1
  35. package/dist/modes/tree-selector.d.ts.map +1 -1
  36. package/dist/modes/tree-selector.js +4 -0
  37. package/dist/modes/tree-selector.js.map +1 -1
  38. package/dist/rpc/client.d.ts +2 -0
  39. package/dist/rpc/client.d.ts.map +1 -1
  40. package/dist/rpc/client.js +3 -0
  41. package/dist/rpc/client.js.map +1 -1
  42. package/dist/rpc/dispatcher.d.ts +14 -0
  43. package/dist/rpc/dispatcher.d.ts.map +1 -1
  44. package/dist/rpc/dispatcher.js +222 -7
  45. package/dist/rpc/dispatcher.js.map +1 -1
  46. package/dist/rpc/protocol.d.ts +12 -5
  47. package/dist/rpc/protocol.d.ts.map +1 -1
  48. package/dist/rpc/protocol.js +34 -0
  49. package/dist/rpc/protocol.js.map +1 -1
  50. package/dist/rpc/server.d.ts +5 -0
  51. package/dist/rpc/server.d.ts.map +1 -1
  52. package/dist/rpc/server.js +4 -0
  53. package/dist/rpc/server.js.map +1 -1
  54. package/dist/rpc-server-entry.d.ts.map +1 -1
  55. package/dist/rpc-server-entry.js +15 -1
  56. package/dist/rpc-server-entry.js.map +1 -1
  57. package/dist/runtime/product-host.d.ts +7 -0
  58. package/dist/runtime/product-host.d.ts.map +1 -1
  59. package/dist/runtime/product-host.js +29 -9
  60. package/dist/runtime/product-host.js.map +1 -1
  61. package/dist/runtime/session-factory.d.ts +18 -2
  62. package/dist/runtime/session-factory.d.ts.map +1 -1
  63. package/dist/runtime/session-factory.js +286 -24
  64. package/dist/runtime/session-factory.js.map +1 -1
  65. package/dist/runtime/session-host.d.ts +46 -1
  66. package/dist/runtime/session-host.d.ts.map +1 -1
  67. package/dist/runtime/session-host.js +38 -6
  68. package/dist/runtime/session-host.js.map +1 -1
  69. package/dist/ui-host.d.ts.map +1 -1
  70. package/dist/ui-host.js +3 -0
  71. package/dist/ui-host.js.map +1 -1
  72. package/dist/web/assets/index-C2SOU925.css +1 -0
  73. package/dist/web/assets/index-knXkInvO.js +41 -0
  74. package/dist/web/assets/index-knXkInvO.js.map +1 -0
  75. package/dist/web/index.html +2 -2
  76. package/dist/web-command.d.ts.map +1 -1
  77. package/dist/web-command.js +8 -1
  78. package/dist/web-command.js.map +1 -1
  79. package/dist/webui-entry.js +9 -1
  80. package/dist/webui-entry.js.map +1 -1
  81. package/dist/webui.d.ts +10 -0
  82. package/dist/webui.d.ts.map +1 -1
  83. package/dist/webui.js +21 -0
  84. package/dist/webui.js.map +1 -1
  85. package/package.json +11 -9
  86. package/dist/web/assets/index-C-NyCPxd.css +0 -1
  87. package/dist/web/assets/index-DgyxuNNG.js +0 -41
  88. package/dist/web/assets/index-DgyxuNNG.js.map +0 -1
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { redactSensitiveText } from "@di-code/plugin-runtime";
3
3
  import { RPC_PROTOCOL_VERSION, RpcProtocolError, } from "./protocol.js";
4
+ const MAX_PROJECTION_BYTES = 256 * 1024;
5
+ const FORBIDDEN_PROJECTION_KEYS = /(?:^|[_-])(path|cwd|root|token|secret|authorization|api[_-]?key)(?:$|[_-])/iu;
4
6
  function transcriptEntryIds(tree, leafId) {
5
7
  if (!leafId)
6
8
  return [];
@@ -108,6 +110,7 @@ export class RpcDispatcher {
108
110
  methods;
109
111
  onError;
110
112
  approvals = new Map();
113
+ interactions = new Map();
111
114
  maxEvents;
112
115
  productState;
113
116
  productHost;
@@ -168,6 +171,33 @@ export class RpcDispatcher {
168
171
  this.listeners.add(listener);
169
172
  return () => this.listeners.delete(listener);
170
173
  }
174
+ /** Publishes a versioned projection only to clients that negotiated projection events. */
175
+ emitProjection(input) {
176
+ if (!this.negotiatedEvents.has("projection"))
177
+ return;
178
+ if (!isBrowserSafeProjection(input.state)) {
179
+ this.onError(new Error("Skipped an unsafe extension projection."));
180
+ return;
181
+ }
182
+ const record = {
183
+ version: RPC_PROTOCOL_VERSION,
184
+ kind: "event",
185
+ requestId: input.requestId ?? "projection",
186
+ sessionId: this.activeSessionId(),
187
+ sequence: ++this.sequence,
188
+ event: {
189
+ type: "projection",
190
+ namespace: input.namespace,
191
+ projectionName: input.projectionName,
192
+ version: input.version,
193
+ state: structuredClone(input.state),
194
+ },
195
+ };
196
+ this.events.push(record);
197
+ if (this.events.length > this.maxEvents)
198
+ this.events.shift();
199
+ this.emit(record);
200
+ }
171
201
  async dispatch(request) {
172
202
  this.pruneOperations();
173
203
  if (this.disposed)
@@ -203,6 +233,16 @@ export class RpcDispatcher {
203
233
  for (const operation of this.operations.values())
204
234
  if (operation.status === "queued" || operation.status === "running")
205
235
  operation.controller.abort();
236
+ for (const pending of this.approvals.values())
237
+ pending.resolve(false);
238
+ this.approvals.clear();
239
+ for (const pending of this.interactions.values())
240
+ pending.resolve({
241
+ requestId: pending.request.requestId,
242
+ toolCallId: pending.request.toolCallId,
243
+ status: "disposed",
244
+ });
245
+ this.interactions.clear();
206
246
  await Promise.allSettled([...this.operations.values()].map((operation) => operation.promise));
207
247
  await this.attachments.dispose();
208
248
  this.listeners.clear();
@@ -256,13 +296,19 @@ export class RpcDispatcher {
256
296
  });
257
297
  case "get_usage":
258
298
  return this.success(request.id, { method: "get_usage", usage: this.host().usage() });
259
- case "list_skills":
299
+ case "list_skills": {
300
+ const host = this.host();
301
+ const ui = host.ui();
302
+ const skills = ui.availableSkills.map(({ name, description, scope }) => ({ name, description, scope }));
303
+ const projectResourcesDetected = host
304
+ .startupStatus()
305
+ .resourceDiagnostics.some((diagnostic) => diagnostic.kind === "skill" && diagnostic.stage === "trust");
260
306
  return this.success(request.id, {
261
307
  method: "list_skills",
262
- skills: this.host()
263
- .ui()
264
- .availableSkills.map(({ name, description, scope }) => ({ name, description, scope })),
308
+ skills,
309
+ projectResourcesDetected,
265
310
  });
311
+ }
266
312
  case "get_resources":
267
313
  return this.success(request.id, {
268
314
  method: "get_resources",
@@ -331,6 +377,22 @@ export class RpcDispatcher {
331
377
  approvalId: request.params.approvalId,
332
378
  approved: request.params.approved,
333
379
  });
380
+ case "respond_interaction": {
381
+ const interactionId = request.params.requestId;
382
+ const pending = this.interactions.get(interactionId);
383
+ if (pending) {
384
+ this.interactions.delete(interactionId);
385
+ pending.resolve({
386
+ requestId: interactionId,
387
+ toolCallId: pending.request.toolCallId,
388
+ status: request.params.status,
389
+ ...(typeof request.params.approved === "boolean" ? { approved: request.params.approved } : {}),
390
+ ...(typeof request.params.value === "string" ? { value: request.params.value } : {}),
391
+ ...(typeof request.params.feedback === "string" ? { feedback: request.params.feedback } : {}),
392
+ });
393
+ }
394
+ return this.success(request.id, { method: "respond_interaction", requestId: interactionId });
395
+ }
334
396
  case "run_command":
335
397
  return this.failure(request.id, "METHOD_NOT_FOUND", "Command execution requires operation dispatch.");
336
398
  default:
@@ -343,8 +405,12 @@ export class RpcDispatcher {
343
405
  }
344
406
  /** Waits for the browser to approve a specific tool invocation. */
345
407
  async requestToolApproval(toolName, parameters, signal) {
346
- if (!this.negotiatedEvents.has("tool_approval"))
347
- return true;
408
+ if (!this.negotiatedEvents.has("tool_approval") && !this.negotiatedEvents.has("interaction_request"))
409
+ return false;
410
+ if (this.negotiatedEvents.has("interaction_request")) {
411
+ const result = await this.requestInteraction({ kind: "approval", prompt: `Allow tool ${toolName}?`, toolCallId: randomUUID(), intent: "tool-approval" }, signal);
412
+ return result.status === "answered" && result.approved === true;
413
+ }
348
414
  const approvalId = randomUUID();
349
415
  const requestId = [...this.operations.values()].find((operation) => operation.status === "running")?.requestId ?? approvalId;
350
416
  return await new Promise((resolve, reject) => {
@@ -361,7 +427,7 @@ export class RpcDispatcher {
361
427
  resolve(approved);
362
428
  },
363
429
  });
364
- this.emit({
430
+ const record = {
365
431
  version: RPC_PROTOCOL_VERSION,
366
432
  kind: "event",
367
433
  requestId,
@@ -373,7 +439,71 @@ export class RpcDispatcher {
373
439
  toolName,
374
440
  arguments: typeof parameters === "object" && parameters !== null ? parameters : {},
375
441
  },
442
+ };
443
+ this.events.push(record);
444
+ if (this.events.length > this.maxEvents)
445
+ this.events.shift();
446
+ this.emit(record);
447
+ });
448
+ }
449
+ /** Requests a structured user response through the negotiated interaction channel. */
450
+ async requestInteraction(input, signal) {
451
+ if (!this.negotiatedEvents.has("interaction_request")) {
452
+ throw Object.assign(new Error("No user interaction channel is available."), { code: "INTERACTION_UNAVAILABLE" });
453
+ }
454
+ const requestId = input.requestId ?? randomUUID();
455
+ const request = { ...input, requestId };
456
+ const existing = this.interactions.get(requestId);
457
+ if (existing)
458
+ return await new Promise((resolve) => {
459
+ const original = existing.resolve;
460
+ existing.resolve = (result) => {
461
+ original(result);
462
+ resolve(result);
463
+ };
464
+ });
465
+ return await new Promise((resolve, reject) => {
466
+ let timer;
467
+ const abort = () => {
468
+ this.interactions.delete(requestId);
469
+ if (timer !== undefined)
470
+ clearTimeout(timer);
471
+ reject(Object.assign(new Error("User interaction was cancelled."), { code: "CANCELLED" }));
472
+ };
473
+ if (signal?.aborted)
474
+ return abort();
475
+ signal?.addEventListener("abort", abort, { once: true });
476
+ this.interactions.set(requestId, {
477
+ request,
478
+ resolve: (result) => {
479
+ signal?.removeEventListener("abort", abort);
480
+ if (timer !== undefined)
481
+ clearTimeout(timer);
482
+ resolve(result);
483
+ },
376
484
  });
485
+ if (input.timeoutMs !== undefined) {
486
+ timer = setTimeout(() => {
487
+ const current = this.interactions.get(requestId);
488
+ if (!current)
489
+ return;
490
+ this.interactions.delete(requestId);
491
+ signal?.removeEventListener("abort", abort);
492
+ resolve({ requestId, toolCallId: input.toolCallId, status: "timeout" });
493
+ }, input.timeoutMs);
494
+ }
495
+ const record = {
496
+ version: RPC_PROTOCOL_VERSION,
497
+ kind: "event",
498
+ requestId,
499
+ sessionId: this.activeSessionId(),
500
+ sequence: ++this.sequence,
501
+ event: { type: "interaction_request", interactionRequestId: requestId, ...request },
502
+ };
503
+ this.events.push(record);
504
+ if (this.events.length > this.maxEvents)
505
+ this.events.shift();
506
+ this.emit(record);
377
507
  });
378
508
  }
379
509
  async runOperation(operation, request) {
@@ -729,11 +859,27 @@ export class RpcDispatcher {
729
859
  description: skill.description,
730
860
  kind: "skill",
731
861
  }));
862
+ if (sessionHost(this.session) && !commands.some((command) => command.name === "plan")) {
863
+ commands.push({ name: "plan", description: "Enter or leave plan mode.", kind: "command" });
864
+ }
732
865
  return [...commands, ...skills].sort((left, right) => left.name.localeCompare(right.name));
733
866
  }
734
867
  async runCommand(request, signal) {
735
868
  const name = request.params.name;
736
869
  const args = request.params.args ?? "";
870
+ if (name === "plan" && sessionHost(this.session)) {
871
+ await this.session.planCommand(args);
872
+ const projection = this.session.planMode();
873
+ if (projection)
874
+ this.emitProjection({
875
+ namespace: "plan",
876
+ projectionName: "mode",
877
+ version: 1,
878
+ state: projection,
879
+ requestId: request.id,
880
+ });
881
+ return { method: "run_command", command: name, action: { command: name, args } };
882
+ }
737
883
  if (!this.commandRegistry?.list().some((command) => command.name === name))
738
884
  throw new RpcProtocolError("NOT_FOUND", `Unknown command: /${name}`, request.id);
739
885
  let action;
@@ -765,9 +911,32 @@ export class RpcDispatcher {
765
911
  "snapshot_required",
766
912
  "session_changed",
767
913
  "tool_approval",
914
+ "interaction_request",
768
915
  "product_audit",
916
+ "projection",
769
917
  ].includes(event))
770
918
  this.negotiatedEvents.add(event);
919
+ if (this.negotiatedEvents.has("projection") && sessionHost(this.session)) {
920
+ const host = this.session;
921
+ const plan = host.planMode?.();
922
+ if (plan)
923
+ this.emitProjection({ namespace: "plan", projectionName: "mode", version: 1, state: plan });
924
+ for (const projection of host.projections?.() ?? [])
925
+ this.emitProjection({
926
+ namespace: projection.namespace,
927
+ projectionName: projection.projectionName,
928
+ version: projection.version,
929
+ state: projection.state,
930
+ });
931
+ const extensions = host.extensions?.();
932
+ if (extensions)
933
+ this.emitProjection({
934
+ namespace: "extension",
935
+ projectionName: "ui",
936
+ version: extensions.apiVersion,
937
+ state: { badges: extensions.badges, ui: extensions.ui },
938
+ });
939
+ }
771
940
  return this.success(request.id, {
772
941
  method: "get_capabilities",
773
942
  methods: RPC_METHODS_FOR_CAPABILITIES,
@@ -858,6 +1027,7 @@ export class RpcDispatcher {
858
1027
  },
859
1028
  transcript: snapshot.transcript,
860
1029
  tree: snapshot.tree,
1030
+ ...(snapshot.events ? { events: snapshot.events } : {}),
861
1031
  stats: snapshot.stats,
862
1032
  readOnly: true,
863
1033
  },
@@ -912,6 +1082,27 @@ export class RpcDispatcher {
912
1082
  });
913
1083
  }
914
1084
  onSessionEvent(event) {
1085
+ if (sessionHost(this.session) && this.negotiatedEvents.has("projection")) {
1086
+ const host = this.session;
1087
+ const plan = host.planMode?.();
1088
+ if (plan)
1089
+ this.emitProjection({ namespace: "plan", projectionName: "mode", version: 1, state: plan });
1090
+ for (const projection of host.projections?.() ?? [])
1091
+ this.emitProjection({
1092
+ namespace: projection.namespace,
1093
+ projectionName: projection.projectionName,
1094
+ version: projection.version,
1095
+ state: projection.state,
1096
+ });
1097
+ const extensions = host.extensions?.();
1098
+ if (extensions)
1099
+ this.emitProjection({
1100
+ namespace: "extension",
1101
+ projectionName: "ui",
1102
+ version: extensions.apiVersion,
1103
+ state: { badges: extensions.badges, ui: extensions.ui },
1104
+ });
1105
+ }
915
1106
  const operation = [...this.operations.values()].find((item) => item.status === "running");
916
1107
  const extended = event.type === "session_changed";
917
1108
  if (extended && !this.negotiatedEvents.has("session_changed"))
@@ -943,6 +1134,30 @@ export class RpcDispatcher {
943
1134
  }
944
1135
  }
945
1136
  }
1137
+ function isBrowserSafeProjection(value) {
1138
+ const visit = (candidate) => {
1139
+ if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean")
1140
+ return true;
1141
+ if (typeof candidate === "number")
1142
+ return Number.isFinite(candidate);
1143
+ if (Array.isArray(candidate))
1144
+ return candidate.every(visit);
1145
+ if (typeof candidate !== "object")
1146
+ return false;
1147
+ for (const [key, item] of Object.entries(candidate))
1148
+ if (FORBIDDEN_PROJECTION_KEYS.test(key) || !visit(item))
1149
+ return false;
1150
+ return true;
1151
+ };
1152
+ if (!visit(value))
1153
+ return false;
1154
+ try {
1155
+ return Buffer.byteLength(JSON.stringify(value), "utf8") <= MAX_PROJECTION_BYTES;
1156
+ }
1157
+ catch {
1158
+ return false;
1159
+ }
1160
+ }
946
1161
  const RPC_METHODS_FOR_CAPABILITIES = [
947
1162
  "get_state",
948
1163
  "get_capabilities",