dsh-better-display 0.3.4 → 0.3.6

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.
@@ -264,6 +264,48 @@ export function activityPhase(entry: Pick<ToolActivityEntry, 'block' | 'draft'>,
264
264
  return 'returned';
265
265
  }
266
266
 
267
+ /**
268
+ * First-seen time for calls the host has not stamped yet. A draft that
269
+ * precedes a landed call carries no timestamp, so without this memory the
270
+ * running clock would restart at zero on every remount — e.g. switching to
271
+ * another conversation and back while the call is still preparing. Keyed by
272
+ * call id, dropped when the call leaves the running phases.
273
+ */
274
+ const clockFirstSeen = new Map<string, number>();
275
+ const CLOCK_FIRST_SEEN_LIMIT = 512;
276
+
277
+ /**
278
+ * Session-stamped start of a call, or null while only a draft exists.
279
+ * A settled result carries `callTime`, the paired tool/call event's time; a
280
+ * still-running call head stamps the same instant in its own `time` field.
281
+ */
282
+ export function callStartTime(entry: Pick<ToolActivityEntry, 'block'>): number | null {
283
+ const block = entry.block;
284
+ if (!block) return null;
285
+ return 'kind' in block ? block.callTime : block.time;
286
+ }
287
+
288
+ /**
289
+ * The instant a still-running call's elapsed clock counts from. The stamped
290
+ * time wins so a remounted view keeps counting from the real start; an
291
+ * un-stamped draft falls back to when this view first saw that call id, which
292
+ * survives remounts for the same reason.
293
+ */
294
+ export function runningClockBase(callId: string, block: ToolCallBlock | undefined, now = Date.now()): number {
295
+ const stamped = callStartTime({ block });
296
+ if (stamped != null) return stamped;
297
+ const seen = clockFirstSeen.get(callId);
298
+ if (seen !== undefined) return seen;
299
+ if (clockFirstSeen.size >= CLOCK_FIRST_SEEN_LIMIT) clockFirstSeen.delete(clockFirstSeen.keys().next().value!);
300
+ clockFirstSeen.set(callId, now);
301
+ return now;
302
+ }
303
+
304
+ /** Forget a call's first-seen time once its clock is no longer live. */
305
+ export function forgetCallClock(callId: string): void {
306
+ clockFirstSeen.delete(callId);
307
+ }
308
+
267
309
  export function activitySummary(entry: Pick<ToolActivityEntry, 'block' | 'draft'>) {
268
310
  const { name, raw } = toolIdentity(entry);
269
311
  const args = inputFields(raw);
@@ -0,0 +1,45 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { callStartTime, forgetCallClock, runningClockBase } from '../src/client/tool-activity.ts';
4
+ import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
5
+
6
+ const startedCall = (callId: string, time: number): RunningToolCall => ({
7
+ phase: 'start', callId, name: 'bash', turn: 1, step: 0, time, argsRaw: '{"command":"sleep 60"}', subCalls: [],
8
+ });
9
+ const resultOf = (callId: string, callTime: number | null, time: number): ToolResultNode => ({
10
+ kind: 'tool-result', seq: 1, callId, time, callTime,
11
+ call: { name: 'bash', argsRaw: '{"command":"sleep 60"}' },
12
+ content: [], isError: false, subCalls: [],
13
+ });
14
+
15
+ test('a dispatched call counts from its stamped start, not the mount time', () => {
16
+ // The view remounts long after the tool/call event: base must stay the
17
+ // session-stamped time, so the clock shows the real elapsed seconds.
18
+ const block = startedCall('c1', 1_000);
19
+ assert.equal(callStartTime({ block }), 1_000);
20
+ assert.equal(runningClockBase('c1', block, 60_000), 1_000);
21
+ });
22
+
23
+ test('a draft-only call keeps the same base across remounts', () => {
24
+ // A draft carries no timestamp; first-seen is remembered per call id so
25
+ // remounting the view does not zero the clock.
26
+ assert.equal(runningClockBase('c2', undefined, 10_000), 10_000);
27
+ assert.equal(runningClockBase('c2', undefined, 30_000), 10_000);
28
+ });
29
+
30
+ test('a landed call head wins over the remembered draft time', () => {
31
+ runningClockBase('c3', undefined, 10_000);
32
+ assert.equal(runningClockBase('c3', startedCall('c3', 8_000), 20_000), 8_000);
33
+ });
34
+
35
+ test('settling forgets the first-seen time so the id can be reused safely', () => {
36
+ runningClockBase('c4', undefined, 10_000);
37
+ forgetCallClock('c4');
38
+ assert.equal(runningClockBase('c4', undefined, 40_000), 40_000);
39
+ });
40
+
41
+ test('a settled result still reports its paired call time', () => {
42
+ assert.equal(callStartTime({ block: resultOf('c5', 1_000, 5_000) }), 1_000);
43
+ // A result whose call fell outside the window stamps nothing.
44
+ assert.equal(callStartTime({ block: resultOf('c6', null, 5_000) }), null);
45
+ });