dsh-ntr 0.2.0 → 0.2.1

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.
@@ -805,6 +805,8 @@ interface SegmentState {
805
805
  index: number;
806
806
  activeType: 'reasoning' | 'text' | 'tool-call' | null;
807
807
  activeText: string;
808
+ /** UTF-16 units of this segment's text and reasoning chunks (the usage estimate). */
809
+ outputChars: number;
808
810
  /** The open tool-call block's snapshot, while `activeType === 'tool-call'`. */
809
811
  activeToolCall: AcpToolCallSnapshot | undefined;
810
812
  /** Whether this segment opened at least one tool-call block. */
@@ -845,7 +847,7 @@ type TurnSettle = {
845
847
  * `transcript.since(cursor)` — events that arrive while no segment is open are
846
848
  * recorded, never lost), the seen-call registry (a call id gets exactly one
847
849
  * card per turn, wherever its restatements land), and the turn-level facts
848
- * (`sawVisibleBlock`, usage) that outlive any single segment.
850
+ * (`sawVisibleBlock`) that outlive any single segment.
849
851
  */
850
852
  declare class TurnStreamController {
851
853
  private readonly manager;
@@ -860,7 +862,6 @@ declare class TurnStreamController {
860
862
  private disposed;
861
863
  private readonly seenCalls;
862
864
  private readonly announcedKinds;
863
- private latestUsage;
864
865
  /** Narrative lines (permission asks) that arrived while no segment was open. */
865
866
  private readonly pendingProse;
866
867
  sawVisibleBlock: boolean;
@@ -908,7 +909,6 @@ declare class TurnStreamController {
908
909
  private pushProse;
909
910
  /** Events that arrive between segments: recorded, replayed into the next one. */
910
911
  private recordWithoutSegment;
911
- private noteUsage;
912
912
  /** Record a tool call's output (idempotent) and wake any waiter. */
913
913
  private noteToolRecord;
914
914
  /**
@@ -86,7 +86,6 @@ export declare function approvalDeadlineFacts(deadlines: AcpApprovalDeadlines):
86
86
  /** A reset is only honest when something is actually being outranked. */
87
87
  overridden: boolean;
88
88
  };
89
- /** Settings configuration section for ACP agents. */
90
89
  export declare function AcpSettingsSection(props: WithStore): import("react").JSX.Element;
91
90
  /** Everything the connection form edits, in one object (kept in the row). */
92
91
  interface AgentForm {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ntr",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive Agent Client Protocol (ACP) client for the DSH web UI: attach any ACP-capable agent and stream its session.",
5
5
  "repository": "https://github.com/GDWhisper/dsh-ntr.git",
6
6
  "type": "module",
@@ -48,9 +48,13 @@
48
48
  "@deepseek-ai/dsh-client-ui-sidebar": "0.1.5-rc.1",
49
49
  "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
50
50
  "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
51
+ "@testing-library/react": "^16.3.3",
51
52
  "@types/node": "^22.19.0",
52
53
  "@types/react": "^19.2.0",
54
+ "@types/react-dom": "^19.3.0",
55
+ "jsdom": "^27.4.0",
53
56
  "react": "^19.2.0",
57
+ "react-dom": "^19.3.1",
54
58
  "tsdown": "^0.23.0",
55
59
  "typescript": "^5.9.0",
56
60
  "vitest": "^3.2.0"
package/src/bridge.ts CHANGED
@@ -2400,6 +2400,8 @@ interface SegmentState {
2400
2400
  index: number
2401
2401
  activeType: 'reasoning' | 'text' | 'tool-call' | null
2402
2402
  activeText: string
2403
+ /** UTF-16 units of this segment's text and reasoning chunks (the usage estimate). */
2404
+ outputChars: number
2403
2405
  /** The open tool-call block's snapshot, while `activeType === 'tool-call'`. */
2404
2406
  activeToolCall: AcpToolCallSnapshot | undefined
2405
2407
  /** Whether this segment opened at least one tool-call block. */
@@ -2407,6 +2409,9 @@ interface SegmentState {
2407
2409
  finished: boolean
2408
2410
  }
2409
2411
 
2412
+ /** ACP `usage_update` is context occupancy, not request tokens; host usage is a 4-chars/token OUTPUT estimate (tool args excluded). */
2413
+ const OUTPUT_CHARS_PER_TOKEN = 4
2414
+
2410
2415
  /** How one segment's stream ends, in `StreamChunk.finish` terms. */
2411
2416
  type SegmentFinishReason =
2412
2417
  | { kind: 'stop' }
@@ -2427,7 +2432,7 @@ type TurnSettle =
2427
2432
  * `transcript.since(cursor)` — events that arrive while no segment is open are
2428
2433
  * recorded, never lost), the seen-call registry (a call id gets exactly one
2429
2434
  * card per turn, wherever its restatements land), and the turn-level facts
2430
- * (`sawVisibleBlock`, usage) that outlive any single segment.
2435
+ * (`sawVisibleBlock`) that outlive any single segment.
2431
2436
  */
2432
2437
  class TurnStreamController {
2433
2438
  private segment: SegmentState | undefined
@@ -2435,7 +2440,6 @@ class TurnStreamController {
2435
2440
  private disposed = false
2436
2441
  private readonly seenCalls = new Set<string>()
2437
2442
  private readonly announcedKinds = new Map<string, string>()
2438
- private latestUsage: { inputTokens: number; outputTokens: number } | null = null
2439
2443
  /** Narrative lines (permission asks) that arrived while no segment was open. */
2440
2444
  private readonly pendingProse: string[] = []
2441
2445
  sawVisibleBlock = false
@@ -2487,6 +2491,7 @@ class TurnStreamController {
2487
2491
  index: -1,
2488
2492
  activeType: null,
2489
2493
  activeText: '',
2494
+ outputChars: 0,
2490
2495
  activeToolCall: undefined,
2491
2496
  hasCard: false,
2492
2497
  finished: false,
@@ -2605,7 +2610,9 @@ class TurnStreamController {
2605
2610
  seg.activeType = null
2606
2611
  seg.activeToolCall = undefined
2607
2612
  }
2608
- if (this.latestUsage !== null) seg.queue.push({ type: 'usage', usage: this.latestUsage })
2613
+ const outputTokens = Math.round(seg.outputChars / OUTPUT_CHARS_PER_TOKEN)
2614
+ // Zero input keeps the host's cache-hit pill hidden instead of faking 0%.
2615
+ if (outputTokens > 0) seg.queue.push({ type: 'usage', usage: { inputTokens: 0, outputTokens } })
2609
2616
  seg.queue.push({ type: 'finish', reason })
2610
2617
  seg.queue.close()
2611
2618
  if (this.segment === seg) this.segment = undefined
@@ -2643,11 +2650,10 @@ class TurnStreamController {
2643
2650
  const seg = this.segment
2644
2651
  if (seg === undefined || seg.finished) {
2645
2652
  // Between segments: tool outputs still reach the shared registry (the
2646
- // guard's virtual tool may be parked on this very record) and usage
2647
- // keeps the turn's tally fresh, but the cursor does NOT advance — the
2648
- // next segment replays everything since it, which re-records those
2649
- // idempotently and folds the prose the agent streamed while its cards
2650
- // executed on the host.
2653
+ // guard's virtual tool may be parked on this very record), but the
2654
+ // cursor does NOT advance — the next segment replays everything since
2655
+ // it, which re-records those idempotently and folds the prose the agent
2656
+ // streamed while its cards executed on the host.
2651
2657
  this.recordWithoutSegment(update)
2652
2658
  return
2653
2659
  }
@@ -2689,17 +2695,7 @@ class TurnStreamController {
2689
2695
  /** Events that arrive between segments: recorded, replayed into the next one. */
2690
2696
  private recordWithoutSegment(update: Record<string, unknown>): void {
2691
2697
  const kind = String(update.sessionUpdate ?? '')
2692
- if (kind === 'usage_update') {
2693
- this.noteUsage(update)
2694
- } else if (kind === 'tool_call' || kind === 'tool_call_update') {
2695
- this.noteToolRecord(update)
2696
- }
2697
- }
2698
-
2699
- private noteUsage(update: Record<string, unknown>): void {
2700
- const used = typeof update.used === 'number' ? update.used : 0
2701
- const size = typeof update.size === 'number' ? update.size : 0
2702
- this.latestUsage = { inputTokens: size, outputTokens: used }
2698
+ if (kind === 'tool_call' || kind === 'tool_call_update') this.noteToolRecord(update)
2703
2699
  }
2704
2700
 
2705
2701
  /** Record a tool call's output (idempotent) and wake any waiter. */
@@ -2725,6 +2721,7 @@ class TurnStreamController {
2725
2721
  this.finishSegment(seg, { kind: 'stop' })
2726
2722
  return false
2727
2723
  }
2724
+ seg.outputChars += text.length
2728
2725
  this.emitProse(seg, kind === 'agent_thought_chunk' ? 'reasoning' : 'text', text)
2729
2726
  return true
2730
2727
  } else if (kind === 'tool_call' || kind === 'tool_call_update') {
@@ -2743,9 +2740,6 @@ class TurnStreamController {
2743
2740
  this.seenCalls.add(snapshot.callId)
2744
2741
  this.openToolCallBlock(seg, snapshot)
2745
2742
  return true
2746
- } else if (kind === 'usage_update') {
2747
- this.noteUsage(update)
2748
- return true
2749
2743
  } else if (!BOOKKEEPING_UPDATES.has(kind)) {
2750
2744
  const text = describeUnhandledUpdate(kind, update)
2751
2745
  if (this.announcedKinds.get(kind) === text) return true