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.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/cordis.patch.yml +17 -1
- package/lib/bridge.js +2436 -82
- package/lib/client/acp-ui.js +103 -3
- package/lib/client/acp-view.js +24 -4
- package/lib/client/activity-pill.js +156 -0
- package/lib/client/branch-action.js +149 -0
- package/lib/client/composer-mode.js +48 -13
- package/lib/client/context-meter.js +86 -0
- package/lib/client/index.js +279 -16
- package/lib/client/model-seat.js +8 -2
- package/lib/client/panel.js +96 -29
- 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 +380 -13
- package/lib/client.js +1364 -70
- package/lib/client.js.map +1 -1
- package/lib/impl.js +1229 -8
- package/lib/index.js +34 -0
- package/lib/protocol.js +19 -0
- package/lib/tps.js +32 -0
- package/lib/types/bridge.d.ts +850 -18
- package/lib/types/client/acp-ui.d.ts +34 -2
- package/lib/types/client/acp-view.d.ts +13 -2
- package/lib/types/client/activity-pill.d.ts +81 -0
- package/lib/types/client/branch-action.d.ts +47 -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/model-seat.d.ts +21 -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 +108 -1
- package/lib/types/impl.d.ts +94 -7
- package/lib/types/protocol.d.ts +145 -0
- package/lib/types/tps.d.ts +17 -0
- package/package.json +2 -1
- package/src/bridge.ts +2754 -86
- package/src/client/acp-ui.tsx +123 -2
- package/src/client/acp-view.tsx +30 -5
- package/src/client/activity-pill.tsx +201 -0
- package/src/client/branch-action.tsx +178 -0
- package/src/client/composer-mode.tsx +59 -13
- package/src/client/context-meter.tsx +128 -0
- package/src/client/index.tsx +298 -15
- package/src/client/model-seat.tsx +28 -3
- package/src/client/panel.tsx +157 -40
- 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 +412 -14
- package/src/impl.ts +1251 -13
- package/src/index.ts +33 -0
- package/src/protocol.ts +145 -0
- package/src/tps.ts +40 -0
|
@@ -81,6 +81,26 @@ export interface AcpChatInjected {
|
|
|
81
81
|
acp: AcpStore;
|
|
82
82
|
/** Resolve the session's command/projection face; `undefined` before the session materializes. */
|
|
83
83
|
faceFor(sessionId: string): AcpSessionFace | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The runtime's `sessions.fork` face (the host's own branching primitive,
|
|
86
|
+
* resolved structurally per call); `undefined` when the host half lacks it —
|
|
87
|
+
* the branch button then renders disabled (`hostUnavailable`), never hidden.
|
|
88
|
+
*/
|
|
89
|
+
hostFork?: () => AcpHostForkFace | undefined;
|
|
90
|
+
/** The workspace's `openSession` face (the branch click's final step). */
|
|
91
|
+
openSession?: (sessionId: string) => void;
|
|
92
|
+
}
|
|
93
|
+
/** The slice of the client runtime's session service the branch button needs. */
|
|
94
|
+
export interface AcpHostForkFace {
|
|
95
|
+
/**
|
|
96
|
+
* Fork the DSH session tree at the latest completed turn. Resolves the CHILD
|
|
97
|
+
* session id; the host owns the cut-point snapping, history copy, `(1)`
|
|
98
|
+
* title and workspace attribution — the plugin rewrites none of it.
|
|
99
|
+
*/
|
|
100
|
+
fork(options: {
|
|
101
|
+
sessionId: string;
|
|
102
|
+
increaseTitle?: boolean;
|
|
103
|
+
}): Promise<unknown>;
|
|
84
104
|
}
|
|
85
105
|
/**
|
|
86
106
|
* The slice of the client runtime's `ConversationSnapshot` our composer entries
|
|
@@ -233,6 +253,13 @@ export interface AcpStagedSession {
|
|
|
233
253
|
/**
|
|
234
254
|
* Which session the composer is currently staged on.
|
|
235
255
|
*
|
|
256
|
+
* The renderer's own current scope binding is the first answer when the caller
|
|
257
|
+
* passes it (`currentSessionId`, read from `uiSession.adapter.current` in the
|
|
258
|
+
* browser entry): that is the exact session the host has selected, while the
|
|
259
|
+
* list can retain more than one row in a multi-view layout. Falling back to
|
|
260
|
+
* the list is still required — a host without that scope adapter, and the old
|
|
261
|
+
* snapshot shape below, have no better source.
|
|
262
|
+
*
|
|
236
263
|
* Old runtimes answer from `snapshot.current` (with `currentAddress` as the
|
|
237
264
|
* subagent marker). rc.2 has no `current`: the staged session is the row the
|
|
238
265
|
* main view retains (`retainedBy.mainView > 0`) — the exact rule
|
|
@@ -240,10 +267,15 @@ export interface AcpStagedSession {
|
|
|
240
267
|
* row with `origin === "subagent"` is an addressed subagent route. `undefined`
|
|
241
268
|
* when nothing is staged yet (the list has not loaded, or no row holds the main
|
|
242
269
|
* view): callers must stand down rather than guess.
|
|
270
|
+
*
|
|
271
|
+
* A current id the list has no row for is ALSO `undefined`: the selection may
|
|
272
|
+
* be a subagent address, and guessing "not a subagent" there would let the
|
|
273
|
+
* gate install ACP chrome on a route the plugin must not drive. The next list
|
|
274
|
+
* notification retries.
|
|
243
275
|
*/
|
|
244
|
-
export declare function stagedSessionOf(snapshot: AcpSessionsSnapshot | undefined): AcpStagedSession | undefined;
|
|
276
|
+
export declare function stagedSessionOf(snapshot: AcpSessionsSnapshot | undefined, currentSessionId?: string): AcpStagedSession | undefined;
|
|
245
277
|
/** Whether the staged session runs the ACP agent preset. */
|
|
246
|
-
export declare function stagedIsAcpMode(snapshot: AcpSessionsSnapshot | undefined, acpMode?: (sessionId: string) => boolean | undefined): boolean;
|
|
278
|
+
export declare function stagedIsAcpMode(snapshot: AcpSessionsSnapshot | undefined, acpMode?: (sessionId: string) => boolean | undefined, currentSessionId?: string): boolean;
|
|
247
279
|
/**
|
|
248
280
|
* The ACP-mode verdict a committed `agent-preset/selected` frame carries. The
|
|
249
281
|
* frame is `[sessionId, agentPreset]` and the runtime broadcasts it the moment
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* spawns. Everything it shows is host state.
|
|
13
13
|
*/
|
|
14
14
|
import type { AcpChatInjected } from './acp-ui.tsx';
|
|
15
|
-
import type { AgentView, ChatSessionView } from '../protocol.ts';
|
|
15
|
+
import type { AgentView, ChatSessionView, SessionView } from '../protocol.ts';
|
|
16
16
|
/** Props of the ACP view entry (injected face + the frame's session id). */
|
|
17
17
|
export interface AcpViewProps extends AcpChatInjected {
|
|
18
18
|
sessionId: string;
|
|
@@ -43,6 +43,17 @@ export declare function diffRows(oldText: string | null, newText: string): DiffR
|
|
|
43
43
|
* the new content — the degenerate case where there is no edit at all.
|
|
44
44
|
*/
|
|
45
45
|
export declare function isNewFileDiff(oldText: string | null, newText: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether an empty stream means "history exists but is not shown yet": the
|
|
48
|
+
* record was restored from disk after a host restart (the agent's replay has
|
|
49
|
+
* not landed), or a resume cleared the buffer ahead of the replay. The view
|
|
50
|
+
* then promises the replay instead of claiming the conversation never
|
|
51
|
+
* happened — "还没有消息" to someone whose chat plane still shows yesterday's
|
|
52
|
+
* turns is exactly the kind of contradiction this file's copy rules exist to
|
|
53
|
+
* prevent. In-memory released sessions keep their buffer, so this only fires
|
|
54
|
+
* when the buffer really is absent.
|
|
55
|
+
*/
|
|
56
|
+
export declare function historyAwaitsReplay(session: Pick<SessionView, 'released' | 'hasHistory'>): boolean;
|
|
46
57
|
/**
|
|
47
58
|
* Everything the view can read that says "this conversation is set up on an
|
|
48
59
|
* agent": a live or parked ACP session, the host's binding (`session/chat-list`'s
|
|
@@ -101,5 +112,5 @@ export declare function acpEmptyCopy(facts: AcpEmptyFacts, t: Translate): AcpEmp
|
|
|
101
112
|
* The ACP view tab body: who serves this conversation and how it is doing,
|
|
102
113
|
* then the agent's own stream.
|
|
103
114
|
*/
|
|
104
|
-
export declare function AcpConversationView(props: AcpViewProps): import("react").JSX.Element;
|
|
115
|
+
export declare function AcpConversationView(props: AcpViewProps): import("react").JSX.Element | null;
|
|
105
116
|
export {};
|
|
@@ -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 {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The NTR branch button (slot `conversation.chat.assistant-actions`): in an
|
|
3
|
+
* ACP conversation this is the ONLY branch entry — the host's like / dislike /
|
|
4
|
+
|
|
5
|
+
* branch icons are hidden under `html[data-ntr-acp]` (see acp-ui.tsx), and
|
|
6
|
+
* this button takes the host branch's place with the same icon, size and
|
|
7
|
+
* row position (order 20, after the hidden feedback entry's order 10).
|
|
8
|
+
*
|
|
9
|
+
* Clicking runs the fixed two-stage branch: the HOST forks the DSH session
|
|
10
|
+
* tree first (`sessions.fork`, structurally resolved), then the plugin adopts
|
|
11
|
+
* the ACP side (`session/chat-fork-adopt` → the agent's own `session/fork`),
|
|
12
|
+
* so the child conversation's agent actually remembers the parent context —
|
|
13
|
+
* see the NTR message-actions plan, §D5/§D7.
|
|
14
|
+
*
|
|
15
|
+
* Availability is the store's `forkStateOf` six-state mapping over facts the
|
|
16
|
+
* button never derives itself: the host's per-row `fork` verdict
|
|
17
|
+
* (`session/chat-list`), the SSE running flag, the host-fork structural probe,
|
|
18
|
+
* and the DOM tail verdict. The tail check reads the host's OWN
|
|
19
|
+
* `[data-turn-tail]` marker (`data-actions-reveal === "always"` = the latest
|
|
20
|
+
* completed turn) and fails CLOSED: a missing marker disables the button
|
|
21
|
+
* rather than risking a fork cut at a point the user is not looking at.
|
|
22
|
+
*/
|
|
23
|
+
import type { ComponentType } from 'react';
|
|
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;
|
|
32
|
+
/**
|
|
33
|
+
* The host's own branch-button icon, picked per host generation. The icon
|
|
34
|
+
* exports MOVED between shipped hosts — desktop 0.2.1-rc.2 has
|
|
35
|
+
* `IconBranchOutlineRegular` / `…Medium` but NOT `…16`, newer packages export
|
|
36
|
+
* `…16` — and a hard import of one name compiles to a plain property read
|
|
37
|
+
* (`primitives.IconBranchOutline16`), so the missing export is `undefined`
|
|
38
|
+
* and `<undefined />` throws "Element type is invalid". The slot's error
|
|
39
|
+
* boundary then abdicates the whole entry: no button, no tooltip, nothing in
|
|
40
|
+
* any log — the exact silent disappearance this picker exists to prevent.
|
|
41
|
+
* Rendering the button never depends on the icon: with all three absent the
|
|
42
|
+
* click still works, only the 28px glyph is missing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function branchIconOf(source: Record<string, unknown>): ComponentType<Record<string, never>> | undefined;
|
|
45
|
+
export declare function AcpBranchAction(props: AcpChatInjected & {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
}): import("react").JSX.Element | null;
|
|
@@ -50,4 +50,32 @@ export interface ComposerModeProps {
|
|
|
50
50
|
* gone and this select is the mode switch the user actually has.
|
|
51
51
|
*/
|
|
52
52
|
export declare function agentModeTitle(agentId: string | undefined, acp: AcpStore): string;
|
|
53
|
+
/**
|
|
54
|
+
* Whether this conversation is one the agent-mode control may serve.
|
|
55
|
+
*
|
|
56
|
+
* An EXPLICIT non-ACP verdict outranks a lingering session row: switching a
|
|
57
|
+
* conversation off the NTR preset does not close its ACP session, so "a row
|
|
58
|
+
* exists" alone would keep this control on an ordinary conversation — the same
|
|
59
|
+
* precedence the agent tag, the context ring and the released banner already
|
|
60
|
+
* apply. Only while the host has not answered (`undefined`) does a bound
|
|
61
|
+
* session count as proof, which is what keeps the control available before the
|
|
62
|
+
* host's first `session/chat-list` answer lands.
|
|
63
|
+
*/
|
|
64
|
+
export declare function agentModeVisible(acpMode: boolean | undefined, hasBoundSession: boolean): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The mode the select shows as in force.
|
|
67
|
+
*
|
|
68
|
+
* A LIVE session is the authority (it is what the agent runs right now). A
|
|
69
|
+
* RELEASED row only describes the mode the agent was parked ON, so a newer
|
|
70
|
+
* pick — which `applyChatChoice` will apply on the next resume — outranks it
|
|
71
|
+
* (and the parked row still beats the catalog default). Same precedence as
|
|
72
|
+
* the model and thought level in `describeRoute`: a pre-switch on a parked
|
|
73
|
+
* session must be visible or the control reads as dead.
|
|
74
|
+
*/
|
|
75
|
+
export declare function modeInForce(target: {
|
|
76
|
+
released?: boolean;
|
|
77
|
+
modes?: {
|
|
78
|
+
currentModeId?: string;
|
|
79
|
+
};
|
|
80
|
+
} | undefined, persisted: string | undefined, fallback?: string): string;
|
|
53
81
|
export declare function AcpComposerMode({ acp, sessionId, useSession }: ComposerModeProps): import("react").JSX.Element | null;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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
|
|
@@ -227,6 +254,16 @@ export interface UiState {
|
|
|
227
254
|
chatSessions: ChatSessionView[];
|
|
228
255
|
/** The DSH chat session id {@link chatSessions} was loaded for. */
|
|
229
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
|
+
}>;
|
|
230
267
|
/**
|
|
231
268
|
* The agent tag's halo preference (settings-editable display choice). The
|
|
232
269
|
* tag reads it per render; the host answer in `resync` is the authority and
|
|
@@ -240,6 +277,12 @@ export interface UiState {
|
|
|
240
277
|
* host answer (returned by the same route) is the authority.
|
|
241
278
|
*/
|
|
242
279
|
approvalDeadlines?: AcpApprovalDeadlines;
|
|
280
|
+
/**
|
|
281
|
+
* The idle-release window in force, as the settings page edits it.
|
|
282
|
+
* Absent on a host half from before the tab existed, which hides the
|
|
283
|
+
* tab rather than showing a number this browser invented.
|
|
284
|
+
*/
|
|
285
|
+
idleRelease?: AcpIdleRelease;
|
|
243
286
|
/**
|
|
244
287
|
* Per-chat-session "does this conversation run the ACP preset" verdicts, as
|
|
245
288
|
* reported by `session/chat-list` (host-read from the session log). The
|
|
@@ -368,6 +411,13 @@ export declare class AcpStore {
|
|
|
368
411
|
private readonly openTurns;
|
|
369
412
|
/** In-flight `session/chat-list` reads, keyed by chat session id. */
|
|
370
413
|
private readonly chatSessionReads;
|
|
414
|
+
/**
|
|
415
|
+
* ACP session ids whose chat-list row was requested mid-turn (one read each).
|
|
416
|
+
* A conversation's first ACP session does not exist when the composer's
|
|
417
|
+
* mount/running read fires, so its first `usage_update` would find no row to
|
|
418
|
+
* patch; `turn/start` asks again once the host has materialized the session.
|
|
419
|
+
*/
|
|
420
|
+
private readonly chatSessionRowReads;
|
|
371
421
|
/**
|
|
372
422
|
* Chat sessions whose list has answered at least once in this page. The
|
|
373
423
|
* FIRST answer only seeds the lock; a lock appearing on a later answer is a
|
|
@@ -601,6 +651,13 @@ export declare class AcpStore {
|
|
|
601
651
|
* being the one authorized exception. Same no-op-when-unchanged discipline as
|
|
602
652
|
* the other accessors, so a frame restating the held verdict costs no
|
|
603
653
|
* listener churn.
|
|
654
|
+
*
|
|
655
|
+
* The seat gate (`syncAcpMode`) is the SECOND caller: it folds the runtime
|
|
656
|
+
* row's own `agentPreset` — the same session-log source the frames announce —
|
|
657
|
+
* through here, in both directions. That hands the row the frame's authority
|
|
658
|
+
* on purpose (one provenance, one rank): a stale projection is corrected by
|
|
659
|
+
* the gate's next evaluation with fresher row data, and only HOST READS are
|
|
660
|
+
* barred from regressing what the runtime itself already said.
|
|
604
661
|
*/
|
|
605
662
|
noteAcpMode(dshSessionId: string, acpMode: boolean): void;
|
|
606
663
|
/**
|
|
@@ -643,6 +700,36 @@ export declare class AcpStore {
|
|
|
643
700
|
* reports (a rejected switch must not leave an optimistic lie on screen).
|
|
644
701
|
*/
|
|
645
702
|
setChatSessionMode(dshSessionId: string, modeId: string, agentId?: string): Promise<void>;
|
|
703
|
+
/**
|
|
704
|
+
* Bring the conversation's released ACP session back on a fresh process —
|
|
705
|
+
* the released banner's explicit form of the "next message resumes it"
|
|
706
|
+
* promise (`session/chat-resume`). A failure is reported through the notice
|
|
707
|
+
* AND rethrown (the `respondPermission` contract), so the banner can pin
|
|
708
|
+
* the message beside the button the user just pressed; the binding is
|
|
709
|
+
* re-read either way so the row follows the host's real answer.
|
|
710
|
+
*/
|
|
711
|
+
resumeChatSession(dshSessionId: string): Promise<void>;
|
|
712
|
+
/**
|
|
713
|
+
* Branch one conversation (the NTR branch button's click): the fixed
|
|
714
|
+
* two-stage order — the HOST forks the DSH session tree first (copied
|
|
715
|
+
* history, `(1)` title, workspace), then the plugin adopts the ACP side via
|
|
716
|
+
* `session/chat-fork-adopt`, which runs the agent's own `session/fork` so
|
|
717
|
+
* the child actually remembers the parent context. Host first is structural:
|
|
718
|
+
* ACP fork has no cut-point parameter and nothing to return to if the host
|
|
719
|
+
* half failed second (see the plan's §D7).
|
|
720
|
+
*
|
|
721
|
+
* Resolves `{ childId }` on success, or `{ failure }` with one actionable
|
|
722
|
+
* line — the reason is ALSO flashed into the notice banner (a user-action
|
|
723
|
+
* failure, so the banner is its surface), and the local line is what the
|
|
724
|
+
* conversation renders beside the button (the user is not on the settings
|
|
725
|
+
* page). A failed adopt leaves the child an ordinary copied-history
|
|
726
|
+
* conversation (its first message starts a fresh `session/new` — today's
|
|
727
|
+
* behavior), never a half-bound one, and NOTHING is opened.
|
|
728
|
+
*
|
|
729
|
+
* `forkHost` is the runtime's `sessions.fork` face, resolved structurally by
|
|
730
|
+
* the entry and injected here: the store stays transport-free.
|
|
731
|
+
*/
|
|
732
|
+
forkChatSession(dshSessionId: string, forkHost: (sessionId: string) => Promise<unknown>): Promise<ChatForkOutcome>;
|
|
646
733
|
/**
|
|
647
734
|
* Start (or refresh) observation of every ACP transcript behind one DSH chat
|
|
648
735
|
* session. Blocks are rebuilt from scratch — the host buffer is the record,
|
|
@@ -702,6 +789,20 @@ export declare class AcpStore {
|
|
|
702
789
|
setApprovalTimeoutMode(mode: PermissionTimeoutMode): Promise<void>;
|
|
703
790
|
/** Drop the persisted override: the config file (or the built-in default) decides again. */
|
|
704
791
|
resetApprovalDeadlines(): Promise<void>;
|
|
792
|
+
/**
|
|
793
|
+
* Write the idle-release window (in ms).
|
|
794
|
+
*
|
|
795
|
+
* The host answer — the same route returns the window now in force — is
|
|
796
|
+
* the authority, so an optimistic write that the host trims self-corrects
|
|
797
|
+
* instead of drifting from what runs.
|
|
798
|
+
*/
|
|
799
|
+
setIdleRelease(idleTimeoutMs: number): Promise<void>;
|
|
800
|
+
/**
|
|
801
|
+
* Drop the persisted override: the config file (or the built-in default)
|
|
802
|
+
* decides again.
|
|
803
|
+
*/
|
|
804
|
+
resetIdleRelease(): Promise<void>;
|
|
805
|
+
private writeIdleRelease;
|
|
705
806
|
private writeApprovalDeadlines;
|
|
706
807
|
/**
|
|
707
808
|
* Bind one DSH chat session to an ACP agent. This is the plugin's own
|
|
@@ -765,4 +866,10 @@ export declare class AcpStore {
|
|
|
765
866
|
export declare function foldUpdate(blocks: Block[], update: Record<string, unknown>): boolean;
|
|
766
867
|
/** Notifications that update the SessionView snapshot instead of the block stream. */
|
|
767
868
|
export declare const STATE_ONLY_UPDATES: ReadonlySet<string>;
|
|
869
|
+
/**
|
|
870
|
+
* Context occupancy carried by one raw `usage_update` frame, in the shape the
|
|
871
|
+
* session snapshots already use. A malformed frame is ignored (the last good
|
|
872
|
+
* sample stays on screen) instead of zeroing the meter.
|
|
873
|
+
*/
|
|
874
|
+
export declare function usageFromUpdate(update: Record<string, unknown>): SessionView['usage'] | undefined;
|
|
768
875
|
export {};
|