paseo-dsh-direct 0.2.12 → 0.2.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paseo-dsh-direct",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "DeepSeek Harness Direct Provider Plugin for Paseo",
5
5
  "repository": {
6
6
  "type": "git",
@@ -188,7 +188,23 @@ function createConnection(
188
188
  ): ProviderConnection {
189
189
  const listeners = new Set<(event: ProviderEvent) => void>();
190
190
  const sessions = new Map<string, SessionState>();
191
+ const pendingOpens = new Set<Promise<void>>();
192
+ const openingBridges = new Set<DshBridge>();
193
+ const bridgeClosures = new WeakMap<DshBridge, Promise<void>>();
194
+ const cleanupErrors: unknown[] = [];
191
195
  let closed = false;
196
+ let closePromise: Promise<void> | undefined;
197
+ const closeBridge = (bridge: DshBridge): Promise<void> => {
198
+ let closing = bridgeClosures.get(bridge);
199
+ if (!closing) {
200
+ closing = Promise.resolve().then(() => bridge.close()).catch((error) => {
201
+ cleanupErrors.push(error);
202
+ throw error;
203
+ });
204
+ bridgeClosures.set(bridge, closing);
205
+ }
206
+ return closing;
207
+ };
192
208
  const emit = (event: ProviderEvent) => {
193
209
  if (closed) return;
194
210
  for (const listener of listeners) listener(event);
@@ -200,28 +216,41 @@ function createConnection(
200
216
  async send(input) {
201
217
  if (closed) throw new Error("Provider connection is closed");
202
218
  validateAdmission(input, sessions, capabilities);
203
- queueMicrotask(() => {
204
- void dispatch(input, { discoveryBridge, createBridge, sessions, emit, capabilities, detectVersion, launchEnv }).catch(
205
- (error) => failInput(input, error, emit),
206
- );
207
- });
219
+ const pending = Promise.resolve().then(async () => {
220
+ if (closed) return;
221
+ await dispatch(input, {
222
+ discoveryBridge, createBridge, sessions, emit, capabilities, detectVersion, launchEnv,
223
+ openingBridges, closeBridge, isClosed: () => closed,
224
+ });
225
+ }).catch((error) => failInput(input, error, emit));
226
+ if (input.type === "session.open") {
227
+ pendingOpens.add(pending);
228
+ void pending.then(() => pendingOpens.delete(pending));
229
+ }
208
230
  },
209
231
  onEvent(listener) {
210
232
  listeners.add(listener);
211
233
  return () => listeners.delete(listener);
212
234
  },
213
- async close() {
214
- if (closed) return;
235
+ close() {
236
+ if (closePromise) return closePromise;
215
237
  closed = true;
216
- const bridges = [discoveryBridge];
238
+ const bridges = new Set([discoveryBridge, ...openingBridges]);
217
239
  for (const session of sessions.values()) {
218
240
  session.closed = true;
219
241
  session.releaseBridgeListeners.forEach((release) => release());
220
- bridges.push(session.bridge);
242
+ bridges.add(session.bridge);
221
243
  }
222
244
  sessions.clear();
223
245
  listeners.clear();
224
- await Promise.all(bridges.map((bridge) => bridge.close()));
246
+ closePromise = (async () => {
247
+ await Promise.allSettled([
248
+ ...Array.from(bridges, closeBridge),
249
+ ...pendingOpens,
250
+ ]);
251
+ if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Could not close DSH bridges");
252
+ })();
253
+ return closePromise;
225
254
  },
226
255
  };
227
256
  }
@@ -363,6 +392,9 @@ interface DispatchState {
363
392
  capabilities: readonly string[];
364
393
  detectVersion: DetectVersion;
365
394
  launchEnv: Readonly<Record<string, string>> | undefined;
395
+ openingBridges: Set<DshBridge>;
396
+ closeBridge(bridge: DshBridge): Promise<void>;
397
+ isClosed(): boolean;
366
398
  }
367
399
 
368
400
  async function dispatch(input: ProviderInput, state: DispatchState): Promise<void> {
@@ -452,8 +484,10 @@ async function openSession(
452
484
  ephemeral: !input.config.persist,
453
485
  mcpServers: bridgeMcpServers(input.config.mcpServers),
454
486
  });
487
+ state.openingBridges.add(bridge);
455
488
  let session: SessionState | undefined;
456
489
  try {
490
+ if (state.isClosed()) return;
457
491
  // Resume from DSH's durable model and preset. Paseo fills catalog defaults into
458
492
  // open config, so forwarding them would accidentally overwrite native history.
459
493
  const selection = nativeId
@@ -473,11 +507,13 @@ async function openSession(
473
507
  ? { preapprovedTools: preapprovedTools(input.config) }
474
508
  : {}),
475
509
  });
510
+ if (state.isClosed()) return;
476
511
  const title = nativeId ? undefined : input.config.title?.trim();
477
512
  const [commands] = await Promise.all([
478
513
  fetchCommands(bridge, opened.sessionId),
479
514
  title ? bridge.request("session.rename", { sessionId: opened.sessionId, title }) : undefined,
480
515
  ]);
516
+ if (state.isClosed()) return;
481
517
  session = {
482
518
  paseoId: input.sessionId,
483
519
  nativeId: opened.sessionId,
@@ -501,10 +537,18 @@ async function openSession(
501
537
  await setPlanMode(session, true);
502
538
  }
503
539
  } catch (error) {
504
- if (session) state.sessions.delete(input.sessionId);
505
- await bridge.close();
540
+ if (session) {
541
+ session.closed = true;
542
+ session.releaseBridgeListeners.forEach((release) => release());
543
+ state.sessions.delete(input.sessionId);
544
+ }
545
+ await state.closeBridge(bridge);
506
546
  throw error;
547
+ } finally {
548
+ state.openingBridges.delete(bridge);
549
+ if (state.isClosed()) await state.closeBridge(bridge);
507
550
  }
551
+ if (state.isClosed()) return;
508
552
  state.emit({
509
553
  type: "session.opened",
510
554
  requestId: input.requestId,
@@ -1085,12 +1129,28 @@ function questionAnswers(
1085
1129
  return questions.map((question) => {
1086
1130
  const key = question.header ?? question.id;
1087
1131
  const raw = answers?.[key];
1088
- const selected = typeof raw === "string" ? raw.split(",").map((value) => value.trim()).filter(Boolean) : [];
1089
- const known = new Set((question.options ?? []).map((option) => option.label));
1090
- const custom = selected.find((value) => !known.has(value));
1132
+ if (typeof raw !== "string" || raw.length === 0) return { id: question.id, selected: [] };
1133
+ const labels = (question.options ?? []).map((option) => option.label);
1134
+ if (!question.multiSelect) {
1135
+ return labels.includes(raw)
1136
+ ? { id: question.id, selected: [raw] }
1137
+ : { id: question.id, selected: [], custom: raw };
1138
+ }
1139
+ // Paseo joins checked labels and the optional typed answer with ", ".
1140
+ // Match complete labels (longest first), keeping the remaining text intact.
1141
+ const candidates = labels.filter(Boolean).sort((left, right) => right.length - left.length);
1142
+ const selected: string[] = [];
1143
+ let custom = raw;
1144
+ while (custom.length > 0) {
1145
+ const label = candidates.find((value) =>
1146
+ !selected.includes(value) && (custom === value || custom.startsWith(`${value}, `)));
1147
+ if (label === undefined) break;
1148
+ selected.push(label);
1149
+ custom = custom === label ? "" : custom.slice(label.length + 2);
1150
+ }
1091
1151
  return {
1092
1152
  id: question.id,
1093
- selected: selected.filter((value) => known.has(value)),
1153
+ selected,
1094
1154
  ...(custom ? { custom } : {}),
1095
1155
  };
1096
1156
  });
@@ -480,32 +480,55 @@ function timeline(
480
480
 
481
481
  function toolDetail(name: string, args: JsonObject): ProviderToolCallDetail {
482
482
  const normalized = name.toLowerCase();
483
- if (/shell|bash|exec|command/u.test(normalized)) {
483
+
484
+ // DSH's built-in tools are matched by exact name. Substring patterns get the
485
+ // common ones wrong (`todo_write` is not a file write, `read_image` is not a
486
+ // text read), and they would also claim unrelated MCP tools.
487
+ if (normalized === "bash" || normalized === "pwsh") {
488
+ // An empty command during streaming keeps the card from showing the raw
489
+ // half-parsed JSON; the final frame fills it in.
484
490
  return {
485
491
  type: "shell",
486
- command: stringValue(args.command) ?? stringValue(args.cmd) ?? stringifyValue(args),
487
- cwd: stringValue(args.cwd),
492
+ command: stringValue(args.command) ?? "",
493
+ cwd: stringValue(args.workdir),
494
+ };
495
+ }
496
+ if (normalized === "read" || normalized === "read_image") {
497
+ return {
498
+ type: "read",
499
+ filePath: stringValue(args.file_path) ?? "",
500
+ offset: numberValue(args.offset),
501
+ limit: numberValue(args.limit),
488
502
  };
489
503
  }
490
- const filePath = stringValue(args.filePath) ?? stringValue(args.path) ?? "";
491
- if (/read/u.test(normalized)) {
492
- return { type: "read", filePath, offset: numberValue(args.offset), limit: numberValue(args.limit) };
504
+ if (normalized === "write") {
505
+ return {
506
+ type: "write",
507
+ filePath: stringValue(args.file_path) ?? "",
508
+ content: stringValue(args.content),
509
+ };
493
510
  }
494
- if (/edit|patch/u.test(normalized)) {
511
+ if (normalized === "edit") {
495
512
  return {
496
513
  type: "edit",
497
- filePath,
498
- oldString: stringValue(args.oldString) ?? stringValue(args.old_string),
499
- newString: stringValue(args.newString) ?? stringValue(args.new_string),
500
- unifiedDiff: stringValue(args.patch) ?? stringValue(args.diff),
514
+ filePath: stringValue(args.file_path) ?? "",
515
+ oldString: stringValue(args.old_string),
516
+ newString: stringValue(args.new_string),
501
517
  };
502
518
  }
503
- if (/write/u.test(normalized)) {
504
- return { type: "write", filePath, content: stringValue(args.content) };
519
+ if (normalized === "grep") {
520
+ return { type: "search", query: stringValue(args.pattern) ?? "", toolName: "grep" };
505
521
  }
506
- if (/search|grep|glob/u.test(normalized)) {
507
- return { type: "search", query: stringValue(args.query) ?? stringValue(args.pattern) ?? "" };
522
+ if (normalized === "glob") {
523
+ return { type: "search", query: stringValue(args.pattern) ?? "", toolName: "glob" };
508
524
  }
525
+ if (normalized === "web_fetch") {
526
+ return { type: "fetch", url: stringValue(args.url) ?? "" };
527
+ }
528
+
529
+ // Everything else, including MCP tools, keeps a generic detail carrying the
530
+ // raw arguments. Guessing a card type from the name would render MCP tools as
531
+ // file operations.
509
532
  return { type: "unknown", input: { raw: stringifyValue(args) }, output: {} };
510
533
  }
511
534