dsh-ntr 0.3.2 → 0.3.3
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.en.md +2 -0
- package/README.md +2 -0
- package/lib/bridge.js +1819 -54
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +16 -0
- package/lib/client/index.js +44 -4
- package/lib/client/model-seat.js +8 -2
- package/lib/client/store.js +32 -0
- package/lib/client.js +228 -5
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1175 -7
- package/lib/index.js +27 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +621 -12
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +8 -1
- package/lib/types/client/model-seat.d.ts +21 -0
- package/lib/types/client/store.d.ts +10 -0
- package/lib/types/impl.d.ts +53 -6
- package/lib/types/protocol.d.ts +22 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +1 -1
- package/src/bridge.ts +2103 -53
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +19 -1
- package/src/client/index.tsx +36 -4
- package/src/client/model-seat.tsx +28 -3
- package/src/client/store.ts +37 -0
- package/src/impl.ts +1167 -11
- package/src/index.ts +25 -0
- package/src/protocol.ts +24 -0
- package/src/tps.ts +40 -0
|
@@ -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
|
package/lib/types/impl.d.ts
CHANGED
|
@@ -446,10 +446,42 @@ export declare function describeUnhandledUpdate(kind: string, update: Record<str
|
|
|
446
446
|
* injected context too (goal/subagent/skill material), and that turn carries no
|
|
447
447
|
* user intent to send anywhere.
|
|
448
448
|
*/
|
|
449
|
+
export interface PromptImageContent {
|
|
450
|
+
type: 'image';
|
|
451
|
+
data: string;
|
|
452
|
+
mimeType: string;
|
|
453
|
+
uri?: string;
|
|
454
|
+
}
|
|
455
|
+
export interface PromptTextContent {
|
|
456
|
+
type: 'text';
|
|
457
|
+
text: string;
|
|
458
|
+
}
|
|
459
|
+
export type PromptContentBlock = PromptTextContent | PromptImageContent;
|
|
460
|
+
/** Helper: guess MIME type from file extension or URL */
|
|
461
|
+
export declare function guessImageMimeType(pathOrUrl?: string): string | undefined;
|
|
462
|
+
/** Helper: parse Data URI */
|
|
463
|
+
export declare function parseImageDataUrl(url: string): {
|
|
464
|
+
data: string;
|
|
465
|
+
mimeType: string;
|
|
466
|
+
} | undefined;
|
|
467
|
+
export declare function isLocalFilePath(str: string): boolean;
|
|
468
|
+
export declare function normalizeFileUrlToPath(uri: string): string;
|
|
469
|
+
/**
|
|
470
|
+
* Detect image MIME type by examining binary magic numbers.
|
|
471
|
+
* Supports Buffer, Uint8Array, and base64 strings.
|
|
472
|
+
*/
|
|
473
|
+
export declare function detectImageMimeFromMagic(raw: Buffer | Uint8Array | string | undefined): string | undefined;
|
|
474
|
+
export declare function resolveImageAttachmentFromContext(id: string, context?: unknown, attachmentMeta?: unknown, rootContext?: unknown): unknown;
|
|
475
|
+
/**
|
|
476
|
+
* Extract an image payload (Base64 data + MIME type) from an image block and optional context.
|
|
477
|
+
*/
|
|
478
|
+
export declare function resolveImageContent(block: unknown, context?: unknown): PromptImageContent | undefined;
|
|
449
479
|
export type ForwardPromptSelection = {
|
|
450
480
|
kind: 'prompt';
|
|
451
481
|
text: string;
|
|
452
482
|
id?: string;
|
|
483
|
+
images?: PromptImageContent[];
|
|
484
|
+
content?: PromptContentBlock[];
|
|
453
485
|
} | {
|
|
454
486
|
kind: 'no-user-input';
|
|
455
487
|
} | {
|
|
@@ -468,19 +500,25 @@ export type ForwardPromptSelection = {
|
|
|
468
500
|
* (tests, headless callers) and counts as user input.
|
|
469
501
|
*
|
|
470
502
|
* @param messages - the request's ordered messages, exactly as the host built them.
|
|
471
|
-
* @
|
|
503
|
+
* @param context - optional host context or attachment resolver for reading attachments.
|
|
504
|
+
* @returns the last real user message's text and any image content blocks, or why there is none to forward.
|
|
472
505
|
*/
|
|
473
506
|
export declare function selectForwardPrompt(messages: readonly {
|
|
474
507
|
role: string;
|
|
475
|
-
content?: Array<{
|
|
508
|
+
content?: string | Array<{
|
|
476
509
|
type?: string;
|
|
477
510
|
text?: string;
|
|
478
|
-
|
|
511
|
+
[k: string]: unknown;
|
|
512
|
+
}> | {
|
|
513
|
+
type?: string;
|
|
514
|
+
text?: string;
|
|
515
|
+
[k: string]: unknown;
|
|
516
|
+
};
|
|
479
517
|
source?: {
|
|
480
518
|
kind?: string;
|
|
481
519
|
};
|
|
482
520
|
id?: string;
|
|
483
|
-
}[]): ForwardPromptSelection;
|
|
521
|
+
}[], context?: unknown): ForwardPromptSelection;
|
|
484
522
|
/** Structural read of ACP `ToolCall.kind`; anything unexpected stays `undefined` (= effect unknown). */
|
|
485
523
|
export declare function permissionToolKind(toolCall: unknown): string | undefined;
|
|
486
524
|
/** Structural read of ACP `ToolCall.locations[].path` — where an edit claims it will write. */
|
|
@@ -552,7 +590,16 @@ export interface RosterPresetRow {
|
|
|
552
590
|
* @returns the status, or `undefined` when the roster is unreadable.
|
|
553
591
|
*/
|
|
554
592
|
export declare function acpPresetStatusFromRoster(rows: readonly RosterPresetRow[] | undefined, id: string): AcpPresetStatus | undefined;
|
|
555
|
-
/**
|
|
593
|
+
/**
|
|
594
|
+
* Bounded log of raw ACP session updates with monotone seq.
|
|
595
|
+
*
|
|
596
|
+
* Consecutive ticks of the same state-only kind share one entry (the entry is
|
|
597
|
+
* replaced in place, not appended): chatty agents emit a usage / phase tick
|
|
598
|
+
* every few hundred milliseconds, and letting each occupy the ring evicts the
|
|
599
|
+
* conversation itself. This costs nothing on the live path — `append` still
|
|
600
|
+
* allocates a fresh seq and the bridge broadcasts that frame — but replay via
|
|
601
|
+
* `since` only sees the newest tick of each run.
|
|
602
|
+
*/
|
|
556
603
|
export declare class Transcript {
|
|
557
604
|
private readonly max;
|
|
558
605
|
private readonly entries;
|
|
@@ -664,7 +711,7 @@ export interface AcpManager {
|
|
|
664
711
|
startSession(agentId: string, cwd: string | undefined): Promise<{
|
|
665
712
|
sessionId: string;
|
|
666
713
|
}>;
|
|
667
|
-
prompt(sessionId: string,
|
|
714
|
+
prompt(sessionId: string, prompt: string | PromptContentBlock[]): Promise<{
|
|
668
715
|
turnId: string;
|
|
669
716
|
}>;
|
|
670
717
|
cancel(sessionId: string): Promise<void>;
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.3.3",
|
|
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",
|