@addozhang/dsh-discord 0.4.0 → 0.5.0-alpha.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.
- package/README.md +13 -14
- package/README.zh.md +12 -13
- package/cordis.patch.yml +1 -1
- package/lib/client/card-controller.d.ts +1 -1
- package/lib/client/card-form.d.ts +2 -1
- package/lib/client/index.d.ts +2 -2
- package/lib/client/slot-contract.d.ts +7 -0
- package/lib/client/snapshot-store.d.ts +1 -1
- package/lib/compose.d.ts +2 -2
- package/lib/compose.js +1 -1
- package/lib/dsh/host-asks.d.ts +96 -0
- package/lib/dsh/host-asks.js +151 -0
- package/lib/dsh/host-events.d.ts +42 -0
- package/lib/dsh/host-events.js +279 -0
- package/lib/dsh/host-face.d.ts +418 -0
- package/lib/dsh/host-face.js +598 -0
- package/lib/features/adapter-status.d.ts +15 -2
- package/lib/features/adapter-status.js +100 -4
- package/lib/features/approval-store.d.ts +1 -1
- package/lib/features/interaction-router.d.ts +2 -2
- package/lib/features/model-control.d.ts +1 -1
- package/lib/features/project-list.d.ts +1 -1
- package/lib/features/project-list.js +1 -1
- package/lib/features/session-resume.d.ts +1 -1
- package/lib/i18n.d.ts +5 -0
- package/lib/i18n.js +14 -0
- package/lib/index.js +121 -67
- package/lib/settings.js +5 -2
- package/lib/startup.d.ts +1 -1
- package/lib/startup.js +14 -7
- package/lib/stream/live.d.ts +13 -36
- package/lib/stream/live.js +157 -86
- package/lib/stream/tool-view.d.ts +12 -2
- package/lib/stream/tool-view.js +32 -2
- package/package.json +10 -12
- package/lib/dsh/api-proxy-face.d.ts +0 -400
- package/lib/dsh/api-proxy-face.js +0 -515
package/lib/stream/live.d.ts
CHANGED
|
@@ -84,10 +84,6 @@ export interface LiveRenderDeps {
|
|
|
84
84
|
threadName?: (channelId: string) => Promise<string | undefined>;
|
|
85
85
|
updateIntervalMs: number;
|
|
86
86
|
typingIntervalMs: number;
|
|
87
|
-
/** Approval ask deadline (approvalTimeoutMs setting). */
|
|
88
|
-
approvalTimeoutMs: number;
|
|
89
|
-
/** Question ask deadline (questionTimeoutMs setting). */
|
|
90
|
-
questionTimeoutMs: number;
|
|
91
87
|
/** Coalescing budget for tool-activity edits (default 1s). */
|
|
92
88
|
activityCoalesceMs?: number;
|
|
93
89
|
verbosity?: DiscordVerbosity;
|
|
@@ -99,42 +95,23 @@ export interface LiveRenderDeps {
|
|
|
99
95
|
}>) => void;
|
|
100
96
|
/** Localized interruption suffix, resolved live (language can change). */
|
|
101
97
|
interruptedMarker?: () => string;
|
|
102
|
-
/** Turn ownership release on turn/end. */
|
|
103
|
-
onTurnEnded?: (sessionId: string) => void;
|
|
104
98
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
99
|
+
* Localized progress-phase copy, resolved live (language can change).
|
|
100
|
+
* Unset: the status line is omitted and only tool rows render (the
|
|
101
|
+
* pre-progress behavior).
|
|
108
102
|
*/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
toolName: string;
|
|
116
|
-
reason?: string | undefined;
|
|
117
|
-
expiresAtMs: number;
|
|
118
|
-
}): void;
|
|
119
|
-
onApprovalResolved(input: {
|
|
120
|
-
sessionId: string;
|
|
121
|
-
approvalId: string;
|
|
122
|
-
outcome?: string | undefined;
|
|
123
|
-
}): void;
|
|
124
|
-
onQuestionRequested(input: {
|
|
125
|
-
sessionId: string;
|
|
126
|
-
threadId: string;
|
|
127
|
-
rpcId: string;
|
|
128
|
-
questions: Array<Record<string, unknown>>;
|
|
129
|
-
expiresAtMs: number;
|
|
130
|
-
}): void;
|
|
131
|
-
onQuestionResolved(input: {
|
|
132
|
-
sessionId: string;
|
|
133
|
-
questionRpcId: string;
|
|
134
|
-
outcome: 'answered' | 'cancelled';
|
|
135
|
-
}): void;
|
|
103
|
+
progressCopy?: () => {
|
|
104
|
+
thinking: string;
|
|
105
|
+
thinkingStep: (step: number) => string;
|
|
106
|
+
writing: string;
|
|
107
|
+
approvalWait: string;
|
|
108
|
+
turnSummary: (total: number, failed: number, breakdown: string) => string;
|
|
136
109
|
};
|
|
110
|
+
/** Turn ownership release on turn/end. */
|
|
111
|
+
onTurnEnded?: (sessionId: string) => void;
|
|
137
112
|
}
|
|
138
113
|
export declare function startLiveRender(deps: LiveRenderDeps): {
|
|
139
114
|
dispose(): void;
|
|
115
|
+
/** Approval-wait steering for the ask patches (turn-progress spec). */
|
|
116
|
+
setApprovalWait(threadId: string, waiting: boolean): void;
|
|
140
117
|
};
|
package/lib/stream/live.js
CHANGED
|
@@ -15,6 +15,7 @@ import { createToolActivitySurface } from './tool-view.js';
|
|
|
15
15
|
import { createAnswerFinalizer } from './finalizer.js';
|
|
16
16
|
import { buildOutboundMessage } from './outbound.js';
|
|
17
17
|
import { toolCategoryIcon } from './icons.js';
|
|
18
|
+
import { shellCommandTitle } from './tool-view.js';
|
|
18
19
|
import { discordChannelNameKey, safeTitle } from '../policy/disclosure.js';
|
|
19
20
|
/** Coalescing budget for activity-message edits under parallel tools. */
|
|
20
21
|
const DEFAULT_ACTIVITY_COALESCE_MS = 1_000;
|
|
@@ -60,6 +61,17 @@ function resultCallId(data) {
|
|
|
60
61
|
const block = Array.isArray(message?.content) ? message.content[0] : undefined;
|
|
61
62
|
return typeof block?.toolCallId === 'string' ? block.toolCallId : undefined;
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The failure flag of a tool/result event. The rc.2 wire carried a top-level
|
|
66
|
+
* `error`; the 0.1.6 journal marks failure on the result block (`isError`).
|
|
67
|
+
*/
|
|
68
|
+
function resultFailed(data) {
|
|
69
|
+
if (data['error'] !== undefined)
|
|
70
|
+
return true;
|
|
71
|
+
const message = data['message'];
|
|
72
|
+
const block = Array.isArray(message?.content) ? message.content[0] : undefined;
|
|
73
|
+
return block?.isError === true;
|
|
74
|
+
}
|
|
63
75
|
/**
|
|
64
76
|
* The Host presentation view's title for one tool event: a terminal call's
|
|
65
77
|
* title IS the command; generic/diff cards title the call. Host-curated
|
|
@@ -96,6 +108,9 @@ export function startLiveRender(deps) {
|
|
|
96
108
|
finalizer: undefined,
|
|
97
109
|
headMessageId: undefined,
|
|
98
110
|
activityMessageId: undefined,
|
|
111
|
+
activityFlush: undefined,
|
|
112
|
+
progressPhase: undefined,
|
|
113
|
+
phaseBeforeApproval: undefined,
|
|
99
114
|
headFlush: undefined,
|
|
100
115
|
stepSeq: 0,
|
|
101
116
|
headAttempted: false,
|
|
@@ -145,6 +160,7 @@ export function startLiveRender(deps) {
|
|
|
145
160
|
runtime.turnId = turnId;
|
|
146
161
|
runtime.headMessageId = undefined;
|
|
147
162
|
runtime.activityMessageId = undefined;
|
|
163
|
+
runtime.activityFlush = undefined;
|
|
148
164
|
runtime.headFlush = undefined;
|
|
149
165
|
runtime.headAttempted = false;
|
|
150
166
|
runtime.activityAttempted = false;
|
|
@@ -167,6 +183,10 @@ export function startLiveRender(deps) {
|
|
|
167
183
|
onFlushError: (cause) => { deps.log?.('discord_live_activity_flush_failed', { threadId, cause: String(cause) }); },
|
|
168
184
|
});
|
|
169
185
|
runtime.finalizer = undefined;
|
|
186
|
+
// The status line opens the turn in the thinking phase (spec
|
|
187
|
+
// turn-progress: the message exists from turn/start, before any tool).
|
|
188
|
+
runtime.progressPhase = { kind: 'thinking', step: 1 };
|
|
189
|
+
runtime.phaseBeforeApproval = undefined;
|
|
170
190
|
// A fresh lifecycle per turn: start() no-ops on a stopped one, so a
|
|
171
191
|
// second turn in the same thread would otherwise never type again.
|
|
172
192
|
runtime.typing.dispose();
|
|
@@ -176,40 +196,84 @@ export function startLiveRender(deps) {
|
|
|
176
196
|
onFailure: (cause) => { deps.log?.('discord_live_typing_threw', { threadId, cause: String(cause) }); },
|
|
177
197
|
});
|
|
178
198
|
runtime.typing.start();
|
|
199
|
+
runtime.activityScheduler.schedule(renderActivityContent(runtime));
|
|
200
|
+
}
|
|
201
|
+
/** The status line's phase entry (undefined when copy is not provided). */
|
|
202
|
+
function progressPhaseLine(runtime) {
|
|
203
|
+
const copy = deps.progressCopy?.();
|
|
204
|
+
if (copy === undefined || runtime.progressPhase === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
const phase = runtime.progressPhase;
|
|
207
|
+
switch (phase.kind) {
|
|
208
|
+
case 'thinking': return phase.step > 1 ? copy.thinkingStep(phase.step) : copy.thinking;
|
|
209
|
+
case 'tool': return `💻 ${truncateText(phase.title ?? phase.label, ACTIVITY_TITLE_MAX)}`;
|
|
210
|
+
case 'writing': return copy.writing;
|
|
211
|
+
case 'approval': return copy.approvalWait;
|
|
212
|
+
}
|
|
179
213
|
}
|
|
180
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* The turn's collapsed one-line summary (decision 5): the durable trace a
|
|
216
|
+
* finished turn leaves behind. Undefined when the turn ran no tools —
|
|
217
|
+
* those keep the pure Q&A thread and the message is deleted instead.
|
|
218
|
+
*/
|
|
219
|
+
function renderTurnSummary(rows) {
|
|
220
|
+
const copy = deps.progressCopy?.();
|
|
221
|
+
if (copy === undefined || rows.length === 0)
|
|
222
|
+
return undefined;
|
|
223
|
+
const counts = new Map();
|
|
224
|
+
for (const row of rows)
|
|
225
|
+
counts.set(row.label, (counts.get(row.label) ?? 0) + 1);
|
|
226
|
+
const breakdown = truncateText([...counts.entries()].map(([label, n]) => `${label} ×${String(n)}`).join(' · '), 200);
|
|
227
|
+
const failed = rows.filter(row => row.state === 'failed').length;
|
|
228
|
+
return copy.turnSummary(rows.length, failed, breakdown);
|
|
229
|
+
}
|
|
230
|
+
/** The activity message body: phase line, then one icon + title per call row. */
|
|
181
231
|
function renderActivityContent(runtime) {
|
|
182
232
|
const rows = runtime.tools.render();
|
|
183
|
-
|
|
233
|
+
const rowLines = rows.map(row => {
|
|
184
234
|
const title = truncateText(row.title ?? row.label, ACTIVITY_TITLE_MAX);
|
|
185
|
-
|
|
186
|
-
|
|
235
|
+
const mark = row.state === 'succeeded' ? '✓ ' : row.state === 'failed' ? '✗ ' : '';
|
|
236
|
+
return `> ${mark}${toolCategoryIcon(row.label)} ${title}`;
|
|
237
|
+
});
|
|
238
|
+
const phaseLine = progressPhaseLine(runtime);
|
|
239
|
+
return phaseLine === undefined ? rowLines.join('\n') : [phaseLine, ...rowLines].join('\n');
|
|
187
240
|
}
|
|
188
241
|
/** Render the tool rows into one bounded activity message (create once, edit after). */
|
|
189
242
|
function renderActivity(threadId, runtime) {
|
|
190
243
|
return async () => {
|
|
191
|
-
if (runtime.tools.render().length === 0)
|
|
244
|
+
if (runtime.tools.render().length === 0 && progressPhaseLine(runtime) === undefined)
|
|
192
245
|
return;
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
246
|
+
// The flush promise records the message id it created so turn/end's
|
|
247
|
+
// cleanup can delete a send that is still in flight (orphan guard).
|
|
248
|
+
const flush = (async () => {
|
|
249
|
+
// Tool titles are Host-presented free text (terminal commands): they
|
|
250
|
+
// go through the same outbound builder as every other message path.
|
|
251
|
+
const payload = buildOutboundMessage({ kind: 'tool', content: renderActivityContent(runtime) });
|
|
252
|
+
if (runtime.activityMessageId === undefined) {
|
|
253
|
+
if (runtime.activityAttempted)
|
|
254
|
+
return undefined;
|
|
255
|
+
const sent = await deps.delivery.send({ channelId: threadId, content: payload.content });
|
|
256
|
+
if (sent.outcome === 'completed') {
|
|
257
|
+
runtime.activityMessageId = sent.messageId;
|
|
258
|
+
return sent.messageId;
|
|
259
|
+
}
|
|
260
|
+
if (sent.outcome === 'unknown') {
|
|
261
|
+
runtime.activityAttempted = true;
|
|
262
|
+
deps.log?.('discord_live_activity_send_unknown', { threadId });
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
205
265
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
266
|
+
await deps.delivery.edit({ channelId: threadId, messageId: runtime.activityMessageId, content: payload.content });
|
|
267
|
+
return undefined;
|
|
268
|
+
})();
|
|
269
|
+
runtime.activityFlush = flush;
|
|
270
|
+
await flush;
|
|
209
271
|
};
|
|
210
272
|
}
|
|
211
273
|
function handleSessionEvent(sessionId, threadId, runtime, event, frameView) {
|
|
212
274
|
const data = event.data;
|
|
275
|
+
if (TRACE)
|
|
276
|
+
trace('handleSessionEvent', event.type, 'keys:', Object.keys(data).join(','));
|
|
213
277
|
const turnId = typeof data['turn'] === 'number' ? String(data['turn']) : undefined;
|
|
214
278
|
const stepId = typeof data['step'] === 'number' ? String(data['step']) : undefined;
|
|
215
279
|
switch (event.type) {
|
|
@@ -228,6 +292,11 @@ export function startLiveRender(deps) {
|
|
|
228
292
|
runtime.headFlush = undefined;
|
|
229
293
|
runtime.headAttempted = false;
|
|
230
294
|
runtime.stepSeq += 1;
|
|
295
|
+
// A step boundary is a thinking boundary: the model is reasoning
|
|
296
|
+
// about the previous step's results before the next tool call.
|
|
297
|
+
// The journal's own step number is the display truth (stepSeq is a
|
|
298
|
+
// generation fence that also bumps at the turn boundary).
|
|
299
|
+
runtime.progressPhase = { kind: 'thinking', step: typeof data['step'] === 'number' ? data['step'] : runtime.stepSeq };
|
|
231
300
|
// The previous step's finalize disposed the scheduler; a fresh one
|
|
232
301
|
// carries the new step's chunk coalescing.
|
|
233
302
|
runtime.scheduler?.dispose();
|
|
@@ -236,6 +305,7 @@ export function startLiveRender(deps) {
|
|
|
236
305
|
onFlush: flushAnswer(threadId, runtime),
|
|
237
306
|
onFlushError: (cause) => { deps.log?.('discord_live_flush_failed', { threadId, cause: String(cause) }); },
|
|
238
307
|
});
|
|
308
|
+
runtime.activityScheduler?.schedule(renderActivityContent(runtime));
|
|
239
309
|
return;
|
|
240
310
|
}
|
|
241
311
|
case 'assistant/chunk': {
|
|
@@ -264,6 +334,9 @@ export function startLiveRender(deps) {
|
|
|
264
334
|
runtime.render.setAuthoritative({ turnId, stepId, text });
|
|
265
335
|
if (interrupted)
|
|
266
336
|
runtime.render.interrupt({ turnId, stepId });
|
|
337
|
+
// The step's answer is committed: the agent is writing its reply.
|
|
338
|
+
runtime.progressPhase = { kind: 'writing' };
|
|
339
|
+
runtime.activityScheduler?.schedule(renderActivityContent(runtime));
|
|
267
340
|
runtime.scheduler?.dispose();
|
|
268
341
|
runtime.scheduler = undefined;
|
|
269
342
|
if (text === '')
|
|
@@ -313,16 +386,19 @@ export function startLiveRender(deps) {
|
|
|
313
386
|
case 'tool/call': {
|
|
314
387
|
if (typeof data['callId'] !== 'string' || typeof data['name'] !== 'string')
|
|
315
388
|
return;
|
|
316
|
-
const
|
|
389
|
+
const rawArguments = typeof data['arguments'] === 'string' ? data['arguments'] : undefined;
|
|
390
|
+
const title = presentationTitle(frameView) ?? shellCommandTitle(data['name'], rawArguments);
|
|
317
391
|
runtime.toolNames.set(data['callId'], data['name']);
|
|
318
392
|
if (title !== undefined)
|
|
319
393
|
runtime.toolTitles.set(data['callId'], title);
|
|
394
|
+
// The status line tracks the most recent call as the running phase.
|
|
395
|
+
runtime.progressPhase = { kind: 'tool', label: data['name'], title };
|
|
320
396
|
runtime.tools.record({
|
|
321
397
|
callId: data['callId'],
|
|
322
398
|
toolName: data['name'],
|
|
323
399
|
state: 'running',
|
|
324
400
|
title,
|
|
325
|
-
rawArguments
|
|
401
|
+
rawArguments,
|
|
326
402
|
});
|
|
327
403
|
runtime.activityScheduler?.schedule(renderActivityContent(runtime));
|
|
328
404
|
return;
|
|
@@ -331,7 +407,7 @@ export function startLiveRender(deps) {
|
|
|
331
407
|
const callId = resultCallId(data);
|
|
332
408
|
if (callId === undefined)
|
|
333
409
|
return;
|
|
334
|
-
const failed = data
|
|
410
|
+
const failed = resultFailed(data);
|
|
335
411
|
runtime.tools.record({
|
|
336
412
|
callId,
|
|
337
413
|
toolName: runtime.toolNames.get(callId) ?? 'tool',
|
|
@@ -347,16 +423,36 @@ export function startLiveRender(deps) {
|
|
|
347
423
|
runtime.scheduler = undefined;
|
|
348
424
|
runtime.activityScheduler?.dispose();
|
|
349
425
|
runtime.activityScheduler = undefined;
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
426
|
+
runtime.progressPhase = undefined;
|
|
427
|
+
runtime.phaseBeforeApproval = undefined;
|
|
428
|
+
// The activity message collapses at turn end (decision 5): a turn
|
|
429
|
+
// that ran tools is EDITED into its one-line summary — the durable
|
|
430
|
+
// process trace — while a zero-tool turn keeps the pure Q&A thread
|
|
431
|
+
// and is deleted. A send still in flight when the cleanup ran would
|
|
432
|
+
// orphan the message, so the collapse waits for the flush to settle
|
|
433
|
+
// first (stepSeq fences a turn that began meanwhile: its message
|
|
434
|
+
// owns the slot and must survive).
|
|
435
|
+
const epochAtEnd = runtime.stepSeq;
|
|
436
|
+
const settledId = runtime.activityMessageId;
|
|
354
437
|
runtime.activityMessageId = undefined;
|
|
355
|
-
|
|
356
|
-
|
|
438
|
+
const summary = renderTurnSummary(runtime.tools.render());
|
|
439
|
+
const pendingActivityFlush = Promise.resolve(runtime.activityFlush).catch(() => undefined);
|
|
440
|
+
void pendingActivityFlush.then(flushedId => {
|
|
441
|
+
const id = runtime.activityMessageId ?? settledId ?? flushedId;
|
|
442
|
+
if (id === undefined || runtime.stepSeq !== epochAtEnd)
|
|
443
|
+
return;
|
|
444
|
+
runtime.activityMessageId = undefined;
|
|
445
|
+
if (summary !== undefined) {
|
|
446
|
+
const payload = buildOutboundMessage({ kind: 'tool', content: summary });
|
|
447
|
+
void deps.delivery.edit({ channelId: threadId, messageId: id, content: payload.content }).catch((cause) => {
|
|
448
|
+
deps.log?.('discord_live_activity_summary_edit_threw', { threadId, cause: String(cause) });
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
void deps.delivery.delete({ channelId: threadId, messageId: id }).catch((cause) => {
|
|
357
453
|
deps.log?.('discord_live_activity_delete_threw', { threadId, cause: String(cause) });
|
|
358
454
|
});
|
|
359
|
-
}
|
|
455
|
+
});
|
|
360
456
|
deps.onTurnEnded?.(sessionId);
|
|
361
457
|
return;
|
|
362
458
|
}
|
|
@@ -375,7 +471,7 @@ export function startLiveRender(deps) {
|
|
|
375
471
|
.join(' '), 120);
|
|
376
472
|
return { id, summary: text === '' ? '(非文本消息)' : text };
|
|
377
473
|
}
|
|
378
|
-
function handleFrame(raw
|
|
474
|
+
function handleFrame(raw) {
|
|
379
475
|
const frame = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
380
476
|
const type = frame['type'];
|
|
381
477
|
const sessionId = typeof frame['sessionId'] === 'string' ? frame['sessionId'] : undefined;
|
|
@@ -436,52 +532,8 @@ export function startLiveRender(deps) {
|
|
|
436
532
|
});
|
|
437
533
|
return;
|
|
438
534
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (threadId === undefined || deps.requests === undefined)
|
|
442
|
-
return;
|
|
443
|
-
deps.requests.onApprovalRequested({
|
|
444
|
-
sessionId,
|
|
445
|
-
threadId,
|
|
446
|
-
rpcId: envelopeRpcId ?? '',
|
|
447
|
-
approvalId: typeof frame['approvalId'] === 'string' ? frame['approvalId'] : '',
|
|
448
|
-
toolName: typeof frame['toolName'] === 'string' ? frame['toolName'] : 'tool',
|
|
449
|
-
reason: typeof frame['reason'] === 'string' ? frame['reason'] : undefined,
|
|
450
|
-
expiresAtMs: Date.now() + deps.approvalTimeoutMs,
|
|
451
|
-
});
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (type === 'approval/resolved') {
|
|
455
|
-
deps.requests?.onApprovalResolved({
|
|
456
|
-
sessionId,
|
|
457
|
-
approvalId: typeof frame['approvalId'] === 'string' ? frame['approvalId'] : '',
|
|
458
|
-
outcome: typeof frame['outcome'] === 'string' ? frame['outcome'] : undefined,
|
|
459
|
-
});
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
if (type === 'question/requested') {
|
|
463
|
-
const threadId = deps.threadForSession(sessionId);
|
|
464
|
-
if (threadId === undefined || deps.requests === undefined)
|
|
465
|
-
return;
|
|
466
|
-
const questions = Array.isArray(frame['questions']) ? frame['questions'] : [];
|
|
467
|
-
deps.requests.onQuestionRequested({
|
|
468
|
-
sessionId,
|
|
469
|
-
threadId,
|
|
470
|
-
rpcId: envelopeRpcId ?? '',
|
|
471
|
-
questions,
|
|
472
|
-
expiresAtMs: Date.now() + deps.questionTimeoutMs,
|
|
473
|
-
});
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (type === 'question/resolved') {
|
|
477
|
-
const outcome = frame['outcome'] === 'cancelled' ? 'cancelled' : 'answered';
|
|
478
|
-
deps.requests?.onQuestionResolved({
|
|
479
|
-
sessionId,
|
|
480
|
-
questionRpcId: typeof frame['questionRpcId'] === 'string' ? frame['questionRpcId'] : '',
|
|
481
|
-
outcome,
|
|
482
|
-
});
|
|
483
|
-
return;
|
|
484
|
-
}
|
|
535
|
+
// Answerable asks no longer ride the frame stream: the 0.1.6 host routes
|
|
536
|
+
// them through the composed-answerer waterfalls (src/dsh/host-asks.ts).
|
|
485
537
|
if (type !== 'session/event')
|
|
486
538
|
return;
|
|
487
539
|
const threadId = deps.threadForSession(sessionId);
|
|
@@ -508,18 +560,12 @@ export function startLiveRender(deps) {
|
|
|
508
560
|
if (isDisposed())
|
|
509
561
|
return;
|
|
510
562
|
try {
|
|
511
|
-
const envelopeRpcId = (typeof frame === 'object' && frame !== null && 'rpcId' in frame)
|
|
512
|
-
? frame.rpcId
|
|
513
|
-
: undefined;
|
|
514
|
-
const payload = (typeof frame === 'object' && frame !== null && 'payload' in frame)
|
|
515
|
-
? frame.payload
|
|
516
|
-
: frame;
|
|
517
563
|
if (TRACE) {
|
|
518
|
-
const t = (typeof
|
|
519
|
-
const sid = (typeof
|
|
564
|
+
const t = (typeof frame === 'object' && frame !== null ? frame.type : undefined);
|
|
565
|
+
const sid = (typeof frame === 'object' && frame !== null ? frame.sessionId : undefined);
|
|
520
566
|
trace('frame', String(t), String(sid));
|
|
521
567
|
}
|
|
522
|
-
handleFrame(
|
|
568
|
+
handleFrame(frame);
|
|
523
569
|
}
|
|
524
570
|
catch (cause) {
|
|
525
571
|
deps.log?.('discord_live_frame_threw', { cause: String(cause) });
|
|
@@ -551,5 +597,30 @@ export function startLiveRender(deps) {
|
|
|
551
597
|
}
|
|
552
598
|
runtimes.clear();
|
|
553
599
|
},
|
|
600
|
+
/**
|
|
601
|
+
* Approval-wait steering for the ask patches (turn-progress spec): a
|
|
602
|
+
* claimed ask suspends the status line on the wait phase; settling
|
|
603
|
+
* restores the suspended phase so progress tracking resumes.
|
|
604
|
+
*/
|
|
605
|
+
setApprovalWait(threadId, waiting) {
|
|
606
|
+
if (state.disposed)
|
|
607
|
+
return;
|
|
608
|
+
const runtime = runtimes.get(threadId);
|
|
609
|
+
if (runtime === undefined)
|
|
610
|
+
return;
|
|
611
|
+
if (waiting) {
|
|
612
|
+
if (runtime.progressPhase?.kind === 'approval')
|
|
613
|
+
return;
|
|
614
|
+
runtime.phaseBeforeApproval = runtime.progressPhase;
|
|
615
|
+
runtime.progressPhase = { kind: 'approval' };
|
|
616
|
+
}
|
|
617
|
+
else {
|
|
618
|
+
if (runtime.progressPhase?.kind !== 'approval')
|
|
619
|
+
return;
|
|
620
|
+
runtime.progressPhase = runtime.phaseBeforeApproval;
|
|
621
|
+
runtime.phaseBeforeApproval = undefined;
|
|
622
|
+
}
|
|
623
|
+
runtime.activityScheduler?.schedule(renderActivityContent(runtime));
|
|
624
|
+
},
|
|
554
625
|
};
|
|
555
626
|
}
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* The bounded tool activity surface (design.md §8, task 11.8). Parallel tool
|
|
3
3
|
* calls each own one status row keyed by `callId`. Labels come from a safe
|
|
4
4
|
* allowlist (generic fallback otherwise — an unknown tool name never reaches
|
|
5
|
-
* Discord). Raw arguments and output are accepted for correlation
|
|
6
|
-
* structurally excluded from every render
|
|
5
|
+
* Discord). Raw arguments and output are accepted for correlation and are
|
|
6
|
+
* structurally excluded from every render, with one curated exception: the
|
|
7
|
+
* shell command title (see {@link shellCommandTitle}) — the field the rc.2
|
|
8
|
+
* host view itself disclosed, now derived locally because the 0.1.6 wire
|
|
9
|
+
* carries no host-curated views. Verbosity gates visibility:
|
|
7
10
|
* `text-only` renders nothing; `essential-tools` and `full-tools` render the
|
|
8
11
|
* same bounded rows in Milestone 1.
|
|
9
12
|
*/
|
|
@@ -18,6 +21,13 @@ export interface ToolRow {
|
|
|
18
21
|
}
|
|
19
22
|
/** The safe label for a tool name: allowlisted categories, generic fallback. */
|
|
20
23
|
export declare function toolLabel(toolName: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* The shell command for one tool call, curated from its JSON arguments the
|
|
26
|
+
* way the rc.2 host view titled terminal cards: only the `command` string of
|
|
27
|
+
* shell-family tools, first line only, sanitized + truncated through the
|
|
28
|
+
* disclosure policy — every other argument field stays unrendered.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shellCommandTitle(toolName: string, rawArguments: string | undefined): string | undefined;
|
|
21
31
|
export interface ToolRecordInput {
|
|
22
32
|
callId: string;
|
|
23
33
|
toolName: string;
|
package/lib/stream/tool-view.js
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* The bounded tool activity surface (design.md §8, task 11.8). Parallel tool
|
|
3
3
|
* calls each own one status row keyed by `callId`. Labels come from a safe
|
|
4
4
|
* allowlist (generic fallback otherwise — an unknown tool name never reaches
|
|
5
|
-
* Discord). Raw arguments and output are accepted for correlation
|
|
6
|
-
* structurally excluded from every render
|
|
5
|
+
* Discord). Raw arguments and output are accepted for correlation and are
|
|
6
|
+
* structurally excluded from every render, with one curated exception: the
|
|
7
|
+
* shell command title (see {@link shellCommandTitle}) — the field the rc.2
|
|
8
|
+
* host view itself disclosed, now derived locally because the 0.1.6 wire
|
|
9
|
+
* carries no host-curated views. Verbosity gates visibility:
|
|
7
10
|
* `text-only` renders nothing; `essential-tools` and `full-tools` render the
|
|
8
11
|
* same bounded rows in Milestone 1.
|
|
9
12
|
*/
|
|
13
|
+
import { safeTitle } from '../policy/disclosure.js';
|
|
10
14
|
/** Safe, category-level labels for allowlisted tools. */
|
|
11
15
|
const TOOL_LABELS = {
|
|
12
16
|
bash: 'Shell',
|
|
@@ -21,6 +25,32 @@ const TOOL_LABELS = {
|
|
|
21
25
|
export function toolLabel(toolName) {
|
|
22
26
|
return TOOL_LABELS[toolName] ?? 'Tool';
|
|
23
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The shell command for one tool call, curated from its JSON arguments the
|
|
30
|
+
* way the rc.2 host view titled terminal cards: only the `command` string of
|
|
31
|
+
* shell-family tools, first line only, sanitized + truncated through the
|
|
32
|
+
* disclosure policy — every other argument field stays unrendered.
|
|
33
|
+
*/
|
|
34
|
+
export function shellCommandTitle(toolName, rawArguments) {
|
|
35
|
+
if (rawArguments === undefined || rawArguments === '')
|
|
36
|
+
return undefined;
|
|
37
|
+
if (toolName !== 'bash' && toolName !== 'pwsh')
|
|
38
|
+
return undefined;
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(rawArguments);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
47
|
+
return undefined;
|
|
48
|
+
const command = parsed.command;
|
|
49
|
+
if (typeof command !== 'string' || command.trim() === '')
|
|
50
|
+
return undefined;
|
|
51
|
+
const firstLine = command.split('\n')[0]?.trim() ?? '';
|
|
52
|
+
return firstLine === '' ? undefined : safeTitle(firstLine);
|
|
53
|
+
}
|
|
24
54
|
export function createToolActivitySurface(options) {
|
|
25
55
|
const rows = new Map();
|
|
26
56
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addozhang/dsh-discord",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-alpha.2",
|
|
4
4
|
"description": "Discord-first adapter for DeepSeek Harness",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,7 +46,6 @@
|
|
|
46
46
|
},
|
|
47
47
|
"client": {
|
|
48
48
|
"inject": [
|
|
49
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
50
49
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
51
50
|
],
|
|
52
51
|
"platform": "web",
|
|
@@ -54,15 +53,14 @@
|
|
|
54
53
|
}
|
|
55
54
|
},
|
|
56
55
|
"peerDependencies": {
|
|
57
|
-
"@deepseek-ai/cordis": "4.0.1"
|
|
58
|
-
"@deepseek-ai/dsh-host-apiproxy": "0.1.1-rc.2"
|
|
56
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
59
57
|
},
|
|
60
58
|
"devDependencies": {
|
|
61
|
-
"@deepseek-ai/cordis": "4.0.
|
|
62
|
-
"@deepseek-ai/dsh-client-
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-
|
|
64
|
-
"@deepseek-ai/dsh-client-ui-
|
|
65
|
-
"@deepseek-ai/dsh-
|
|
59
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "0.1.6-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
|
|
66
64
|
"@eslint/js": "9.39.1",
|
|
67
65
|
"@types/node": "24.10.1",
|
|
68
66
|
"@types/react": "18.3.1",
|
|
@@ -77,9 +75,9 @@
|
|
|
77
75
|
"vitest": "4.1.11"
|
|
78
76
|
},
|
|
79
77
|
"dependencies": {
|
|
80
|
-
"@deepseek-ai/dsh-credentials": "0.1.
|
|
81
|
-
"@deepseek-ai/dsh-settings": "0.1.
|
|
82
|
-
"@deepseek-ai/dsh-storage-domain": "0.1.
|
|
78
|
+
"@deepseek-ai/dsh-credentials": "0.1.6-alpha.2",
|
|
79
|
+
"@deepseek-ai/dsh-settings": "0.1.6-alpha.2",
|
|
80
|
+
"@deepseek-ai/dsh-storage-domain": "0.1.6-alpha.2",
|
|
83
81
|
"@deepseek-ai/schemastery": "3.18.1",
|
|
84
82
|
"zod": "^4.4.3"
|
|
85
83
|
},
|