dsh-ntr 0.3.0 → 0.3.2
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 +1 -1
- package/README.md +1 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +614 -25
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/branch-action.js +133 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +238 -15
- package/lib/client/panel.js +127 -30
- package/lib/client/released-banner.js +66 -0
- package/lib/client/session-controls.js +38 -13
- package/lib/client/slash-isolation.js +2 -2
- package/lib/client/store.js +403 -16
- package/lib/client.js +1267 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +54 -1
- package/lib/index.js +7 -0
- package/lib/protocol.js +19 -0
- package/lib/types/bridge.d.ts +230 -7
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/branch-action.d.ts +40 -0
- package/lib/types/client/composer-mode.d.ts +28 -0
- package/lib/types/client/context-meter.d.ts +51 -0
- package/lib/types/client/released-banner.d.ts +34 -0
- package/lib/types/client/slash-isolation.d.ts +1 -1
- package/lib/types/client/store.d.ts +131 -1
- package/lib/types/impl.d.ts +41 -1
- package/lib/types/protocol.d.ts +130 -0
- package/lib/types/update.d.ts +7 -4
- package/lib/update.js +25 -10
- package/package.json +2 -1
- package/src/bridge.ts +645 -27
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/branch-action.tsx +160 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +265 -14
- package/src/client/panel.tsx +229 -41
- package/src/client/primitives.d.ts +27 -0
- package/src/client/released-banner.tsx +97 -0
- package/src/client/session-controls.tsx +34 -11
- package/src/client/slash-isolation.ts +2 -1
- package/src/client/store.ts +463 -20
- package/src/impl.ts +84 -2
- package/src/index.ts +8 -0
- package/src/protocol.ts +129 -0
- package/src/update.ts +21 -8
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer context-occupancy ring at `conversation.composer.dock` — the bottom
|
|
3
|
+
* info row below the input card, where the host also renders its ContextMeter.
|
|
4
|
+
*
|
|
5
|
+
* WHY A PLUGIN-OWNED RING: the host's meter reads its `contextPressure`
|
|
6
|
+
* projection, folded from DSH provider-usage samples plus a route capacity. An
|
|
7
|
+
* ACP conversation produces neither — the agent owns the request and the host
|
|
8
|
+
* never sees its prompt — so the native meter renders nothing there. This ring
|
|
9
|
+
* reports the agent's OWN `usage_update` used/size instead: occupancy only. ACP
|
|
10
|
+
* declares no system/tools/messages split, so no breakdown is invented.
|
|
11
|
+
*
|
|
12
|
+
* Registered for every conversation and gating itself: it renders only for an
|
|
13
|
+
* ACP conversation whose session row carries usage. The row comes from
|
|
14
|
+
* `session/chat-list`; the store live-patches it from the coalesced
|
|
15
|
+
* `usage_update` frames (see `AcpStore.flushUpdates`).
|
|
16
|
+
*/
|
|
17
|
+
import type { AcpSessionHook } from './acp-ui.tsx';
|
|
18
|
+
import type { AcpStore, UiState } from './store.ts';
|
|
19
|
+
import type { SessionView } from '../protocol.ts';
|
|
20
|
+
/** Props of the composer entry (injected face + the frame's standard kit). */
|
|
21
|
+
export interface AcpContextMeterProps {
|
|
22
|
+
/** Injected face: the chat session id this entry belongs to. */
|
|
23
|
+
sessionId: string;
|
|
24
|
+
/** Framework-bound conversation snapshot hook (`SessionStandardProps.useSession`). */
|
|
25
|
+
useSession: AcpSessionHook;
|
|
26
|
+
acp: AcpStore;
|
|
27
|
+
}
|
|
28
|
+
export interface ContextOccupancy {
|
|
29
|
+
/** Rounded occupancy percentage, clamped to 0..100. */
|
|
30
|
+
percent: number;
|
|
31
|
+
used: number;
|
|
32
|
+
size: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The sample this conversation's ring should show, or `undefined` when the
|
|
36
|
+
* entry must render nothing. An explicit `acpMode === false` outranks a stale
|
|
37
|
+
* `chatSessions` row: the conversation's preset left NTR while its ACP session
|
|
38
|
+
* was still alive, and the ring must leave with it (same precedence as the
|
|
39
|
+
* agent tag's `agentTagVisible`). While the verdict is unknown, a LIVE row is
|
|
40
|
+
* proof; a PARKED one is not — it can outlive the preset switch, and the ring
|
|
41
|
+
* must not appear on an ordinary conversation during the answer window.
|
|
42
|
+
*/
|
|
43
|
+
export declare function acpMeterUsage(ui: Pick<UiState, 'acpMode' | 'chatSessions' | 'chatSessionsFor'>, dshSessionId: string): SessionView['usage'] | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Bounded display occupancy from one agent sample. `undefined` (no meter) for
|
|
46
|
+
* an absent / malformed / zero-capacity sample rather than a made-up 0%.
|
|
47
|
+
*/
|
|
48
|
+
export declare function contextOccupancy(usage: SessionView['usage'] | undefined): ContextOccupancy | undefined;
|
|
49
|
+
/** Compact token count for the tooltip, in the host meter's K/M shape. */
|
|
50
|
+
export declare function compactTokenCount(value: number): string;
|
|
51
|
+
export declare function AcpContextMeter({ acp, sessionId, useSession }: AcpContextMeterProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The conversation's released-session banner: one row above the composer
|
|
3
|
+
* (`conversation.input.dock`) saying the agent process behind this
|
|
4
|
+
* conversation has been released, with the explicit resume button.
|
|
5
|
+
*
|
|
6
|
+
* WHY A BANNER: releasing a process is deliberately silent (constraint 19 —
|
|
7
|
+
* an idle teardown is the expected lifecycle, no `agent/notice`), and the
|
|
8
|
+
* only other trace was the NTR view tab's status badge — a surface most
|
|
9
|
+
* users never open. The "your next message resumes it" promise is real but
|
|
10
|
+
* unverifiable from the chat itself, so the banner states it here and the
|
|
11
|
+
* button turns it into a gesture (`session/chat-resume`), the explicit form
|
|
12
|
+
* of the same recovery the next prompt performs lazily.
|
|
13
|
+
*
|
|
14
|
+
* The gate is a pure function ({@link releasedBannerVisible}) — the seam the
|
|
15
|
+
* composer's other visibility decisions use, so the rules are testable with
|
|
16
|
+
* no DOM. Like the agent tag and the context ring, an explicit
|
|
17
|
+
* `acpMode === false` outranks a lingering `chatSessions` row: a conversation
|
|
18
|
+
* that left NTR mode must not keep claiming a parked NTR process.
|
|
19
|
+
*/
|
|
20
|
+
import type { AcpStore, UiState } from './store.ts';
|
|
21
|
+
export interface ReleasedBannerProps {
|
|
22
|
+
/** Injected face: the store plus the chat session id this banner belongs to. */
|
|
23
|
+
acp: AcpStore;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the banner should render: this conversation's ACP session records
|
|
28
|
+
* are all released — the process is parked (idle release or crash) and
|
|
29
|
+
* nothing is live to serve the next message yet. An empty list (no agent has
|
|
30
|
+
* run here) and a live row anywhere both mean "nothing to recover", so the
|
|
31
|
+
* banner stays out of the way.
|
|
32
|
+
*/
|
|
33
|
+
export declare function releasedBannerVisible(ui: Pick<UiState, 'acpMode' | 'chatSessions' | 'chatSessionsFor'>, dshSessionId: string): boolean;
|
|
34
|
+
export declare function ReleasedBanner({ acp, sessionId }: ReleasedBannerProps): import("react").JSX.Element | null;
|
|
@@ -107,7 +107,7 @@ export interface SlashIsolation {
|
|
|
107
107
|
* caller reads as "not ACP" (fail-open: an ordinary conversation must never
|
|
108
108
|
* lose its native commands to a guess).
|
|
109
109
|
*/
|
|
110
|
-
export declare function slashAcpVerdict(snapshot: AcpSessionsSnapshot | undefined, sessionId: string, verdict: (sessionId: string) => boolean | undefined): boolean | undefined;
|
|
110
|
+
export declare function slashAcpVerdict(snapshot: AcpSessionsSnapshot | undefined, sessionId: string, verdict: (sessionId: string) => boolean | undefined, currentSessionId?: string): boolean | undefined;
|
|
111
111
|
/**
|
|
112
112
|
* The agent's commands as menu rows, ranked the way the host ranks its own
|
|
113
113
|
* menu: the query matches as a case-insensitive ordered subsequence of the
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Deliberately framework-free: React binds through `useSyncExternalStore`.
|
|
7
7
|
*/
|
|
8
|
-
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, SessionView } from '../protocol.ts';
|
|
8
|
+
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpIdleRelease, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, SessionView } from '../protocol.ts';
|
|
9
9
|
/** One slash command the ACP agent declared, whole row (the menu draws name, description and `input.hint`). */
|
|
10
10
|
export type AgentCommand = NonNullable<ChatSessionView['availableCommands']>[number];
|
|
11
11
|
/** The unary RPC face (narrowed `ctx.connection.rpc.call`). */
|
|
@@ -117,6 +117,33 @@ export declare const PERMISSION_DEADLINE_EXPIRED = "expired";
|
|
|
117
117
|
* settings-panel ledger.
|
|
118
118
|
*/
|
|
119
119
|
export declare function permissionsForSession(permissions: readonly PendingPermission[], dshSessionId: string): PendingPermission[];
|
|
120
|
+
/** The branch button's six states (the NTR message-actions plan, §D4). */
|
|
121
|
+
export type ChatForkState = 'ready' | 'unsupported' | 'released' | 'busy' | 'notTail' | 'hostUnavailable';
|
|
122
|
+
/**
|
|
123
|
+
* The branch click's outcome: the child DSH session id on success, or one
|
|
124
|
+
* actionable failure line (also flashed into the notice banner — the line is
|
|
125
|
+
* the conversation's local copy of the same failure).
|
|
126
|
+
*/
|
|
127
|
+
export type ChatForkOutcome = {
|
|
128
|
+
childId: string;
|
|
129
|
+
} | {
|
|
130
|
+
failure: string;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The branch button's state for one conversation, in the precedence it renders:
|
|
134
|
+
* the host service missing beats everything (nothing can work), then the
|
|
135
|
+
* agent's fork capability, then the session's own state (a parked session
|
|
136
|
+
* before a running one), then the DOM's tail verdict, and only then ready.
|
|
137
|
+
*
|
|
138
|
+
* Every input is a fact someone else owns — the row's `fork` verdict is the
|
|
139
|
+
* host's, `hostFork` is the structural probe of the runtime's `sessions.fork`,
|
|
140
|
+
* `isTail` is read from the host's own `[data-turn-tail]` marker, and
|
|
141
|
+
* `liveRunning` is the SSE-driven running flag (the row's own `running` only
|
|
142
|
+
* refreshes on a chat-list read, so it would answer last turn). `undefined`
|
|
143
|
+
* facts degrade CLOSED: no row and no `fork` verdict both read as not-ready,
|
|
144
|
+
* never as a click-through.
|
|
145
|
+
*/
|
|
146
|
+
export declare function forkStateOf(row: ChatSessionView | undefined, hostFork: boolean, isTail: boolean, liveRunning?: boolean): ChatForkState;
|
|
120
147
|
/**
|
|
121
148
|
* Compact countdown for one ask, or `undefined` when there is nothing to count
|
|
122
149
|
* down (`wait` mode, or a frame without a deadline). Pure so the formatting is
|
|
@@ -141,6 +168,20 @@ export interface ChatTranscript {
|
|
|
141
168
|
/** Highest folded transcript seq (gap detection for the live stream). */
|
|
142
169
|
seq: number;
|
|
143
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* One line of the settings page's ACP runtime log. Agent-side notices and
|
|
173
|
+
* session-release reasons land here (bounded, append order oldest→newest);
|
|
174
|
+
* failures of the settings page's OWN actions stay in {@link UiState.notice}.
|
|
175
|
+
*/
|
|
176
|
+
export interface AcpLogEntry {
|
|
177
|
+
id: number;
|
|
178
|
+
at: number;
|
|
179
|
+
level: 'info' | 'error';
|
|
180
|
+
text: string;
|
|
181
|
+
agentId?: string;
|
|
182
|
+
}
|
|
183
|
+
/** Cap on retained runtime-log lines; older entries fall off the front. */
|
|
184
|
+
export declare const LOG_MAX_ENTRIES = 200;
|
|
144
185
|
export interface UiState {
|
|
145
186
|
open: boolean;
|
|
146
187
|
/**
|
|
@@ -162,7 +203,15 @@ export interface UiState {
|
|
|
162
203
|
blocks: Block[];
|
|
163
204
|
permissions: PendingPermission[];
|
|
164
205
|
busy: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* The settings page's one-line banner for its OWN action failures. Agent-side
|
|
208
|
+
* runtime messages do not land here; they append to {@link logs} instead.
|
|
209
|
+
*/
|
|
165
210
|
notice?: string;
|
|
211
|
+
/** Agent-side runtime log, append order oldest→newest, capped at {@link LOG_MAX_ENTRIES}. */
|
|
212
|
+
logs: AcpLogEntry[];
|
|
213
|
+
/** Lines appended since the user last opened the log; reset by {@link AcpStore.markLogsRead}. */
|
|
214
|
+
logUnread: number;
|
|
166
215
|
/**
|
|
167
216
|
* The plugin's OWN default ACP agent (the panel's ★ gesture) — which agent a
|
|
168
217
|
* new ACP-mode conversation starts on. NOT the deployment default model: that
|
|
@@ -218,6 +267,12 @@ export interface UiState {
|
|
|
218
267
|
* host answer (returned by the same route) is the authority.
|
|
219
268
|
*/
|
|
220
269
|
approvalDeadlines?: AcpApprovalDeadlines;
|
|
270
|
+
/**
|
|
271
|
+
* The idle-release window in force, as the settings page edits it.
|
|
272
|
+
* Absent on a host half from before the tab existed, which hides the
|
|
273
|
+
* tab rather than showing a number this browser invented.
|
|
274
|
+
*/
|
|
275
|
+
idleRelease?: AcpIdleRelease;
|
|
221
276
|
/**
|
|
222
277
|
* Per-chat-session "does this conversation run the ACP preset" verdicts, as
|
|
223
278
|
* reported by `session/chat-list` (host-read from the session log). The
|
|
@@ -339,11 +394,20 @@ export declare class AcpStore {
|
|
|
339
394
|
private state;
|
|
340
395
|
private readonly listeners;
|
|
341
396
|
private source?;
|
|
397
|
+
/** Monotonic id source for {@link AcpLogEntry}s (never reset by `clearLogs`). */
|
|
398
|
+
private logSeq;
|
|
342
399
|
private readonly lastSeq;
|
|
343
400
|
/** Sessions with a finalized turn (dedups the cancel-fallback + late real turn/end pair). */
|
|
344
401
|
private readonly openTurns;
|
|
345
402
|
/** In-flight `session/chat-list` reads, keyed by chat session id. */
|
|
346
403
|
private readonly chatSessionReads;
|
|
404
|
+
/**
|
|
405
|
+
* ACP session ids whose chat-list row was requested mid-turn (one read each).
|
|
406
|
+
* A conversation's first ACP session does not exist when the composer's
|
|
407
|
+
* mount/running read fires, so its first `usage_update` would find no row to
|
|
408
|
+
* patch; `turn/start` asks again once the host has materialized the session.
|
|
409
|
+
*/
|
|
410
|
+
private readonly chatSessionRowReads;
|
|
347
411
|
/**
|
|
348
412
|
* Chat sessions whose list has answered at least once in this page. The
|
|
349
413
|
* FIRST answer only seeds the lock; a lock appearing on a later answer is a
|
|
@@ -407,6 +471,11 @@ export declare class AcpStore {
|
|
|
407
471
|
subscribe(listener: Listener): () => void;
|
|
408
472
|
getSnapshot(): UiState;
|
|
409
473
|
private mutate;
|
|
474
|
+
/**
|
|
475
|
+
* Build the log patch for one line. Does NOT mutate: the caller merges it
|
|
476
|
+
* into the single `mutate` call it was already making for the same event.
|
|
477
|
+
*/
|
|
478
|
+
private logPatch;
|
|
410
479
|
/**
|
|
411
480
|
* Live `session/update` frames waiting to be folded. Agents stream at token
|
|
412
481
|
* granularity — hundreds of frames a second with usage ticks interleaved —
|
|
@@ -572,6 +641,13 @@ export declare class AcpStore {
|
|
|
572
641
|
* being the one authorized exception. Same no-op-when-unchanged discipline as
|
|
573
642
|
* the other accessors, so a frame restating the held verdict costs no
|
|
574
643
|
* listener churn.
|
|
644
|
+
*
|
|
645
|
+
* The seat gate (`syncAcpMode`) is the SECOND caller: it folds the runtime
|
|
646
|
+
* row's own `agentPreset` — the same session-log source the frames announce —
|
|
647
|
+
* through here, in both directions. That hands the row the frame's authority
|
|
648
|
+
* on purpose (one provenance, one rank): a stale projection is corrected by
|
|
649
|
+
* the gate's next evaluation with fresher row data, and only HOST READS are
|
|
650
|
+
* barred from regressing what the runtime itself already said.
|
|
575
651
|
*/
|
|
576
652
|
noteAcpMode(dshSessionId: string, acpMode: boolean): void;
|
|
577
653
|
/**
|
|
@@ -614,6 +690,36 @@ export declare class AcpStore {
|
|
|
614
690
|
* reports (a rejected switch must not leave an optimistic lie on screen).
|
|
615
691
|
*/
|
|
616
692
|
setChatSessionMode(dshSessionId: string, modeId: string, agentId?: string): Promise<void>;
|
|
693
|
+
/**
|
|
694
|
+
* Bring the conversation's released ACP session back on a fresh process —
|
|
695
|
+
* the released banner's explicit form of the "next message resumes it"
|
|
696
|
+
* promise (`session/chat-resume`). A failure is reported through the notice
|
|
697
|
+
* AND rethrown (the `respondPermission` contract), so the banner can pin
|
|
698
|
+
* the message beside the button the user just pressed; the binding is
|
|
699
|
+
* re-read either way so the row follows the host's real answer.
|
|
700
|
+
*/
|
|
701
|
+
resumeChatSession(dshSessionId: string): Promise<void>;
|
|
702
|
+
/**
|
|
703
|
+
* Branch one conversation (the NTR branch button's click): the fixed
|
|
704
|
+
* two-stage order — the HOST forks the DSH session tree first (copied
|
|
705
|
+
* history, `(1)` title, workspace), then the plugin adopts the ACP side via
|
|
706
|
+
* `session/chat-fork-adopt`, which runs the agent's own `session/fork` so
|
|
707
|
+
* the child actually remembers the parent context. Host first is structural:
|
|
708
|
+
* ACP fork has no cut-point parameter and nothing to return to if the host
|
|
709
|
+
* half failed second (see the plan's §D7).
|
|
710
|
+
*
|
|
711
|
+
* Resolves `{ childId }` on success, or `{ failure }` with one actionable
|
|
712
|
+
* line — the reason is ALSO flashed into the notice banner (a user-action
|
|
713
|
+
* failure, so the banner is its surface), and the local line is what the
|
|
714
|
+
* conversation renders beside the button (the user is not on the settings
|
|
715
|
+
* page). A failed adopt leaves the child an ordinary copied-history
|
|
716
|
+
* conversation (its first message starts a fresh `session/new` — today's
|
|
717
|
+
* behavior), never a half-bound one, and NOTHING is opened.
|
|
718
|
+
*
|
|
719
|
+
* `forkHost` is the runtime's `sessions.fork` face, resolved structurally by
|
|
720
|
+
* the entry and injected here: the store stays transport-free.
|
|
721
|
+
*/
|
|
722
|
+
forkChatSession(dshSessionId: string, forkHost: (sessionId: string) => Promise<unknown>): Promise<ChatForkOutcome>;
|
|
617
723
|
/**
|
|
618
724
|
* Start (or refresh) observation of every ACP transcript behind one DSH chat
|
|
619
725
|
* session. Blocks are rebuilt from scratch — the host buffer is the record,
|
|
@@ -673,6 +779,20 @@ export declare class AcpStore {
|
|
|
673
779
|
setApprovalTimeoutMode(mode: PermissionTimeoutMode): Promise<void>;
|
|
674
780
|
/** Drop the persisted override: the config file (or the built-in default) decides again. */
|
|
675
781
|
resetApprovalDeadlines(): Promise<void>;
|
|
782
|
+
/**
|
|
783
|
+
* Write the idle-release window (in ms).
|
|
784
|
+
*
|
|
785
|
+
* The host answer — the same route returns the window now in force — is
|
|
786
|
+
* the authority, so an optimistic write that the host trims self-corrects
|
|
787
|
+
* instead of drifting from what runs.
|
|
788
|
+
*/
|
|
789
|
+
setIdleRelease(idleTimeoutMs: number): Promise<void>;
|
|
790
|
+
/**
|
|
791
|
+
* Drop the persisted override: the config file (or the built-in default)
|
|
792
|
+
* decides again.
|
|
793
|
+
*/
|
|
794
|
+
resetIdleRelease(): Promise<void>;
|
|
795
|
+
private writeIdleRelease;
|
|
676
796
|
private writeApprovalDeadlines;
|
|
677
797
|
/**
|
|
678
798
|
* Bind one DSH chat session to an ACP agent. This is the plugin's own
|
|
@@ -713,6 +833,10 @@ export declare class AcpStore {
|
|
|
713
833
|
*/
|
|
714
834
|
refreshLocale(): void;
|
|
715
835
|
clearNotice(): void;
|
|
836
|
+
/** Drop every retained runtime-log line (the unread counter restarts too). */
|
|
837
|
+
clearLogs(): void;
|
|
838
|
+
/** The user looked at the log: clear the unread badge. */
|
|
839
|
+
markLogsRead(): void;
|
|
716
840
|
private flashError;
|
|
717
841
|
private applyEvent;
|
|
718
842
|
}
|
|
@@ -732,4 +856,10 @@ export declare class AcpStore {
|
|
|
732
856
|
export declare function foldUpdate(blocks: Block[], update: Record<string, unknown>): boolean;
|
|
733
857
|
/** Notifications that update the SessionView snapshot instead of the block stream. */
|
|
734
858
|
export declare const STATE_ONLY_UPDATES: ReadonlySet<string>;
|
|
859
|
+
/**
|
|
860
|
+
* Context occupancy carried by one raw `usage_update` frame, in the shape the
|
|
861
|
+
* session snapshots already use. A malformed frame is ignored (the last good
|
|
862
|
+
* sample stays on screen) instead of zeroing the meter.
|
|
863
|
+
*/
|
|
864
|
+
export declare function usageFromUpdate(update: Record<string, unknown>): SessionView['usage'] | undefined;
|
|
735
865
|
export {};
|
package/lib/types/impl.d.ts
CHANGED
|
@@ -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
|
|
@@ -698,6 +707,19 @@ export interface AcpManager {
|
|
|
698
707
|
* applies again. Applies to the running bridges without a restart.
|
|
699
708
|
*/
|
|
700
709
|
setApprovalDeadlines?(patch: ApprovalDeadlinePatch, reset?: boolean): Promise<void>;
|
|
710
|
+
/**
|
|
711
|
+
* The idle-release window in force for chat processes, plus the built-in
|
|
712
|
+
* default a reset returns to. `undefined` on a host half from before the
|
|
713
|
+
* card existed, which hides the settings tab instead of showing a number
|
|
714
|
+
* this browser invented.
|
|
715
|
+
*/
|
|
716
|
+
idleRelease?(): AcpIdleRelease | undefined;
|
|
717
|
+
/**
|
|
718
|
+
* Write the idle-release window (in ms), or drop the persisted
|
|
719
|
+
* override with `reset` so the built-in default applies again. Applies to
|
|
720
|
+
* the running bridges without a restart.
|
|
721
|
+
*/
|
|
722
|
+
setIdleRelease?(idleTimeoutMs: number | undefined, reset?: boolean): Promise<void>;
|
|
701
723
|
detectAgents?(): Promise<DetectedAgentInfo[]>;
|
|
702
724
|
saveAgent?(def: AcpAgentDef): Promise<void>;
|
|
703
725
|
deleteAgent?(agentId: string): Promise<void>;
|
|
@@ -770,6 +792,24 @@ export interface AcpManager {
|
|
|
770
792
|
* without it the live (non-released) session is picked.
|
|
771
793
|
*/
|
|
772
794
|
setChatSessionMode?(dshSessionId: string, modeId: string, agentId?: string): Promise<void>;
|
|
795
|
+
/**
|
|
796
|
+
* Bring one chat conversation's released session back on a fresh process on
|
|
797
|
+
* the user's explicit gesture (`session/chat-resume`, the released-session
|
|
798
|
+
* banner's button). Same recovery the next prompt performs lazily, minus
|
|
799
|
+
* the create path; no-op when the session is already live.
|
|
800
|
+
*/
|
|
801
|
+
resumeChatSession?(dshSessionId: string): Promise<void>;
|
|
802
|
+
/**
|
|
803
|
+
* Adopt the ACP side of a branched conversation (`session/chat-fork-adopt`):
|
|
804
|
+
* the browser has already asked the HOST to fork the DSH session tree, and
|
|
805
|
+
* this runs the agent's own `session/fork` of the parent's live ACP session,
|
|
806
|
+
* then binds the child to it. All-or-nothing — nothing is bound on failure,
|
|
807
|
+
* so the child stays an ordinary copied-history conversation.
|
|
808
|
+
*/
|
|
809
|
+
adoptForkedSession?(childDshSessionId: string, parentDshSessionId: string): Promise<{
|
|
810
|
+
sessionId: string;
|
|
811
|
+
acpSessionId: string;
|
|
812
|
+
}>;
|
|
773
813
|
/**
|
|
774
814
|
* Point one DSH chat conversation at an ACP agent — the plugin's own
|
|
775
815
|
* conversation↔agent mapping, never a DSH model route (see
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -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
|
|
@@ -321,6 +333,11 @@ export interface AgentView {
|
|
|
321
333
|
*/
|
|
322
334
|
idleTimeoutMs?: number;
|
|
323
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Why the host released a session's agent process. The browser turns this into
|
|
338
|
+
* one runtime-log line per cause; `idle` stays silent (expected lifecycle).
|
|
339
|
+
*/
|
|
340
|
+
export type AcpReleaseCause = 'idle' | 'workspace' | 'resume-failed' | 'stalled' | 'died';
|
|
324
341
|
export type AcpEvent = {
|
|
325
342
|
type: 'agent/status';
|
|
326
343
|
agentId: string;
|
|
@@ -350,6 +367,8 @@ export type AcpEvent = {
|
|
|
350
367
|
sessionId: string;
|
|
351
368
|
session: SessionView;
|
|
352
369
|
reason: string;
|
|
370
|
+
/** Host's release-cause classification; absent on an older host half. */
|
|
371
|
+
cause?: AcpReleaseCause;
|
|
353
372
|
} | {
|
|
354
373
|
/**
|
|
355
374
|
* The session was resumed through `session/load` on a fresh agent
|
|
@@ -543,6 +562,14 @@ export interface SessionView {
|
|
|
543
562
|
* recover") when the agent declares the `loadSession` capability.
|
|
544
563
|
*/
|
|
545
564
|
released?: boolean;
|
|
565
|
+
/**
|
|
566
|
+
* The agent-side session has history while the local buffer is empty: the
|
|
567
|
+
* record was restored from disk after a host restart (the `session/load`
|
|
568
|
+
* replay has not landed), or a resume cleared the buffer ahead of the
|
|
569
|
+
* replay. The ACP view then says history replays on restore instead of
|
|
570
|
+
* claiming "nothing yet".
|
|
571
|
+
*/
|
|
572
|
+
hasHistory?: boolean;
|
|
546
573
|
/** Highest transcript seq included in this snapshot (for gap detection). */
|
|
547
574
|
seq: number;
|
|
548
575
|
}
|
|
@@ -606,8 +633,36 @@ export interface ChatSessionView {
|
|
|
606
633
|
* agent can never see.
|
|
607
634
|
*/
|
|
608
635
|
availableCommands?: SessionView['availableCommands'];
|
|
636
|
+
/**
|
|
637
|
+
* Context occupancy the agent last reported (`usage_update`): prompt-side
|
|
638
|
+
* used/size, not request tokens. Absent until the agent reports one; the
|
|
639
|
+
* browser live-patches this row from the update frames (see AGENTS/ui.md).
|
|
640
|
+
*/
|
|
641
|
+
usage?: SessionView['usage'];
|
|
609
642
|
/** The agent process died; the session awaits a `session/load` resume. */
|
|
610
643
|
released?: boolean;
|
|
644
|
+
/** The agent is running a turn on this session right now. */
|
|
645
|
+
running?: boolean;
|
|
646
|
+
/**
|
|
647
|
+
* Whether this session's ACP agent can be forked (`session/fork`) into a
|
|
648
|
+
* child conversation, as the host computed it. The browser does not derive
|
|
649
|
+
* this itself — the capability is host knowledge (the agent's
|
|
650
|
+
* `initialize`), and the row is the one place it is delivered.
|
|
651
|
+
* `supported: false` carries the reason; no reason on `false` means the
|
|
652
|
+
* host half predates the field (treat as unsupported).
|
|
653
|
+
*/
|
|
654
|
+
fork?: ChatSessionForkInfo;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* The host's per-row verdict on forking one chat session's ACP session
|
|
658
|
+
* (`ChatSessionView.fork`). Computed from the agent's LAST KNOWN
|
|
659
|
+
* `sessionCapabilities.fork` (persisted with the model catalog) plus the
|
|
660
|
+
* session's own state — the browser never reads capability bits itself.
|
|
661
|
+
*/
|
|
662
|
+
export interface ChatSessionForkInfo {
|
|
663
|
+
supported: boolean;
|
|
664
|
+
/** Why {@link supported} is false: the agent cannot fork, or the session cannot be forked right now. */
|
|
665
|
+
reason?: 'unsupported' | 'released' | 'busy';
|
|
611
666
|
}
|
|
612
667
|
export type ChatSessionsListResult = {
|
|
613
668
|
sessions: ChatSessionView[];
|
|
@@ -763,6 +818,60 @@ export interface TagHaloSetPayload {
|
|
|
763
818
|
}
|
|
764
819
|
/** The approval deadline the settings page can edit, by field name. ONE window. */
|
|
765
820
|
export type ApprovalDeadlineKey = 'permissionTimeoutMs';
|
|
821
|
+
/**
|
|
822
|
+
* The idle-release windows the settings page offers, in MILLISECONDS.
|
|
823
|
+
*
|
|
824
|
+
* ONE ladder, shared by both halves: the browser snaps its slider to these
|
|
825
|
+
* values and the host accepts only these values, so a window the slider can
|
|
826
|
+
* produce is always a window the host runs. The first rung is 1 minute (an
|
|
827
|
+
* aggressive release), the rest are 5-minute steps up to an hour.
|
|
828
|
+
*/
|
|
829
|
+
export declare const IDLE_RELEASE_MS: readonly number[];
|
|
830
|
+
/**
|
|
831
|
+
* The built-in idle-release window for chat processes: 30 minutes.
|
|
832
|
+
*
|
|
833
|
+
* Lives beside the ladder it is a rung of, so the two cannot drift: a
|
|
834
|
+
* deployment that names no window of its own runs this, and the settings
|
|
835
|
+
* page's reset returns to it.
|
|
836
|
+
*/
|
|
837
|
+
export declare const DEFAULT_IDLE_TIMEOUT_MS = 1800000;
|
|
838
|
+
/**
|
|
839
|
+
* The idle-release window for chat processes, as the settings page edits it.
|
|
840
|
+
*
|
|
841
|
+
* The window is the HOST default: an agent that names its own
|
|
842
|
+
* {@link AcpAgentDef.idleTimeoutMs} keeps it, and a probe-only process (one
|
|
843
|
+
* that never hosted a session) still honors the short probe window — see
|
|
844
|
+
* `AGENTS/protocol.md` §5.
|
|
845
|
+
*/
|
|
846
|
+
export interface AcpIdleRelease {
|
|
847
|
+
/** The window in force, in ms. */
|
|
848
|
+
idleTimeoutMs: number;
|
|
849
|
+
/**
|
|
850
|
+
* The window a RESET returns to: the deployment baseline when the config
|
|
851
|
+
* file names one, else the built-in default (30 minutes).
|
|
852
|
+
*/
|
|
853
|
+
defaultIdleTimeoutMs: number;
|
|
854
|
+
/**
|
|
855
|
+
* The window `cordis.patch.yml` names, when it names one. Reported only
|
|
856
|
+
* when the file actually names one: the settings card must not say "the
|
|
857
|
+
* file asks for 30 minutes" about a file that says nothing.
|
|
858
|
+
*/
|
|
859
|
+
configuredIdleTimeoutMs?: number;
|
|
860
|
+
}
|
|
861
|
+
/** `ui/idle-release`: read the idle-release window in force. */
|
|
862
|
+
export type IdleReleaseResult = {
|
|
863
|
+
idleRelease?: AcpIdleRelease;
|
|
864
|
+
};
|
|
865
|
+
/**
|
|
866
|
+
* `ui/idle-release-set`: write the idle-release window, or drop the persisted
|
|
867
|
+
* override with `reset` so the built-in default applies again.
|
|
868
|
+
*/
|
|
869
|
+
export interface IdleReleaseSetPayload {
|
|
870
|
+
/** The window to run, in ms; omitted when `reset` is true. */
|
|
871
|
+
idleTimeoutMs?: number;
|
|
872
|
+
/** Drop the persisted override so the built-in default applies again. */
|
|
873
|
+
reset?: boolean;
|
|
874
|
+
}
|
|
766
875
|
/**
|
|
767
876
|
* What an EXPIRED approval deadline does. Three modes, and only three — an
|
|
768
877
|
* unanswered ask either gets answered for you, keeps waiting, or is refused.
|
|
@@ -861,6 +970,27 @@ export interface ChatConfigSetPayload {
|
|
|
861
970
|
value: string | boolean;
|
|
862
971
|
agentId?: string;
|
|
863
972
|
}
|
|
973
|
+
/**
|
|
974
|
+
* `session/chat-fork-adopt`: bind the DSH chat session a host `sessions.fork`
|
|
975
|
+
* just created to the SAME agent process, through the agent's own
|
|
976
|
+
* `session/fork` of the parent conversation's ACP session — the second half of
|
|
977
|
+
* the two-stage branch (host builds the DSH tree first, the plugin then adopts
|
|
978
|
+
* the ACP side; see the NTR message-actions plan, §D5/D7).
|
|
979
|
+
*
|
|
980
|
+
* On success the child conversation's next message reaches an agent that
|
|
981
|
+
* actually remembers the parent context. On failure NOTHING is bound: the
|
|
982
|
+
* child stays an ordinary copied-history conversation and its first message
|
|
983
|
+
* starts a fresh `session/new` (today's behavior).
|
|
984
|
+
*/
|
|
985
|
+
export interface ChatForkAdoptPayload {
|
|
986
|
+
dshSessionId: string;
|
|
987
|
+
parentDshSessionId: string;
|
|
988
|
+
}
|
|
989
|
+
/** `session/chat-fork-adopt` success: the adopted ACP session's ids. */
|
|
990
|
+
export type ChatForkAdoptResult = {
|
|
991
|
+
sessionId: string;
|
|
992
|
+
acpSessionId: string;
|
|
993
|
+
};
|
|
864
994
|
/** Host-detected agent presence and tested defaults. */
|
|
865
995
|
export interface DetectedAgentInfo {
|
|
866
996
|
id: string;
|
package/lib/types/update.d.ts
CHANGED
|
@@ -77,10 +77,13 @@ export type BundleEligibility = {
|
|
|
77
77
|
};
|
|
78
78
|
/**
|
|
79
79
|
* Whether a `listBundles().source` value names a registry-resolvable version
|
|
80
|
-
* spec — the only install shape this updater may replace. The
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
80
|
+
* spec — the only install shape this updater may replace. The harness records
|
|
81
|
+
* a registry dependency as `<package>@<spec>` (`dependencySpec` in the plugin
|
|
82
|
+
* manager), so the spec half after the last `@` is what gets judged. The test
|
|
83
|
+
* is positive on purpose: link/file/git/workspace/URL/tarball/relative forms
|
|
84
|
+
* all carry a `:` / `/` / `\` or start with `.`, so they fall out without an
|
|
85
|
+
* ever-growing blacklist (a new source scheme must not become silently
|
|
86
|
+
* updatable).
|
|
84
87
|
*/
|
|
85
88
|
export declare function isRegistrySpec(source: string): boolean;
|
|
86
89
|
/**
|
package/lib/update.js
CHANGED
|
@@ -46,12 +46,26 @@ function describeFieldError(error) {
|
|
|
46
46
|
.filter((part) => typeof part === 'string' && part !== '');
|
|
47
47
|
return parts.length > 0 ? parts.join(': ') : 'the host reported a bundle error';
|
|
48
48
|
}
|
|
49
|
+
/** The version-spec half of a registry source: a range, wildcard, or dist-tag. */
|
|
50
|
+
function isRegistryRange(value) {
|
|
51
|
+
if (value === '')
|
|
52
|
+
return false;
|
|
53
|
+
const lowered = value.toLowerCase();
|
|
54
|
+
if (lowered === '*' || lowered === 'x')
|
|
55
|
+
return true;
|
|
56
|
+
if (REGISTRY_TAGS.has(lowered))
|
|
57
|
+
return true;
|
|
58
|
+
return REGISTRY_VERSION_SPEC.test(value);
|
|
59
|
+
}
|
|
49
60
|
/**
|
|
50
61
|
* Whether a `listBundles().source` value names a registry-resolvable version
|
|
51
|
-
* spec — the only install shape this updater may replace. The
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
62
|
+
* spec — the only install shape this updater may replace. The harness records
|
|
63
|
+
* a registry dependency as `<package>@<spec>` (`dependencySpec` in the plugin
|
|
64
|
+
* manager), so the spec half after the last `@` is what gets judged. The test
|
|
65
|
+
* is positive on purpose: link/file/git/workspace/URL/tarball/relative forms
|
|
66
|
+
* all carry a `:` / `/` / `\` or start with `.`, so they fall out without an
|
|
67
|
+
* ever-growing blacklist (a new source scheme must not become silently
|
|
68
|
+
* updatable).
|
|
55
69
|
*/
|
|
56
70
|
export function isRegistrySpec(source) {
|
|
57
71
|
const value = typeof source === 'string' ? source.trim() : '';
|
|
@@ -61,12 +75,13 @@ export function isRegistrySpec(source) {
|
|
|
61
75
|
return false;
|
|
62
76
|
if (value.includes(':') || value.includes('/') || value.includes('\\'))
|
|
63
77
|
return false;
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
const at = value.lastIndexOf('@');
|
|
79
|
+
if (at === -1)
|
|
80
|
+
return isRegistryRange(value);
|
|
81
|
+
const name = value.slice(0, at);
|
|
82
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name))
|
|
83
|
+
return false;
|
|
84
|
+
return isRegistryRange(value.slice(at + 1));
|
|
70
85
|
}
|
|
71
86
|
/**
|
|
72
87
|
* Compare two semver strings (build metadata ignored, prerelease precedence per
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-ntr",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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"
|