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/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
- toolCall: entry.view.toolCall,
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 for a ledger ask (the `/acp` RPC path, or a
1531
- * native route that degraded) that card is the ONLY answer entry, so the ask is
1532
- * invisible to exactly the client that needs it. OmniTerm closes the same hole
1533
- * with `PermissionManager::pending_events()` + a `PermissionsSynced` marker on
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(p => ({
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
- // `approvalRouted` is a capability report, not a command: the browser
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
- // `approval` / `agents` are deliberately NOT listed here: they are optional
11
- // collaborators probed structurally below. A hard dependency would make the
12
- // whole plugin fail to load on a deployment without them (see the LLM service
13
- // probe, which follows the same shape).
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; kept separate from the
223
- // approval probe so a deployment missing one service still wires the other.
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:
@@ -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`). `ctx.agents.get()` needs it to name the agent
43
- * whose open turn carries the native approval question; sessions started
44
- * over the `/acp` RPC have no chat session and stay on the ledger path.
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 both routes. An ask can be asked in the conversation (the
102
- * composer takeover) or in the settings panel's ledger (no approval service,
103
- * or no chat identity to route into) — but both ask the same question, "is
104
- * anyone there?", so both get the same answer. Two windows made the settings
105
- * card look like two unrelated knobs.
106
- *
107
- * `0` means 一直等 — no deadline at all. What happens when it fires is
108
- * {@link BridgeTuning.permissionTimeoutMode}'s business, never this knob's.
109
- *
110
- * The deadline is what keeps a turn from hanging on a question nobody can
111
- * see: a routed ask shows no card and broadcasts no frame, so a host that
112
- * takes the question and never renders it would otherwise leave the turn
113
- * hanging forever. Past the deadline the ask is settled IN PLACE — it is not
114
- * handed anywhere else. A human who was still deciding finds the ask REFUSED
115
- * (or auto-answered, in `auto` mode), not relocated.
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 ask (not captured) so a requester wired after this bridge
248
- * was built still applies. Absent → every ask rides the ledger.
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 ask (not captured) so a requester wired after this bridge
339
- * was built still applies. Absent → every ask rides the ledger.
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. Every ask reaches a human: the native
575
- * approval chain first (answered in the chat composer), the settings-panel
576
- * ledger otherwise.
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
- * Why one ledger ask is on the ledger, as recorded by
581
- * {@link handlePermissionRequest}. The manager's `streamPrompt` reads it to
582
- * word the chat line by the path the ask took; `undefined` once the ask is
583
- * answered, timed out, or settled.
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
- ledgerFallbackOf(requestId: string): LedgerFallback | undefined;
539
+ private announceExpiredAsk;
586
540
  /**
587
- * Ask the host approval service for this permission (`ctx.approval` → the
588
- * browser's composer takeover). Only sessions bound to a DSH chat session
589
- * qualify: the service appends its audit pair to that session inside an
590
- * open turn, so a session started over `/acp` (no chat turn) cannot be
591
- * routed and must stay on the ledger path.
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
- * (`askNativeApproval`). See docs/acp-convergence.md §2.3.
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 native approval card is about.
587
+ * One line saying WHAT the permission ask is about.
665
588
  *
666
- * WHY: the host renders `reason ?? "工具 <name> 请求越权执行"`. Without a
667
- * reason the human only sees the tool's NAME ("Bash") and cannot judge the
668
- * request; the ACP tool call already declares the missing half — its title and
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`, usage) that outlive any single segment.
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, bridge: AgentBridge, session: LiveSession, turnId: string,
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
- * Route ACP permission asks through the host approval service (answered in
1104
- * the chat composer). Absent → every ask stays on the settings-panel ledger.
1105
- * Setting it also flips {@link approvalRouting}, which the browser reads to
1106
- * stop rendering the settings-panel permission card.
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 asks are answered in the
6
- * chat composer through the host approval chain. The ledger card answers
7
- * whatever did NOT reach the conversation — a host without the chain, a
8
- * session with no chat identity behind it, or an ask whose native route
9
- * degraded to unavailable (see `permissionLedgerCard`).
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
- * A pending ask ALWAYS gets an answerable card here, whatever `approvalRouted`
34
- * says. That flag reports that the host HAS an approval service — it does not
35
- * promise THIS ask reached the conversation. An ask lands in `ui.permissions`
36
- * only on the ledger path (`AgentBridge.handlePermissionRequest` broadcasts
37
- * `permission/request` there and nowhere else), so a pending entry is by
38
- * definition a question the conversation is not showing. The previous rule
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, 'approvalRouted' | 'permissions'>): {
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;