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.
@@ -6,6 +6,39 @@
6
6
  * Deliberately framework-free: React binds through `useSyncExternalStore`.
7
7
  */
8
8
  import { DEFAULT_TAG_HALO } from "../protocol.js";
9
+ /** Returned by {@link deadlineRemainingLabel} once the deadline has passed. */
10
+ export const PERMISSION_DEADLINE_EXPIRED = 'expired';
11
+ /**
12
+ * The asks that belong to one DSH chat conversation. An ask without a
13
+ * `dshSessionId` belongs to no conversation (an `/acp` RPC-only session), so it
14
+ * is deliberately absent from every conversation's dock and stays in the
15
+ * settings-panel ledger.
16
+ */
17
+ export function permissionsForSession(permissions, dshSessionId) {
18
+ return permissions.filter(permission => permission.dshSessionId === dshSessionId);
19
+ }
20
+ /**
21
+ * Compact countdown for one ask, or `undefined` when there is nothing to count
22
+ * down (`wait` mode, or a frame without a deadline). Pure so the formatting is
23
+ * testable without a clock: the dock passes `Date.now()` on its tick.
24
+ *
25
+ * Past the deadline the answer is {@link PERMISSION_DEADLINE_EXPIRED} rather
26
+ * than a negative number — the host has already armed the expiry, and a card
27
+ * counting backwards would claim a deadline that no longer exists.
28
+ */
29
+ export function deadlineRemainingLabel(deadlineAt, now, mode) {
30
+ if (mode === 'wait' || deadlineAt === undefined)
31
+ return undefined;
32
+ const remainingMs = deadlineAt - now;
33
+ if (remainingMs <= 0)
34
+ return PERMISSION_DEADLINE_EXPIRED;
35
+ const seconds = Math.ceil(remainingMs / 1000);
36
+ if (seconds < 60)
37
+ return `${String(seconds)}s`;
38
+ const minutes = Math.floor(seconds / 60);
39
+ const rest = seconds % 60;
40
+ return rest === 0 ? `${String(minutes)}m` : `${String(minutes)}m ${String(rest)}s`;
41
+ }
9
42
  const MAX_BLOCKS = 400;
10
43
  /** How many observed ACP transcripts stay live at once (insertion-ordered eviction). */
11
44
  const MAX_WATCHED = 6;
@@ -298,7 +331,6 @@ export class AcpStore {
298
331
  seatCatalogs: {},
299
332
  probingModels: {},
300
333
  probeErrors: {},
301
- approvalRouted: false,
302
334
  chatSessions: [],
303
335
  tagHalo: { ...DEFAULT_TAG_HALO },
304
336
  approvalDeadlines: undefined,
@@ -501,7 +533,7 @@ export class AcpStore {
501
533
  /** Snapshot refresh used on panel open and after a stream outage. */
502
534
  async resync() {
503
535
  try {
504
- const [{ agents, approvalRouted }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
536
+ const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
505
537
  this.call('agents/list'),
506
538
  this.call('sessions/list'),
507
539
  // The plugin's OWN default ACP agent — a plugin-private id, not a DSH
@@ -526,7 +558,6 @@ export class AcpStore {
526
558
  sessions,
527
559
  defaultAgent: defaultAgentInfo?.agentId,
528
560
  detectedAgents,
529
- approvalRouted: approvalRouted === true,
530
561
  preset: presetResult?.preset ?? undefined,
531
562
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
532
563
  approvalDeadlines: deadlinesResult.deadlines,
@@ -1244,12 +1275,21 @@ export class AcpStore {
1244
1275
  this.flashError(error);
1245
1276
  }
1246
1277
  }
1278
+ /**
1279
+ * Answer one ask (`optionId` absent = cancel it). A failure is reported
1280
+ * through the store's notice — the settings panel renders that — AND
1281
+ * rethrown, so the conversation dock can pin the message next to the card
1282
+ * that failed instead of leaving it on a settings page the user may not
1283
+ * have open. Callers that cannot show it (the settings panel) swallow it
1284
+ * after the notice.
1285
+ */
1247
1286
  async respondPermission(requestId, optionId) {
1248
1287
  try {
1249
1288
  await this.call('permission/respond', { requestId, ...(optionId === undefined ? {} : { optionId }) });
1250
1289
  }
1251
1290
  catch (error) {
1252
1291
  this.flashError(error);
1292
+ throw error;
1253
1293
  }
1254
1294
  }
1255
1295
  async stopAgent(agentId) {
@@ -1601,16 +1641,22 @@ export class AcpStore {
1601
1641
  return this.loadAgentCatalog(event.agentId);
1602
1642
  }
1603
1643
  case 'permission/request': {
1604
- const title = typeof event.toolCall?.title === 'string'
1605
- ? String(event.toolCall.title)
1606
- : this.locale('工具调用', 'tool call');
1644
+ // Verbatim copy of the host's frame. The host decides what the ask is
1645
+ // called (`toolName` / `reason`) and which options exist; deriving a
1646
+ // title from `toolCall` here used to make the card say something the
1647
+ // host never said.
1648
+ const permission = {
1649
+ requestId: event.requestId,
1650
+ sessionId: event.sessionId,
1651
+ ...(event.dshSessionId === undefined ? {} : { dshSessionId: event.dshSessionId }),
1652
+ toolName: event.toolName,
1653
+ ...(event.reason === undefined ? {} : { reason: event.reason }),
1654
+ options: event.options,
1655
+ timeoutMode: event.timeoutMode,
1656
+ ...(event.deadlineAt === undefined ? {} : { deadlineAt: event.deadlineAt }),
1657
+ };
1607
1658
  this.mutate({
1608
- permissions: [...this.state.permissions.filter(p => p.requestId !== event.requestId), {
1609
- requestId: event.requestId,
1610
- sessionId: event.sessionId,
1611
- title,
1612
- options: event.options,
1613
- }],
1659
+ permissions: [...this.state.permissions.filter(p => p.requestId !== event.requestId), permission],
1614
1660
  });
1615
1661
  return Promise.resolve();
1616
1662
  }