@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.
@@ -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
- * Answerable server-request frames (interaction-routing spec): approvals
106
- * and questions arrive as mux frames with an envelope rpcId the response
107
- * must echo. The composition owns rendering, ownership, and expiry.
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
- requests?: {
110
- onApprovalRequested(input: {
111
- sessionId: string;
112
- threadId: string;
113
- rpcId: string;
114
- approvalId: string;
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
  };
@@ -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
- /** The activity message body: one icon + presentation title per call row. */
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
- return rows.map(row => {
233
+ const rowLines = rows.map(row => {
184
234
  const title = truncateText(row.title ?? row.label, ACTIVITY_TITLE_MAX);
185
- return `> ${toolCategoryIcon(row.label)} ${title}`;
186
- }).join('\n');
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
- // Tool titles are Host-presented free text (terminal commands): they
194
- // go through the same outbound builder as every other message path.
195
- const payload = buildOutboundMessage({ kind: 'tool', content: renderActivityContent(runtime) });
196
- if (runtime.activityMessageId === undefined) {
197
- if (runtime.activityAttempted)
198
- return;
199
- const sent = await deps.delivery.send({ channelId: threadId, content: payload.content });
200
- if (sent.outcome === 'completed')
201
- runtime.activityMessageId = sent.messageId;
202
- else if (sent.outcome === 'unknown') {
203
- runtime.activityAttempted = true;
204
- deps.log?.('discord_live_activity_send_unknown', { threadId });
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
- return;
207
- }
208
- await deps.delivery.edit({ channelId: threadId, messageId: runtime.activityMessageId, content: payload.content });
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 title = presentationTitle(frameView);
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: typeof data['arguments'] === 'string' ? data['arguments'] : undefined,
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['error'] !== undefined;
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
- // The activity message is the live "what's happening" surface only:
351
- // at turn end it is deleted — the durable record is the Session log
352
- // and the assistant's answer.
353
- const activityMessageId = runtime.activityMessageId;
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
- if (activityMessageId !== undefined) {
356
- void deps.delivery.delete({ channelId: threadId, messageId: activityMessageId }).catch((cause) => {
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, envelopeRpcId) {
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
- if (type === 'approval/requested') {
440
- const threadId = deps.threadForSession(sessionId);
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 payload === 'object' && payload !== null ? payload.type : undefined);
519
- const sid = (typeof payload === 'object' && payload !== null ? payload.sessionId : undefined);
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(payload, typeof envelopeRpcId === 'string' ? envelopeRpcId : undefined);
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 but are
6
- * structurally excluded from every render. Verbosity gates visibility:
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;
@@ -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 but are
6
- * structurally excluded from every render. Verbosity gates visibility:
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.4.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.1",
62
- "@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
63
- "@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",
64
- "@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
65
- "@deepseek-ai/dsh-host-apiproxy": "0.1.1-rc.2",
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.1-rc.2",
81
- "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
82
- "@deepseek-ai/dsh-storage-domain": "0.1.1-rc.2",
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
  },