dsh-ntr 0.3.2 → 0.3.4

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.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The composer dock's `x轮x步 · xx tok/s` pill on an ACP conversation.
3
+ *
4
+ * WHY SHADOW INSTEAD OF A SECOND PILL: the host's `ActivityPill` already states
5
+ * the two things this conversation has — turn/step counts and a whole-session
6
+ * decode speed — in the one place a speed belongs. Only its speed is wrong for
7
+ * ACP: the host divides the tokens a step emitted by `firstTokenTime →
8
+ * completedTime`, and that window contains every tool wait. Rendering a second
9
+ * speed beside it would leave two numbers on screen and no way to tell which
10
+ * is the honest one. So this module takes the host pill's own cell (`list` slot
11
+ * `conversation.composer.dock`, id `activity`) at a lower priority, renders the
12
+ * HOST's component, and hands it a patched `sessionStats` whose decode fields
13
+ * are the plugin's burst aggregate. Style, counts, dialog, aria and locale stay
14
+ * the host's; only the number changes.
15
+ *
16
+ * NON-ACP CONVERSATIONS PASS THROUGH. The shadow is installed only while the
17
+ * gate confirms the staged conversation runs the ACP preset, but a list entry
18
+ * is projected into EVERY session's composer. The wrapper therefore renders the
19
+ * host component with the ORIGINAL projection whenever `acpMode` is not
20
+ * affirmatively true, making the shadow visually indistinguishable from the
21
+ * shipped entry there.
22
+ *
23
+ * FAIL-OPEN IS STRUCTURAL: if the host entry, its inject face, its locale or
24
+ * its `performanceUsage` source is missing, no claim is created and the native
25
+ * pill stays exactly as shipped. If the host component rejects the patched
26
+ * projection, the slot's error boundary retires this entry and the native
27
+ * `activity` entry wins again. No CSS hiding and no DOM patching are involved.
28
+ */
29
+ import type { SessionBurst } from '../protocol.ts';
30
+ import type { AcpStore, UiState } from './store.ts';
31
+ import type { ModelSeatClaim, SeatSlots } from './model-seat.tsx';
32
+ export declare const DOCK_SLOT = "conversation.composer.dock";
33
+ export declare const ACTIVITY_ENTRY_ID = "activity";
34
+ type ActivityProjection = (key: string, selector?: (value: unknown) => unknown) => unknown;
35
+ type ActivityPerformanceHook = (selector?: (value: unknown) => unknown) => unknown;
36
+ type ActivityTranslate = (key: string, params?: Record<string, unknown>) => string;
37
+ /** The standard kit + injected face the shadow renders the host component with. */
38
+ export interface ActivityPillHostProps {
39
+ sessionId: string;
40
+ useChat: unknown;
41
+ useProjection: ActivityProjection;
42
+ usePerformanceUsage: ActivityPerformanceHook;
43
+ t?: ActivityTranslate;
44
+ acp: AcpStore;
45
+ }
46
+ /**
47
+ * The aggregate the dock should show: the sum of every ACP session backing this
48
+ * chat (the host pill's counts are whole-chat too), with a live `turn/metrics`
49
+ * entry preferred per session over its persisted row. `undefined` means "no
50
+ * measurable burst yet" — the wrapper then hides the speed instead of showing
51
+ * the host's tool-inclusive number.
52
+ */
53
+ export declare function activityBurst(ui: Pick<UiState, 'chatSessions' | 'chatSessionsFor' | 'burstStats'>, dshSessionId: string): SessionBurst | undefined;
54
+ /**
55
+ * Replace ONLY the decode half of the host's session stats.
56
+ *
57
+ * `decodeTokens` and `decodeMs` are the two fields the host's `decodeSpeed`
58
+ * reads (`decodeTokens / (decodeMs / 1000)`), so writing the plugin's aggregate
59
+ * there makes the host's own formatter, dialog row and aria label display the
60
+ * plugin's burst rate. Every other field — turns, steps, LLM/tool/TTFT — stays
61
+ * host-native. With no measurable burst the two decode fields are zeroed: the
62
+ * host then hides the speed (`decodeMs > 0` guard) while keeping the counts.
63
+ */
64
+ export declare function patchedActivityStats(native: Record<string, unknown> | undefined, burst: SessionBurst | undefined, acp: boolean): Record<string, unknown> | undefined;
65
+ export interface ActivityPillClaimOptions {
66
+ slots: SeatSlots;
67
+ /** Session-scoped face factory (the gate's `chatFace`). */
68
+ inject(sessionId: unknown, actions?: unknown): unknown;
69
+ onDiag?(line: string): void;
70
+ maxClaims?: number;
71
+ }
72
+ /**
73
+ * Take the dock's `activity` cell for one ACP-mode spell.
74
+ *
75
+ * Returns `undefined` on any host shape this bundle cannot serve — an older
76
+ * host without the `activity` entry (0.1.5 ships `stats`), a slot kind change,
77
+ * a missing inject/locale, or no `performanceUsage` source. The native pill is
78
+ * then left untouched; that is the honest failure mode, not a hidden control.
79
+ */
80
+ export declare function createActivityPillClaim(options: ActivityPillClaimOptions): ModelSeatClaim | undefined;
81
+ export {};
@@ -21,7 +21,14 @@
21
21
  * rather than risking a fork cut at a point the user is not looking at.
22
22
  */
23
23
  import type { ComponentType } from 'react';
24
- import type { AcpChatInjected } from './acp-ui.tsx';
24
+ import type { AcpChatInjected, AcpHostForkFace } from './acp-ui.tsx';
25
+ /**
26
+ * Resolves the runtime's `sessions.fork` primitive, binding it to the service
27
+ * instance so that internal `this` accesses (such as `this.list.getSnapshot()`)
28
+ * resolve correctly when called through `face.fork(...)`.
29
+ * Returns `undefined` if the service or `fork` method is absent or not a function.
30
+ */
31
+ export declare function resolveHostFork(sessionsService: unknown): AcpHostForkFace | undefined;
25
32
  /**
26
33
  * The host's own branch-button icon, picked per host generation. The icon
27
34
  * exports MOVED between shipped hosts — desktop 0.2.1-rc.2 has
@@ -122,7 +122,13 @@ export interface SeatEntry {
122
122
  options?: {
123
123
  priority?: number;
124
124
  registrant?: string;
125
+ id?: string;
126
+ order?: number;
125
127
  };
128
+ /** Entry-owned inject face; needed when the claim reuses a shipped occupant. */
129
+ inject?: (...args: unknown[]) => unknown;
130
+ /** Locale namespace the occupant's `t` is bound to; needed for the same reuse. */
131
+ locale?: string;
126
132
  }
127
133
  /** The slot-registry face this module uses (structural: the registry is not a dependency of this bundle). */
128
134
  export interface SeatSlots {
@@ -142,6 +148,12 @@ export interface SeatSlots {
142
148
  */
143
149
  export interface SeatRegisterOptions {
144
150
  name: string;
151
+ /** List-slot cell to shadow; omitted for single/keyed registers. */
152
+ id?: string;
153
+ /** Display position within the list; only meaningful with `id`. */
154
+ order?: number;
155
+ /** Locale namespace the occupant's `t` is bound to. */
156
+ locale?: string;
145
157
  priority?: number;
146
158
  /**
147
159
  * The session-scoped face factory the renderer calls with the scope binding's
@@ -218,6 +230,15 @@ export interface PermissionSeatClaimOptions {
218
230
  export interface SeatClaimOptions {
219
231
  slots: SeatSlots;
220
232
  slotKey: string;
233
+ /**
234
+ * List-slot cell id to hold. Omitted for single/keyed seats; a list slot
235
+ * needs it because its winner view is one entry PER id, not one per slot.
236
+ */
237
+ entryId?: string;
238
+ /** Display order for the installed list entry (same cell, so usually the shipped order). */
239
+ order?: number;
240
+ /** Locale namespace the installed occupant resolves `t` against. */
241
+ locale?: string;
221
242
  basePriority: number;
222
243
  occupant(): unknown;
223
244
  inject?(sessionId: unknown, actions?: unknown): unknown;
@@ -254,6 +254,16 @@ export interface UiState {
254
254
  chatSessions: ChatSessionView[];
255
255
  /** The DSH chat session id {@link chatSessions} was loaded for. */
256
256
  chatSessionsFor?: string;
257
+ /**
258
+ * Live burst aggregate per ACP session id, from `turn/metrics`. Absent means
259
+ * no measured burst yet; a row's persisted `burst` is the reload seed.
260
+ */
261
+ burstStats: Record<string, {
262
+ outputTokens: number;
263
+ burstMs: number;
264
+ turnId: string;
265
+ final: boolean;
266
+ }>;
257
267
  /**
258
268
  * The agent tag's halo preference (settings-editable display choice). The
259
269
  * tag reads it per render; the host answer in `resync` is the authority and
@@ -248,6 +248,14 @@ export declare function childEnv(parent: NodeJS.ProcessEnv, extra: Record<string
248
248
  * anything unusable is skipped silently.
249
249
  */
250
250
  export declare function parseCatalogOptions(configOptions: unknown): AcpAgentCatalog;
251
+ /**
252
+ * Whether a payload names ONE model selector without relying on category
253
+ * alone. When an agent publishes several `category: 'model'` options and none
254
+ * is explicitly the `model` id, the category is not trustworthy enough to
255
+ * reject the user's pick — callers must fail open instead of reporting
256
+ * `model_unavailable`.
257
+ */
258
+ export declare function hasUnambiguousModelSelector(configOptions: unknown): boolean;
251
259
  /**
252
260
  * Fold one per-session option answer's SHAPE into an agent-level catalog.
253
261
  *
@@ -446,10 +454,42 @@ export declare function describeUnhandledUpdate(kind: string, update: Record<str
446
454
  * injected context too (goal/subagent/skill material), and that turn carries no
447
455
  * user intent to send anywhere.
448
456
  */
457
+ export interface PromptImageContent {
458
+ type: 'image';
459
+ data: string;
460
+ mimeType: string;
461
+ uri?: string;
462
+ }
463
+ export interface PromptTextContent {
464
+ type: 'text';
465
+ text: string;
466
+ }
467
+ export type PromptContentBlock = PromptTextContent | PromptImageContent;
468
+ /** Helper: guess MIME type from file extension or URL */
469
+ export declare function guessImageMimeType(pathOrUrl?: string): string | undefined;
470
+ /** Helper: parse Data URI */
471
+ export declare function parseImageDataUrl(url: string): {
472
+ data: string;
473
+ mimeType: string;
474
+ } | undefined;
475
+ export declare function isLocalFilePath(str: string): boolean;
476
+ export declare function normalizeFileUrlToPath(uri: string): string;
477
+ /**
478
+ * Detect image MIME type by examining binary magic numbers.
479
+ * Supports Buffer, Uint8Array, and base64 strings.
480
+ */
481
+ export declare function detectImageMimeFromMagic(raw: Buffer | Uint8Array | string | undefined): string | undefined;
482
+ export declare function resolveImageAttachmentFromContext(id: string, context?: unknown, attachmentMeta?: unknown, rootContext?: unknown): unknown;
483
+ /**
484
+ * Extract an image payload (Base64 data + MIME type) from an image block and optional context.
485
+ */
486
+ export declare function resolveImageContent(block: unknown, context?: unknown): PromptImageContent | undefined;
449
487
  export type ForwardPromptSelection = {
450
488
  kind: 'prompt';
451
489
  text: string;
452
490
  id?: string;
491
+ images?: PromptImageContent[];
492
+ content?: PromptContentBlock[];
453
493
  } | {
454
494
  kind: 'no-user-input';
455
495
  } | {
@@ -468,19 +508,25 @@ export type ForwardPromptSelection = {
468
508
  * (tests, headless callers) and counts as user input.
469
509
  *
470
510
  * @param messages - the request's ordered messages, exactly as the host built them.
471
- * @returns the last real user message's text, or why there is none to forward.
511
+ * @param context - optional host context or attachment resolver for reading attachments.
512
+ * @returns the last real user message's text and any image content blocks, or why there is none to forward.
472
513
  */
473
514
  export declare function selectForwardPrompt(messages: readonly {
474
515
  role: string;
475
- content?: Array<{
516
+ content?: string | Array<{
476
517
  type?: string;
477
518
  text?: string;
478
- }>;
519
+ [k: string]: unknown;
520
+ }> | {
521
+ type?: string;
522
+ text?: string;
523
+ [k: string]: unknown;
524
+ };
479
525
  source?: {
480
526
  kind?: string;
481
527
  };
482
528
  id?: string;
483
- }[]): ForwardPromptSelection;
529
+ }[], context?: unknown): ForwardPromptSelection;
484
530
  /** Structural read of ACP `ToolCall.kind`; anything unexpected stays `undefined` (= effect unknown). */
485
531
  export declare function permissionToolKind(toolCall: unknown): string | undefined;
486
532
  /** Structural read of ACP `ToolCall.locations[].path` — where an edit claims it will write. */
@@ -552,7 +598,16 @@ export interface RosterPresetRow {
552
598
  * @returns the status, or `undefined` when the roster is unreadable.
553
599
  */
554
600
  export declare function acpPresetStatusFromRoster(rows: readonly RosterPresetRow[] | undefined, id: string): AcpPresetStatus | undefined;
555
- /** Bounded append-only log of raw ACP session updates with monotone seq. */
601
+ /**
602
+ * Bounded log of raw ACP session updates with monotone seq.
603
+ *
604
+ * Consecutive ticks of the same state-only kind share one entry (the entry is
605
+ * replaced in place, not appended): chatty agents emit a usage / phase tick
606
+ * every few hundred milliseconds, and letting each occupy the ring evicts the
607
+ * conversation itself. This costs nothing on the live path — `append` still
608
+ * allocates a fresh seq and the bridge broadcasts that frame — but replay via
609
+ * `since` only sees the newest tick of each run.
610
+ */
556
611
  export declare class Transcript {
557
612
  private readonly max;
558
613
  private readonly entries;
@@ -664,7 +719,7 @@ export interface AcpManager {
664
719
  startSession(agentId: string, cwd: string | undefined): Promise<{
665
720
  sessionId: string;
666
721
  }>;
667
- prompt(sessionId: string, text: string): Promise<{
722
+ prompt(sessionId: string, prompt: string | PromptContentBlock[]): Promise<{
668
723
  turnId: string;
669
724
  }>;
670
725
  cancel(sessionId: string): Promise<void>;
@@ -485,6 +485,17 @@ export type AcpEvent = {
485
485
  sessionId: string;
486
486
  turnId: string;
487
487
  text: string;
488
+ } | {
489
+ type: 'turn/metrics';
490
+ /** ACP session id (the store keys live burst state by it). */
491
+ sessionId: string;
492
+ /** DSH chat session id (routing/context only). */
493
+ dshSessionId: string;
494
+ turnId: string;
495
+ /** Session aggregate INCLUDING the current turn when measurable. */
496
+ outputTokens: number;
497
+ burstMs: number;
498
+ final: boolean;
488
499
  } | {
489
500
  type: 'turn/end';
490
501
  sessionId: string;
@@ -523,6 +534,13 @@ export type PermissionOutcome = {
523
534
  } | {
524
535
  outcome: 'cancelled';
525
536
  };
537
+ /** Plugin-side measurable burst aggregate for one ACP session (not host turn timings). */
538
+ export interface SessionBurst {
539
+ /** Sum of estimated output tokens over measurable turns. */
540
+ outputTokens: number;
541
+ /** Sum of within-burst chunk gaps over those turns; tool/approval/long pauses excluded. */
542
+ burstMs: number;
543
+ }
526
544
  /** Derived session snapshot for replay/reconnect (source of truth stays in the transcript). */
527
545
  export interface SessionView {
528
546
  sessionId: string;
@@ -554,6 +572,8 @@ export interface SessionView {
554
572
  currency: string;
555
573
  };
556
574
  };
575
+ /** Measurable burst aggregate over this session's turns; absent until measured. */
576
+ burst?: SessionBurst;
557
577
  running: boolean;
558
578
  closed: boolean;
559
579
  /**
@@ -639,6 +659,8 @@ export interface ChatSessionView {
639
659
  * browser live-patches this row from the update frames (see AGENTS/ui.md).
640
660
  */
641
661
  usage?: SessionView['usage'];
662
+ /** Measurable burst aggregate over this session's turns; absent until measured. */
663
+ burst?: SessionView['burst'];
642
664
  /** The agent process died; the session awaits a `session/load` resume. */
643
665
  released?: boolean;
644
666
  /** The agent is running a turn on this session right now. */
@@ -0,0 +1,17 @@
1
+ /** ACP `usage_update` is context occupancy, not request tokens; this is the plugin's 4-chars/token OUTPUT estimate (tool args excluded). */
2
+ export declare const OUTPUT_CHARS_PER_TOKEN = 4;
3
+ export declare const BURST_GAP_MS = 1000;
4
+ export interface TurnTpsSnapshot {
5
+ outputTokens: number;
6
+ burstMs: number;
7
+ }
8
+ export declare class TurnTpsTracker {
9
+ private readonly gapMs;
10
+ private lastProseAt;
11
+ private outputChars;
12
+ private burstMs;
13
+ constructor(gapMs?: number);
14
+ onProse(text: string, at: number): void;
15
+ onBreak(): void;
16
+ snapshot(): TurnTpsSnapshot;
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ntr",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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",