dsh-ntr 0.1.5 → 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/lib/client.js CHANGED
@@ -14,6 +14,36 @@ window.__ModuleLoader__.load({
14
14
  };
15
15
  //#endregion
16
16
  //#region src/client/store.ts
17
+ /** Returned by {@link deadlineRemainingLabel} once the deadline has passed. */
18
+ const PERMISSION_DEADLINE_EXPIRED = "expired";
19
+ /**
20
+ * The asks that belong to one DSH chat conversation. An ask without a
21
+ * `dshSessionId` belongs to no conversation (an `/acp` RPC-only session), so it
22
+ * is deliberately absent from every conversation's dock and stays in the
23
+ * settings-panel ledger.
24
+ */
25
+ function permissionsForSession(permissions, dshSessionId) {
26
+ return permissions.filter((permission) => permission.dshSessionId === dshSessionId);
27
+ }
28
+ /**
29
+ * Compact countdown for one ask, or `undefined` when there is nothing to count
30
+ * down (`wait` mode, or a frame without a deadline). Pure so the formatting is
31
+ * testable without a clock: the dock passes `Date.now()` on its tick.
32
+ *
33
+ * Past the deadline the answer is {@link PERMISSION_DEADLINE_EXPIRED} rather
34
+ * than a negative number — the host has already armed the expiry, and a card
35
+ * counting backwards would claim a deadline that no longer exists.
36
+ */
37
+ function deadlineRemainingLabel(deadlineAt, now, mode) {
38
+ if (mode === "wait" || deadlineAt === void 0) return void 0;
39
+ const remainingMs = deadlineAt - now;
40
+ if (remainingMs <= 0) return PERMISSION_DEADLINE_EXPIRED;
41
+ const seconds = Math.ceil(remainingMs / 1e3);
42
+ if (seconds < 60) return `${String(seconds)}s`;
43
+ const minutes = Math.floor(seconds / 60);
44
+ const rest = seconds % 60;
45
+ return rest === 0 ? `${String(minutes)}m` : `${String(minutes)}m ${String(rest)}s`;
46
+ }
17
47
  const MAX_BLOCKS = 400;
18
48
  /** How many observed ACP transcripts stay live at once (insertion-ordered eviction). */
19
49
  const MAX_WATCHED = 6;
@@ -284,7 +314,6 @@ window.__ModuleLoader__.load({
284
314
  seatCatalogs: {},
285
315
  probingModels: {},
286
316
  probeErrors: {},
287
- approvalRouted: false,
288
317
  chatSessions: [],
289
318
  tagHalo: { ...DEFAULT_TAG_HALO },
290
319
  approvalDeadlines: void 0,
@@ -470,7 +499,7 @@ window.__ModuleLoader__.load({
470
499
  /** Snapshot refresh used on panel open and after a stream outage. */
471
500
  async resync() {
472
501
  try {
473
- const [{ agents, approvalRouted }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
502
+ const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
474
503
  this.call("agents/list"),
475
504
  this.call("sessions/list"),
476
505
  this.call("default/agent").catch(() => ({})),
@@ -486,7 +515,6 @@ window.__ModuleLoader__.load({
486
515
  sessions,
487
516
  defaultAgent: defaultAgentInfo?.agentId,
488
517
  detectedAgents,
489
- approvalRouted: approvalRouted === true,
490
518
  preset: presetResult?.preset ?? void 0,
491
519
  tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
492
520
  approvalDeadlines: deadlinesResult.deadlines
@@ -726,9 +754,10 @@ window.__ModuleLoader__.load({
726
754
  async loadTranscript(sessionId) {
727
755
  try {
728
756
  const { session, entries } = await this.call("transcript/get", { sessionId });
757
+ const seen = this.lastSeq.get(sessionId) ?? 0;
758
+ if ((entries.at(-1)?.seq ?? 0) < seen) return;
729
759
  const blocks = [];
730
760
  for (const entry of entries) {
731
- blocks.length;
732
761
  foldUpdate(blocks, entry.update);
733
762
  this.lastSeq.set(sessionId, Math.max(this.lastSeq.get(sessionId) ?? 0, entry.seq));
734
763
  }
@@ -1112,6 +1141,14 @@ window.__ModuleLoader__.load({
1112
1141
  this.flashError(error);
1113
1142
  }
1114
1143
  }
1144
+ /**
1145
+ * Answer one ask (`optionId` absent = cancel it). A failure is reported
1146
+ * through the store's notice — the settings panel renders that — AND
1147
+ * rethrown, so the conversation dock can pin the message next to the card
1148
+ * that failed instead of leaving it on a settings page the user may not
1149
+ * have open. Callers that cannot show it (the settings panel) swallow it
1150
+ * after the notice.
1151
+ */
1115
1152
  async respondPermission(requestId, optionId) {
1116
1153
  try {
1117
1154
  await this.call("permission/respond", {
@@ -1120,6 +1157,7 @@ window.__ModuleLoader__.load({
1120
1157
  });
1121
1158
  } catch (error) {
1122
1159
  this.flashError(error);
1160
+ throw error;
1123
1161
  }
1124
1162
  }
1125
1163
  async stopAgent(agentId) {
@@ -1160,7 +1198,7 @@ window.__ModuleLoader__.load({
1160
1198
  }
1161
1199
  }
1162
1200
  /**
1163
- * Write one approval deadline (settings card), or drop the persisted override
1201
+ * Write the approval deadline (settings card), or drop the persisted override
1164
1202
  * with `reset` so the `cordis.patch.yml` baseline applies again.
1165
1203
  *
1166
1204
  * The host answer — the same route returns the deadlines now in force — is the
@@ -1168,8 +1206,17 @@ window.__ModuleLoader__.load({
1168
1206
  * merged with a value this browser did not know about) self-corrects instead
1169
1207
  * of drifting from what runs.
1170
1208
  */
1171
- async setApprovalDeadline(key, ms) {
1172
- await this.writeApprovalDeadlines({ [key]: ms });
1209
+ async setApprovalDeadline(ms) {
1210
+ await this.writeApprovalDeadlines({ permissionTimeoutMs: ms });
1211
+ }
1212
+ /**
1213
+ * Write the mode: what an expired deadline DOES (`abort` refuses, `auto`
1214
+ * answers with the option the agent's own kinds imply, `wait` never
1215
+ * expires). Same write path as the window, so the host answer stays
1216
+ * the authority.
1217
+ */
1218
+ async setApprovalTimeoutMode(mode) {
1219
+ await this.writeApprovalDeadlines({ mode });
1173
1220
  }
1174
1221
  /** Drop the persisted override: the config file (or the built-in default) decides again. */
1175
1222
  async resetApprovalDeadlines() {
@@ -1179,8 +1226,8 @@ window.__ModuleLoader__.load({
1179
1226
  const previous = this.state.approvalDeadlines;
1180
1227
  if (previous !== void 0) this.mutate({ approvalDeadlines: {
1181
1228
  ...previous,
1182
- ...payload.nativeApprovalTimeoutMs === void 0 ? {} : { nativeApprovalTimeoutMs: payload.nativeApprovalTimeoutMs },
1183
- ...payload.permissionFallbackTimeoutMs === void 0 ? {} : { permissionFallbackTimeoutMs: payload.permissionFallbackTimeoutMs }
1229
+ ...payload.permissionTimeoutMs === void 0 ? {} : { permissionTimeoutMs: payload.permissionTimeoutMs },
1230
+ ...payload.mode === void 0 ? {} : { mode: payload.mode }
1184
1231
  } });
1185
1232
  try {
1186
1233
  const result = await this.call("ui/approval-deadlines-set", payload);
@@ -1427,13 +1474,17 @@ window.__ModuleLoader__.load({
1427
1474
  return this.loadAgentCatalog(event.agentId);
1428
1475
  }
1429
1476
  case "permission/request": {
1430
- const title = typeof event.toolCall?.title === "string" ? String(event.toolCall.title) : this.locale("工具调用", "tool call");
1431
- this.mutate({ permissions: [...this.state.permissions.filter((p) => p.requestId !== event.requestId), {
1477
+ const permission = {
1432
1478
  requestId: event.requestId,
1433
1479
  sessionId: event.sessionId,
1434
- title,
1435
- options: event.options
1436
- }] });
1480
+ ...event.dshSessionId === void 0 ? {} : { dshSessionId: event.dshSessionId },
1481
+ toolName: event.toolName,
1482
+ ...event.reason === void 0 ? {} : { reason: event.reason },
1483
+ options: event.options,
1484
+ timeoutMode: event.timeoutMode,
1485
+ ...event.deadlineAt === void 0 ? {} : { deadlineAt: event.deadlineAt }
1486
+ };
1487
+ this.mutate({ permissions: [...this.state.permissions.filter((p) => p.requestId !== event.requestId), permission] });
1437
1488
  return Promise.resolve();
1438
1489
  }
1439
1490
  case "permission/resolved":
@@ -2370,9 +2421,9 @@ window.__ModuleLoader__.load({
2370
2421
  .ntr-acp-seat:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
2371
2422
  .ntr-acp-seat:disabled { opacity: 0.5; cursor: default; }
2372
2423
  .ntr-acp-seat.is-open { border-color: var(--dsw-alias-state-business-primary, #2563eb); color: var(--dsw-alias-state-business-primary, #2563eb); }
2373
- .ntr-acp-seat.is-detached { border-color: var(--ntr-warn, #d97706); color: var(--ntr-warn, #d97706); }
2424
+ .ntr-acp-seat.is-detached { border-color: var(--dsw-alias-state-warn-label, #d97706); color: var(--dsw-alias-state-warn-label, #d97706); }
2374
2425
  .ntr-acp-seat-main { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2375
- .ntr-acp-seat-sub { color: var(--dsw-alias-text-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2426
+ .ntr-acp-seat-sub { color: var(--dsw-alias-label-secondary, #6b7280); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2376
2427
  .ntr-acp-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
2377
2428
  .ntr-acp-tick { margin-left: auto; color: var(--dsw-alias-state-business-primary, #2563eb); }
2378
2429
  .ntr-acp-check { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 13px; height: 13px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: 3px; font-size: 10px; line-height: 1; color: var(--dsw-alias-state-business-primary, #2563eb); }
@@ -2381,80 +2432,108 @@ window.__ModuleLoader__.load({
2381
2432
  .ntr-acp-menu-head { padding: 8px 10px 6px; font-weight: 600; display: flex; align-items: center; gap: 6px; }
2382
2433
  .ntr-acp-menu-title { flex: 1; }
2383
2434
  .ntr-acp-menu-spin { opacity: 0.5; }
2384
- .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); line-height: 1.5; }
2385
- .ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: #fee2e2; color: #b91c1c; font-size: 11px; }
2435
+ .ntr-acp-menu-note { padding: 0 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); line-height: 1.5; }
2436
+ .ntr-acp-menu-error { margin: 0 10px 6px; padding: 6px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 11px; }
2386
2437
  .ntr-acp-menu-body { overflow: auto; padding: 0 4px 6px; }
2387
2438
  .ntr-acp-menu-section { display: flex; flex-direction: column; padding-top: 4px; }
2388
- .ntr-acp-menu-section-title { padding: 6px 8px 4px; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); }
2439
+ .ntr-acp-menu-section-title { padding: 6px 8px 4px; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); }
2389
2440
  .ntr-acp-menu-item { display: flex; align-items: center; gap: 6px; font: inherit; font-size: 12px; text-align: left; padding: 6px 8px; border: none; border-radius: 6px; background: none; color: inherit; cursor: pointer; width: 100%; }
2390
2441
  .ntr-acp-menu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
2391
2442
  .ntr-acp-menu-item:disabled { opacity: 0.45; cursor: default; }
2392
2443
  .ntr-acp-menu-item.is-active { font-weight: 600; }
2393
2444
  .ntr-acp-menu-item-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2394
- .ntr-acp-menu-item-detail { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; }
2395
- .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-text-secondary, #6b7280); }
2445
+ .ntr-acp-menu-item-detail { color: var(--dsw-alias-label-secondary, #6b7280); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; text-align: right; }
2446
+ .ntr-acp-menu-empty { padding: 10px; text-align: center; color: var(--dsw-alias-label-secondary, #6b7280); }
2396
2447
  /* ---- ACP view tab ---- */
2397
2448
  .ntr-acp-view { display: flex; flex-direction: column; height: 100%; min-height: 0; font-size: 13px; line-height: 1.6; }
2398
- .ntr-acp-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2, #e5e7eb); font-size: 12px; color: var(--dsw-alias-text-secondary, #6b7280); }
2449
+ .ntr-acp-view-head { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-bottom: 1px solid var(--dsw-alias-border-l2, #e5e7eb); font-size: 12px; color: var(--dsw-alias-label-secondary, #6b7280); }
2399
2450
  .ntr-acp-view-agent { font-weight: 600; color: inherit; }
2400
2451
  .ntr-acp-view-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40%; direction: rtl; text-align: left; }
2401
2452
  .ntr-acp-view-head .ntr-acp-more { margin-left: auto; }
2402
2453
  .ntr-acp-stream { flex: 1; min-height: 0; overflow: auto; padding: 12px 14px 24px; display: flex; flex-direction: column; gap: 10px; }
2403
2454
  .ntr-acp-badge { font-size: 11px; padding: 1px 8px; border-radius: 999px; border: 1px solid transparent; }
2404
- .ntr-acp-badge.is-done { color: #15803d; background: #dcfce7; }
2405
- .ntr-acp-badge.is-busy { color: #1d4ed8; background: #dbeafe; }
2406
- .ntr-acp-badge.is-warn { color: #b45309; background: #fef3c7; }
2407
- .ntr-acp-badge.is-fail { color: #b91c1c; background: #fee2e2; }
2408
- .ntr-acp-badge.is-idle { color: var(--dsw-alias-text-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
2455
+ .ntr-acp-badge.is-done { color: color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 72%, var(--dsw-alias-label-primary, #111827)); background: var(--dsw-alias-state-success-tertiary, #dcfce7); }
2456
+ .ntr-acp-badge.is-busy { color: var(--dsw-alias-state-business-primary, #1d4ed8); background: var(--dsw-alias-state-business-tertiary, #dbeafe); }
2457
+ .ntr-acp-badge.is-warn { color: var(--dsw-alias-state-warn-label, #b45309); background: var(--dsw-alias-state-warn-tertiary, #fef3c7); }
2458
+ .ntr-acp-badge.is-fail { color: var(--dsw-alias-state-error-primary, #b91c1c); background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); }
2459
+ .ntr-acp-badge.is-idle { color: var(--dsw-alias-label-secondary, #6b7280); background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
2409
2460
  .ntr-acp-bubble { max-width: 88%; padding: 8px 12px; border-radius: 12px; white-space: pre-wrap; word-break: break-word; }
2410
2461
  .ntr-acp-bubble.is-user { align-self: flex-end; background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 12%, transparent); }
2411
2462
  .ntr-acp-bubble.is-agent { align-self: flex-start; background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
2412
2463
  .ntr-acp-inline-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; padding: 1px 4px; border-radius: 4px; background: color-mix(in srgb, currentColor 10%, transparent); }
2413
2464
  .ntr-acp-code { margin: 6px 0; padding: 8px 10px; border-radius: 8px; background: #0f172a; color: #e2e8f0; overflow: auto; font-size: 12px; line-height: 1.5; }
2414
- .ntr-acp-thought { border-left: 3px solid var(--dsw-alias-border-l2, #d1d5db); padding-left: 8px; color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
2465
+ .ntr-acp-thought { border-left: 3px solid var(--dsw-alias-border-l2, #d1d5db); padding-left: 8px; color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12px; }
2415
2466
  .ntr-acp-thought > summary { cursor: pointer; font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; }
2416
2467
  .ntr-acp-thought-body { white-space: pre-wrap; padding: 4px 0 2px; }
2417
2468
  .ntr-acp-tool { border: 1px solid var(--dsw-alias-border-l2, #e5e7eb); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); overflow: hidden; }
2418
- .ntr-acp-tool.is-fail { border-color: #fca5a5; }
2469
+ .ntr-acp-tool.is-fail { border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
2419
2470
  .ntr-acp-tool-head { display: flex; align-items: center; gap: 8px; padding: 7px 10px; font-size: 12px; }
2420
2471
  .ntr-acp-tool-title { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2421
2472
  .ntr-acp-tool-body { margin: 0; padding: 8px 10px; border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); background: var(--dsw-alias-bg-layer-2, #f9fafb); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 340px; overflow: auto; }
2422
- .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-text-primary, #111827); max-height: 240px; }
2423
- .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
2473
+ .ntr-acp-tool-input { background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #111827); max-height: 240px; }
2474
+ .ntr-acp-tool-meta { font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
2424
2475
  .ntr-acp-diff { border-top: 1px solid var(--dsw-alias-border-l2, #e5e7eb); }
2425
2476
  .ntr-acp-diff-head { display: flex; align-items: center; gap: 8px; padding: 5px 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; background: var(--dsw-alias-bg-layer-2, #f9fafb); }
2426
2477
  .ntr-acp-diff-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2427
- .ntr-acp-diff-stats .is-add { color: #15803d; }
2428
- .ntr-acp-diff-stats .is-del { color: #b91c1c; }
2478
+ .ntr-acp-diff-stats .is-add { color: var(--dsw-alias-state-success-primary, #15803d); }
2479
+ .ntr-acp-diff-stats .is-del { color: var(--dsw-alias-state-error-primary, #b91c1c); }
2429
2480
  .ntr-acp-diff-body { max-height: 360px; overflow: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; line-height: 1.55; }
2430
2481
  .ntr-acp-diff-row { display: flex; gap: 8px; padding: 0 10px; white-space: pre; }
2431
- .ntr-acp-diff-row.is-add { background: #dcfce7; }
2432
- .ntr-acp-diff-row.is-del { background: #fee2e2; }
2433
- .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-text-secondary, #6b7280); }
2482
+ .ntr-acp-diff-row.is-add { background: var(--dsw-alias-file-diff-added-bg, #dcfce7); }
2483
+ .ntr-acp-diff-row.is-del { background: var(--dsw-alias-file-diff-deleted-bg, #fee2e2); }
2484
+ .ntr-acp-diff-row.is-ctx { color: var(--dsw-alias-label-secondary, #6b7280); }
2434
2485
  .ntr-acp-diff-sign { width: 1ch; flex: none; opacity: 0.7; }
2435
2486
  .ntr-acp-diff-text { overflow-x: auto; }
2436
- .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-text-secondary, #6b7280); }
2487
+ .ntr-acp-diff-note { padding: 4px 10px 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); }
2437
2488
  .ntr-acp-more { font: inherit; font-size: 11.5px; padding: 3px 10px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: inherit; cursor: pointer; }
2438
2489
  .ntr-acp-more:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
2439
2490
  .ntr-acp-more:disabled { opacity: 0.5; cursor: default; }
2440
- .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-button-primary-invert, #fff); }
2491
+ .ntr-acp-more.is-primary { border-color: transparent; background: var(--dsw-alias-button-primary-fill, #2563eb); color: var(--dsw-alias-label-primary-foreground, #fff); }
2441
2492
  .ntr-acp-tool > .ntr-acp-more, .ntr-acp-empty-actions .ntr-acp-more { margin: 6px 10px; }
2442
2493
  .ntr-acp-plan { border: 1px solid var(--dsw-alias-border-l2, #e5e7eb); border-radius: 10px; padding: 8px 10px; background: var(--dsw-alias-bg-layer-1, #fff); }
2443
- .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); margin-bottom: 4px; }
2494
+ .ntr-acp-plan-head { font-size: 11px; letter-spacing: 0.04em; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); margin-bottom: 4px; }
2444
2495
  .ntr-acp-plan-row { display: flex; align-items: baseline; gap: 8px; font-size: 12.5px; padding: 1px 0; }
2445
- .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-text-secondary, #6b7280); text-decoration: line-through; }
2496
+ .ntr-acp-plan-row.is-completed { color: var(--dsw-alias-label-secondary, #6b7280); text-decoration: line-through; }
2446
2497
  .ntr-acp-plan-row.is-in_progress { font-weight: 600; }
2447
2498
  .ntr-acp-plan-glyph { width: 1ch; flex: none; }
2448
- .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-text-secondary, #6b7280); }
2499
+ .ntr-acp-plan-prio { margin-left: auto; font-size: 10px; text-transform: uppercase; color: var(--dsw-alias-label-secondary, #6b7280); }
2449
2500
  .ntr-acp-notice { padding: 6px 10px; border-radius: 8px; font-size: 12px; }
2450
- .ntr-acp-notice.is-error { background: #fee2e2; color: #b91c1c; }
2451
- .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-text-secondary, #6b7280); }
2501
+ .ntr-acp-notice.is-error { background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); }
2502
+ .ntr-acp-notice.is-info { background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--dsw-alias-label-secondary, #6b7280); }
2452
2503
  .ntr-acp-empty { margin: auto; max-width: 460px; text-align: center; display: flex; flex-direction: column; gap: 10px; padding: 24px; }
2453
2504
  .ntr-acp-empty-title { font-size: 14px; font-weight: 600; }
2454
- .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
2455
- .ntr-acp-empty-hint { color: var(--dsw-alias-text-secondary, #6b7280); font-size: 12px; }
2456
- .ntr-acp-seat-diag { font-family: var(--dsw-alias-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
2505
+ .ntr-acp-empty-body { margin: 0; color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12.5px; line-height: 1.7; }
2506
+ .ntr-acp-empty-hint { color: var(--dsw-alias-label-secondary, #6b7280); font-size: 12px; }
2507
+ .ntr-acp-seat-diag { font-family: var(--ds-font-family-code, ui-monospace, SFMono-Regular, Menlo, monospace); opacity: 0.72; word-break: break-word; }
2457
2508
  .ntr-acp-empty-actions { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
2509
+ /* ---- Pending permission asks (conversation.input.dock) ---- */
2510
+ /* The dock slot is the full conversation column, but the composer card inside
2511
+ it is narrower (InputBar: side clearance + card max width). Mirror that
2512
+ formula so each ask lines up with the input box instead of spanning the
2513
+ column. The list owns a bounded height and scrolls so a burst of asks cannot
2514
+ push the composer off screen. */
2515
+ .ntr-permission-dock {
2516
+ box-sizing: border-box;
2517
+ width: calc(100% - var(--dsh-composer-side-clearance, 16px) - var(--dsh-composer-side-clearance, 16px));
2518
+ max-width: var(--dsh-composer-card-max-width, 920px);
2519
+ margin: 0 auto;
2520
+ display: flex; flex-direction: column; gap: 6px;
2521
+ max-height: min(38vh, 320px); overflow-y: auto;
2522
+ font-size: 12px; line-height: 1.5;
2523
+ }
2524
+ .ntr-permission-card { display: flex; flex-direction: column; gap: 6px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #fff); }
2525
+ .ntr-permission-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
2526
+ .ntr-permission-tool { flex: none; font-weight: 600; }
2527
+ .ntr-permission-reason { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #6b7280); }
2528
+ .ntr-permission-deadline { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary, #6b7280); font-variant-numeric: tabular-nums; }
2529
+ .ntr-permission-deadline.is-expired { color: var(--dsw-alias-state-warn-label, #b45309); font-weight: 600; }
2530
+ .ntr-permission-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
2531
+ .ntr-permission-option, .ntr-permission-cancel { font: inherit; font-size: 12px; line-height: 1.4; padding: 4px 10px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: inherit; cursor: pointer; white-space: normal; text-align: left; }
2532
+ .ntr-permission-option:hover:not(:disabled), .ntr-permission-cancel:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active, #0000000d); }
2533
+ .ntr-permission-option:disabled, .ntr-permission-cancel:disabled { opacity: 0.5; cursor: default; }
2534
+ .ntr-permission-option:focus-visible, .ntr-permission-cancel:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #2563eb); outline-offset: 2px; }
2535
+ .ntr-permission-cancel { margin-left: auto; color: var(--dsw-alias-label-secondary, #6b7280); }
2536
+ .ntr-permission-error { padding: 4px 8px; border-radius: 6px; background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #b91c1c) 12%, transparent); color: var(--dsw-alias-state-error-primary, #b91c1c); font-size: 11px; word-break: break-word; }
2458
2537
  /* ---- NTR settings dialog (shell.overlay) ---- */
2459
2538
  /* Frame-wide surface opened from the agent base's empty-roster menu. The
2460
2539
  overlay layer itself is click-through, so the layer opts back in and blocks
@@ -2491,11 +2570,11 @@ window.__ModuleLoader__.load({
2491
2570
  * React surface for ACP: the settings configuration section.
2492
2571
  *
2493
2572
  * Scope is deliberately narrow — this is a CONFIGURATION surface. Runtime
2494
- * interaction lives in the conversation: permission asks are answered in the
2495
- * chat composer through the host approval chain. The ledger card answers
2496
- * whatever did NOT reach the conversation — a host without the chain, a
2497
- * session with no chat identity behind it, or an ask whose native route
2498
- * degraded to unavailable (see `permissionLedgerCard`).
2573
+ * interaction lives in the conversation: every permission ask with a chat
2574
+ * identity is answered on a card above that conversation's composer
2575
+ * (`permission-dock.tsx`). The ledger card here is the cross-session mirror —
2576
+ * asks whose conversation is not open, or which have no chat identity at all —
2577
+ * and is where those are answered (see `permissionLedgerCard`).
2499
2578
  *
2500
2579
  * Each configured agent is one compact row (status + current model + badges)
2501
2580
  * that expands into the full connection form and model catalog. The row stays
@@ -2527,8 +2606,10 @@ window.__ModuleLoader__.load({
2527
2606
  --ntr-radius: 10px; --ntr-radius-sm: 7px;
2528
2607
  --ntr-font: 13px; --ntr-font-sm: 12px; --ntr-font-xs: 11px;
2529
2608
  --ntr-line: 1px solid var(--dsw-alias-border-l2, #e5e7eb);
2530
- --ntr-muted: var(--dsw-alias-text-secondary, #6b7280);
2531
- --ntr-danger: #dc2626; --ntr-warn: #b45309;
2609
+ --ntr-muted: var(--dsw-alias-label-secondary, #6b7280);
2610
+ --ntr-danger: var(--dsw-alias-state-error-primary, #dc2626);
2611
+ --ntr-warn: var(--dsw-alias-state-warn-label, #b45309);
2612
+ --ntr-success: color-mix(in srgb, var(--dsw-alias-state-success-primary, #16a34a) 72%, var(--dsw-alias-label-primary, #111827));
2532
2613
  --ntr-accent: var(--dsw-alias-button-primary-fill, #2563eb);
2533
2614
  width: 100%; max-width: 960px; box-sizing: border-box; margin: 0 auto;
2534
2615
  padding: 20px; overflow: auto; font-size: var(--ntr-font); line-height: 1.55;
@@ -2552,25 +2633,25 @@ window.__ModuleLoader__.load({
2552
2633
  }
2553
2634
  .dwa-btn:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); border-color: var(--dsw-alias-border-l1, #9ca3af); }
2554
2635
  .dwa-btn:disabled { opacity: 0.5; cursor: not-allowed; }
2555
- .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); border-color: transparent; font-weight: 550; }
2636
+ .dwa-btn-primary { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); border-color: transparent; font-weight: 550; }
2556
2637
  .dwa-btn-primary:hover { background: var(--ntr-accent); border-color: transparent; opacity: 0.88; }
2557
2638
  .dwa-btn-secondary { background: var(--dsw-alias-bg-layer-2, #f3f4f6); }
2558
- .dwa-btn-danger { color: var(--ntr-danger); border-color: #fca5a5; }
2559
- .dwa-btn-danger:hover { background: #fee2e2; }
2639
+ .dwa-btn-danger { color: var(--ntr-danger); border-color: var(--dsw-alias-state-error-secondary, #fca5a5); }
2640
+ .dwa-btn-danger:hover { background: var(--dsw-alias-interactive-bg-hover-danger, #fee2e2); }
2560
2641
  .dwa-section { margin-bottom: 24px; }
2561
2642
  .dwa-section-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
2562
2643
  .dwa-section-title { font-size: 14px; font-weight: 650; margin: 0; display: flex; align-items: center; gap: 8px; }
2563
2644
  .dwa-section-copy { margin: 3px 0 0; font-size: var(--ntr-font-xs); color: var(--ntr-muted); }
2564
2645
  .dwa-card { border: var(--ntr-line); border-radius: var(--ntr-radius); padding: 16px; background: var(--dsw-alias-bg-layer-1, #fff); font-size: var(--ntr-font); line-height: 1.55; }
2565
- .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); }
2646
+ .dwa-card-warn { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-tertiary, #fffbeb); }
2566
2647
  .dwa-mode-banner { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 5px 16px; align-items: start; margin-bottom: 24px; padding: 14px 16px; }
2567
2648
  .dwa-mode-title { display: flex; align-items: center; gap: 8px; font-weight: 650; }
2568
2649
  .dwa-mode-copy { grid-column: 1 / -1; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
2569
2650
  .dwa-mode-source { display: block; margin-top: 5px; overflow-wrap: anywhere; opacity: 0.85; }
2570
2651
  .dwa-badge { font-size: var(--ntr-font-xs); line-height: 1.4; border-radius: 999px; padding: 2px 8px; background: var(--dsw-alias-bg-layer-2, #f3f4f6); color: var(--ntr-muted); font-weight: 550; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }
2571
- .dwa-badge-success { background: #22c55e1c; color: #15803d; }
2572
- .dwa-badge-warn { background: #f59e0b1f; color: #b45309; }
2573
- .dwa-badge-info { background: #3b82f61a; color: #2563eb; }
2652
+ .dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
2653
+ .dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
2654
+ .dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
2574
2655
  .dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
2575
2656
  .dwa-agent-list { display: flex; flex-direction: column; gap: 8px; }
2576
2657
  .dwa-agent-card { border: var(--ntr-line); border-radius: var(--ntr-radius); background: var(--dsw-alias-bg-layer-1, #fff); overflow: visible; transition: border-color 0.15s ease; }
@@ -2592,7 +2673,7 @@ window.__ModuleLoader__.load({
2592
2673
  .dwa-subsection-copy { margin: 3px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
2593
2674
  .dwa-form-row { display: flex; gap: var(--ntr-gap-3); margin-bottom: 14px; align-items: flex-start; }
2594
2675
  .dwa-form-col { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
2595
- .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-text-primary, #374151); }
2676
+ .dwa-form-label { font-size: var(--ntr-font-sm); font-weight: 600; color: var(--dsw-alias-label-primary, #374151); }
2596
2677
  .dwa-hint { font-size: var(--ntr-font-xs); line-height: 1.55; color: var(--ntr-muted); }
2597
2678
  .dwa-error { font-size: var(--ntr-font-xs); color: var(--ntr-danger); }
2598
2679
  .dwa-input { min-height: 36px; font: inherit; font-size: var(--ntr-font-sm); padding: 7px 10px; border: 1px solid var(--dsw-alias-border-l2, #d1d5db); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); color: inherit; width: 100%; box-sizing: border-box; }
@@ -2617,7 +2698,7 @@ window.__ModuleLoader__.load({
2617
2698
  .dwa-add-content { border-top: var(--ntr-line); padding: 14px 16px 16px; }
2618
2699
  .dwa-chip { min-height: 30px; font: inherit; font-size: var(--ntr-font-sm); border: var(--ntr-line); border-radius: 999px; padding: 4px 12px; background: var(--dsw-alias-bg-layer-1, #fff); cursor: pointer; color: inherit; }
2619
2700
  .dwa-chip:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
2620
- .dwa-chip-active { border-color: var(--ntr-accent); background: #3b82f614; color: #2563eb; font-weight: 550; }
2701
+ .dwa-chip-active { border-color: var(--ntr-accent); background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 8%, transparent); color: var(--dsw-alias-state-business-primary, #2563eb); font-weight: 550; }
2621
2702
  .dwa-chip-list { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin: 0 0 12px; }
2622
2703
  .dwa-switch { display: inline-flex; align-items: center; gap: 7px; font-size: var(--ntr-font-sm); cursor: pointer; user-select: none; }
2623
2704
  .dwa-inline-setting { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-2, #f9fafb); }
@@ -2630,7 +2711,7 @@ window.__ModuleLoader__.load({
2630
2711
  /* Tall enough for a comfortable selection (about ten rows) before scrolling;
2631
2712
  the cap keeps the two stacked panes from pushing the rest of the card away. */
2632
2713
  .dwa-model-pane { min-width: 0; max-height: 360px; display: flex; flex-direction: column; border: var(--ntr-line); border-radius: var(--ntr-radius-sm); background: var(--dsw-alias-bg-layer-1, #fff); overflow: hidden; }
2633
- .dwa-model-pane-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: var(--ntr-line); font-size: var(--ntr-font-xs); font-weight: 600; color: var(--dsw-alias-text-secondary, #4b5563); }
2714
+ .dwa-model-pane-head { display: flex; align-items: center; gap: 6px; padding: 7px 9px; border-bottom: var(--ntr-line); font-size: var(--ntr-font-xs); font-weight: 600; color: var(--dsw-alias-label-secondary, #4b5563); }
2634
2715
  .dwa-model-pane-head .dwa-btn { margin-left: auto; min-height: 26px; padding: 2px 8px; font-size: var(--ntr-font-xs); }
2635
2716
  .dwa-model-pane-head .dwa-caret-btn { flex: none; width: 22px; height: 22px; background: transparent; }
2636
2717
  .dwa-model-pane-head .dwa-caret-btn:hover { background: var(--dsw-alias-interactive-bg-active, #e5e7eb); }
@@ -2648,7 +2729,7 @@ window.__ModuleLoader__.load({
2648
2729
  .dwa-model-empty { padding: 6px; }
2649
2730
  .dwa-model-row { display: flex; align-items: center; gap: 6px; width: 100%; border: none; background: none; padding: 5px 6px; border-radius: 5px; cursor: pointer; color: inherit; font: inherit; font-size: var(--ntr-font-sm); text-align: left; }
2650
2731
  .dwa-model-row:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
2651
- .dwa-model-row.is-on { background: #3b82f614; color: #2563eb; }
2732
+ .dwa-model-row.is-on { background: color-mix(in srgb, var(--dsw-alias-state-business-primary, #2563eb) 8%, transparent); color: var(--dsw-alias-state-business-primary, #2563eb); }
2652
2733
  .dwa-model-row-mark { width: 12px; flex: none; text-align: center; opacity: 0.75; }
2653
2734
  .dwa-model-row-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2654
2735
  /* Name and credit keep their room; the id is the only cell that collapses. */
@@ -2670,15 +2751,31 @@ window.__ModuleLoader__.load({
2670
2751
  .dwa-seg > button { min-height: 30px; font: inherit; font-size: var(--ntr-font-sm); color: inherit; border: none; background: var(--dsw-alias-bg-layer-1, #fff); padding: 4px 14px; cursor: pointer; }
2671
2752
  .dwa-seg > button + button { border-left: 1px solid var(--dsw-alias-border-l2, #d1d5db); }
2672
2753
  .dwa-seg > button:hover { background: var(--dsw-alias-interactive-bg-active, #f3f4f6); }
2673
- .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-button-primary-invert, #fff); font-weight: 550; }
2754
+ .dwa-seg > button.is-on { background: var(--ntr-accent); color: var(--dsw-alias-label-primary-foreground, #fff); font-weight: 550; }
2674
2755
  .dwa-preference-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 0; }
2675
2756
  .dwa-preference-row + .dwa-preference-row { border-top: var(--ntr-line); }
2757
+ /* A control that needs the whole card: the label sits above it, not beside
2758
+ it. A right-aligned segmented control squeezed its labels into two lines
2759
+ each (中/止, 自动推/进), which read as broken rather than as choices. */
2760
+ .dwa-preference-block { padding: 12px 0; }
2761
+ .dwa-preference-block + .dwa-preference-block { border-top: var(--ntr-line); }
2676
2762
  .dwa-preference-label { font-size: var(--ntr-font-sm); font-weight: 600; }
2677
2763
  .dwa-preference-copy { margin-top: 2px; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
2678
- .dwa-deadline-controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
2679
- .dwa-deadline-input { width: 76px; min-height: 30px; padding: 4px 8px; text-align: right; }
2680
- .dwa-deadline-input[aria-invalid="true"] { border-color: var(--ntr-danger); }
2681
- .dwa-deadline-unit { white-space: nowrap; }
2764
+ .dwa-seg-wide { display: flex; width: 100%; margin-top: 10px; }
2765
+ .dwa-seg-wide > button { flex: 1; }
2766
+ /* The wait window, as a slider: the number is the headline, the rungs are the
2767
+ scale under the track. */
2768
+ .dwa-deadline-value { margin-top: 8px; font-size: 20px; font-weight: 600; }
2769
+ .dwa-deadline-range { width: 100%; margin-top: 6px; accent-color: var(--ntr-accent); }
2770
+ .dwa-deadline-range:disabled { opacity: 0.4; }
2771
+ .dwa-deadline-ticks { display: flex; justify-content: space-between; margin-top: 2px; color: var(--ntr-muted); font-size: 10px; }
2772
+ .dwa-deadline-ticks > span { white-space: nowrap; }
2773
+ /* The facts that make a number here meaningless, in warning ink rather than
2774
+ the muted gray every other hint uses: the value the config file is
2775
+ outranking, on the row it belongs to. */
2776
+ .dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
2777
+ .dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
2778
+ .dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
2682
2779
  @media (max-width: 640px) {
2683
2780
  .dwa-root { padding: 14px; }
2684
2781
  .dwa-page-header { flex-direction: column; }
@@ -2688,6 +2785,11 @@ window.__ModuleLoader__.load({
2688
2785
  .dwa-model-search { flex: 1 1 auto; width: auto; }
2689
2786
  .dwa-subsection-head, .dwa-preference-row { align-items: flex-start; flex-direction: column; }
2690
2787
  .dwa-default-hint { align-items: flex-start; flex-direction: column; }
2788
+ /* The slider's scale: up to eight rungs with full unit words ("15 分钟")
2789
+ need a smaller face before they can collide on a narrow panel. The rung
2790
+ count follows the mode (自动推进 adds 不等待), and the row is a flex
2791
+ space-between, so a shorter scale just spreads wider. */
2792
+ .dwa-deadline-ticks { font-size: 9px; }
2691
2793
  }
2692
2794
  @media (prefers-reduced-motion: reduce) {
2693
2795
  .dwa-root *, .dwa-root *::before, .dwa-root *::after { transition: none !important; }
@@ -2708,19 +2810,19 @@ window.__ModuleLoader__.load({
2708
2810
  function stateVisual(state, acp) {
2709
2811
  switch (state) {
2710
2812
  case "ready": return {
2711
- color: "#22c55e",
2813
+ color: "var(--dsw-alias-state-success-primary, #22c55e)",
2712
2814
  text: acp.label("已连接", "Connected")
2713
2815
  };
2714
2816
  case "starting": return {
2715
- color: "#eab308",
2817
+ color: "var(--dsw-alias-state-warn-primary, #eab308)",
2716
2818
  text: acp.label("启动中", "Starting")
2717
2819
  };
2718
2820
  case "error": return {
2719
- color: "#ef4444",
2821
+ color: "var(--dsw-alias-state-error-primary, #ef4444)",
2720
2822
  text: acp.label("错误", "Error")
2721
2823
  };
2722
2824
  default: return {
2723
- color: "#9ca3af",
2825
+ color: "var(--dsw-alias-state-idle-primary, #9ca3af)",
2724
2826
  text: acp.label("已配置", "Configured"),
2725
2827
  title: acp.label("已配置,进程不常驻。在 DSH 聊天中首次使用时自动连接;展开区可用「测试连接」验证命令能否正常起来。", "Configured; the process is not kept resident. It connects automatically the first time a DSH chat turn uses it — use “Test connection” in the expanded panel to verify the command starts.")
2726
2828
  };
@@ -2893,139 +2995,270 @@ window.__ModuleLoader__.load({
2893
2995
  /**
2894
2996
  * The settings panel's permission-ledger rule.
2895
2997
  *
2896
- * A pending ask ALWAYS gets an answerable card here, whatever `approvalRouted`
2897
- * says. That flag reports that the host HAS an approval service — it does not
2898
- * promise THIS ask reached the conversation. An ask lands in `ui.permissions`
2899
- * only on the ledger path (`AgentBridge.handlePermissionRequest` broadcasts
2900
- * `permission/request` there and nowhere else), so a pending entry is by
2901
- * definition a question the conversation is not showing. The previous rule
2902
- * (`approvalRouted` → a read-only count, no buttons) hid the only answer
2903
- * entry exactly when the native route had degraded to unavailable, and the
2904
- * ask then hung until its timeout.
2905
- *
2906
- * `fellBackFromConversation` is the case the card has to explain: the host
2907
- * can route asks, and this one still did not make it.
2998
+ * Pending asks are shown in the conversation card while that conversation is
2999
+ * open; this card is the cross-session mirror for the ones it cannot show —
3000
+ * asks belonging to another conversation, and `/acp` RPC-only asks with no
3001
+ * chat identity at all. It therefore appears whenever ANY ask is pending: a
3002
+ * pending entry with no conversation card would otherwise have no answer
3003
+ * surface anywhere.
2908
3004
  */
2909
3005
  function permissionLedgerCard(ui) {
2910
- const visible = ui.permissions.length > 0;
2911
- return {
2912
- visible,
2913
- fellBackFromConversation: visible && ui.approvalRouted
2914
- };
3006
+ return { visible: ui.permissions.length > 0 };
2915
3007
  }
2916
3008
  /**
2917
- * One-click windows for an approval deadline, in ms. `0` is spelled out rather
2918
- * than hidden behind a big number because "never" is the value a cautious user
2919
- * picks first, and a preset row that cannot express it would make them type.
3009
+ * The ONE wait window, as the rungs the slider snaps to.
3010
+ *
3011
+ * Shortest wait first, and `0` (不等待) FIRST: the slider reads left to right
3012
+ * as "wait longer", and "do not wait at all" is the shortest wait there is.
3013
+ * Never expiring is NOT on this scale — that is the `wait` MODE's business,
3014
+ * and having 「一直等待」 in both places made the card say the same thing twice
3015
+ * in two ways.
3016
+ *
3017
+ * A window the config file names that is not on this ladder (45 seconds, say)
3018
+ * still runs as configured: the card prints the exact number and parks the
3019
+ * thumb on the nearest rung, so the slider never pretends a number the host
3020
+ * is not using.
2920
3021
  */
2921
- const DEADLINE_PRESETS = [
3022
+ const DEADLINE_LADDER = [
2922
3023
  0,
2923
3024
  3e4,
2924
3025
  6e4,
2925
3026
  3e5,
2926
- 9e5
3027
+ 9e5,
3028
+ 18e5,
3029
+ 36e5,
3030
+ 216e5
3031
+ ];
3032
+ /**
3033
+ * The shortest REAL wait — the first rung above 不等待.
3034
+ *
3035
+ * Hand-copied from the host half (`APPROVAL_DEADLINE_MIN_WAIT_MS` in
3036
+ * `src/impl.ts`): the browser half may not import the host module, so the
3037
+ * equivalence is pinned by a test instead (see `tests/acp-view.spec.ts`).
3038
+ */
3039
+ const DEADLINE_MIN_WAIT_MS = 3e4;
3040
+ /**
3041
+ * The window the card shows under a mode.
3042
+ *
3043
+ * 不等待 (`0`) is the **auto** mode's rung: it answers with the option the
3044
+ * agent's own kinds imply, so the agent CONTINUES. `abort` has no such rung —
3045
+ * refusing every ask the instant it arrives stops the agent at every gate it
3046
+ * meets, which is not a timeout but a wall. So under `abort` the window floors
3047
+ * to the shortest real wait, and the card never shows a zero it cannot run.
3048
+ *
3049
+ * `undefined` (the `wait` mode) stays undefined: there is no window at all.
3050
+ */
3051
+ function deadlineValueFor(mode, ms) {
3052
+ if (ms === void 0) return void 0;
3053
+ return mode === "abort" ? Math.max(ms, DEADLINE_MIN_WAIT_MS) : ms;
3054
+ }
3055
+ /**
3056
+ * The rungs a mode offers.
3057
+ *
3058
+ * 不等待 (`0`) is the **auto** mode's rung: it answers with the option the
3059
+ * agent's own kinds imply, so the agent CONTINUES. `abort` has no such rung —
3060
+ * refusing every ask the instant it arrives stops the agent at every gate it
3061
+ * meets, which is not a timeout but a wall. So `abort` starts at the shortest
3062
+ * real wait.
3063
+ */
3064
+ function deadlineLadder(mode) {
3065
+ return mode === "auto" ? DEADLINE_LADDER : DEADLINE_LADDER.filter((ms) => ms > 0);
3066
+ }
3067
+ /**
3068
+ * The rung the slider shows for a window. Exact hits keep their rung; anything
3069
+ * else takes the nearest one. A tie goes to the SHORTER wait: the thumb must
3070
+ * never promise more patience than the window has.
3071
+ */
3072
+ function deadlineSliderIndex(ms, mode = "auto") {
3073
+ const ladder = deadlineLadder(mode);
3074
+ const exact = ladder.indexOf(ms);
3075
+ if (exact >= 0) return exact;
3076
+ let best = 0;
3077
+ let bestDistance = Number.POSITIVE_INFINITY;
3078
+ for (let i = 0; i < ladder.length; i++) {
3079
+ const distance = Math.abs(ms - ladder[i]);
3080
+ if (distance < bestDistance) {
3081
+ bestDistance = distance;
3082
+ best = i;
3083
+ }
3084
+ }
3085
+ return best;
3086
+ }
3087
+ /** The three modes, in the order a user reads them: refuse, answer, wait. */
3088
+ const DEADLINE_MODES = [
3089
+ "abort",
3090
+ "auto",
3091
+ "wait"
2927
3092
  ];
2928
- /** Both editable windows, for the loops that treat them alike. */
2929
- const DEADLINE_FIELDS = ["nativeApprovalTimeoutMs", "permissionFallbackTimeoutMs"];
2930
- /** `5 分钟` / `30 秒` / `关` — the readout and the preset label, one formatter. */
3093
+ /** `5 分钟` / `30 秒` / `不等待` — the readout and the tick label, one formatter. */
2931
3094
  function deadlineText(ms, acp) {
2932
- if (ms <= 0) return acp.label("关", "Off");
3095
+ if (ms <= 0) return acp.label("不等待", "No wait");
2933
3096
  const seconds = Math.round(ms / 1e3);
2934
3097
  if (seconds < 60) return acp.label(`${String(seconds)} 秒`, `${String(seconds)}s`);
2935
3098
  const minutes = Math.round(seconds / 60);
2936
- return acp.label(`${String(minutes)} 分`, `${String(minutes)}m`);
3099
+ if (minutes < 60) return acp.label(`${String(minutes)} 分钟`, `${String(minutes)}m`);
3100
+ const hours = Math.round(minutes / 60);
3101
+ return acp.label(`${String(hours)} 小时`, `${String(hours)}h`);
3102
+ }
3103
+ /** The mode's label — one formatter, so the picker and the override note agree. */
3104
+ function modeText(mode, acp) {
3105
+ if (mode === "abort") return acp.label("超时中止", "Abort");
3106
+ if (mode === "auto") return acp.label("自动推进", "Auto");
3107
+ return acp.label("一直等待", "Wait");
2937
3108
  }
2938
3109
  /**
2939
- * One editable deadline: presets for the windows people actually pick, plus a
2940
- * free-form seconds field for everything else.
3110
+ * The mode block: what happens when nobody answers.
3111
+ *
3112
+ * Three values and no more — refuse, answer, wait — so the picker is a
3113
+ * segmented control, not a dropdown. The buttons stretch across the whole
3114
+ * card: a right-aligned segmented control squeezed the labels into two lines
3115
+ * each (中/止, 自动推/进), which read as broken rather than as choices.
2941
3116
  *
2942
- * The field is a DRAFT, not the value: it only commits a parseable whole number
2943
- * of seconds (0 allowed), and an unparseable draft reverts on blur instead of
2944
- * writing garbage the host would refuse. The host answer is what the row then
2945
- * shows, so a value the host trims never lingers in the UI.
3117
+ * The copy under the buttons says what the SELECTED mode does, in one
3118
+ * sentence — the labels alone ("超时中止") cannot. `auto` carries a warning of
3119
+ * its own: it approves things nobody looked at.
2946
3120
  */
2947
- function DeadlineRow(props) {
2948
- const { acp, field, label, copy, value } = props;
2949
- const [draft, setDraft] = (0, react.useState)(void 0);
2950
- const text = draft ?? String(Math.round(value / 1e3));
2951
- const parsed = /^\d+$/.test(text.trim()) ? Number(text.trim()) * 1e3 : void 0;
2952
- const commit = () => {
2953
- const next = parsed;
2954
- setDraft(void 0);
2955
- if (next !== void 0 && next !== value) acp.setApprovalDeadline(field, next);
2956
- };
3121
+ function DeadlineModeBlock(props) {
3122
+ const { acp, mode, override } = props;
3123
+ const copy = mode === "wait" ? acp.label("一直等待应答,不设超时时间。", "Wait until you answer — no deadline.") : mode === "auto" ? acp.label("超过设定时间未应答,自动代替你选择一个选项让 agent 继续执行。", "Past the deadline, an option is chosen for you and the agent continues.") : acp.label("超过设定时间未应答,自动拒绝该操作,agent 继续执行。", "Past the deadline, the operation is refused and the agent continues.");
2957
3124
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2958
- className: "dwa-preference-row",
2959
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2960
- className: "dwa-preference-label",
2961
- children: label
2962
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2963
- className: "dwa-preference-copy",
2964
- children: copy
2965
- })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2966
- className: "dwa-deadline-controls",
2967
- children: [
2968
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2969
- className: "dwa-seg",
2970
- children: DEADLINE_PRESETS.map((preset) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2971
- type: "button",
2972
- className: value === preset ? "is-on" : "",
2973
- title: deadlineText(preset, acp),
2974
- onClick: () => void acp.setApprovalDeadline(field, preset),
2975
- children: deadlineText(preset, acp)
2976
- }, preset))
2977
- }),
2978
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2979
- className: "dwa-input dwa-deadline-input",
2980
- type: "text",
2981
- inputMode: "numeric",
2982
- "aria-label": label,
2983
- "aria-invalid": parsed === void 0,
2984
- value: text,
2985
- onChange: (event) => setDraft(event.target.value),
2986
- onBlur: commit,
2987
- onKeyDown: (event) => {
2988
- if (event.key === "Enter") commit();
2989
- }
2990
- }),
2991
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2992
- className: "dwa-hint dwa-deadline-unit",
2993
- children: acp.label("秒", "sec")
2994
- })
2995
- ]
2996
- })]
3125
+ className: "dwa-preference-block",
3126
+ children: [
3127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3128
+ className: "dwa-preference-label",
3129
+ children: acp.label("超时处理方式", "When nobody answers")
3130
+ }),
3131
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3132
+ className: "dwa-seg dwa-seg-wide",
3133
+ children: DEADLINE_MODES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3134
+ type: "button",
3135
+ className: mode === option ? "is-on" : "",
3136
+ onClick: () => void acp.setApprovalTimeoutMode(option),
3137
+ children: modeText(option, acp)
3138
+ }, option))
3139
+ }),
3140
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3141
+ className: "dwa-preference-copy",
3142
+ children: copy
3143
+ }),
3144
+ mode === "auto" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3145
+ className: "dwa-deadline-warn",
3146
+ children: acp.label("请确保你的 agent 值得信任,请谨慎选择。", "Only pick this if you trust your agent — choose it carefully.")
3147
+ }),
3148
+ override !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3149
+ className: "dwa-deadline-flag",
3150
+ children: ["⚠ ", override]
3151
+ })
3152
+ ]
2997
3153
  });
2998
3154
  }
2999
3155
  /**
3156
+ * The duration block: how long to wait before the deadline fires.
3157
+ *
3158
+ * ONE slider, because there is ONE window: an ask in the conversation and an
3159
+ * ask in the settings ledger are the same question, so they get the same
3160
+ * answer. The slider's rungs are the windows people actually pick; a window
3161
+ * the config file names outside them still runs as configured (the readout
3162
+ * prints the exact number and the thumb parks on the nearest rung).
3163
+ *
3164
+ * `value === undefined` is the `wait` mode: there is no deadline at all, so
3165
+ * the scale has nothing to say — the slider is not drawn, and the readout
3166
+ * names the state that is actually in force. The stored window is NOT touched
3167
+ * by that (the mode write carries only the mode), so switching back restores
3168
+ * it.
3169
+ */
3170
+ function DeadlineSliderBlock(props) {
3171
+ const { acp, value, mode, override } = props;
3172
+ const effective = deadlineValueFor(mode, value);
3173
+ const ladder = deadlineLadder(mode);
3174
+ const index = effective === void 0 ? 0 : deadlineSliderIndex(effective, mode);
3175
+ const copy = effective === void 0 ? acp.label("选择「一直等待」时不设超时时长。", "“Wait” sets no deadline.") : effective <= 0 ? acp.label("不设等待时间,请求到达即按上方所选的方式处理。", "No wait: the ask is settled on arrival, by whatever you picked above.") : acp.label("超过该时长仍未应答,将按上方所选的方式处理。", "Past this, whatever you picked above is what happens.");
3176
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3177
+ className: "dwa-preference-block",
3178
+ children: [
3179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3180
+ className: "dwa-preference-label",
3181
+ children: acp.label("等待时长", "How long to wait")
3182
+ }),
3183
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3184
+ className: "dwa-deadline-value",
3185
+ children: effective === void 0 ? acp.label("一直等待", "Always") : deadlineText(effective, acp)
3186
+ }),
3187
+ effective !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3188
+ className: "dwa-deadline-range",
3189
+ type: "range",
3190
+ min: 0,
3191
+ max: ladder.length - 1,
3192
+ step: 1,
3193
+ value: index,
3194
+ "aria-label": acp.label("等待时长", "How long to wait"),
3195
+ onChange: (event) => {
3196
+ const next = ladder[Number(event.target.value)];
3197
+ if (next !== void 0 && next !== effective) acp.setApprovalDeadline(next);
3198
+ }
3199
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3200
+ className: "dwa-deadline-ticks",
3201
+ children: ladder.map((rung) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: deadlineText(rung, acp) }, rung))
3202
+ })] }),
3203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3204
+ className: "dwa-preference-copy",
3205
+ children: copy
3206
+ }),
3207
+ override !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3208
+ className: "dwa-deadline-flag",
3209
+ children: ["⚠ ", override]
3210
+ })
3211
+ ]
3212
+ });
3213
+ }
3214
+ /**
3215
+ * The facts the card has to show, decided from what the host reported.
3216
+ *
3217
+ * The card carries TWO controls, and they are orthogonal: one window (how
3218
+ * long) and one mode (what happens). An override can sit on either of them,
3219
+ * so each is decided on its own.
3220
+ *
3221
+ * `overridden` is what gates the reset button. A baseline that AGREES with the
3222
+ * running value is not an override, and offering a reset for it would be a
3223
+ * button that does nothing (observed live).
3224
+ */
3225
+ function approvalDeadlineFacts(deadlines) {
3226
+ const configured = deadlines.configured?.permissionTimeoutMs;
3227
+ const baseline = configured === void 0 || configured === deadlines.permissionTimeoutMs ? void 0 : configured;
3228
+ const modeBaseline = deadlines.configuredMode !== void 0 && deadlines.configuredMode !== deadlines.mode ? deadlines.configuredMode : void 0;
3229
+ return {
3230
+ baseline,
3231
+ modeBaseline,
3232
+ overridden: baseline !== void 0 || modeBaseline !== void 0
3233
+ };
3234
+ }
3235
+ /**
3000
3236
  * The approval-deadline card.
3001
3237
  *
3002
- * Both windows are about the SAME failure — an ask nobody answered — reached
3003
- * from two different routes, which is exactly why they need an explanation
3004
- * rather than two bare sliders: the first bounds a host that took the question
3005
- * into the composer and never rendered it, the second bounds an ask that never
3006
- * reached the conversation at all. Neither is an answer on the user's behalf
3007
- * being hidden from them: withdrawing re-asks on the ledger, and the fallback
3008
- * says what it did in a notice.
3238
+ * TWO controls, and they are orthogonal: one window (how long) and one mode
3239
+ * (what happens). One window covers both places an ask can wait — the
3240
+ * conversation's approval card and the settings panel's ledger — because they
3241
+ * ask the same question and the answer should not differ by where the card
3242
+ * happens to be drawn.
3243
+ *
3244
+ * The two controls are ONE system, and the card says so in a sentence: the
3245
+ * window says how long to wait, the mode says what happens when nobody
3246
+ * answered. That sentence is the thing the previous three-row layout kept
3247
+ * missing.
3009
3248
  *
3010
- * `configured` and `permissionTimeoutMs` are reported by the host only when
3011
- * they are actually in play, and both are shown here rather than swallowed:
3012
- * a user tuning a number that something else is overriding deserves to be told.
3249
+ * The facts that make a number here meaningless are shown, not swallowed: the
3250
+ * deployment baseline this card outranks. Each rides on the control it belongs
3251
+ * to, and the reset button appears only when something is actually being
3252
+ * outranked.
3013
3253
  */
3014
3254
  function ApprovalDeadlinesCard(props) {
3015
3255
  const { acp, ui } = props;
3016
3256
  const deadlines = ui.approvalDeadlines;
3017
3257
  if (deadlines === void 0) return null;
3018
- const configured = deadlines.configured ?? {};
3019
- const inForce = {
3020
- nativeApprovalTimeoutMs: deadlines.nativeApprovalTimeoutMs,
3021
- permissionFallbackTimeoutMs: deadlines.permissionFallbackTimeoutMs
3022
- };
3023
- const overrideNote = (field) => {
3024
- const baseline = configured[field];
3025
- if (baseline === void 0 || baseline === inForce[field]) return void 0;
3026
- return acp.label(`cordis.patch.yml 里是 ${deadlineText(baseline, acp)},设置页的值优先。`, `cordis.patch.yml asks for ${deadlineText(baseline, acp)}; the value set here wins.`);
3027
- };
3028
- const overridden = DEADLINE_FIELDS.some((field) => overrideNote(field) !== void 0);
3258
+ const facts = approvalDeadlineFacts(deadlines);
3259
+ const baselineNote = facts.baseline === void 0 ? void 0 : acp.label(`配置文件(cordis.patch.yml)设为 ${deadlineText(facts.baseline, acp)},此处设置优先。`, `The config file (cordis.patch.yml) asks for ${deadlineText(facts.baseline, acp)}; the value set here wins.`);
3260
+ const modeNote = facts.modeBaseline === void 0 ? void 0 : acp.label(`配置文件(cordis.patch.yml)设为「${modeText(facts.modeBaseline, acp)}」,此处设置优先。`, `The config file (cordis.patch.yml) asks for "${modeText(facts.modeBaseline, acp)}"; the value set here wins.`);
3261
+ const waitMode = deadlines.mode === "wait";
3029
3262
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
3030
3263
  className: "dwa-section",
3031
3264
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -3035,7 +3268,7 @@ window.__ModuleLoader__.load({
3035
3268
  children: acp.label("审批时限", "Approval deadlines")
3036
3269
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3037
3270
  className: "dwa-section-copy",
3038
- children: acp.label("没人答复的权限询问多久后由插件接手。两者都不是替你批准:撤回后会改到设置页再问一次,代答前会先发一条说明。", "How long an unanswered permission ask waits before the plugin takes over. Neither approves on your behalf: a withdrawal re-asks in Settings, and an auto-answer announces itself first.")
3271
+ children: acp.label("权限请求的等待时长与超时处理方式。", "How long to wait for a permission request, and what happens when nobody answers.")
3039
3272
  })] })
3040
3273
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3041
3274
  className: "dwa-card",
@@ -3044,46 +3277,25 @@ window.__ModuleLoader__.load({
3044
3277
  paddingBottom: 4
3045
3278
  },
3046
3279
  children: [
3047
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeadlineRow, {
3280
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeadlineModeBlock, {
3048
3281
  acp,
3049
- field: "nativeApprovalTimeoutMs",
3050
- label: acp.label("对话审批撤回窗口", "Composer approval withdrawal"),
3051
- copy: acp.label("宿主把询问接进对话后,多久没出现就撤回并改到设置页问。会渲染的接管是立即出现的,所以这个期限只为「收了却不渲染」而响。", "How long an ask the host took into the composer may stay invisible before it is withdrawn and re-asked in Settings. A takeover that renders does so immediately, so this only fires for one that never showed up."),
3052
- value: deadlines.nativeApprovalTimeoutMs
3282
+ mode: deadlines.mode,
3283
+ override: modeNote
3053
3284
  }),
3054
- overrideNote("nativeApprovalTimeoutMs") !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3055
- className: "dwa-hint",
3056
- style: { paddingBottom: 8 },
3057
- children: overrideNote("nativeApprovalTimeoutMs")
3058
- }),
3059
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeadlineRow, {
3285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeadlineSliderBlock, {
3060
3286
  acp,
3061
- field: "permissionFallbackTimeoutMs",
3062
- label: acp.label("降级询问代答窗口", "Degraded-ask auto-answer"),
3063
- copy: acp.label("询问没能进入对话时(审批服务不可用,或已被撤回),多久后答复 cancelled。", "How long an ask that never reached the conversation (approval service unavailable, or withdrawn above) waits before it is answered cancelled."),
3064
- value: deadlines.permissionFallbackTimeoutMs
3065
- }),
3066
- overrideNote("permissionFallbackTimeoutMs") !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3067
- className: "dwa-hint",
3068
- style: { paddingBottom: 8 },
3069
- children: overrideNote("permissionFallbackTimeoutMs")
3287
+ value: waitMode ? void 0 : deadlines.permissionTimeoutMs,
3288
+ mode: deadlines.mode,
3289
+ override: baselineNote
3070
3290
  }),
3071
- deadlines.permissionTimeoutMs !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3072
- className: "dwa-hint",
3073
- style: { paddingBottom: 8 },
3074
- children: acp.label(`注意:cordis.patch.yml 里 permissionTimeoutMs = ${deadlineText(deadlines.permissionTimeoutMs, acp)},它一视同仁,覆盖上面两个窗口。`, `Note: cordis.patch.yml sets permissionTimeoutMs = ${deadlineText(deadlines.permissionTimeoutMs, acp)}, which governs every path alike and overrides both windows above.`)
3075
- }),
3076
- overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3077
- className: "dwa-preference-row",
3078
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3079
- className: "dwa-preference-copy",
3080
- children: acp.label("设置页的值写在插件自己的状态文件里,重启后仍然有效;重置后回到 cordis.patch.yml 的配置。", "A value set here lives in the plugin's own state file and survives a restart; resetting returns to whatever cordis.patch.yml asks for.")
3081
- }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3291
+ facts.overridden && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3292
+ className: "dwa-deadline-reset",
3293
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3082
3294
  type: "button",
3083
- className: "dwa-btn",
3295
+ className: "dwa-btn dwa-btn-secondary",
3084
3296
  onClick: () => void acp.resetApprovalDeadlines(),
3085
3297
  children: acp.label("恢复配置文件的值", "Use config file value")
3086
- })]
3298
+ })
3087
3299
  })
3088
3300
  ]
3089
3301
  })]
@@ -3121,7 +3333,7 @@ window.__ModuleLoader__.load({
3121
3333
  children: [
3122
3334
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3123
3335
  className: "dwa-dot",
3124
- style: { background: ui.stream === "live" ? "#22c55e" : ui.stream === "down" ? "#ef4444" : "#9ca3af" },
3336
+ style: { background: ui.stream === "live" ? "var(--dsw-alias-state-success-primary, #22c55e)" : ui.stream === "down" ? "var(--dsw-alias-state-error-primary, #ef4444)" : "var(--dsw-alias-state-idle-primary, #9ca3af)" },
3125
3337
  title: ui.stream
3126
3338
  }),
3127
3339
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: streamText(ui.stream, acp) }),
@@ -3149,14 +3361,14 @@ window.__ModuleLoader__.load({
3149
3361
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3150
3362
  style: {
3151
3363
  fontWeight: 600,
3152
- marginBottom: 8
3364
+ marginBottom: 4
3153
3365
  },
3154
3366
  children: acp.label("待处理权限请求", "Pending Permission Requests")
3155
3367
  }),
3156
- ledger.fellBackFromConversation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3368
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3157
3369
  className: "dwa-hint",
3158
3370
  style: { marginBottom: 8 },
3159
- children: acp.label("这些询问没有进入对话(审批服务本次不可用、问题从未到达对话,或会话没有对话身份),请在此回答。", "These asks did not reach the conversation (the approval service was unavailable, the question never reached the conversation, or the session has no chat identity) — answer them here.")
3371
+ children: acp.label("对话打开时在那边的卡片上回答;这里兜底显示其他会话的询问。", "Answer in the conversation card while that conversation is open; this list covers the asks whose conversation is not.")
3160
3372
  }),
3161
3373
  ui.permissions.map((permission) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3162
3374
  style: {
@@ -3167,21 +3379,31 @@ window.__ModuleLoader__.load({
3167
3379
  flexWrap: "wrap"
3168
3380
  },
3169
3381
  children: [
3170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3171
3383
  style: {
3172
3384
  flex: 1,
3173
3385
  minWidth: 160
3174
3386
  },
3175
- children: permission.title
3387
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3388
+ style: { fontWeight: 600 },
3389
+ children: permission.toolName
3390
+ }), permission.reason !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3391
+ style: { color: "var(--ntr-muted)" },
3392
+ children: [" · ", permission.reason]
3393
+ })]
3176
3394
  }),
3177
3395
  permission.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3178
3396
  className: "dwa-btn dwa-btn-primary",
3179
- onClick: () => void acp.respondPermission(permission.requestId, option.optionId),
3397
+ onClick: () => {
3398
+ acp.respondPermission(permission.requestId, option.optionId).catch(() => void 0);
3399
+ },
3180
3400
  children: option.name
3181
3401
  }, option.optionId)),
3182
3402
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3183
3403
  className: "dwa-btn",
3184
- onClick: () => void acp.respondPermission(permission.requestId),
3404
+ onClick: () => {
3405
+ acp.respondPermission(permission.requestId).catch(() => void 0);
3406
+ },
3185
3407
  children: acp.label("拒绝", "Dismiss")
3186
3408
  })
3187
3409
  ]
@@ -3871,7 +4093,7 @@ window.__ModuleLoader__.load({
3871
4093
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
3872
4094
  className: "dwa-input-mono",
3873
4095
  style: {
3874
- background: "#0000000a",
4096
+ background: "var(--dsw-alias-markdown-inline-code, #0000000a)",
3875
4097
  padding: "2px 6px",
3876
4098
  borderRadius: 4
3877
4099
  },
@@ -3927,7 +4149,13 @@ window.__ModuleLoader__.load({
3927
4149
  }),
3928
4150
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3929
4151
  className: idleInvalid ? "dwa-error" : "dwa-hint",
3930
- children: idleInvalid ? acp.label("必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。", "Must be 0 (never release) or an integer in [1000, 86400000] ms.") : acp.label("没有进行中的回合或待答审批时,空闲超过此时长就挂起进程(会话保留,下一条消息自动重启并恢复);0 = 永不释放;有回合在跑、取消尚未结算或审批待答的进程不会被释放。此窗口只对承载过会话的进程生效:保存后拉起的模型探测进程(面板显示「已连接」的那个)固定 5 分钟释放,填 0 也拦不住。", "With no turn in flight or approval awaiting an answer, the process is parked once it has been idle this long (sessions are kept and resume on the next message); 0 = never; a process with a running turn, an unsettled cancel, or a pending approval is never released. This window applies only to a process that has hosted a session: the model-probe process (the one the panel shows as \"connected\" after a save) is always released after 5 minutes, and 0 does not stop it.")
4152
+ children: idleInvalid ? acp.label("必须是 0(永不释放)或 [1000, 86400000] 之间的整数毫秒。", "Must be 0 (never release) or an integer in [1000, 86400000] ms.") : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4153
+ acp.label("空闲超过此时长就挂起进程(会话保留,下一条消息自动恢复)。", "The process is parked once it has been idle this long (the session is kept and resumes on the next message)."),
4154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
4155
+ acp.label("0 = 永不释放;回合在跑、取消未结算或审批待答时不释放。", "0 = never; a process with a running turn, an unsettled cancel, or a pending approval is never released."),
4156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}),
4157
+ acp.label("只对承载过会话的进程生效;探测进程固定 5 分钟释放,填 0 也拦不住。", "This window applies only to a process that has hosted a session; the model-probe process is always released after 5 minutes, and 0 does not stop it.")
4158
+ ] })
3931
4159
  })
3932
4160
  ]
3933
4161
  })
@@ -4064,7 +4292,7 @@ window.__ModuleLoader__.load({
4064
4292
  fontSize: 12,
4065
4293
  whiteSpace: "pre-wrap",
4066
4294
  wordBreak: "break-all",
4067
- color: testResult.ok ? "#16a34a" : "var(--ntr-danger)"
4295
+ color: testResult.ok ? "var(--ntr-success)" : "var(--ntr-danger)"
4068
4296
  },
4069
4297
  children: [testResult.ok ? "✓ " : "✕ ", testResult.text]
4070
4298
  })
@@ -4453,7 +4681,7 @@ window.__ModuleLoader__.load({
4453
4681
  onChange: (e) => void toggleAutoDiscover(e.target.checked),
4454
4682
  style: {
4455
4683
  margin: 0,
4456
- accentColor: "#2563eb"
4684
+ accentColor: "var(--dsw-alias-state-business-primary, #2563eb)"
4457
4685
  }
4458
4686
  }), acp.label("自动发现新模型", "Auto-discover new models")]
4459
4687
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -4931,10 +5159,10 @@ window.__ModuleLoader__.load({
4931
5159
  * the entry point for conversations no agent has served.
4932
5160
  */
4933
5161
  const STATE_COLORS = {
4934
- ready: "#22c55e",
4935
- starting: "#eab308",
4936
- error: "#ef4444",
4937
- stopped: "#9ca3af"
5162
+ ready: "var(--dsw-alias-state-success-primary, #22c55e)",
5163
+ starting: "var(--dsw-alias-state-warn-primary, #eab308)",
5164
+ error: "var(--dsw-alias-state-error-primary, #ef4444)",
5165
+ stopped: "var(--dsw-alias-state-idle-primary, #9ca3af)"
4938
5166
  };
4939
5167
  function agentStateText(acp, state) {
4940
5168
  switch (state) {
@@ -6308,6 +6536,130 @@ window.__ModuleLoader__.load({
6308
6536
  });
6309
6537
  }
6310
6538
  //#endregion
6539
+ //#region src/client/permission-dock.tsx
6540
+ /**
6541
+ * Pending ACP permission asks for the staged DSH conversation, rendered as
6542
+ * composer-width entries above the composer card (`conversation.input.dock`).
6543
+ * The slot itself spans the conversation column; the dock mirrors the host's
6544
+ * InputBar width formula so the cards line up with the input box.
6545
+ *
6546
+ * The host broadcasts every ask as `permission/request`, carrying the DSH chat
6547
+ * session id when the ask belongs to a conversation. This dock is the primary
6548
+ * answer surface: it renders exactly the asks whose `dshSessionId` is the
6549
+ * conversation it was mounted for, and nothing otherwise. There is no
6550
+ * ACP-mode gate — the frame set itself is the gate, so an ordinary
6551
+ * conversation shows this card if and only if it actually has an ask.
6552
+ *
6553
+ * The ask's description and options are the agent's own: buttons appear in the
6554
+ * agent's order with the agent's names (including "Always Allow"), and
6555
+ * `kind` is not rendered. A separate cancel button answers with no option,
6556
+ * which the host forwards as `cancelled`. No Enter/Escape handlers: the
6557
+ * composer owns its keyboard, and these are ordinary focusable buttons.
6558
+ *
6559
+ * Multiple asks stack in ARRIVAL order, oldest first: the question ignored
6560
+ * longest stays at the top. The list is capped in height by CSS and scrolls.
6561
+ * Asks without a chat identity never appear here — they stay in the
6562
+ * settings-panel ledger, the cross-session mirror.
6563
+ */
6564
+ /** Tick interval for the countdown (seconds are all the label shows). */
6565
+ const COUNTDOWN_TICK_MS = 1e3;
6566
+ function PermissionDock({ acp, sessionId }) {
6567
+ ensureAcpStyle();
6568
+ const ui = useUi$2(acp);
6569
+ const dshSessionId = String(sessionId);
6570
+ const pending = permissionsForSession(ui.permissions, dshSessionId);
6571
+ const counting = pending.some((ask) => ask.timeoutMode !== "wait" && ask.deadlineAt !== void 0);
6572
+ const [now, setNow] = (0, react.useState)(() => Date.now());
6573
+ (0, react.useEffect)(() => {
6574
+ if (!counting) return;
6575
+ setNow(Date.now());
6576
+ const timer = setInterval(() => {
6577
+ setNow(Date.now());
6578
+ }, COUNTDOWN_TICK_MS);
6579
+ return () => {
6580
+ clearInterval(timer);
6581
+ };
6582
+ }, [counting]);
6583
+ if (pending.length === 0) return null;
6584
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6585
+ className: "ntr-permission-dock",
6586
+ role: "region",
6587
+ "aria-label": acp.label("待处理的权限请求", "Pending permission requests"),
6588
+ children: pending.map((permission) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PermissionAskCard, {
6589
+ acp,
6590
+ permission,
6591
+ now
6592
+ }, permission.requestId))
6593
+ });
6594
+ }
6595
+ function PermissionAskCard(props) {
6596
+ const { acp, permission, now } = props;
6597
+ const [busy, setBusy] = (0, react.useState)(false);
6598
+ const [error, setError] = (0, react.useState)();
6599
+ const remaining = deadlineRemainingLabel(permission.deadlineAt, now, permission.timeoutMode);
6600
+ const expired = remaining === PERMISSION_DEADLINE_EXPIRED;
6601
+ const respond = (optionId) => {
6602
+ if (busy) return;
6603
+ setBusy(true);
6604
+ setError(void 0);
6605
+ acp.respondPermission(permission.requestId, optionId).catch((failure) => {
6606
+ setError(failure instanceof Error ? failure.message : String(failure));
6607
+ }).finally(() => {
6608
+ setBusy(false);
6609
+ });
6610
+ };
6611
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6612
+ className: "ntr-permission-card",
6613
+ children: [
6614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6615
+ className: "ntr-permission-head",
6616
+ children: [
6617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6618
+ className: "ntr-permission-tool",
6619
+ children: permission.toolName
6620
+ }),
6621
+ permission.reason !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6622
+ className: "ntr-permission-reason",
6623
+ title: permission.reason,
6624
+ children: permission.reason
6625
+ }),
6626
+ remaining !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6627
+ className: `ntr-permission-deadline${expired ? " is-expired" : ""}`,
6628
+ children: expired ? acp.label("已超时", "expired") : acp.label(`剩余 ${remaining}`, `${remaining} left`)
6629
+ })
6630
+ ]
6631
+ }),
6632
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6633
+ className: "ntr-permission-actions",
6634
+ children: [permission.options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6635
+ type: "button",
6636
+ className: "ntr-permission-option",
6637
+ disabled: busy,
6638
+ title: option.name,
6639
+ onClick: () => {
6640
+ respond(option.optionId);
6641
+ },
6642
+ children: option.name
6643
+ }, option.optionId)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6644
+ type: "button",
6645
+ className: "ntr-permission-cancel",
6646
+ disabled: busy,
6647
+ title: acp.label("取消这次询问(不选择任何选项)", "Cancel this ask without choosing an option"),
6648
+ onClick: () => {
6649
+ respond();
6650
+ },
6651
+ children: acp.label("取消", "Cancel")
6652
+ })]
6653
+ }),
6654
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
6655
+ className: "ntr-permission-error",
6656
+ role: "alert",
6657
+ children: error
6658
+ })
6659
+ ]
6660
+ });
6661
+ }
6662
+ //#endregion
6311
6663
  //#region src/client/model-seat.tsx
6312
6664
  /** Exact key of the composer's model seat (a `single`, session-scoped slot). */
6313
6665
  const MODEL_SEAT_KEY = "conversation.input.model";
@@ -6968,6 +7320,12 @@ window.__ModuleLoader__.load({
6968
7320
  order: 5,
6969
7321
  inject: (sessionId) => chatFace(sessionId)
6970
7322
  }, AcpComposerAgentTag)), "dsh-ntr: composer agent tag");
7323
+ ctx.effect(() => ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
7324
+ name: "conversation.input.dock",
7325
+ id: "acp-permissions",
7326
+ order: 10,
7327
+ inject: (sessionId) => chatFace(sessionId)
7328
+ }, PermissionDock)), "dsh-ntr: conversation permission dock");
6971
7329
  /**
6972
7330
  * The client runtime's session list store, read structurally. Used to find
6973
7331
  * the session the composer is staged on and whether it runs the ACP preset —