dsh-ntr 0.3.1 → 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.
Files changed (57) hide show
  1. package/README.en.md +3 -1
  2. package/README.md +3 -1
  3. package/cordis.patch.yml +17 -1
  4. package/lib/bridge.js +2436 -82
  5. package/lib/client/acp-ui.js +103 -3
  6. package/lib/client/acp-view.js +24 -4
  7. package/lib/client/activity-pill.js +156 -0
  8. package/lib/client/branch-action.js +149 -0
  9. package/lib/client/composer-mode.js +48 -13
  10. package/lib/client/context-meter.js +86 -0
  11. package/lib/client/index.js +279 -16
  12. package/lib/client/model-seat.js +8 -2
  13. package/lib/client/panel.js +96 -29
  14. package/lib/client/released-banner.js +66 -0
  15. package/lib/client/session-controls.js +38 -13
  16. package/lib/client/slash-isolation.js +2 -2
  17. package/lib/client/store.js +380 -13
  18. package/lib/client.js +1364 -70
  19. package/lib/client.js.map +1 -1
  20. package/lib/impl.js +1229 -8
  21. package/lib/index.js +34 -0
  22. package/lib/protocol.js +19 -0
  23. package/lib/tps.js +32 -0
  24. package/lib/types/bridge.d.ts +850 -18
  25. package/lib/types/client/acp-ui.d.ts +34 -2
  26. package/lib/types/client/acp-view.d.ts +13 -2
  27. package/lib/types/client/activity-pill.d.ts +81 -0
  28. package/lib/types/client/branch-action.d.ts +47 -0
  29. package/lib/types/client/composer-mode.d.ts +28 -0
  30. package/lib/types/client/context-meter.d.ts +51 -0
  31. package/lib/types/client/model-seat.d.ts +21 -0
  32. package/lib/types/client/released-banner.d.ts +34 -0
  33. package/lib/types/client/slash-isolation.d.ts +1 -1
  34. package/lib/types/client/store.d.ts +108 -1
  35. package/lib/types/impl.d.ts +94 -7
  36. package/lib/types/protocol.d.ts +145 -0
  37. package/lib/types/tps.d.ts +17 -0
  38. package/package.json +2 -1
  39. package/src/bridge.ts +2754 -86
  40. package/src/client/acp-ui.tsx +123 -2
  41. package/src/client/acp-view.tsx +30 -5
  42. package/src/client/activity-pill.tsx +201 -0
  43. package/src/client/branch-action.tsx +178 -0
  44. package/src/client/composer-mode.tsx +59 -13
  45. package/src/client/context-meter.tsx +128 -0
  46. package/src/client/index.tsx +298 -15
  47. package/src/client/model-seat.tsx +28 -3
  48. package/src/client/panel.tsx +157 -40
  49. package/src/client/primitives.d.ts +27 -0
  50. package/src/client/released-banner.tsx +97 -0
  51. package/src/client/session-controls.tsx +34 -11
  52. package/src/client/slash-isolation.ts +2 -1
  53. package/src/client/store.ts +412 -14
  54. package/src/impl.ts +1251 -13
  55. package/src/index.ts +33 -0
  56. package/src/protocol.ts +145 -0
  57. package/src/tps.ts +40 -0
@@ -5,7 +5,7 @@
5
5
  * fakes.
6
6
  */
7
7
  import { randomUUID } from './ids.ts';
8
- import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult } from './protocol.ts';
8
+ import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpIdleRelease, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult } from './protocol.ts';
9
9
  export { randomUUID };
10
10
  /** Structured failure the RPC layer converts to `{ok:false, error}` and `apply()` surfaces at boot. */
11
11
  export declare class AcpError extends Error {
@@ -82,6 +82,15 @@ export declare function validateApprovalDeadlineMs(raw: unknown, label: string,
82
82
  * reset the number. `reset` is not a deadline and is dropped here.
83
83
  */
84
84
  export declare function validateApprovalDeadlines(value: unknown): Partial<Record<ApprovalDeadlineKey, number>>;
85
+ /**
86
+ * Validate the idle-release window the settings page writes.
87
+ *
88
+ * The window is expressed in MILLISECONDS on the shared ladder
89
+ * ({@link IDLE_RELEASE_MS}), so a value the slider can produce is always a
90
+ * value the host runs — the same one-ladder rule the approval deadline card
91
+ * follows.
92
+ */
93
+ export declare function validateIdleReleaseMs(raw: unknown, code: 'bad_config' | 'bad_payload'): number;
85
94
  /**
86
95
  * Validate the mode a write asks for. Three values, and the list is the whole
87
96
  * vocabulary: an unanswered ask is either answered for you, kept waiting, or
@@ -437,10 +446,42 @@ export declare function describeUnhandledUpdate(kind: string, update: Record<str
437
446
  * injected context too (goal/subagent/skill material), and that turn carries no
438
447
  * user intent to send anywhere.
439
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;
440
479
  export type ForwardPromptSelection = {
441
480
  kind: 'prompt';
442
481
  text: string;
443
482
  id?: string;
483
+ images?: PromptImageContent[];
484
+ content?: PromptContentBlock[];
444
485
  } | {
445
486
  kind: 'no-user-input';
446
487
  } | {
@@ -459,19 +500,25 @@ export type ForwardPromptSelection = {
459
500
  * (tests, headless callers) and counts as user input.
460
501
  *
461
502
  * @param messages - the request's ordered messages, exactly as the host built them.
462
- * @returns the last real user message's text, or why there is none to forward.
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.
463
505
  */
464
506
  export declare function selectForwardPrompt(messages: readonly {
465
507
  role: string;
466
- content?: Array<{
508
+ content?: string | Array<{
467
509
  type?: string;
468
510
  text?: string;
469
- }>;
511
+ [k: string]: unknown;
512
+ }> | {
513
+ type?: string;
514
+ text?: string;
515
+ [k: string]: unknown;
516
+ };
470
517
  source?: {
471
518
  kind?: string;
472
519
  };
473
520
  id?: string;
474
- }[]): ForwardPromptSelection;
521
+ }[], context?: unknown): ForwardPromptSelection;
475
522
  /** Structural read of ACP `ToolCall.kind`; anything unexpected stays `undefined` (= effect unknown). */
476
523
  export declare function permissionToolKind(toolCall: unknown): string | undefined;
477
524
  /** Structural read of ACP `ToolCall.locations[].path` — where an edit claims it will write. */
@@ -543,7 +590,16 @@ export interface RosterPresetRow {
543
590
  * @returns the status, or `undefined` when the roster is unreadable.
544
591
  */
545
592
  export declare function acpPresetStatusFromRoster(rows: readonly RosterPresetRow[] | undefined, id: string): AcpPresetStatus | undefined;
546
- /** Bounded append-only log of raw ACP session updates with monotone seq. */
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
+ */
547
603
  export declare class Transcript {
548
604
  private readonly max;
549
605
  private readonly entries;
@@ -655,7 +711,7 @@ export interface AcpManager {
655
711
  startSession(agentId: string, cwd: string | undefined): Promise<{
656
712
  sessionId: string;
657
713
  }>;
658
- prompt(sessionId: string, text: string): Promise<{
714
+ prompt(sessionId: string, prompt: string | PromptContentBlock[]): Promise<{
659
715
  turnId: string;
660
716
  }>;
661
717
  cancel(sessionId: string): Promise<void>;
@@ -698,6 +754,19 @@ export interface AcpManager {
698
754
  * applies again. Applies to the running bridges without a restart.
699
755
  */
700
756
  setApprovalDeadlines?(patch: ApprovalDeadlinePatch, reset?: boolean): Promise<void>;
757
+ /**
758
+ * The idle-release window in force for chat processes, plus the built-in
759
+ * default a reset returns to. `undefined` on a host half from before the
760
+ * card existed, which hides the settings tab instead of showing a number
761
+ * this browser invented.
762
+ */
763
+ idleRelease?(): AcpIdleRelease | undefined;
764
+ /**
765
+ * Write the idle-release window (in ms), or drop the persisted
766
+ * override with `reset` so the built-in default applies again. Applies to
767
+ * the running bridges without a restart.
768
+ */
769
+ setIdleRelease?(idleTimeoutMs: number | undefined, reset?: boolean): Promise<void>;
701
770
  detectAgents?(): Promise<DetectedAgentInfo[]>;
702
771
  saveAgent?(def: AcpAgentDef): Promise<void>;
703
772
  deleteAgent?(agentId: string): Promise<void>;
@@ -770,6 +839,24 @@ export interface AcpManager {
770
839
  * without it the live (non-released) session is picked.
771
840
  */
772
841
  setChatSessionMode?(dshSessionId: string, modeId: string, agentId?: string): Promise<void>;
842
+ /**
843
+ * Bring one chat conversation's released session back on a fresh process on
844
+ * the user's explicit gesture (`session/chat-resume`, the released-session
845
+ * banner's button). Same recovery the next prompt performs lazily, minus
846
+ * the create path; no-op when the session is already live.
847
+ */
848
+ resumeChatSession?(dshSessionId: string): Promise<void>;
849
+ /**
850
+ * Adopt the ACP side of a branched conversation (`session/chat-fork-adopt`):
851
+ * the browser has already asked the HOST to fork the DSH session tree, and
852
+ * this runs the agent's own `session/fork` of the parent's live ACP session,
853
+ * then binds the child to it. All-or-nothing — nothing is bound on failure,
854
+ * so the child stays an ordinary copied-history conversation.
855
+ */
856
+ adoptForkedSession?(childDshSessionId: string, parentDshSessionId: string): Promise<{
857
+ sessionId: string;
858
+ acpSessionId: string;
859
+ }>;
773
860
  /**
774
861
  * Point one DSH chat conversation at an ACP agent — the plugin's own
775
862
  * conversation↔agent mapping, never a DSH model route (see
@@ -178,6 +178,18 @@ export interface AcpPluginConfig {
178
178
  * deployment baseline that override falls back to. See {@link AcpApprovalDeadlines}.
179
179
  */
180
180
  permissionTimeoutMs?: number;
181
+ /**
182
+ * How long a chat process may sit idle before it is released (parked, then
183
+ * resumed by the next message). In MILLISECONDS; default 1800000 (30 min).
184
+ *
185
+ * This is the HOST DEFAULT: an agent that names its own
186
+ * {@link AcpAgentDef.idleTimeoutMs} keeps its own. Editable from the
187
+ * settings page (「进程管理」 → `ui/idle-release-set`), which persists an
188
+ * override in the plugin's own state file and applies it to the running
189
+ * bridges without restarting anything; the value here is the deployment
190
+ * baseline that override falls back to. See {@link AcpIdleRelease}.
191
+ */
192
+ idleTimeoutMs?: number;
181
193
  /**
182
194
  * LEGACY: the old conversation-route window, kept so an existing
183
195
  * `cordis.patch.yml` still says what it meant. Folded into
@@ -473,6 +485,17 @@ export type AcpEvent = {
473
485
  sessionId: string;
474
486
  turnId: string;
475
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;
476
499
  } | {
477
500
  type: 'turn/end';
478
501
  sessionId: string;
@@ -511,6 +534,13 @@ export type PermissionOutcome = {
511
534
  } | {
512
535
  outcome: 'cancelled';
513
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
+ }
514
544
  /** Derived session snapshot for replay/reconnect (source of truth stays in the transcript). */
515
545
  export interface SessionView {
516
546
  sessionId: string;
@@ -542,6 +572,8 @@ export interface SessionView {
542
572
  currency: string;
543
573
  };
544
574
  };
575
+ /** Measurable burst aggregate over this session's turns; absent until measured. */
576
+ burst?: SessionBurst;
545
577
  running: boolean;
546
578
  closed: boolean;
547
579
  /**
@@ -550,6 +582,14 @@ export interface SessionView {
550
582
  * recover") when the agent declares the `loadSession` capability.
551
583
  */
552
584
  released?: boolean;
585
+ /**
586
+ * The agent-side session has history while the local buffer is empty: the
587
+ * record was restored from disk after a host restart (the `session/load`
588
+ * replay has not landed), or a resume cleared the buffer ahead of the
589
+ * replay. The ACP view then says history replays on restore instead of
590
+ * claiming "nothing yet".
591
+ */
592
+ hasHistory?: boolean;
553
593
  /** Highest transcript seq included in this snapshot (for gap detection). */
554
594
  seq: number;
555
595
  }
@@ -613,8 +653,38 @@ export interface ChatSessionView {
613
653
  * agent can never see.
614
654
  */
615
655
  availableCommands?: SessionView['availableCommands'];
656
+ /**
657
+ * Context occupancy the agent last reported (`usage_update`): prompt-side
658
+ * used/size, not request tokens. Absent until the agent reports one; the
659
+ * browser live-patches this row from the update frames (see AGENTS/ui.md).
660
+ */
661
+ usage?: SessionView['usage'];
662
+ /** Measurable burst aggregate over this session's turns; absent until measured. */
663
+ burst?: SessionView['burst'];
616
664
  /** The agent process died; the session awaits a `session/load` resume. */
617
665
  released?: boolean;
666
+ /** The agent is running a turn on this session right now. */
667
+ running?: boolean;
668
+ /**
669
+ * Whether this session's ACP agent can be forked (`session/fork`) into a
670
+ * child conversation, as the host computed it. The browser does not derive
671
+ * this itself — the capability is host knowledge (the agent's
672
+ * `initialize`), and the row is the one place it is delivered.
673
+ * `supported: false` carries the reason; no reason on `false` means the
674
+ * host half predates the field (treat as unsupported).
675
+ */
676
+ fork?: ChatSessionForkInfo;
677
+ }
678
+ /**
679
+ * The host's per-row verdict on forking one chat session's ACP session
680
+ * (`ChatSessionView.fork`). Computed from the agent's LAST KNOWN
681
+ * `sessionCapabilities.fork` (persisted with the model catalog) plus the
682
+ * session's own state — the browser never reads capability bits itself.
683
+ */
684
+ export interface ChatSessionForkInfo {
685
+ supported: boolean;
686
+ /** Why {@link supported} is false: the agent cannot fork, or the session cannot be forked right now. */
687
+ reason?: 'unsupported' | 'released' | 'busy';
618
688
  }
619
689
  export type ChatSessionsListResult = {
620
690
  sessions: ChatSessionView[];
@@ -770,6 +840,60 @@ export interface TagHaloSetPayload {
770
840
  }
771
841
  /** The approval deadline the settings page can edit, by field name. ONE window. */
772
842
  export type ApprovalDeadlineKey = 'permissionTimeoutMs';
843
+ /**
844
+ * The idle-release windows the settings page offers, in MILLISECONDS.
845
+ *
846
+ * ONE ladder, shared by both halves: the browser snaps its slider to these
847
+ * values and the host accepts only these values, so a window the slider can
848
+ * produce is always a window the host runs. The first rung is 1 minute (an
849
+ * aggressive release), the rest are 5-minute steps up to an hour.
850
+ */
851
+ export declare const IDLE_RELEASE_MS: readonly number[];
852
+ /**
853
+ * The built-in idle-release window for chat processes: 30 minutes.
854
+ *
855
+ * Lives beside the ladder it is a rung of, so the two cannot drift: a
856
+ * deployment that names no window of its own runs this, and the settings
857
+ * page's reset returns to it.
858
+ */
859
+ export declare const DEFAULT_IDLE_TIMEOUT_MS = 1800000;
860
+ /**
861
+ * The idle-release window for chat processes, as the settings page edits it.
862
+ *
863
+ * The window is the HOST default: an agent that names its own
864
+ * {@link AcpAgentDef.idleTimeoutMs} keeps it, and a probe-only process (one
865
+ * that never hosted a session) still honors the short probe window — see
866
+ * `AGENTS/protocol.md` §5.
867
+ */
868
+ export interface AcpIdleRelease {
869
+ /** The window in force, in ms. */
870
+ idleTimeoutMs: number;
871
+ /**
872
+ * The window a RESET returns to: the deployment baseline when the config
873
+ * file names one, else the built-in default (30 minutes).
874
+ */
875
+ defaultIdleTimeoutMs: number;
876
+ /**
877
+ * The window `cordis.patch.yml` names, when it names one. Reported only
878
+ * when the file actually names one: the settings card must not say "the
879
+ * file asks for 30 minutes" about a file that says nothing.
880
+ */
881
+ configuredIdleTimeoutMs?: number;
882
+ }
883
+ /** `ui/idle-release`: read the idle-release window in force. */
884
+ export type IdleReleaseResult = {
885
+ idleRelease?: AcpIdleRelease;
886
+ };
887
+ /**
888
+ * `ui/idle-release-set`: write the idle-release window, or drop the persisted
889
+ * override with `reset` so the built-in default applies again.
890
+ */
891
+ export interface IdleReleaseSetPayload {
892
+ /** The window to run, in ms; omitted when `reset` is true. */
893
+ idleTimeoutMs?: number;
894
+ /** Drop the persisted override so the built-in default applies again. */
895
+ reset?: boolean;
896
+ }
773
897
  /**
774
898
  * What an EXPIRED approval deadline does. Three modes, and only three — an
775
899
  * unanswered ask either gets answered for you, keeps waiting, or is refused.
@@ -868,6 +992,27 @@ export interface ChatConfigSetPayload {
868
992
  value: string | boolean;
869
993
  agentId?: string;
870
994
  }
995
+ /**
996
+ * `session/chat-fork-adopt`: bind the DSH chat session a host `sessions.fork`
997
+ * just created to the SAME agent process, through the agent's own
998
+ * `session/fork` of the parent conversation's ACP session — the second half of
999
+ * the two-stage branch (host builds the DSH tree first, the plugin then adopts
1000
+ * the ACP side; see the NTR message-actions plan, §D5/D7).
1001
+ *
1002
+ * On success the child conversation's next message reaches an agent that
1003
+ * actually remembers the parent context. On failure NOTHING is bound: the
1004
+ * child stays an ordinary copied-history conversation and its first message
1005
+ * starts a fresh `session/new` (today's behavior).
1006
+ */
1007
+ export interface ChatForkAdoptPayload {
1008
+ dshSessionId: string;
1009
+ parentDshSessionId: string;
1010
+ }
1011
+ /** `session/chat-fork-adopt` success: the adopted ACP session's ids. */
1012
+ export type ChatForkAdoptResult = {
1013
+ sessionId: string;
1014
+ acpSessionId: string;
1015
+ };
871
1016
  /** Host-detected agent presence and tested defaults. */
872
1017
  export interface DetectedAgentInfo {
873
1018
  id: string;
@@ -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.1",
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",
@@ -72,6 +72,7 @@
72
72
  "@deepseek-ai/dsh-client-connection",
73
73
  "@deepseek-ai/dsh-client-ui-conversation",
74
74
  "@deepseek-ai/dsh-client-ui-layout",
75
+ "@deepseek-ai/dsh-client-ui-primitives",
75
76
  "@deepseek-ai/dsh-client-ui-settings",
76
77
  "@deepseek-ai/dsh-client-ui-sidebar",
77
78
  "@deepseek-ai/dsh-client-ui-slots"