dsh-ntr 0.1.4 → 0.1.6

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.
@@ -301,6 +301,7 @@ export class AcpStore {
301
301
  approvalRouted: false,
302
302
  chatSessions: [],
303
303
  tagHalo: { ...DEFAULT_TAG_HALO },
304
+ approvalDeadlines: undefined,
304
305
  chatTranscripts: {},
305
306
  acpMode: {},
306
307
  chatBoundAgent: {},
@@ -500,7 +501,7 @@ export class AcpStore {
500
501
  /** Snapshot refresh used on panel open and after a stream outage. */
501
502
  async resync() {
502
503
  try {
503
- const [{ agents, approvalRouted }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult] = await Promise.all([
504
+ const [{ agents, approvalRouted }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
504
505
  this.call('agents/list'),
505
506
  this.call('sessions/list'),
506
507
  // The plugin's OWN default ACP agent — a plugin-private id, not a DSH
@@ -511,6 +512,9 @@ export class AcpStore {
511
512
  this.call('preset/status').catch(() => null),
512
513
  // Host without the endpoint (older half) keeps the local default.
513
514
  this.call('ui/tag-halo').catch(() => ({ halo: { ...DEFAULT_TAG_HALO } })),
515
+ // `undefined` (older half) hides the deadline card instead of showing a
516
+ // number this browser made up.
517
+ this.call('ui/approval-deadlines').catch(() => ({ deadlines: undefined })),
514
518
  ]);
515
519
  const detectedAgents = (detectResult && detectResult.agents.length > 0)
516
520
  ? detectResult.agents
@@ -525,6 +529,7 @@ export class AcpStore {
525
529
  approvalRouted: approvalRouted === true,
526
530
  preset: presetResult?.preset ?? undefined,
527
531
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
532
+ approvalDeadlines: deadlinesResult.deadlines,
528
533
  });
529
534
  const active = this.state.activeSessionId ?? sessions[0]?.sessionId;
530
535
  if (active !== undefined)
@@ -785,12 +790,23 @@ export class AcpStore {
785
790
  async loadTranscript(sessionId) {
786
791
  try {
787
792
  const { session, entries } = await this.call('transcript/get', { sessionId });
793
+ // An answer BEHIND what this page already folded must not replace it.
794
+ //
795
+ // The host fills its transcript from the notification pipeline, which the
796
+ // SDK paces independently of the `session/prompt` response, so a read
797
+ // taken right after a turn can still be missing the agent's last chunk.
798
+ // `lastSeq` already refuses to go backwards; the blocks must not either,
799
+ // or switching away and back inside that window truncates a turn the user
800
+ // just watched finish. A resumed session replays as fresh updates, but
801
+ // only after `session/reset` zeroes the watermark — so this cannot swallow
802
+ // a legitimate replay.
803
+ const seen = this.lastSeq.get(sessionId) ?? 0;
804
+ if ((entries.at(-1)?.seq ?? 0) < seen)
805
+ return;
788
806
  const blocks = [];
789
807
  for (const entry of entries) {
790
- const before = blocks.length;
791
808
  foldUpdate(blocks, entry.update);
792
809
  this.lastSeq.set(sessionId, Math.max(this.lastSeq.get(sessionId) ?? 0, entry.seq));
793
- void before;
794
810
  }
795
811
  const sessions = this.state.sessions.some(s => s.sessionId === sessionId)
796
812
  ? this.state.sessions.map(s => (s.sessionId === sessionId ? session : s))
@@ -1276,6 +1292,54 @@ export class AcpStore {
1276
1292
  this.flashError(error);
1277
1293
  }
1278
1294
  }
1295
+ /**
1296
+ * Write the approval deadline (settings card), or drop the persisted override
1297
+ * with `reset` so the `cordis.patch.yml` baseline applies again.
1298
+ *
1299
+ * The host answer — the same route returns the deadlines now in force — is the
1300
+ * authority, so an optimistic write that the host trims (out of range, or
1301
+ * merged with a value this browser did not know about) self-corrects instead
1302
+ * of drifting from what runs.
1303
+ */
1304
+ async setApprovalDeadline(ms) {
1305
+ await this.writeApprovalDeadlines({ permissionTimeoutMs: ms });
1306
+ }
1307
+ /**
1308
+ * Write the mode: what an expired deadline DOES (`abort` refuses, `auto`
1309
+ * answers with the option the agent's own kinds imply, `wait` never
1310
+ * expires). Same write path as the window, so the host answer stays
1311
+ * the authority.
1312
+ */
1313
+ async setApprovalTimeoutMode(mode) {
1314
+ await this.writeApprovalDeadlines({ mode });
1315
+ }
1316
+ /** Drop the persisted override: the config file (or the built-in default) decides again. */
1317
+ async resetApprovalDeadlines() {
1318
+ await this.writeApprovalDeadlines({ reset: true });
1319
+ }
1320
+ async writeApprovalDeadlines(payload) {
1321
+ const previous = this.state.approvalDeadlines;
1322
+ // Optimistic only where it is safe: the effective numbers and the mode move
1323
+ // now, and the override facts (`configured`) stay as the host last reported
1324
+ // them. A failure restores the whole view.
1325
+ if (previous !== undefined) {
1326
+ this.mutate({
1327
+ approvalDeadlines: {
1328
+ ...previous,
1329
+ ...(payload.permissionTimeoutMs === undefined ? {} : { permissionTimeoutMs: payload.permissionTimeoutMs }),
1330
+ ...(payload.mode === undefined ? {} : { mode: payload.mode }),
1331
+ },
1332
+ });
1333
+ }
1334
+ try {
1335
+ const result = await this.call('ui/approval-deadlines-set', payload);
1336
+ this.mutate({ approvalDeadlines: result.deadlines });
1337
+ }
1338
+ catch (error) {
1339
+ this.mutate({ approvalDeadlines: previous });
1340
+ this.flashError(error);
1341
+ }
1342
+ }
1279
1343
  /**
1280
1344
  * Bind one DSH chat session to an ACP agent. This is the plugin's own
1281
1345
  * conversation↔agent mapping: it does NOT write a DSH model route, so it
@@ -1554,6 +1618,16 @@ export class AcpStore {
1554
1618
  this.mutate({ permissions: this.state.permissions.filter(p => p.requestId !== event.requestId) });
1555
1619
  return Promise.resolve();
1556
1620
  }
1621
+ case 'permission/synced': {
1622
+ // Terminator of the reconnect replay: the host just re-sent every
1623
+ // outstanding ask, and this set is the authoritative live list. Drop
1624
+ // anything this client still holds from before the disconnect — an ask
1625
+ // resolved while it was away is gone host-side, and answering it would
1626
+ // only earn `unknown_request`.
1627
+ const live = new Set(event.requestIds);
1628
+ this.mutate({ permissions: this.state.permissions.filter(p => live.has(p.requestId)) });
1629
+ return Promise.resolve();
1630
+ }
1557
1631
  case 'chat/agent-locked': {
1558
1632
  // The host locks a conversation at the START of its first ACP turn and
1559
1633
  // says so at once; this frame is what makes the hand-off animation run