viveworker 0.1.11 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -8
- package/package.json +7 -2
- package/scripts/viveworker-bridge.mjs +794 -43
- package/scripts/viveworker-claude-hook.mjs +839 -0
- package/scripts/viveworker.mjs +71 -0
- package/web/app.css +171 -0
- package/web/app.js +523 -59
- package/web/i18n.js +95 -57
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
|
|
366
|
-
? allEntries.
|
|
367
|
-
:
|
|
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
|
|
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
|
|
566
|
+
return filtered;
|
|
473
567
|
}
|
|
474
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
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
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
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
|
}
|