dsh-ntr 0.1.6 → 0.2.1
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 +6 -3
- package/README.md +6 -3
- package/cordis.patch.yml +18 -18
- package/lib/bridge.js +134 -314
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +127 -114
- package/lib/client/permission-dock.js +72 -0
- package/lib/client/session-controls.js +4 -4
- package/lib/client/store.js +58 -12
- package/lib/client.js +696 -393
- package/lib/client.js.map +1 -1
- package/lib/impl.js +30 -19
- package/lib/index.js +6 -51
- package/lib/types/bridge.d.ts +63 -157
- package/lib/types/client/panel.d.ts +12 -20
- package/lib/types/client/permission-dock.d.ts +31 -0
- package/lib/types/client/store.d.ts +42 -12
- package/lib/types/impl.d.ts +31 -17
- package/lib/types/protocol.d.ts +27 -12
- package/package.json +5 -1
- package/src/bridge.ts +163 -379
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +243 -258
- package/src/client/permission-dock.tsx +133 -0
- package/src/client/session-controls.tsx +4 -4
- package/src/client/store.ts +81 -25
- package/src/impl.ts +49 -29
- package/src/index.ts +6 -65
- package/src/protocol.ts +32 -12
package/lib/impl.js
CHANGED
|
@@ -1513,13 +1513,35 @@ export class PermissionLedger {
|
|
|
1513
1513
|
views.push({
|
|
1514
1514
|
requestId: entry.requestId,
|
|
1515
1515
|
sessionId: entry.sessionId,
|
|
1516
|
-
|
|
1517
|
-
options: entry.view.options,
|
|
1516
|
+
...entry.view,
|
|
1518
1517
|
});
|
|
1519
1518
|
}
|
|
1520
1519
|
return views;
|
|
1521
1520
|
}
|
|
1522
1521
|
}
|
|
1522
|
+
/**
|
|
1523
|
+
* The `permission/request` frame for one ask.
|
|
1524
|
+
*
|
|
1525
|
+
* ONE builder for both the live broadcast and the reconnect replay, so the two
|
|
1526
|
+
* can never drift: a client that reconnects must receive exactly the frame the
|
|
1527
|
+
* live one carried (its conversation card filters on `dshSessionId` and shows
|
|
1528
|
+
* `toolName` / `deadlineAt`, so a replay missing them would silently drop the
|
|
1529
|
+
* ask).
|
|
1530
|
+
*/
|
|
1531
|
+
export function permissionRequestFrame(view) {
|
|
1532
|
+
return {
|
|
1533
|
+
type: 'permission/request',
|
|
1534
|
+
requestId: view.requestId,
|
|
1535
|
+
sessionId: view.sessionId,
|
|
1536
|
+
...(view.dshSessionId === undefined ? {} : { dshSessionId: view.dshSessionId }),
|
|
1537
|
+
toolCall: view.toolCall,
|
|
1538
|
+
toolName: view.toolName,
|
|
1539
|
+
...(view.reason === undefined ? {} : { reason: view.reason }),
|
|
1540
|
+
options: view.options,
|
|
1541
|
+
timeoutMode: view.timeoutMode,
|
|
1542
|
+
...(view.deadlineAt === undefined ? {} : { deadlineAt: view.deadlineAt }),
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1523
1545
|
/**
|
|
1524
1546
|
* Frames that re-establish a freshly connected client's view of the pending
|
|
1525
1547
|
* permission asks: every outstanding ask re-sent as its original
|
|
@@ -1527,10 +1549,10 @@ export class PermissionLedger {
|
|
|
1527
1549
|
*
|
|
1528
1550
|
* Why a replay at all: a `permission/request` is broadcast ONCE, when the ask is
|
|
1529
1551
|
* created. A client that connects later — page reload, second tab, stream
|
|
1530
|
-
* outage — never receives it, and
|
|
1531
|
-
*
|
|
1532
|
-
*
|
|
1533
|
-
*
|
|
1552
|
+
* outage — never receives it, and that card (conversation or settings ledger)
|
|
1553
|
+
* is the ONLY answer entry for the ask, so the ask is invisible to exactly the
|
|
1554
|
+
* client that needs it. OmniTerm closes the same hole with
|
|
1555
|
+
* `PermissionManager::pending_events()` + a `PermissionsSynced` marker on
|
|
1534
1556
|
* every WS connect.
|
|
1535
1557
|
*
|
|
1536
1558
|
* Why the terminator is mandatory: the replay is a snapshot, so an ask resolved
|
|
@@ -1539,13 +1561,7 @@ export class PermissionLedger {
|
|
|
1539
1561
|
* `respond` would answer `unknown_request`.
|
|
1540
1562
|
*/
|
|
1541
1563
|
export function permissionReplayFrames(pending) {
|
|
1542
|
-
const requests = pending.map(
|
|
1543
|
-
type: 'permission/request',
|
|
1544
|
-
requestId: p.requestId,
|
|
1545
|
-
sessionId: p.sessionId,
|
|
1546
|
-
toolCall: p.toolCall,
|
|
1547
|
-
options: p.options,
|
|
1548
|
-
}));
|
|
1564
|
+
const requests = pending.map(permissionRequestFrame);
|
|
1549
1565
|
return [...requests, { type: 'permission/synced', requestIds: pending.map(p => p.requestId) }];
|
|
1550
1566
|
}
|
|
1551
1567
|
function ok(value) {
|
|
@@ -1579,12 +1595,7 @@ export async function dispatchRpc(manager, endpoint, payload) {
|
|
|
1579
1595
|
try {
|
|
1580
1596
|
switch (endpoint) {
|
|
1581
1597
|
case 'agents/list':
|
|
1582
|
-
|
|
1583
|
-
// uses it to decide whether the settings panel still owns approvals.
|
|
1584
|
-
return ok({
|
|
1585
|
-
agents: manager.agentViews(),
|
|
1586
|
-
approvalRouted: manager.approvalRouting?.() ?? false,
|
|
1587
|
-
});
|
|
1598
|
+
return ok({ agents: manager.agentViews() });
|
|
1588
1599
|
case 'agents/detect':
|
|
1589
1600
|
return ok({ agents: await (manager.detectAgents?.() ?? Promise.resolve([])) });
|
|
1590
1601
|
case 'agent/save': {
|
package/lib/index.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from "./bridge.js";
|
|
8
8
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from "./impl.js";
|
|
9
9
|
export const name = 'dsh-ntr';
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Deliberately minimal: every other host service is probed structurally
|
|
11
|
+
// (`ctx.get` + a late `ctx.inject`) so a deployment without one still loads
|
|
12
|
+
// this plugin. ACP permission asks are answered through this plugin's own
|
|
13
|
+
// ledger, so the host approval service is not a collaborator at all.
|
|
14
14
|
export const inject = ['webServer', 'connection'];
|
|
15
15
|
/** How many stored events the cold-read folds before giving up on scanning. */
|
|
16
16
|
const COLD_READ_MAX_EVENTS = 4096;
|
|
@@ -86,43 +86,6 @@ export function apply(ctx, config) {
|
|
|
86
86
|
// Late arrival: the registry may still be loading when this plugin applies.
|
|
87
87
|
ctx.inject(['agentPresets'], () => { wirePresetRoster(); });
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Native approval routing: answer ACP `session/request_permission` through
|
|
91
|
-
* the host approval chain so the question takes over the chat composer
|
|
92
|
-
* (`ApprovalPanel`) instead of the settings panel.
|
|
93
|
-
*
|
|
94
|
-
* `approval.request` needs the live agent object (its session carries the
|
|
95
|
-
* audit pair, and the ask must land inside an open turn) and the chat
|
|
96
|
-
* session id our adapter already knows. Any missing piece — service absent,
|
|
97
|
-
* unknown session, a rejecting service — degrades to `'unavailable'`, which
|
|
98
|
-
* sends the ask back to the ledger/panel path rather than losing it.
|
|
99
|
-
*/
|
|
100
|
-
const wireApproval = () => {
|
|
101
|
-
const approval = (typeof ctx.get === 'function' ? ctx.get('approval') : ctx.approval);
|
|
102
|
-
const agents = (typeof ctx.get === 'function' ? ctx.get('agents') : ctx.agents);
|
|
103
|
-
if (approval === undefined || agents === undefined)
|
|
104
|
-
return;
|
|
105
|
-
manager.setApprovalRequester(async ({ dshSessionId, toolName, reason, callId, signal }) => {
|
|
106
|
-
if (dshSessionId === undefined)
|
|
107
|
-
return 'unavailable';
|
|
108
|
-
const agent = agents.get(dshSessionId);
|
|
109
|
-
if (agent === undefined)
|
|
110
|
-
return 'unavailable';
|
|
111
|
-
try {
|
|
112
|
-
return await approval.request({
|
|
113
|
-
agent,
|
|
114
|
-
toolName,
|
|
115
|
-
...(reason === undefined ? {} : { reason }),
|
|
116
|
-
...(callId === undefined ? {} : { callId }),
|
|
117
|
-
...(signal === undefined ? {} : { signal }),
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
catch {
|
|
121
|
-
// Never propagate: a failed ask must fall back, not break the turn.
|
|
122
|
-
return 'unavailable';
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
89
|
/**
|
|
127
90
|
* Workspace routing: the sidebar's selected workspace IS the chat session's
|
|
128
91
|
* workspace, so the agent's session and its process both run there (see
|
|
@@ -216,11 +179,10 @@ export function apply(ctx, config) {
|
|
|
216
179
|
return undefined;
|
|
217
180
|
}
|
|
218
181
|
};
|
|
219
|
-
wireApproval();
|
|
220
182
|
wireWorkspace();
|
|
221
183
|
if (typeof ctx.inject === 'function') {
|
|
222
|
-
// Late arrival for the workspace collaborators
|
|
223
|
-
//
|
|
184
|
+
// Late arrival for the workspace collaborators: a deployment missing one
|
|
185
|
+
// service must not fail this plugin's activation.
|
|
224
186
|
ctx.inject(['sandboxPolicy', 'sessions'], () => {
|
|
225
187
|
wireWorkspace();
|
|
226
188
|
});
|
|
@@ -233,13 +195,6 @@ export function apply(ctx, config) {
|
|
|
233
195
|
wireAcpMode();
|
|
234
196
|
});
|
|
235
197
|
}
|
|
236
|
-
if (typeof ctx.inject === 'function') {
|
|
237
|
-
// Late arrival: both services may still be loading when this plugin
|
|
238
|
-
// applies (profile order is not ours to control).
|
|
239
|
-
ctx.inject(['approval', 'agents'], () => {
|
|
240
|
-
wireApproval();
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
198
|
const registerLlm = (llmHandle) => {
|
|
244
199
|
ctx.effect(() => {
|
|
245
200
|
// The route exists so the host's driver can reach this adapter at all:
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -39,9 +39,9 @@ interface LiveSession {
|
|
|
39
39
|
released: boolean;
|
|
40
40
|
/**
|
|
41
41
|
* Owning DSH chat session id, when the session was opened through the LLM
|
|
42
|
-
* adapter (`AcpLlmAdapter`).
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* adapter (`AcpLlmAdapter`). It is what the conversation's permission card
|
|
43
|
+
* filters on (`permission/request.dshSessionId`); sessions started over the
|
|
44
|
+
* `/acp` RPC have no chat session, so their asks stay on the settings panel.
|
|
45
45
|
*/
|
|
46
46
|
dshSessionId?: string;
|
|
47
47
|
/**
|
|
@@ -98,21 +98,24 @@ export interface BridgeTuning {
|
|
|
98
98
|
/**
|
|
99
99
|
* How long a permission ask may sit unanswered before the deadline fires.
|
|
100
100
|
*
|
|
101
|
-
* ONE window for
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* anyone there?"
|
|
105
|
-
* card
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
101
|
+
* ONE window for every ask. Every ask is broadcast as `permission/request`
|
|
102
|
+
* and answered from the conversation's dock card; an `/acp`-RPC ask has no
|
|
103
|
+
* conversation, so the settings page mirrors the same ask as its answer
|
|
104
|
+
* entry. One question, one window: "is anyone there?" gets the same answer
|
|
105
|
+
* wherever the card is rendered, and two numbers made the settings card look
|
|
106
|
+
* like two unrelated knobs.
|
|
107
|
+
*
|
|
108
|
+
* `0` means 不等待, not "wait forever": `auto` settles the ask on arrival,
|
|
109
|
+
* `abort` floors the window to the shortest real wait (refusing every ask on
|
|
110
|
+
* arrival is a brick wall, not a timeout), and `wait` ignores the window
|
|
111
|
+
* entirely.
|
|
112
|
+
*
|
|
113
|
+
* The deadline is what keeps a turn from hanging on a question nobody
|
|
114
|
+
* answers: the card stays visible, but an unattended session would otherwise
|
|
115
|
+
* wait indefinitely. Past the deadline the ask is settled IN PLACE — never
|
|
116
|
+
* relocated: `abort` refuses it, `auto` answers it with the option the
|
|
117
|
+
* agent's own kinds imply (broadest grant first), and `wait` never expires
|
|
118
|
+
* it at all.
|
|
116
119
|
*
|
|
117
120
|
* Default 300000 ms. OmniTerm's equivalent is its reaper's permission-timeout
|
|
118
121
|
* threshold (30 min default, 30 s steps, hot-updatable).
|
|
@@ -167,47 +170,6 @@ export interface BridgeTuning {
|
|
|
167
170
|
*/
|
|
168
171
|
probeIdleTimeoutMs?: number;
|
|
169
172
|
}
|
|
170
|
-
/** One native approval question, as the host approval service defines it. */
|
|
171
|
-
export interface ApprovalRequestContext {
|
|
172
|
-
/** DSH chat session that owns the open turn the question is asked in. */
|
|
173
|
-
dshSessionId?: string;
|
|
174
|
-
agentId: string;
|
|
175
|
-
/** Human-facing identity of the operation awaiting a decision. */
|
|
176
|
-
toolName: string;
|
|
177
|
-
reason?: string;
|
|
178
|
-
/** The ACP tool call this ask belongs to; the host card attaches its detail to it. */
|
|
179
|
-
callId?: string;
|
|
180
|
-
signal?: AbortSignal;
|
|
181
|
-
}
|
|
182
|
-
/** Outcome vocabulary of the host approval chain (`'unavailable'` = no answerer). */
|
|
183
|
-
export type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable';
|
|
184
|
-
/**
|
|
185
|
-
* Why one permission ask rides the settings-panel ledger instead of the native
|
|
186
|
-
* approval chain, recorded when the ask is broadcast.
|
|
187
|
-
*
|
|
188
|
-
* `no-requester` and `no-chat-session` are the DESIGNED ledger paths: a host
|
|
189
|
-
* without the approval service, and an `/acp`-RPC session that has no chat
|
|
190
|
-
* turn to route into. `unavailable` is a DEGRADATION — the service is wired
|
|
191
|
-
* and this ask still failed, so the conversation the user is looking at will
|
|
192
|
-
* never show the question. The reason decides the chat line's wording (see
|
|
193
|
-
* `BridgeManager.streamPrompt`); the deadline and the mode decide what
|
|
194
|
-
* happens if nobody answers ({@link AgentBridge.handlePermissionRequest}).
|
|
195
|
-
*/
|
|
196
|
-
export type LedgerFallbackReason = 'no-requester' | 'no-chat-session' | 'unavailable';
|
|
197
|
-
/** One recorded ledger fallback: why the ask is here, and the deadline armed for it. */
|
|
198
|
-
export interface LedgerFallback {
|
|
199
|
-
reason: LedgerFallbackReason;
|
|
200
|
-
/** Effective deadline in ms; `0` = no timer (wait for the human). */
|
|
201
|
-
timeoutMs: number;
|
|
202
|
-
/** What the deadline does when it fires. */
|
|
203
|
-
mode: PermissionTimeoutMode;
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Asks the host approval service (`ctx.approval`) for one decision. Never
|
|
207
|
-
* throws: any failure is reported as `'unavailable'` so the caller can fall
|
|
208
|
-
* back to the settings-panel ledger.
|
|
209
|
-
*/
|
|
210
|
-
export type ApprovalRequester = (request: ApprovalRequestContext) => Promise<ApprovalOutcome>;
|
|
211
173
|
/**
|
|
212
174
|
* Resolves the workspace root of one chat session (`dshSessionId` = DSH session
|
|
213
175
|
* id) — the sidebar's selected workspace, which the host's sandbox policy keeps
|
|
@@ -244,13 +206,8 @@ export declare class AgentBridge {
|
|
|
244
206
|
/** Observes `availableCommands` from any real session so the agent's slash menu survives its sessions. */
|
|
245
207
|
private readonly onCommands?;
|
|
246
208
|
/**
|
|
247
|
-
* Resolved per
|
|
248
|
-
* was built
|
|
249
|
-
*/
|
|
250
|
-
private readonly approvalRequester?;
|
|
251
|
-
/**
|
|
252
|
-
* Resolved per turn, like {@link approvalRequester}: the host entry may
|
|
253
|
-
* wire the access-mode mapping after this bridge was built.
|
|
209
|
+
* Resolved per turn: the host entry may wire the workspace reader after
|
|
210
|
+
* this bridge was built.
|
|
254
211
|
*/
|
|
255
212
|
private readonly sessionWorkspace?;
|
|
256
213
|
state: BridgeState;
|
|
@@ -267,15 +224,6 @@ export declare class AgentBridge {
|
|
|
267
224
|
/** Inflight `session/load` resumes, shared so concurrent prompts don't double-resume. */
|
|
268
225
|
private readonly resuming;
|
|
269
226
|
readonly permissions: PermissionLedger;
|
|
270
|
-
/**
|
|
271
|
-
* Why each outstanding ledger ask is on the ledger, keyed by request id and
|
|
272
|
-
* recorded before the `permission/request` broadcast (the hub is
|
|
273
|
-
* synchronous, so a subscriber can read it the moment the frame goes out).
|
|
274
|
-
* Entries live exactly as long as the ask: the ledger's resolve callback
|
|
275
|
-
* deletes them, and every terminal path (answer, timeout, session close,
|
|
276
|
-
* process death) resolves through it.
|
|
277
|
-
*/
|
|
278
|
-
private readonly ledgerFallbacks;
|
|
279
227
|
private readonly startupTimeoutMs;
|
|
280
228
|
/**
|
|
281
229
|
* The ONE approval deadline, and the mode it settles with. Neither is
|
|
@@ -335,13 +283,8 @@ export declare class AgentBridge {
|
|
|
335
283
|
/** Observes `availableCommands` from any real session so the agent's slash menu survives its sessions. */
|
|
336
284
|
onCommands?: ((agentId: string, commands: NonNullable<SessionView["availableCommands"]>) => void) | undefined, tuning?: BridgeTuning,
|
|
337
285
|
/**
|
|
338
|
-
* Resolved per
|
|
339
|
-
* was built
|
|
340
|
-
*/
|
|
341
|
-
approvalRequester?: (() => ApprovalRequester | undefined) | undefined,
|
|
342
|
-
/**
|
|
343
|
-
* Resolved per turn, like {@link approvalRequester}: the host entry may
|
|
344
|
-
* wire the access-mode mapping after this bridge was built.
|
|
286
|
+
* Resolved per turn: the host entry may wire the workspace reader after
|
|
287
|
+
* this bridge was built.
|
|
345
288
|
*/
|
|
346
289
|
sessionWorkspace?: (() => SessionWorkspaceResolver | undefined) | undefined);
|
|
347
290
|
/**
|
|
@@ -571,56 +514,36 @@ export declare class AgentBridge {
|
|
|
571
514
|
stop(reason?: 'closed' | 'agent-stopped' | 'agent-died'): Promise<void>;
|
|
572
515
|
private killChild;
|
|
573
516
|
/**
|
|
574
|
-
* Answer one ACP permission ask.
|
|
575
|
-
*
|
|
576
|
-
* ledger
|
|
517
|
+
* Answer one ACP permission ask.
|
|
518
|
+
*
|
|
519
|
+
* EVERY ask is answered by a human through the plugin's own ledger. The
|
|
520
|
+
* host's approval chain is deliberately not involved: it could accept an ask
|
|
521
|
+
* and render nothing, leaving the turn hanging behind something invisible
|
|
522
|
+
* with no deadline (issue #2). The ledger broadcasts `permission/request`,
|
|
523
|
+
* the browser renders the answerable card, and `permission/respond` settles
|
|
524
|
+
* it.
|
|
525
|
+
*
|
|
526
|
+
* The host answers on its own only when a deadline expires: `abort` refuses,
|
|
527
|
+
* `auto` picks the option the agent's own kinds imply, `wait` never expires.
|
|
528
|
+
* The ACP request's own `signal` (the agent withdrawing the ask, or the
|
|
529
|
+
* connection dying) settles the same entry cancelled, so a withdrawn ask can
|
|
530
|
+
* never leave a stale card behind.
|
|
577
531
|
*/
|
|
578
532
|
private handlePermissionRequest;
|
|
579
533
|
/**
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
534
|
+
* Announce and compute one ask's expiry. Shared by the 不等待 arrival path
|
|
535
|
+
* and the ledger deadline, so an expiry can never broadcast two notices or
|
|
536
|
+
* skip the `permission/expired` line: once the card is gone that frame is
|
|
537
|
+
* the conversation's only trace of what happened to the question.
|
|
584
538
|
*/
|
|
585
|
-
|
|
539
|
+
private announceExpiredAsk;
|
|
586
540
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
* The ask is raced against {@link BridgeTuning.permissionTimeoutMs}. A
|
|
594
|
-
* routed ask has exactly one answer entry — the composer takeover — and no
|
|
595
|
-
* broadcast at all, so a host that takes the question and never renders it
|
|
596
|
-
* would leave the turn hanging behind something invisible. Past the deadline
|
|
597
|
-
* the ask is settled RIGHT THERE, by
|
|
598
|
-
* {@link BridgeTuning.permissionTimeoutMode}: refused (`abort`), answered
|
|
599
|
-
* with the option the agent's own kinds imply (`auto`), or never expiring
|
|
600
|
-
* at all (`wait`).
|
|
601
|
-
*
|
|
602
|
-
* It is NOT handed somewhere else. An earlier revision withdrew the ask and
|
|
603
|
-
* re-asked it on the settings ledger, which made the question travel to a
|
|
604
|
-
* page the user was not looking at — and then cancelled it there a few
|
|
605
|
-
* minutes later anyway. A deadline now ends the ask where it was asked.
|
|
606
|
-
*
|
|
607
|
-
* @returns the agent-facing response, or the reason the ask must fall back
|
|
608
|
-
* to the ledger (see {@link LedgerFallbackReason}).
|
|
609
|
-
*/
|
|
610
|
-
private askNativeApproval;
|
|
611
|
-
/**
|
|
612
|
-
* Settle an ask whose deadline fired, WHERE IT WAS ASKED.
|
|
613
|
-
*
|
|
614
|
-
* `abort` refuses it — the operation does not happen and the agent carries
|
|
615
|
-
* on with that answer. `auto` answers it with the option the agent's own
|
|
616
|
-
* kinds imply ({@link pickAutoOption}) so the agent CONTINUES; it says which
|
|
617
|
-
* option it picked, because an unattended approval nobody looked at has to
|
|
618
|
-
* be visible afterwards. A mode that cannot pick an option (no options, or
|
|
619
|
-
* none with a usable id) degrades to `abort`: refusing is safer than
|
|
620
|
-
* guessing, and hanging is worse than either.
|
|
621
|
-
*
|
|
622
|
-
* The `permission/resolved` frame keeps the event stream symmetric with the
|
|
623
|
-
* ledger path, so the browser drops any card it may hold for this session.
|
|
541
|
+
* Settle the 不等待 (`auto` + zero window) ask on arrival: no ledger entry
|
|
542
|
+
* and no card — nothing asks a human, so there is nothing to show. `auto`
|
|
543
|
+
* answers with the option the agent's own kinds imply ({@link pickAutoOption})
|
|
544
|
+
* and the agent CONTINUES. A mode that cannot pick an option (no options, or
|
|
545
|
+
* none with a usable id) degrades to refusing: safer than guessing, and
|
|
546
|
+
* hanging is worse than either.
|
|
624
547
|
*/
|
|
625
548
|
private settleExpiredAsk;
|
|
626
549
|
private settlePermissions;
|
|
@@ -639,7 +562,7 @@ export declare class AgentBridge {
|
|
|
639
562
|
* cannot reach into the agent's own process, so a second gate here would only
|
|
640
563
|
* be a fiction in front of a tool that can write without asking us at all.
|
|
641
564
|
* What the agent chooses to ask about goes to the human unchanged
|
|
642
|
-
* (`
|
|
565
|
+
* (`handlePermissionRequest`). See docs/acp-convergence.md §2.3.
|
|
643
566
|
*/
|
|
644
567
|
private handleFsWrite;
|
|
645
568
|
private guardFsPath;
|
|
@@ -661,12 +584,11 @@ export declare class AgentBridge {
|
|
|
661
584
|
*/
|
|
662
585
|
export declare function permissionToolName(toolCall: unknown): string;
|
|
663
586
|
/**
|
|
664
|
-
* One line saying WHAT the
|
|
587
|
+
* One line saying WHAT the permission ask is about.
|
|
665
588
|
*
|
|
666
|
-
* WHY: the
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
|
|
589
|
+
* WHY: without it the human only sees the tool's NAME ("Bash") and cannot
|
|
590
|
+
* judge the request; the ACP tool call already declares the missing half — its
|
|
591
|
+
* title and (for execute/edit tools) a short scalar in `rawInput`. Only ONE declared
|
|
670
592
|
* string is used, never a serialized object or a whole file, and the line is
|
|
671
593
|
* capped so a pathological input cannot fill the card or the audit log.
|
|
672
594
|
*/
|
|
@@ -883,6 +805,8 @@ interface SegmentState {
|
|
|
883
805
|
index: number;
|
|
884
806
|
activeType: 'reasoning' | 'text' | 'tool-call' | null;
|
|
885
807
|
activeText: string;
|
|
808
|
+
/** UTF-16 units of this segment's text and reasoning chunks (the usage estimate). */
|
|
809
|
+
outputChars: number;
|
|
886
810
|
/** The open tool-call block's snapshot, while `activeType === 'tool-call'`. */
|
|
887
811
|
activeToolCall: AcpToolCallSnapshot | undefined;
|
|
888
812
|
/** Whether this segment opened at least one tool-call block. */
|
|
@@ -923,12 +847,11 @@ type TurnSettle = {
|
|
|
923
847
|
* `transcript.since(cursor)` — events that arrive while no segment is open are
|
|
924
848
|
* recorded, never lost), the seen-call registry (a call id gets exactly one
|
|
925
849
|
* card per turn, wherever its restatements land), and the turn-level facts
|
|
926
|
-
* (`sawVisibleBlock
|
|
850
|
+
* (`sawVisibleBlock`) that outlive any single segment.
|
|
927
851
|
*/
|
|
928
852
|
declare class TurnStreamController {
|
|
929
853
|
private readonly manager;
|
|
930
854
|
private readonly hub;
|
|
931
|
-
private readonly bridge;
|
|
932
855
|
readonly session: LiveSession;
|
|
933
856
|
readonly turnId: string;
|
|
934
857
|
/** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
|
|
@@ -939,12 +862,11 @@ declare class TurnStreamController {
|
|
|
939
862
|
private disposed;
|
|
940
863
|
private readonly seenCalls;
|
|
941
864
|
private readonly announcedKinds;
|
|
942
|
-
private latestUsage;
|
|
943
865
|
/** Narrative lines (permission asks) that arrived while no segment was open. */
|
|
944
866
|
private readonly pendingProse;
|
|
945
867
|
sawVisibleBlock: boolean;
|
|
946
868
|
private cursorSeq;
|
|
947
|
-
constructor(manager: BridgeManager, hub: EventHub,
|
|
869
|
+
constructor(manager: BridgeManager, hub: EventHub, session: LiveSession, turnId: string,
|
|
948
870
|
/** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
|
|
949
871
|
endTurn: () => void, rollbackFirstTurn: (session: LiveSession) => void);
|
|
950
872
|
private readonly unsubscribe;
|
|
@@ -987,7 +909,6 @@ declare class TurnStreamController {
|
|
|
987
909
|
private pushProse;
|
|
988
910
|
/** Events that arrive between segments: recorded, replayed into the next one. */
|
|
989
911
|
private recordWithoutSegment;
|
|
990
|
-
private noteUsage;
|
|
991
912
|
/** Record a tool call's output (idempotent) and wake any waiter. */
|
|
992
913
|
private noteToolRecord;
|
|
993
914
|
/**
|
|
@@ -1100,20 +1021,10 @@ export declare class BridgeManager {
|
|
|
1100
1021
|
probeModelsOnBoot?: boolean, tuning?: BridgeTuning);
|
|
1101
1022
|
private makeBridge;
|
|
1102
1023
|
/**
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
*/
|
|
1108
|
-
setApprovalRequester(requester?: ApprovalRequester): void;
|
|
1109
|
-
/** True when permission asks go to native approval instead of the settings panel. */
|
|
1110
|
-
approvalRouting(): boolean;
|
|
1111
|
-
/**
|
|
1112
|
-
* Every outstanding ledger ask, across all agents, with the view the browser
|
|
1113
|
-
* was shown it — the source of the reconnect replay (see
|
|
1114
|
-
* `permissionReplayFrames`). Asks on the native route are deliberately
|
|
1115
|
-
* absent: they broadcast nothing and their answer entry is the composer
|
|
1116
|
-
* takeover the client already holds.
|
|
1024
|
+
* Every outstanding ask, across all agents, with the view the browser was
|
|
1025
|
+
* shown it — the source of the reconnect replay (see
|
|
1026
|
+
* `permissionReplayFrames`). Every ACP ask is a ledger entry, so this is the
|
|
1027
|
+
* whole live set.
|
|
1117
1028
|
*/
|
|
1118
1029
|
pendingPermissionViews(): PendingPermissionView[];
|
|
1119
1030
|
/**
|
|
@@ -1689,11 +1600,6 @@ export declare class BridgeManager {
|
|
|
1689
1600
|
private readonly chatAgentIntent;
|
|
1690
1601
|
/** Drop every chat-keyed record pointing at one host session (close/stop paths). */
|
|
1691
1602
|
private forgetDshSession;
|
|
1692
|
-
/**
|
|
1693
|
-
* Latest native-approval requester. Held as a field (read through a thunk by
|
|
1694
|
-
* each bridge) so a requester wired after bridges were built still applies.
|
|
1695
|
-
*/
|
|
1696
|
-
private approvalRequester?;
|
|
1697
1603
|
private sessionWorkspaceResolver?;
|
|
1698
1604
|
private acpModeResolver?;
|
|
1699
1605
|
/** Cached `chatSessionAcpMode` answers, keyed by DSH chat session id. */
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
* React surface for ACP: the settings configuration section.
|
|
3
3
|
*
|
|
4
4
|
* Scope is deliberately narrow — this is a CONFIGURATION surface. Runtime
|
|
5
|
-
* interaction lives in the conversation: permission
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* interaction lives in the conversation: every permission ask with a chat
|
|
6
|
+
* identity is answered on a card above that conversation's composer
|
|
7
|
+
* (`permission-dock.tsx`). The ledger card here is the cross-session mirror —
|
|
8
|
+
* asks whose conversation is not open, or which have no chat identity at all —
|
|
9
|
+
* and is where those are answered (see `permissionLedgerCard`).
|
|
10
10
|
*
|
|
11
11
|
* Each configured agent is one compact row (status + current model + badges)
|
|
12
12
|
* that expands into the full connection form and model catalog. The row stays
|
|
@@ -30,22 +30,15 @@ export declare function payloadEquals(payload: AgentSavePayload, current: AcpAge
|
|
|
30
30
|
/**
|
|
31
31
|
* The settings panel's permission-ledger rule.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* (`approvalRouted` → a read-only count, no buttons) hid the only answer
|
|
40
|
-
* entry exactly when the native route had degraded to unavailable, and the
|
|
41
|
-
* ask then hung until its timeout.
|
|
42
|
-
*
|
|
43
|
-
* `fellBackFromConversation` is the case the card has to explain: the host
|
|
44
|
-
* can route asks, and this one still did not make it.
|
|
33
|
+
* Pending asks are shown in the conversation card while that conversation is
|
|
34
|
+
* open; this card is the cross-session mirror for the ones it cannot show —
|
|
35
|
+
* asks belonging to another conversation, and `/acp` RPC-only asks with no
|
|
36
|
+
* chat identity at all. It therefore appears whenever ANY ask is pending: a
|
|
37
|
+
* pending entry with no conversation card would otherwise have no answer
|
|
38
|
+
* surface anywhere.
|
|
45
39
|
*/
|
|
46
|
-
export declare function permissionLedgerCard(ui: Pick<UiState, '
|
|
40
|
+
export declare function permissionLedgerCard(ui: Pick<UiState, 'permissions'>): {
|
|
47
41
|
visible: boolean;
|
|
48
|
-
fellBackFromConversation: boolean;
|
|
49
42
|
};
|
|
50
43
|
/**
|
|
51
44
|
* The shortest REAL wait — the first rung above 不等待.
|
|
@@ -93,7 +86,6 @@ export declare function approvalDeadlineFacts(deadlines: AcpApprovalDeadlines):
|
|
|
93
86
|
/** A reset is only honest when something is actually being outranked. */
|
|
94
87
|
overridden: boolean;
|
|
95
88
|
};
|
|
96
|
-
/** Settings configuration section for ACP agents. */
|
|
97
89
|
export declare function AcpSettingsSection(props: WithStore): import("react").JSX.Element;
|
|
98
90
|
/** Everything the connection form edits, in one object (kept in the row). */
|
|
99
91
|
interface AgentForm {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending ACP permission asks for the staged DSH conversation, rendered as
|
|
3
|
+
* composer-width entries above the composer card (`conversation.input.dock`).
|
|
4
|
+
* The slot itself spans the conversation column; the dock mirrors the host's
|
|
5
|
+
* InputBar width formula so the cards line up with the input box.
|
|
6
|
+
*
|
|
7
|
+
* The host broadcasts every ask as `permission/request`, carrying the DSH chat
|
|
8
|
+
* session id when the ask belongs to a conversation. This dock is the primary
|
|
9
|
+
* answer surface: it renders exactly the asks whose `dshSessionId` is the
|
|
10
|
+
* conversation it was mounted for, and nothing otherwise. There is no
|
|
11
|
+
* ACP-mode gate — the frame set itself is the gate, so an ordinary
|
|
12
|
+
* conversation shows this card if and only if it actually has an ask.
|
|
13
|
+
*
|
|
14
|
+
* The ask's description and options are the agent's own: buttons appear in the
|
|
15
|
+
* agent's order with the agent's names (including "Always Allow"), and
|
|
16
|
+
* `kind` is not rendered. A separate cancel button answers with no option,
|
|
17
|
+
* which the host forwards as `cancelled`. No Enter/Escape handlers: the
|
|
18
|
+
* composer owns its keyboard, and these are ordinary focusable buttons.
|
|
19
|
+
*
|
|
20
|
+
* Multiple asks stack in ARRIVAL order, oldest first: the question ignored
|
|
21
|
+
* longest stays at the top. The list is capped in height by CSS and scrolls.
|
|
22
|
+
* Asks without a chat identity never appear here — they stay in the
|
|
23
|
+
* settings-panel ledger, the cross-session mirror.
|
|
24
|
+
*/
|
|
25
|
+
import type { AcpStore } from './store.ts';
|
|
26
|
+
export interface PermissionDockProps {
|
|
27
|
+
/** Injected face: the store plus the chat session id this dock belongs to. */
|
|
28
|
+
acp: AcpStore;
|
|
29
|
+
sessionId: string;
|
|
30
|
+
}
|
|
31
|
+
export declare function PermissionDock({ acp, sessionId }: PermissionDockProps): import("react").JSX.Element | null;
|