dsh-ntr 0.1.6 → 0.2.0
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 +18 -18
- package/lib/bridge.js +120 -291
- package/lib/client/acp-ui.js +60 -32
- package/lib/client/index.js +17 -0
- package/lib/client/panel.js +37 -42
- 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 +296 -95
- 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 +60 -154
- package/lib/types/client/panel.d.ts +12 -19
- 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 +1 -1
- package/src/bridge.ts +147 -357
- package/src/client/acp-ui.tsx +60 -32
- package/src/client/index.tsx +18 -0
- package/src/client/panel.tsx +54 -58
- 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
|
*/
|
|
@@ -928,7 +850,6 @@ type TurnSettle = {
|
|
|
928
850
|
declare class TurnStreamController {
|
|
929
851
|
private readonly manager;
|
|
930
852
|
private readonly hub;
|
|
931
|
-
private readonly bridge;
|
|
932
853
|
readonly session: LiveSession;
|
|
933
854
|
readonly turnId: string;
|
|
934
855
|
/** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
|
|
@@ -944,7 +865,7 @@ declare class TurnStreamController {
|
|
|
944
865
|
private readonly pendingProse;
|
|
945
866
|
sawVisibleBlock: boolean;
|
|
946
867
|
private cursorSeq;
|
|
947
|
-
constructor(manager: BridgeManager, hub: EventHub,
|
|
868
|
+
constructor(manager: BridgeManager, hub: EventHub, session: LiveSession, turnId: string,
|
|
948
869
|
/** Releases the agent-side turn slot (the `beginTurn` settler; idempotent). */
|
|
949
870
|
endTurn: () => void, rollbackFirstTurn: (session: LiveSession) => void);
|
|
950
871
|
private readonly unsubscribe;
|
|
@@ -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 不等待.
|
|
@@ -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;
|
|
@@ -84,16 +84,49 @@ export type Block = {
|
|
|
84
84
|
tone: 'info' | 'error';
|
|
85
85
|
text: string;
|
|
86
86
|
};
|
|
87
|
+
/**
|
|
88
|
+
* One permission ask waiting for a human. Every field is copied VERBATIM from
|
|
89
|
+
* the host's `permission/request` frame: the host owns what the ask is called
|
|
90
|
+
* (`toolName` / `reason`), which options the agent offered (order included),
|
|
91
|
+
* and whether the ask carries a deadline. The browser does not derive a title
|
|
92
|
+
* from `toolCall` and does not second-guess the agent's option labels.
|
|
93
|
+
*/
|
|
87
94
|
export interface PendingPermission {
|
|
88
95
|
requestId: string;
|
|
96
|
+
/** ACP session id. */
|
|
89
97
|
sessionId: string;
|
|
90
|
-
|
|
98
|
+
/** DSH chat session id; absent for `/acp` RPC-only sessions (settings-only ask). */
|
|
99
|
+
dshSessionId?: string;
|
|
100
|
+
toolName: string;
|
|
101
|
+
reason?: string;
|
|
91
102
|
options: Array<{
|
|
92
103
|
optionId: string;
|
|
93
104
|
name: string;
|
|
94
105
|
kind: string;
|
|
95
106
|
}>;
|
|
107
|
+
timeoutMode: PermissionTimeoutMode;
|
|
108
|
+
/** Epoch ms; absent in `wait` mode. */
|
|
109
|
+
deadlineAt?: number;
|
|
96
110
|
}
|
|
111
|
+
/** Returned by {@link deadlineRemainingLabel} once the deadline has passed. */
|
|
112
|
+
export declare const PERMISSION_DEADLINE_EXPIRED = "expired";
|
|
113
|
+
/**
|
|
114
|
+
* The asks that belong to one DSH chat conversation. An ask without a
|
|
115
|
+
* `dshSessionId` belongs to no conversation (an `/acp` RPC-only session), so it
|
|
116
|
+
* is deliberately absent from every conversation's dock and stays in the
|
|
117
|
+
* settings-panel ledger.
|
|
118
|
+
*/
|
|
119
|
+
export declare function permissionsForSession(permissions: readonly PendingPermission[], dshSessionId: string): PendingPermission[];
|
|
120
|
+
/**
|
|
121
|
+
* Compact countdown for one ask, or `undefined` when there is nothing to count
|
|
122
|
+
* down (`wait` mode, or a frame without a deadline). Pure so the formatting is
|
|
123
|
+
* testable without a clock: the dock passes `Date.now()` on its tick.
|
|
124
|
+
*
|
|
125
|
+
* Past the deadline the answer is {@link PERMISSION_DEADLINE_EXPIRED} rather
|
|
126
|
+
* than a negative number — the host has already armed the expiry, and a card
|
|
127
|
+
* counting backwards would claim a deadline that no longer exists.
|
|
128
|
+
*/
|
|
129
|
+
export declare function deadlineRemainingLabel(deadlineAt: number | undefined, now: number, mode: PermissionTimeoutMode): string | undefined;
|
|
97
130
|
/**
|
|
98
131
|
* One observed ACP transcript, keyed by ACP session id. This is the ACP view
|
|
99
132
|
* tab's data source: the native chat plane can only render text/reasoning
|
|
@@ -164,17 +197,6 @@ export interface UiState {
|
|
|
164
197
|
* that never answers, …) reads as a dead button.
|
|
165
198
|
*/
|
|
166
199
|
probeErrors: Record<string, string>;
|
|
167
|
-
/**
|
|
168
|
-
* Host reports that it HAS the native approval chain (permission asks are
|
|
169
|
-
* answered in the chat composer). This is a capability report, not a
|
|
170
|
-
* per-ask promise: an individual ask still falls back to the settings-panel
|
|
171
|
-
* ledger when the service is unavailable this time, or when the session has
|
|
172
|
-
* no chat identity behind it (`AgentBridge.askNativeApproval`). The panel
|
|
173
|
-
* therefore answers ANY pending ask, whatever this flag says — see
|
|
174
|
-
* `permissionLedgerCard` in panel.tsx. False on an older host or when the
|
|
175
|
-
* approval service is missing, in which case the ledger is the only path.
|
|
176
|
-
*/
|
|
177
|
-
approvalRouted: boolean;
|
|
178
200
|
/**
|
|
179
201
|
* ACP agent sessions backing the chat session the composer control sits in.
|
|
180
202
|
* Only meaningful together with {@link chatSessionsFor}: the control reads it
|
|
@@ -556,6 +578,14 @@ export declare class AcpStore {
|
|
|
556
578
|
/** Mirror the running flag onto a transcript the ACP view is showing. */
|
|
557
579
|
private markWatchedRunning;
|
|
558
580
|
setConfigOption(configId: string, value: string): Promise<void>;
|
|
581
|
+
/**
|
|
582
|
+
* Answer one ask (`optionId` absent = cancel it). A failure is reported
|
|
583
|
+
* through the store's notice — the settings panel renders that — AND
|
|
584
|
+
* rethrown, so the conversation dock can pin the message next to the card
|
|
585
|
+
* that failed instead of leaving it on a settings page the user may not
|
|
586
|
+
* have open. Callers that cannot show it (the settings panel) swallow it
|
|
587
|
+
* after the notice.
|
|
588
|
+
*/
|
|
559
589
|
respondPermission(requestId: string, optionId?: string): Promise<void>;
|
|
560
590
|
stopAgent(agentId: string): Promise<void>;
|
|
561
591
|
/**
|