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 +1 -1
- package/server/provider.ts +76 -16
- package/server/timeline.ts +38 -15
package/package.json
CHANGED
package/server/provider.ts
CHANGED
|
@@ -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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
214
|
-
if (
|
|
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.
|
|
242
|
+
bridges.add(session.bridge);
|
|
221
243
|
}
|
|
222
244
|
sessions.clear();
|
|
223
245
|
listeners.clear();
|
|
224
|
-
|
|
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)
|
|
505
|
-
|
|
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
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
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
|
|
1153
|
+
selected,
|
|
1094
1154
|
...(custom ? { custom } : {}),
|
|
1095
1155
|
};
|
|
1096
1156
|
});
|
package/server/timeline.ts
CHANGED
|
@@ -480,32 +480,55 @@ function timeline(
|
|
|
480
480
|
|
|
481
481
|
function toolDetail(name: string, args: JsonObject): ProviderToolCallDetail {
|
|
482
482
|
const normalized = name.toLowerCase();
|
|
483
|
-
|
|
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) ??
|
|
487
|
-
cwd: stringValue(args.
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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 (
|
|
511
|
+
if (normalized === "edit") {
|
|
495
512
|
return {
|
|
496
513
|
type: "edit",
|
|
497
|
-
filePath,
|
|
498
|
-
oldString: stringValue(args.
|
|
499
|
-
newString: stringValue(args.
|
|
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 (
|
|
504
|
-
return { type: "
|
|
519
|
+
if (normalized === "grep") {
|
|
520
|
+
return { type: "search", query: stringValue(args.pattern) ?? "", toolName: "grep" };
|
|
505
521
|
}
|
|
506
|
-
if (
|
|
507
|
-
return { type: "search", query: stringValue(args.
|
|
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
|
|