viveworker 0.1.10 → 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/web/app.js CHANGED
@@ -25,6 +25,7 @@ const state = {
25
25
  inboxSubtab: "pending",
26
26
  timelineThreadFilter: "all",
27
27
  timelineKindFilter: "all",
28
+ providerFilter: "all",
28
29
  timelineKindFilterOpen: false,
29
30
  diffThreadFilter: "all",
30
31
  completedThreadFilter: "all",
@@ -42,6 +43,7 @@ const state = {
42
43
  detailDiffExpanded: {},
43
44
  choiceLocalDrafts: {},
44
45
  completionReplyDrafts: {},
46
+ pendingActionUrls: new Set(),
45
47
  pairError: "",
46
48
  pairNotice: "",
47
49
  pushStatus: null,
@@ -69,6 +71,7 @@ const app = document.querySelector("#app");
69
71
  const params = new URLSearchParams(window.location.search);
70
72
  const initialItem = params.get("item") || "";
71
73
  const initialPairToken = params.get("pairToken") || "";
74
+ const initialFocusPending = params.get("focusPending") || "";
72
75
  let didReloadForServiceWorker = false;
73
76
  let lastViewportMode = isDesktopLayout();
74
77
 
@@ -138,6 +141,15 @@ async function boot() {
138
141
  await consumePendingNotificationIntent();
139
142
  await syncDetectedLocalePreference();
140
143
  await refreshAuthenticatedState();
144
+ // `?focusPending=claude` marks this tab as the Claude-hook-opened popup:
145
+ // auto-navigate to the newest unresolved Claude pending (plan/question)
146
+ // detail view — but only when the user is not already in the middle of
147
+ // answering another pending item. Handled by `maybeAutoFocusClaudePending`
148
+ // both on boot and on every polling refresh below.
149
+ if (initialFocusPending === "claude" && !state.currentItem) {
150
+ state.claudePopupMode = true;
151
+ }
152
+ maybeAutoFocusClaudePending();
141
153
  ensureCurrentSelection();
142
154
  await renderShell();
143
155
 
@@ -150,6 +162,7 @@ async function boot() {
150
162
  return;
151
163
  }
152
164
  await refreshAuthenticatedState();
165
+ maybeAutoFocusClaudePending();
153
166
  if (!shouldDeferRenderForActiveInteraction()) {
154
167
  await renderShell();
155
168
  }
@@ -362,9 +375,17 @@ function ensureCurrentSelection() {
362
375
  const allEntries = allSelectableEntries();
363
376
  const preferredEntries = listEntriesForCurrentTab();
364
377
  const previousItem = state.currentItem ? { ...state.currentItem } : null;
365
- const hasCurrent = state.currentItem
366
- ? allEntries.some((entry) => isSameItemRef(state.currentItem, entry.item))
367
- : false;
378
+ const currentEntry = state.currentItem
379
+ ? allEntries.find((entry) => isSameItemRef(state.currentItem, entry.item))
380
+ : null;
381
+ const currentStatus = currentEntry?.status || null;
382
+ if (state.currentItem && currentStatus && state.currentItemStatus && currentStatus !== state.currentItemStatus) {
383
+ // Status transitioned (e.g. pending → completed because PC user answered).
384
+ // Drop the cached detail so the next render fetches the updated view.
385
+ state.currentDetail = null;
386
+ }
387
+ state.currentItemStatus = currentStatus;
388
+ const hasCurrent = Boolean(currentEntry);
368
389
  const hasCurrentInPreferred = state.currentItem
369
390
  ? preferredEntries.some((entry) => isSameItemRef(state.currentItem, entry.item))
370
391
  : false;
@@ -405,6 +426,59 @@ function allInboxEntries() {
405
426
  ];
406
427
  }
407
428
 
429
+ function pickNewestClaudePendingItem() {
430
+ const pending = Array.isArray(state.inbox?.pending) ? state.inbox.pending : [];
431
+ let best = null;
432
+ let bestTs = -Infinity;
433
+ for (const item of pending) {
434
+ if (normalizeProviderClient(item?.provider) !== "claude") continue;
435
+ const kind = normalizeClientText(item?.kind);
436
+ if (kind !== "approval" && kind !== "question") continue;
437
+ const ts = Number(item?.createdAtMs) || 0;
438
+ if (ts > bestTs) {
439
+ best = item;
440
+ bestTs = ts;
441
+ }
442
+ }
443
+ return best;
444
+ }
445
+
446
+ // True when the user is currently viewing the detail of an item that still
447
+ // exists in `state.inbox.pending` — i.e. they are actively answering it. In
448
+ // that case we must not yank them to a different pending.
449
+ function isViewingUnresolvedPendingItem() {
450
+ if (!state.currentItem || !state.detailOpen) return false;
451
+ const pending = Array.isArray(state.inbox?.pending) ? state.inbox.pending : [];
452
+ return pending.some(
453
+ (item) =>
454
+ normalizeClientText(item?.kind) === normalizeClientText(state.currentItem?.kind) &&
455
+ normalizeClientText(item?.token) === normalizeClientText(state.currentItem?.token)
456
+ );
457
+ }
458
+
459
+ // Claude-hook popup mode: auto-navigate to the newest unresolved Claude
460
+ // pending item whenever a new one appears, but only when the user is idle on
461
+ // the list/completed view (so we never disturb an in-progress answer).
462
+ function maybeAutoFocusClaudePending() {
463
+ if (!state.claudePopupMode) return;
464
+ const newest = pickNewestClaudePendingItem();
465
+ if (!newest) return;
466
+ const ts = Number(newest.createdAtMs) || 0;
467
+ if (ts <= (state.lastSeenClaudePendingTs || 0)) return;
468
+ // Preserve the user's current answer-in-progress view. Do NOT record
469
+ // `lastSeenClaudePendingTs` here, so the next polling cycle re-evaluates
470
+ // once the user finishes their current item.
471
+ if (isViewingUnresolvedPendingItem()) return;
472
+ state.lastSeenClaudePendingTs = ts;
473
+ state.currentItem = { kind: newest.kind, token: newest.token };
474
+ state.currentTab = tabForItemKind(newest.kind, state.currentTab);
475
+ if (state.currentTab === "inbox") {
476
+ state.inboxSubtab = inboxSubtabForItemKind(newest.kind);
477
+ }
478
+ state.detailOpen = true;
479
+ syncCurrentItemUrl(state.currentItem);
480
+ }
481
+
408
482
  function allTimelineEntries() {
409
483
  if (!state.timeline?.entries) {
410
484
  return [];
@@ -445,7 +519,25 @@ function listInboxEntries() {
445
519
  if (state.inboxSubtab === "completed") {
446
520
  return filteredCompletedEntries().map((item) => ({ item, status: "completed" }));
447
521
  }
448
- return state.inbox.pending.map((item) => ({ item, status: "pending" }));
522
+ return state.inbox.pending
523
+ .filter((item) => entryMatchesProviderFilter(item))
524
+ .map((item) => ({ item, status: "pending" }));
525
+ }
526
+
527
+ function normalizeProviderClient(value) {
528
+ const normalized = String(value || "").toLowerCase();
529
+ return normalized === "claude" ? "claude" : "codex";
530
+ }
531
+
532
+ function providerDisplayName(provider) {
533
+ return L(normalizeProviderClient(provider) === "claude" ? "common.claude" : "common.codex");
534
+ }
535
+
536
+ function entryMatchesProviderFilter(item) {
537
+ if (!state.providerFilter || state.providerFilter === "all") {
538
+ return true;
539
+ }
540
+ return normalizeProviderClient(item?.provider) === state.providerFilter;
449
541
  }
450
542
 
451
543
  function filteredTimelineEntries() {
@@ -457,6 +549,7 @@ function filteredTimelineEntries() {
457
549
  if (state.timelineThreadFilter && state.timelineThreadFilter !== "all") {
458
550
  filtered = filtered.filter((entry) => entry.threadId === state.timelineThreadFilter);
459
551
  }
552
+ filtered = filtered.filter((entry) => entryMatchesProviderFilter(entry));
460
553
  if (!state.timelineKindFilter || state.timelineKindFilter === "all") {
461
554
  return filtered;
462
555
  }
@@ -468,10 +561,11 @@ function filteredCompletedEntries() {
468
561
  if (!entries.length) {
469
562
  return [];
470
563
  }
564
+ let filtered = entries.filter((entry) => entryMatchesProviderFilter(entry));
471
565
  if (!state.completedThreadFilter || state.completedThreadFilter === "all") {
472
- return entries;
566
+ return filtered;
473
567
  }
474
- return entries.filter((entry) => entry.threadId === state.completedThreadFilter);
568
+ return filtered.filter((entry) => entry.threadId === state.completedThreadFilter);
475
569
  }
476
570
 
477
571
  function filteredDiffEntries() {
@@ -1088,7 +1182,11 @@ function syncCompletionReplyComposerLiveState(replyForm, draft) {
1088
1182
  if (submitButton) {
1089
1183
  submitButton.disabled = normalizedDraft.sending === true || !normalizeClientText(normalizedDraft.text);
1090
1184
  if (!normalizedDraft.sending) {
1091
- submitButton.textContent = L(normalizedDraft.confirmOverride ? "reply.sendConfirm" : "reply.send");
1185
+ const providerAttr = replyForm.getAttribute("data-provider") || "";
1186
+ submitButton.textContent = L(
1187
+ normalizedDraft.confirmOverride ? "reply.sendConfirm" : "reply.send",
1188
+ { provider: providerDisplayName(providerAttr) },
1189
+ );
1092
1190
  }
1093
1191
  }
1094
1192
 
@@ -1347,10 +1445,11 @@ function buildActionOutcomeDetail({ kind, title, message }) {
1347
1445
  };
1348
1446
  }
1349
1447
 
1350
- function approvalOutcomeMessage(actionUrl) {
1448
+ function approvalOutcomeMessage(actionUrl, provider) {
1449
+ const vars = { provider: providerDisplayName(provider) };
1351
1450
  return /\/accept$/u.test(String(actionUrl || ""))
1352
- ? L("server.message.approvalAccepted")
1353
- : L("server.message.approvalRejected");
1451
+ ? L("server.message.approvalAccepted", vars)
1452
+ : L("server.message.approvalRejected", vars);
1354
1453
  }
1355
1454
 
1356
1455
  function renderDesktopWorkspace(detail) {
@@ -1456,6 +1555,7 @@ function renderListPanel({ tab, entries, desktop }) {
1456
1555
  function renderInboxPanel({ entries, desktop }) {
1457
1556
  const meta = tabMeta("inbox");
1458
1557
  const subtabControls = renderInboxSubtabs();
1558
+ const providerFilterHtml = renderProviderFilter();
1459
1559
  const threadFilterHtml = state.inboxSubtab === "completed" ? renderCompletedThreadDropdown() : "";
1460
1560
  const bodyHtml = entries.length
1461
1561
  ? `<div class="card-list ${desktop ? "card-list--desktop" : ""}">
@@ -1471,6 +1571,7 @@ function renderInboxPanel({ entries, desktop }) {
1471
1571
  <span class="count-chip">${entries.length}</span>
1472
1572
  </div>
1473
1573
  ${subtabControls}
1574
+ ${providerFilterHtml}
1474
1575
  ${threadFilterHtml}
1475
1576
  ${bodyHtml}
1476
1577
  </div>
@@ -1488,6 +1589,7 @@ function renderInboxPanel({ entries, desktop }) {
1488
1589
  </div>
1489
1590
  <p class="screen-copy">${escapeHtml(meta.description)}</p>
1490
1591
  ${subtabControls}
1592
+ ${providerFilterHtml}
1491
1593
  ${threadFilterHtml}
1492
1594
  ${bodyHtml}
1493
1595
  </div>
@@ -1543,6 +1645,49 @@ function renderInboxEmptyState() {
1543
1645
  `;
1544
1646
  }
1545
1647
 
1648
+ function providerBadgeMeta(provider) {
1649
+ const normalized = normalizeProviderClient(provider);
1650
+ if (normalized === "claude") {
1651
+ return { id: "claude", label: L("common.claude"), glyph: "C" };
1652
+ }
1653
+ return { id: "codex", label: L("common.codex"), glyph: "X" };
1654
+ }
1655
+
1656
+ function renderProviderBadge(provider) {
1657
+ const meta = providerBadgeMeta(provider);
1658
+ return `<span class="provider-badge provider-badge--${escapeHtml(meta.id)}" aria-label="${escapeHtml(meta.label)}" title="${escapeHtml(meta.label)}"><span class="provider-badge__icon" aria-hidden="true">${escapeHtml(meta.glyph)}</span><span class="provider-badge__label">${escapeHtml(meta.label)}</span></span>`;
1659
+ }
1660
+
1661
+ function providerFilterOptions() {
1662
+ return [
1663
+ { id: "all", label: L("timeline.allThreads") },
1664
+ { id: "codex", label: L("common.codex") },
1665
+ { id: "claude", label: L("common.claude") },
1666
+ ];
1667
+ }
1668
+
1669
+ function renderProviderFilter() {
1670
+ const options = providerFilterOptions();
1671
+ const current = state.providerFilter || "all";
1672
+ return `
1673
+ <div class="provider-filter" role="tablist" aria-label="Provider filter">
1674
+ ${options
1675
+ .map(
1676
+ (option) => `
1677
+ <button
1678
+ type="button"
1679
+ class="provider-filter__button ${current === option.id ? "is-active" : ""}"
1680
+ data-provider-filter="${escapeHtml(option.id)}"
1681
+ role="tab"
1682
+ aria-selected="${current === option.id ? "true" : "false"}"
1683
+ >${escapeHtml(option.label)}</button>
1684
+ `
1685
+ )
1686
+ .join("")}
1687
+ </div>
1688
+ `;
1689
+ }
1690
+
1546
1691
  function renderItemCard(entry, sourceTab, desktop) {
1547
1692
  if (entry.status === "completed" && entry.item.kind === "completion") {
1548
1693
  return renderCompletedCompletionCard(entry, sourceTab);
@@ -1550,7 +1695,7 @@ function renderItemCard(entry, sourceTab, desktop) {
1550
1695
  const kindInfo = kindMeta(entry.item.kind);
1551
1696
  const cardTitle = cardTitleForEntry(entry);
1552
1697
  const statusText = entry.status === "completed" ? L("common.completed") : L("common.actionNeeded");
1553
- const intentText = itemIntentText(entry.item.kind, entry.status);
1698
+ const intentText = itemIntentText(entry.item.kind, entry.status, entry.item.provider);
1554
1699
  const showCompletedTimestamp = entry.status === "completed" && sourceTab === "completed";
1555
1700
  const timestampLabel = showCompletedTimestamp ? formatTimelineTimestamp(entry.item.createdAtMs) : "";
1556
1701
  return `
@@ -1563,6 +1708,7 @@ function renderItemCard(entry, sourceTab, desktop) {
1563
1708
  <div class="item-card__header">
1564
1709
  <div class="item-card__meta">
1565
1710
  <span class="type-pill type-pill--${escapeHtml(kindInfo.tone)}">${escapeHtml(kindInfo.label)}</span>
1711
+ ${renderProviderBadge(entry.item.provider)}
1566
1712
  ${
1567
1713
  desktop && sourceTab === "inbox"
1568
1714
  ? `<span class="status-pill status-pill--${escapeHtml(entry.status)}">${escapeHtml(statusText)}</span>`
@@ -1580,7 +1726,7 @@ function renderItemCard(entry, sourceTab, desktop) {
1580
1726
  <span class="item-card__intent-icon" aria-hidden="true">${renderIcon(kindInfo.icon)}</span>
1581
1727
  <span>${escapeHtml(intentText)}</span>
1582
1728
  </p>
1583
- <p class="item-card__summary">${escapeHtml(entry.item.summary || fallbackSummaryForKind(entry.item.kind, entry.status))}</p>
1729
+ <p class="item-card__summary">${escapeHtml(entry.item.summary || fallbackSummaryForKind(entry.item.kind, entry.status, entry.item.provider))}</p>
1584
1730
  ${
1585
1731
  !desktop && sourceTab === "inbox"
1586
1732
  ? `<p class="item-card__status-note">${escapeHtml(statusText)}</p>`
@@ -1616,7 +1762,7 @@ function cardTitleForEntry(entry) {
1616
1762
  function renderCompletedCompletionCard(entry, sourceTab) {
1617
1763
  const item = entry.item;
1618
1764
  const kindInfo = kindMeta(item.kind);
1619
- const summaryText = item.summary || fallbackSummaryForKind(item.kind, entry.status);
1765
+ const summaryText = item.summary || fallbackSummaryForKind(item.kind, entry.status, item.provider);
1620
1766
  const threadLabel = timelineEntryThreadLabel(item, true);
1621
1767
  const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
1622
1768
 
@@ -1631,6 +1777,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
1631
1777
  <div class="item-card__header">
1632
1778
  <div class="item-card__meta">
1633
1779
  <span class="type-pill type-pill--completion">${escapeHtml(L("common.task"))}</span>
1780
+ ${renderProviderBadge(item.provider)}
1634
1781
  </div>
1635
1782
  <div class="item-card__header-right">
1636
1783
  ${timestampLabel ? `<span class="item-card__timestamp">${escapeHtml(timestampLabel)}</span>` : ""}
@@ -1648,6 +1795,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
1648
1795
  function renderTimelinePanel({ entries, desktop }) {
1649
1796
  const meta = tabMeta("timeline");
1650
1797
  const listClassName = desktop ? "timeline-list timeline-list--desktop" : "timeline-list";
1798
+ const providerFilterHtml = renderProviderFilter();
1651
1799
  const threadsHtml = renderTimelineThreadDropdown();
1652
1800
  const bodyHtml = entries.length
1653
1801
  ? `<div class="${listClassName}">${entries.map((entry) => renderTimelineEntry(entry, { desktop })).join("")}</div>`
@@ -1660,6 +1808,7 @@ function renderTimelinePanel({ entries, desktop }) {
1660
1808
  <p class="screen-copy">${escapeHtml(meta.description)}</p>
1661
1809
  <span class="count-chip">${entries.length}</span>
1662
1810
  </div>
1811
+ ${providerFilterHtml}
1663
1812
  ${threadsHtml}
1664
1813
  ${bodyHtml}
1665
1814
  </div>
@@ -1676,6 +1825,7 @@ function renderTimelinePanel({ entries, desktop }) {
1676
1825
  <span class="count-chip">${entries.length}</span>
1677
1826
  </div>
1678
1827
  <p class="screen-copy">${escapeHtml(meta.description)}</p>
1828
+ ${providerFilterHtml}
1679
1829
  ${threadsHtml}
1680
1830
  ${bodyHtml}
1681
1831
  </div>
@@ -1857,6 +2007,7 @@ function renderTimelineEntry(entry, { desktop }) {
1857
2007
  <span class="timeline-entry__kind">
1858
2008
  <span class="timeline-entry__kind-icon" aria-hidden="true">${renderIcon(kindInfo.icon)}</span>
1859
2009
  <span>${escapeHtml(kindInfo.label)}</span>
2010
+ ${renderProviderBadge(item.provider)}
1860
2011
  </span>
1861
2012
  <span class="timeline-entry__meta-right">
1862
2013
  <span class="timeline-entry__time">${escapeHtml(timestampLabel)}</span>
@@ -1998,11 +2149,11 @@ function timelineEntryThreadLabel(item, isMessage) {
1998
2149
 
1999
2150
  function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
2000
2151
  if (isMessageLike) {
2001
- return item.summary || fallbackSummaryForKind(item.kind, status);
2152
+ return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
2002
2153
  }
2003
2154
 
2004
2155
  if (isFileEvent) {
2005
- return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status);
2156
+ return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status, item.provider);
2006
2157
  }
2007
2158
 
2008
2159
  return timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: true }) || L("common.untitledItem");
@@ -2013,7 +2164,7 @@ function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike =
2013
2164
  return "";
2014
2165
  }
2015
2166
 
2016
- const summaryText = normalizeClientText(item.summary || fallbackSummaryForKind(item.kind, status));
2167
+ const summaryText = normalizeClientText(item.summary || fallbackSummaryForKind(item.kind, status, item.provider));
2017
2168
  if (!summaryText || summaryText === normalizeClientText(primaryText || "")) {
2018
2169
  return "";
2019
2170
  }
@@ -2667,6 +2818,13 @@ function settingsPageMeta(page) {
2667
2818
  description: L("settings.technical.copy"),
2668
2819
  icon: "settings",
2669
2820
  };
2821
+ case "awayMode":
2822
+ return {
2823
+ id: "awayMode",
2824
+ title: L("settings.awayMode.title"),
2825
+ description: L("settings.awayMode.copy"),
2826
+ icon: "settings",
2827
+ };
2670
2828
  default:
2671
2829
  return settingsPageMeta("notifications");
2672
2830
  }
@@ -2695,6 +2853,12 @@ function renderSettingsRoot(context, { mobile }) {
2695
2853
  value: L(context.setupState.install.labelKey),
2696
2854
  })
2697
2855
  : "",
2856
+ renderSettingsNavRow({
2857
+ page: "awayMode",
2858
+ icon: "settings",
2859
+ title: L("settings.awayMode.title"),
2860
+ value: state.session?.claudeAwayMode === true ? L("settings.claudeAway.on") : L("settings.claudeAway.off"),
2861
+ }),
2698
2862
  ].filter(Boolean);
2699
2863
  const deviceRows = [
2700
2864
  renderSettingsNavRow({
@@ -2770,6 +2934,9 @@ function renderSettingsSubpage(context, { mobile }) {
2770
2934
  case "advanced":
2771
2935
  content = renderSettingsAdvancedPage(context);
2772
2936
  break;
2937
+ case "awayMode":
2938
+ content = renderSettingsAwayModePage();
2939
+ break;
2773
2940
  default:
2774
2941
  content = "";
2775
2942
  }
@@ -2962,6 +3129,29 @@ function renderSettingsNavRow({ page, icon, title, subtitle, value }) {
2962
3129
  `;
2963
3130
  }
2964
3131
 
3132
+ function renderSettingsAwayModePage() {
3133
+ const enabled = state.session?.claudeAwayMode === true;
3134
+ const stateLabel = enabled ? L("settings.claudeAway.on") : L("settings.claudeAway.off");
3135
+ return `
3136
+ <div class="settings-page">
3137
+ ${renderSettingsGroup("", [`
3138
+ <label class="reply-mode-switch" data-claude-away-toggle>
3139
+ <input type="checkbox" class="reply-mode-switch__input" ${enabled ? "checked" : ""} data-claude-away-checkbox />
3140
+ <span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
3141
+ <span class="reply-mode-switch__copy">
3142
+ <span class="reply-mode-switch__title">
3143
+ <span>${escapeHtml(L("settings.claudeAway.title"))}</span>
3144
+ <span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
3145
+ </span>
3146
+ <span class="reply-mode-switch__hint">${escapeHtml(L("settings.claudeAway.description"))}</span>
3147
+ </span>
3148
+ </label>
3149
+ `])}
3150
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.awayMode.codexNote"))}</p>
3151
+ </div>
3152
+ `;
3153
+ }
3154
+
2965
3155
  function renderSettingsInfoRow(label, value, options = {}) {
2966
3156
  const rowClassName = ["settings-info-row", options.rowClassName || ""].filter(Boolean).join(" ");
2967
3157
  const valueClassName = ["settings-info-row__value", options.valueClassName || ""].filter(Boolean).join(" ");
@@ -3086,6 +3276,8 @@ function renderStandardDetailDesktop(detail) {
3086
3276
  </section>
3087
3277
  `
3088
3278
  }
3279
+ ${renderClaudePlanSection(detail)}
3280
+ ${renderClaudeQuestionSection(detail)}
3089
3281
  ${renderDetailImageGallery(detail)}
3090
3282
  ${renderDetailDiffPanel(detail)}
3091
3283
  ${renderDetailDiffThreadGroups(detail)}
@@ -3117,6 +3309,8 @@ function renderStandardDetailMobile(detail) {
3117
3309
  </section>
3118
3310
  `
3119
3311
  }
3312
+ ${renderClaudePlanSection(detail, { mobile: true })}
3313
+ ${renderClaudeQuestionSection(detail, { mobile: true })}
3120
3314
  ${renderDetailImageGallery(detail, { mobile: true })}
3121
3315
  ${renderDetailDiffPanel(detail, { mobile: true })}
3122
3316
  ${renderDetailDiffThreadGroups(detail, { mobile: true })}
@@ -3451,6 +3645,116 @@ function diffLineClassName(line) {
3451
3645
  return "detail-diff-line--context";
3452
3646
  }
3453
3647
 
3648
+ function renderClaudePlanSection(detail, options = {}) {
3649
+ if (detail?.kind !== "approval") return "";
3650
+ if (normalizeClientText(detail?.approvalKind || "") !== "plan") return "";
3651
+ const planText = String(detail.planText || "");
3652
+ if (!planText) return "";
3653
+ const planHtml = String(detail.planHtml || "");
3654
+ const provider = providerDisplayName(detail.provider);
3655
+ const readOnlyNotice = detail.readOnly
3656
+ ? `<p class="claude-question-notice">${escapeHtml(L("claudeAway.notifyOnly.notice", { provider }))}</p>`
3657
+ : "";
3658
+ const bodyHtml = planHtml
3659
+ ? `<div class="detail-body detail-body--message markdown">${planHtml}</div>`
3660
+ : `<pre class="detail-body detail-body--message" style="white-space: pre-wrap; word-break: break-word;">${escapeHtml(planText)}</pre>`;
3661
+ return `
3662
+ <section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
3663
+ <h3 class="detail-section-title">${escapeHtml(L("claudePlan.title", { provider }))}</h3>
3664
+ ${bodyHtml}
3665
+ ${readOnlyNotice}
3666
+ </section>
3667
+ `;
3668
+ }
3669
+
3670
+ function renderClaudeQuestionSection(detail, options = {}) {
3671
+ if (detail?.kind !== "approval") return "";
3672
+ if (normalizeClientText(detail?.approvalKind || "") !== "question") return "";
3673
+ const questions = Array.isArray(detail.questions) ? detail.questions : [];
3674
+ if (questions.length === 0) return "";
3675
+ const provider = providerDisplayName(detail.provider);
3676
+ const token = detail.token || "";
3677
+ const draft = getClaudeQuestionDraft(token);
3678
+ const isReadOnly = detail.readOnly === true;
3679
+ const isSent = Boolean(draft.sent) || isReadOnly;
3680
+
3681
+ const questionsHtml = questions
3682
+ .map((q, qi) => {
3683
+ const inputType = q.multiSelect ? "checkbox" : "radio";
3684
+ const options = Array.isArray(q.options) ? q.options : [];
3685
+ const selected = new Set((draft.answers?.[qi]?.optionIndices) || []);
3686
+ const optionsHtml = options
3687
+ .map((opt, oi) => {
3688
+ const checked = selected.has(oi) ? "checked" : "";
3689
+ return `
3690
+ <label class="claude-question-option">
3691
+ <input type="${inputType}" name="q-${qi}" value="${oi}" ${checked} ${isSent ? "disabled" : ""} data-claude-question-input data-q-index="${qi}" data-o-index="${oi}" />
3692
+ <span class="claude-question-option__label"><strong>${escapeHtml(String(opt.label || ""))}</strong></span>
3693
+ ${opt.description ? `<span class="claude-question-option__desc">${escapeHtml(String(opt.description))}</span>` : ""}
3694
+ </label>
3695
+ `;
3696
+ })
3697
+ .join("");
3698
+ const noteValue = String(draft.answers?.[qi]?.note || "");
3699
+ return `
3700
+ <div class="claude-question-item">
3701
+ <p class="claude-question-text"><strong>${escapeHtml(String(q.question || ""))}</strong></p>
3702
+ <div class="claude-question-options">${optionsHtml}</div>
3703
+ <textarea
3704
+ class="claude-question-note"
3705
+ data-claude-question-note
3706
+ data-q-index="${qi}"
3707
+ placeholder="${escapeHtml(L("claudeQuestion.notePlaceholder"))}"
3708
+ rows="2"
3709
+ ${isSent ? "disabled" : ""}
3710
+ >${escapeHtml(noteValue)}</textarea>
3711
+ </div>
3712
+ `;
3713
+ })
3714
+ .join("");
3715
+
3716
+ const noticeHtml = isReadOnly
3717
+ ? `<p class="claude-question-notice">${escapeHtml(L("claudeAway.notifyOnly.notice", { provider }))}</p>`
3718
+ : draft.notice
3719
+ ? `<p class="claude-question-notice">${escapeHtml(draft.notice)}</p>`
3720
+ : "";
3721
+ const errorHtml = draft.error
3722
+ ? `<p class="claude-question-error">${escapeHtml(draft.error)}</p>`
3723
+ : "";
3724
+ const sendLabel = L("claudeQuestion.send", { provider });
3725
+
3726
+ return `
3727
+ <section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
3728
+ <h3 class="detail-section-title">${escapeHtml(L("claudeQuestion.title", { provider }))}</h3>
3729
+ <form data-claude-question-form data-token="${escapeHtml(token)}" data-answer-url="${escapeHtml(detail.answerUrl || "")}">
3730
+ ${questionsHtml}
3731
+ ${errorHtml}
3732
+ ${noticeHtml}
3733
+ ${isReadOnly ? "" : `
3734
+ <div class="claude-question-actions">
3735
+ <button type="submit" class="action-button action-button--primary" ${isSent || draft.sending ? "disabled" : ""}>
3736
+ ${escapeHtml(draft.sending ? L("claudeQuestion.send", { provider }) + "…" : sendLabel)}
3737
+ </button>
3738
+ </div>
3739
+ `}
3740
+ </form>
3741
+ </section>
3742
+ `;
3743
+ }
3744
+
3745
+ const claudeQuestionDrafts = new Map();
3746
+ function getClaudeQuestionDraft(token) {
3747
+ if (!claudeQuestionDrafts.has(token)) {
3748
+ claudeQuestionDrafts.set(token, { answers: {}, sending: false, sent: false, notice: "", error: "" });
3749
+ }
3750
+ return claudeQuestionDrafts.get(token);
3751
+ }
3752
+ function setClaudeQuestionDraft(token, partial) {
3753
+ const next = { ...getClaudeQuestionDraft(token), ...partial };
3754
+ claudeQuestionDrafts.set(token, next);
3755
+ return next;
3756
+ }
3757
+
3454
3758
  function renderCompletionReplyComposer(detail, options = {}) {
3455
3759
  if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
3456
3760
  return "";
@@ -3458,11 +3762,12 @@ function renderCompletionReplyComposer(detail, options = {}) {
3458
3762
 
3459
3763
  const draft = getCompletionReplyDraft(detail.token);
3460
3764
  const planMode = draft.mode === "plan";
3765
+ const providerVars = { provider: providerDisplayName(detail?.provider) };
3461
3766
  const sendLabel = draft.sending
3462
3767
  ? L("reply.sendSending")
3463
3768
  : draft.confirmOverride
3464
3769
  ? L("reply.sendConfirm")
3465
- : L("reply.send");
3770
+ : L("reply.send", providerVars);
3466
3771
  const disabled = draft.sending || !normalizeClientText(draft.text);
3467
3772
  const warningTimestamp = draft.warning?.createdAtMs ? formatTimelineTimestamp(draft.warning.createdAtMs) : "";
3468
3773
  const showCollapsedState =
@@ -3474,8 +3779,8 @@ function renderCompletionReplyComposer(detail, options = {}) {
3474
3779
  <div class="reply-composer">
3475
3780
  <div class="reply-composer__copy">
3476
3781
  <span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("reply.eyebrow"))}</span>
3477
- <h3 class="reply-composer__title">${escapeHtml(L("reply.title"))}</h3>
3478
- <p class="muted reply-composer__description">${escapeHtml(L("reply.copy"))}</p>
3782
+ <h3 class="reply-composer__title">${escapeHtml(L("reply.title", providerVars))}</h3>
3783
+ <p class="muted reply-composer__description">${escapeHtml(L("reply.copy", providerVars))}</p>
3479
3784
  </div>
3480
3785
  ${draft.notice ? `<p class="inline-alert inline-alert--success">${escapeHtml(draft.notice)}</p>` : ""}
3481
3786
  ${draft.error ? `<p class="inline-alert inline-alert--danger">${escapeHtml(draft.error)}</p>` : ""}
@@ -3521,7 +3826,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
3521
3826
  </div>
3522
3827
  `
3523
3828
  : `
3524
- <form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}">
3829
+ <form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}" data-provider="${escapeHtml(normalizeProviderClient(detail?.provider))}">
3525
3830
  <label class="field reply-field">
3526
3831
  <span class="field-label">${escapeHtml(L("reply.fieldLabel"))}</span>
3527
3832
  <div class="reply-field__shell">
@@ -3529,7 +3834,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
3529
3834
  class="reply-field__input"
3530
3835
  name="text"
3531
3836
  rows="4"
3532
- placeholder="${escapeHtml(L("reply.placeholder"))}"
3837
+ placeholder="${escapeHtml(L("reply.placeholder", providerVars))}"
3533
3838
  data-completion-reply-textarea
3534
3839
  data-reply-token="${escapeHtml(detail.token)}"
3535
3840
  >${escapeHtml(draft.text)}</textarea>
@@ -3776,20 +4081,24 @@ function renderActionButtons(actions, options = {}) {
3776
4081
  if (!actions.length) {
3777
4082
  return "";
3778
4083
  }
4084
+ const pendingUrl = actions.find((a) => state.pendingActionUrls.has(a.url))?.url ?? null;
3779
4085
  const actionsHtml = `
3780
4086
  <div class="actions actions--stack ${options.mobileSticky ? "actions--sticky" : ""}">
3781
4087
  ${actions
3782
- .map(
3783
- (action) => `
4088
+ .map((action) => {
4089
+ const isPending = pendingUrl === action.url;
4090
+ const isDisabled = pendingUrl !== null;
4091
+ return `
3784
4092
  <button
3785
- class="${escapeHtml(actionClassForTone(action.tone))}"
4093
+ class="${escapeHtml(actionClassForTone(action.tone))}${isPending ? " is-loading" : ""}"
3786
4094
  data-action-url="${escapeHtml(action.url)}"
3787
4095
  data-action-body='${escapeHtml(JSON.stringify(action.body || {}))}'
4096
+ ${isDisabled ? 'disabled aria-busy="true"' : ""}
3788
4097
  >
3789
- ${escapeHtml(action.label)}
4098
+ ${isPending ? `<span class="action-spinner" aria-hidden="true"></span><span>${escapeHtml(action.label)}</span>` : escapeHtml(action.label)}
3790
4099
  </button>
3791
- `
3792
- )
4100
+ `;
4101
+ })
3793
4102
  .join("")}
3794
4103
  </div>
3795
4104
  `;
@@ -4087,6 +4396,19 @@ function bindShellInteractions() {
4087
4396
  });
4088
4397
  }
4089
4398
 
4399
+ for (const button of document.querySelectorAll("[data-provider-filter]")) {
4400
+ button.addEventListener("click", async (event) => {
4401
+ event.preventDefault();
4402
+ const next = button.dataset.providerFilter || "all";
4403
+ if (state.providerFilter === next) {
4404
+ return;
4405
+ }
4406
+ state.providerFilter = next;
4407
+ alignCurrentItemToVisibleEntries();
4408
+ await renderShell();
4409
+ });
4410
+ }
4411
+
4090
4412
  for (const button of document.querySelectorAll("[data-timeline-kind-filter-toggle]")) {
4091
4413
  button.addEventListener("click", async (event) => {
4092
4414
  event.preventDefault();
@@ -4199,26 +4521,62 @@ function bindShellInteractions() {
4199
4521
 
4200
4522
  for (const button of document.querySelectorAll("[data-action-url]")) {
4201
4523
  button.addEventListener("click", async () => {
4524
+ const actionUrl = button.dataset.actionUrl;
4525
+ if (state.pendingActionUrls.has(actionUrl)) {
4526
+ return;
4527
+ }
4202
4528
  const body = button.dataset.actionBody ? JSON.parse(button.dataset.actionBody) : {};
4203
4529
  const activeItem = state.currentItem ? { ...state.currentItem } : null;
4204
4530
  const keepDetailOpen = shouldKeepDetailAfterAction(activeItem);
4205
- await apiPost(button.dataset.actionUrl, body);
4206
- if (keepDetailOpen && activeItem?.kind === "approval") {
4207
- pinActionOutcomeDetail(
4208
- activeItem,
4209
- buildActionOutcomeDetail({
4210
- kind: "approval",
4211
- title: state.currentDetail?.title,
4212
- message: approvalOutcomeMessage(button.dataset.actionUrl),
4213
- })
4214
- );
4531
+
4532
+ // Mark as pending in state so re-renders during the request also show loading
4533
+ state.pendingActionUrls.add(actionUrl);
4534
+
4535
+ // Visual feedback on current DOM nodes (before any re-render)
4536
+ const siblingButtons = button.parentElement
4537
+ ? Array.from(button.parentElement.querySelectorAll("[data-action-url]"))
4538
+ : [button];
4539
+ const originalLabels = new Map();
4540
+ for (const sibling of siblingButtons) {
4541
+ originalLabels.set(sibling, sibling.innerHTML);
4542
+ sibling.disabled = true;
4543
+ sibling.setAttribute("aria-busy", "true");
4215
4544
  }
4216
- await refreshAuthenticatedState();
4217
- if (!keepDetailOpen && !isDesktopLayout()) {
4218
- state.detailOpen = false;
4219
- syncCurrentItemUrl(null);
4545
+ button.classList.add("is-loading");
4546
+ button.innerHTML = `<span class="action-spinner" aria-hidden="true"></span><span>${escapeHtml(L("reply.sendSending"))}</span>`;
4547
+
4548
+ try {
4549
+ await apiPost(actionUrl, body);
4550
+ if (keepDetailOpen && activeItem?.kind === "approval") {
4551
+ pinActionOutcomeDetail(
4552
+ activeItem,
4553
+ buildActionOutcomeDetail({
4554
+ kind: "approval",
4555
+ title: state.currentDetail?.title,
4556
+ message: approvalOutcomeMessage(actionUrl, activeItem?.provider),
4557
+ })
4558
+ );
4559
+ }
4560
+ await refreshAuthenticatedState();
4561
+ if (!keepDetailOpen && !isDesktopLayout()) {
4562
+ state.detailOpen = false;
4563
+ syncCurrentItemUrl(null);
4564
+ }
4565
+ await renderShell();
4566
+ state.pendingActionUrls.delete(actionUrl);
4567
+ } catch (error) {
4568
+ state.pendingActionUrls.delete(actionUrl);
4569
+ // Restore buttons on failure so the user can retry
4570
+ for (const sibling of siblingButtons) {
4571
+ if (originalLabels.has(sibling)) {
4572
+ sibling.innerHTML = originalLabels.get(sibling);
4573
+ }
4574
+ sibling.disabled = false;
4575
+ sibling.removeAttribute("aria-busy");
4576
+ }
4577
+ button.classList.remove("is-loading");
4578
+ throw error;
4220
4579
  }
4221
- await renderShell();
4222
4580
  });
4223
4581
  }
4224
4582
 
@@ -4375,6 +4733,22 @@ function bindShellInteractions() {
4375
4733
  });
4376
4734
  }
4377
4735
 
4736
+ for (const checkbox of document.querySelectorAll("[data-claude-away-checkbox]")) {
4737
+ checkbox.addEventListener("change", async () => {
4738
+ const next = checkbox.checked === true;
4739
+ try {
4740
+ const result = await apiPost("/api/settings/claude-away-mode", { enabled: next });
4741
+ if (state.session) {
4742
+ state.session.claudeAwayMode = result?.enabled === true;
4743
+ }
4744
+ await refreshAuthenticatedState();
4745
+ } catch (error) {
4746
+ state.pushError = error.message || String(error);
4747
+ }
4748
+ await renderShell();
4749
+ });
4750
+ }
4751
+
4378
4752
  for (const button of document.querySelectorAll("[data-locale-option]")) {
4379
4753
  button.addEventListener("click", async () => {
4380
4754
  state.pushError = "";
@@ -4424,7 +4798,7 @@ function bindShellInteractions() {
4424
4798
  buildActionOutcomeDetail({
4425
4799
  kind: "choice",
4426
4800
  title: state.currentDetail?.title,
4427
- message: L("server.message.choiceSubmitted"),
4801
+ message: L("server.message.choiceSubmitted", { provider: providerDisplayName(activeItem?.provider) }),
4428
4802
  })
4429
4803
  );
4430
4804
  } else if (!isDesktopLayout()) {
@@ -4458,6 +4832,7 @@ function bindShellInteractions() {
4458
4832
 
4459
4833
  replyForm.addEventListener("submit", async (event) => {
4460
4834
  event.preventDefault();
4835
+ const replyProvider = replyForm.dataset.provider || "";
4461
4836
  const draft = getCompletionReplyDraft(token);
4462
4837
  const text = normalizeClientText(new FormData(replyForm).get("text"));
4463
4838
  if (!text) {
@@ -4500,7 +4875,7 @@ function bindShellInteractions() {
4500
4875
  mode: draft.mode,
4501
4876
  sending: false,
4502
4877
  error: "",
4503
- notice: L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault"),
4878
+ notice: L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) }),
4504
4879
  warning: null,
4505
4880
  confirmOverride: false,
4506
4881
  collapsedAfterSend: true,
@@ -4541,9 +4916,95 @@ function bindShellInteractions() {
4541
4916
  });
4542
4917
  }
4543
4918
 
4919
+ bindClaudeQuestionForm(renderShell);
4544
4920
  bindSharedUi(renderShell);
4545
4921
  }
4546
4922
 
4923
+ function bindClaudeQuestionForm(renderShell) {
4924
+ const form = document.querySelector("[data-claude-question-form]");
4925
+ if (!form) return;
4926
+ const token = form.dataset.token || "";
4927
+ const answerUrl = form.dataset.answerUrl || "";
4928
+
4929
+ const updateAnswerForInput = (input) => {
4930
+ const draft = getClaudeQuestionDraft(token);
4931
+ const qi = Number(input.dataset.qIndex || 0);
4932
+ const oi = Number(input.dataset.oIndex || 0);
4933
+ const answers = { ...(draft.answers || {}) };
4934
+ const existing = answers[qi] || { optionIndices: [], note: "" };
4935
+ let optionIndices = Array.isArray(existing.optionIndices) ? [...existing.optionIndices] : [];
4936
+ if (input.type === "radio") {
4937
+ optionIndices = input.checked ? [oi] : [];
4938
+ } else if (input.type === "checkbox") {
4939
+ if (input.checked) {
4940
+ if (!optionIndices.includes(oi)) optionIndices.push(oi);
4941
+ } else {
4942
+ optionIndices = optionIndices.filter((idx) => idx !== oi);
4943
+ }
4944
+ }
4945
+ answers[qi] = { ...existing, optionIndices };
4946
+ setClaudeQuestionDraft(token, { answers });
4947
+ };
4948
+
4949
+ for (const input of form.querySelectorAll("[data-claude-question-input]")) {
4950
+ input.addEventListener("change", () => updateAnswerForInput(input));
4951
+ }
4952
+ for (const note of form.querySelectorAll("[data-claude-question-note]")) {
4953
+ note.addEventListener("input", () => {
4954
+ const draft = getClaudeQuestionDraft(token);
4955
+ const qi = Number(note.dataset.qIndex || 0);
4956
+ const answers = { ...(draft.answers || {}) };
4957
+ const existing = answers[qi] || { optionIndices: [], note: "" };
4958
+ answers[qi] = { ...existing, note: note.value };
4959
+ setClaudeQuestionDraft(token, { answers });
4960
+ });
4961
+ }
4962
+
4963
+ form.addEventListener("submit", async (event) => {
4964
+ event.preventDefault();
4965
+ if (!answerUrl) return;
4966
+ const draft = getClaudeQuestionDraft(token);
4967
+ const answersMap = draft.answers || {};
4968
+ const answersArr = [];
4969
+ const totalQuestions = form.querySelectorAll(".claude-question-item").length;
4970
+ let allAnswered = true;
4971
+ for (let i = 0; i < totalQuestions; i++) {
4972
+ const a = answersMap[i] || { optionIndices: [], note: "" };
4973
+ const hasSelection = Array.isArray(a.optionIndices) && a.optionIndices.length > 0;
4974
+ const hasNote = typeof a.note === "string" && a.note.trim().length > 0;
4975
+ if (!hasSelection && !hasNote) {
4976
+ allAnswered = false;
4977
+ }
4978
+ answersArr.push({ questionIndex: i, optionIndices: a.optionIndices || [], note: a.note || "" });
4979
+ }
4980
+ if (!allAnswered) {
4981
+ setClaudeQuestionDraft(token, { error: L("claudeQuestion.requireAll"), notice: "" });
4982
+ await renderShell();
4983
+ return;
4984
+ }
4985
+
4986
+ setClaudeQuestionDraft(token, { sending: true, error: "", notice: "" });
4987
+ await renderShell();
4988
+
4989
+ try {
4990
+ await apiPost(answerUrl, { answers: answersArr });
4991
+ setClaudeQuestionDraft(token, {
4992
+ sending: false,
4993
+ sent: true,
4994
+ notice: L("claudeQuestion.sent", { provider: providerDisplayName("claude") }),
4995
+ error: "",
4996
+ });
4997
+ await refreshAuthenticatedState();
4998
+ } catch (error) {
4999
+ setClaudeQuestionDraft(token, {
5000
+ sending: false,
5001
+ error: error.message || String(error),
5002
+ });
5003
+ }
5004
+ await renderShell();
5005
+ });
5006
+ }
5007
+
4547
5008
  function bindSharedUi(renderFn) {
4548
5009
  for (const button of document.querySelectorAll("[data-close-image-viewer]")) {
4549
5010
  button.addEventListener("click", async (event) => {
@@ -4840,7 +5301,8 @@ function renderTypePillContent(kindInfo) {
4840
5301
  `;
4841
5302
  }
4842
5303
 
4843
- function itemIntentText(kind, status = "pending") {
5304
+ function itemIntentText(kind, status = "pending", provider) {
5305
+ const vars = { provider: providerDisplayName(provider) };
4844
5306
  if (kind === "diff_thread") {
4845
5307
  return L("intent.diffThread");
4846
5308
  }
@@ -4854,7 +5316,7 @@ function itemIntentText(kind, status = "pending") {
4854
5316
  return L("intent.assistantCommentary");
4855
5317
  }
4856
5318
  if (kind === "assistant_final") {
4857
- return L("intent.assistantFinal");
5319
+ return L("intent.assistantFinal", vars);
4858
5320
  }
4859
5321
  if (status === "completed") {
4860
5322
  return L("intent.completed");
@@ -4863,9 +5325,9 @@ function itemIntentText(kind, status = "pending") {
4863
5325
  case "approval":
4864
5326
  return L("intent.approval");
4865
5327
  case "plan":
4866
- return L("intent.plan");
5328
+ return L("intent.plan", vars);
4867
5329
  case "choice":
4868
- return L("intent.choice");
5330
+ return L("intent.choice", vars);
4869
5331
  case "completion":
4870
5332
  return L("intent.completed");
4871
5333
  default:
@@ -4874,19 +5336,20 @@ function itemIntentText(kind, status = "pending") {
4874
5336
  }
4875
5337
 
4876
5338
  function detailIntentText(detail) {
5339
+ const provider = detail?.provider;
4877
5340
  if (detail.kind === "diff_thread") {
4878
- return itemIntentText(detail.kind, "diff");
5341
+ return itemIntentText(detail.kind, "diff", provider);
4879
5342
  }
4880
5343
  if (detail.kind === "file_event") {
4881
- return itemIntentText(detail.kind, "timeline");
5344
+ return itemIntentText(detail.kind, "timeline", provider);
4882
5345
  }
4883
5346
  if (TIMELINE_MESSAGE_KINDS.has(detail.kind)) {
4884
- return itemIntentText(detail.kind, "timeline");
5347
+ return itemIntentText(detail.kind, "timeline", provider);
4885
5348
  }
4886
5349
  if (detail.readOnly) {
4887
5350
  return L("intent.completed");
4888
5351
  }
4889
- return itemIntentText(detail.kind, "pending");
5352
+ return itemIntentText(detail.kind, "pending", provider);
4890
5353
  }
4891
5354
 
4892
5355
  function detailDisplayTitle(detail) {
@@ -4928,7 +5391,8 @@ function detailDisplayTitle(detail) {
4928
5391
  return title;
4929
5392
  }
4930
5393
 
4931
- function fallbackSummaryForKind(kind, status) {
5394
+ function fallbackSummaryForKind(kind, status, provider) {
5395
+ const vars = { provider: providerDisplayName(provider) };
4932
5396
  if (status === "completed") {
4933
5397
  return L("summary.completed");
4934
5398
  }
@@ -4936,20 +5400,20 @@ function fallbackSummaryForKind(kind, status) {
4936
5400
  case "diff_thread":
4937
5401
  return L("summary.diffThread");
4938
5402
  case "file_event":
4939
- return L("summary.fileEvent");
5403
+ return L("summary.fileEvent", vars);
4940
5404
  case "user_message":
4941
5405
  return L("summary.userMessage");
4942
5406
  case "assistant_commentary":
4943
- return L("summary.assistantCommentary");
5407
+ return L("summary.assistantCommentary", vars);
4944
5408
  case "assistant_final":
4945
- return L("summary.assistantFinal");
5409
+ return L("summary.assistantFinal", vars);
4946
5410
  case "approval":
4947
- return L("summary.approval");
5411
+ return L("summary.approval", vars);
4948
5412
  case "plan":
4949
5413
  case "plan_ready":
4950
- return L("summary.plan");
5414
+ return L("summary.plan", vars);
4951
5415
  case "choice":
4952
- return L("summary.choice");
5416
+ return L("summary.choice", vars);
4953
5417
  default:
4954
5418
  return L("summary.default");
4955
5419
  }