viveworker 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/viveworker-bridge.mjs +1749 -25
- package/web/app.css +633 -0
- package/web/app.js +1225 -90
- package/web/i18n.js +103 -0
- package/web/sw.js +52 -5
package/web/app.js
CHANGED
|
@@ -8,19 +8,25 @@ const TIMELINE_MESSAGE_KINDS = new Set(["user_message", "assistant_commentary",
|
|
|
8
8
|
const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice", "completion"]);
|
|
9
9
|
const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
|
|
10
10
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
11
|
+
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
12
|
+
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
11
13
|
|
|
12
14
|
const state = {
|
|
13
15
|
session: null,
|
|
14
16
|
inbox: null,
|
|
15
17
|
timeline: null,
|
|
16
18
|
devices: [],
|
|
17
|
-
currentTab: "
|
|
19
|
+
currentTab: "inbox",
|
|
18
20
|
currentItem: null,
|
|
19
21
|
currentDetail: null,
|
|
20
22
|
currentDetailLoading: false,
|
|
21
23
|
detailLoadingItem: null,
|
|
22
24
|
detailOpen: false,
|
|
25
|
+
inboxSubtab: "pending",
|
|
23
26
|
timelineThreadFilter: "all",
|
|
27
|
+
timelineKindFilter: "all",
|
|
28
|
+
timelineKindFilterOpen: false,
|
|
29
|
+
diffThreadFilter: "all",
|
|
24
30
|
completedThreadFilter: "all",
|
|
25
31
|
settingsSubpage: "",
|
|
26
32
|
settingsScrollState: null,
|
|
@@ -32,6 +38,7 @@ const state = {
|
|
|
32
38
|
listScrollState: null,
|
|
33
39
|
pendingListScrollRestore: false,
|
|
34
40
|
threadFilterInteractionUntilMs: 0,
|
|
41
|
+
diffThreadExpandedFiles: {},
|
|
35
42
|
choiceLocalDrafts: {},
|
|
36
43
|
completionReplyDrafts: {},
|
|
37
44
|
pairError: "",
|
|
@@ -86,6 +93,9 @@ async function boot() {
|
|
|
86
93
|
await registerServiceWorker();
|
|
87
94
|
navigator.serviceWorker?.addEventListener("message", handleServiceWorkerMessage);
|
|
88
95
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
96
|
+
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
97
|
+
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
98
|
+
document.addEventListener("visibilitychange", handleDocumentVisibilityChange);
|
|
89
99
|
|
|
90
100
|
await refreshSession();
|
|
91
101
|
|
|
@@ -107,6 +117,9 @@ async function boot() {
|
|
|
107
117
|
if (parsedInitialItem) {
|
|
108
118
|
state.currentItem = parsedInitialItem;
|
|
109
119
|
state.currentTab = tabForItemKind(parsedInitialItem.kind, state.currentTab);
|
|
120
|
+
if (state.currentTab === "inbox") {
|
|
121
|
+
state.inboxSubtab = inboxSubtabForItemKind(parsedInitialItem.kind);
|
|
122
|
+
}
|
|
110
123
|
state.detailOpen = true;
|
|
111
124
|
if (isFastPathItemRef(parsedInitialItem)) {
|
|
112
125
|
state.launchItemIntent = {
|
|
@@ -121,6 +134,7 @@ async function boot() {
|
|
|
121
134
|
return;
|
|
122
135
|
}
|
|
123
136
|
|
|
137
|
+
await consumePendingNotificationIntent();
|
|
124
138
|
await syncDetectedLocalePreference();
|
|
125
139
|
await refreshAuthenticatedState();
|
|
126
140
|
ensureCurrentSelection();
|
|
@@ -130,6 +144,10 @@ async function boot() {
|
|
|
130
144
|
if (!state.session?.authenticated) {
|
|
131
145
|
return;
|
|
132
146
|
}
|
|
147
|
+
const consumedNotificationIntent = await consumePendingNotificationIntent();
|
|
148
|
+
if (consumedNotificationIntent) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
133
151
|
await refreshAuthenticatedState();
|
|
134
152
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
135
153
|
await renderShell();
|
|
@@ -308,12 +326,15 @@ async function getClientPushState() {
|
|
|
308
326
|
|
|
309
327
|
async function refreshInbox() {
|
|
310
328
|
state.inbox = await apiGet("/api/inbox");
|
|
329
|
+
syncDiffThreadFilter();
|
|
311
330
|
syncCompletedThreadFilter();
|
|
331
|
+
syncInboxSubtab();
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
async function refreshTimeline() {
|
|
315
335
|
state.timeline = await apiGet("/api/timeline");
|
|
316
336
|
syncTimelineThreadFilter();
|
|
337
|
+
syncTimelineKindFilter();
|
|
317
338
|
}
|
|
318
339
|
|
|
319
340
|
async function refreshDevices() {
|
|
@@ -378,6 +399,7 @@ function allInboxEntries() {
|
|
|
378
399
|
}
|
|
379
400
|
return [
|
|
380
401
|
...state.inbox.pending.map((item) => ({ item, status: "pending" })),
|
|
402
|
+
...(Array.isArray(state.inbox.diff) ? state.inbox.diff.map((item) => ({ item, status: "diff" })) : []),
|
|
381
403
|
...state.inbox.completed.map((item) => ({ item, status: "completed" })),
|
|
382
404
|
];
|
|
383
405
|
}
|
|
@@ -399,14 +421,14 @@ function listEntriesForTab(tab) {
|
|
|
399
421
|
return [];
|
|
400
422
|
}
|
|
401
423
|
}
|
|
402
|
-
if (tab === "
|
|
403
|
-
return
|
|
424
|
+
if (tab === "inbox") {
|
|
425
|
+
return listInboxEntries();
|
|
404
426
|
}
|
|
405
427
|
if (tab === "timeline") {
|
|
406
428
|
return filteredTimelineEntries().map((item) => ({ item, status: "timeline" }));
|
|
407
429
|
}
|
|
408
|
-
if (tab === "
|
|
409
|
-
return
|
|
430
|
+
if (tab === "diff") {
|
|
431
|
+
return filteredDiffEntries().map((item) => ({ item, status: "diff" }));
|
|
410
432
|
}
|
|
411
433
|
return [];
|
|
412
434
|
}
|
|
@@ -415,15 +437,29 @@ function listEntriesForCurrentTab() {
|
|
|
415
437
|
return listEntriesForTab(state.currentTab);
|
|
416
438
|
}
|
|
417
439
|
|
|
440
|
+
function listInboxEntries() {
|
|
441
|
+
if (!state.inbox) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
if (state.inboxSubtab === "completed") {
|
|
445
|
+
return filteredCompletedEntries().map((item) => ({ item, status: "completed" }));
|
|
446
|
+
}
|
|
447
|
+
return state.inbox.pending.map((item) => ({ item, status: "pending" }));
|
|
448
|
+
}
|
|
449
|
+
|
|
418
450
|
function filteredTimelineEntries() {
|
|
419
451
|
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
420
452
|
if (!entries.length) {
|
|
421
453
|
return [];
|
|
422
454
|
}
|
|
423
|
-
|
|
424
|
-
|
|
455
|
+
let filtered = entries;
|
|
456
|
+
if (state.timelineThreadFilter && state.timelineThreadFilter !== "all") {
|
|
457
|
+
filtered = filtered.filter((entry) => entry.threadId === state.timelineThreadFilter);
|
|
458
|
+
}
|
|
459
|
+
if (!state.timelineKindFilter || state.timelineKindFilter === "all") {
|
|
460
|
+
return filtered;
|
|
425
461
|
}
|
|
426
|
-
return
|
|
462
|
+
return filtered.filter((entry) => timelineEntryMatchesKindFilter(entry, state.timelineKindFilter));
|
|
427
463
|
}
|
|
428
464
|
|
|
429
465
|
function filteredCompletedEntries() {
|
|
@@ -437,6 +473,14 @@ function filteredCompletedEntries() {
|
|
|
437
473
|
return entries.filter((entry) => entry.threadId === state.completedThreadFilter);
|
|
438
474
|
}
|
|
439
475
|
|
|
476
|
+
function filteredDiffEntries() {
|
|
477
|
+
const entries = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
|
|
478
|
+
if (!entries.length) {
|
|
479
|
+
return [];
|
|
480
|
+
}
|
|
481
|
+
return entries.slice();
|
|
482
|
+
}
|
|
483
|
+
|
|
440
484
|
function syncTimelineThreadFilter() {
|
|
441
485
|
const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
|
|
442
486
|
if (!state.timelineThreadFilter || state.timelineThreadFilter === "all") {
|
|
@@ -448,6 +492,52 @@ function syncTimelineThreadFilter() {
|
|
|
448
492
|
}
|
|
449
493
|
}
|
|
450
494
|
|
|
495
|
+
function syncTimelineKindFilter() {
|
|
496
|
+
const validIds = new Set(timelineKindFilterOptions().map((option) => option.id));
|
|
497
|
+
if (!state.timelineKindFilter || !validIds.has(state.timelineKindFilter)) {
|
|
498
|
+
state.timelineKindFilter = "all";
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function timelineKindFilterOptions() {
|
|
503
|
+
return [
|
|
504
|
+
{ id: "all", label: L("timeline.kindFilter.all"), icon: "filter" },
|
|
505
|
+
{ id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
|
|
506
|
+
{ id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
|
|
507
|
+
{ id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
|
|
508
|
+
{ id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
|
|
509
|
+
{ id: "choices", label: L("timeline.kindFilter.choices"), icon: "choice" },
|
|
510
|
+
{ id: "completions", label: L("timeline.kindFilter.completions"), icon: "completion-item" },
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function currentTimelineKindFilterOption() {
|
|
515
|
+
return (
|
|
516
|
+
timelineKindFilterOptions().find((option) => option.id === state.timelineKindFilter) ||
|
|
517
|
+
timelineKindFilterOptions()[0]
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
522
|
+
const kind = normalizeClientText(entry?.kind || entry?.item?.kind || "");
|
|
523
|
+
switch (filterId) {
|
|
524
|
+
case "messages":
|
|
525
|
+
return TIMELINE_MESSAGE_KINDS.has(kind);
|
|
526
|
+
case "files":
|
|
527
|
+
return kind === "file_event";
|
|
528
|
+
case "approvals":
|
|
529
|
+
return kind === "approval";
|
|
530
|
+
case "plans":
|
|
531
|
+
return kind === "plan" || kind === "plan_ready";
|
|
532
|
+
case "choices":
|
|
533
|
+
return kind === "choice";
|
|
534
|
+
case "completions":
|
|
535
|
+
return kind === "completion";
|
|
536
|
+
default:
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
451
541
|
function completedThreads() {
|
|
452
542
|
const items = Array.isArray(state.inbox?.completed) ? state.inbox.completed : [];
|
|
453
543
|
if (!items.length) {
|
|
@@ -473,6 +563,42 @@ function completedThreads() {
|
|
|
473
563
|
return [...byThread.values()].sort((left, right) => right.latestAtMs - left.latestAtMs);
|
|
474
564
|
}
|
|
475
565
|
|
|
566
|
+
function diffThreads() {
|
|
567
|
+
const items = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
|
|
568
|
+
if (!items.length) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
const byThread = new Map();
|
|
572
|
+
for (const item of items) {
|
|
573
|
+
const threadId = normalizeClientText(item.threadId || "");
|
|
574
|
+
if (!threadId) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const latestAtMs = Number(item.createdAtMs || 0);
|
|
578
|
+
const label = resolvedThreadLabel(threadId, item.threadLabel || "");
|
|
579
|
+
const previous = byThread.get(threadId);
|
|
580
|
+
if (!previous || latestAtMs >= previous.latestAtMs) {
|
|
581
|
+
byThread.set(threadId, {
|
|
582
|
+
id: threadId,
|
|
583
|
+
label,
|
|
584
|
+
latestAtMs,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return [...byThread.values()].sort((left, right) => right.latestAtMs - left.latestAtMs);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function syncDiffThreadFilter() {
|
|
592
|
+
const threads = diffThreads();
|
|
593
|
+
if (!state.diffThreadFilter || state.diffThreadFilter === "all") {
|
|
594
|
+
state.diffThreadFilter = "all";
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (!threads.some((thread) => thread.id === state.diffThreadFilter)) {
|
|
598
|
+
state.diffThreadFilter = "all";
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
476
602
|
function syncCompletedThreadFilter() {
|
|
477
603
|
const threads = completedThreads();
|
|
478
604
|
if (!state.completedThreadFilter || state.completedThreadFilter === "all") {
|
|
@@ -484,6 +610,14 @@ function syncCompletedThreadFilter() {
|
|
|
484
610
|
}
|
|
485
611
|
}
|
|
486
612
|
|
|
613
|
+
function syncInboxSubtab() {
|
|
614
|
+
if (state.inboxSubtab === "completed") {
|
|
615
|
+
state.inboxSubtab = "completed";
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
state.inboxSubtab = "pending";
|
|
619
|
+
}
|
|
620
|
+
|
|
487
621
|
function renderPair() {
|
|
488
622
|
const shouldInstallFromHomeScreen = Boolean(initialPairToken) && !shouldAutoPairFromBootstrapToken();
|
|
489
623
|
app.innerHTML = `
|
|
@@ -706,7 +840,7 @@ function shouldDeferRenderForActiveInteraction() {
|
|
|
706
840
|
}
|
|
707
841
|
if (
|
|
708
842
|
activeElement instanceof HTMLSelectElement &&
|
|
709
|
-
activeElement.matches("[data-timeline-thread-select], [data-completed-thread-select]")
|
|
843
|
+
activeElement.matches("[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]")
|
|
710
844
|
) {
|
|
711
845
|
return true;
|
|
712
846
|
}
|
|
@@ -1269,11 +1403,16 @@ function renderMobileWorkspace(detail) {
|
|
|
1269
1403
|
}
|
|
1270
1404
|
|
|
1271
1405
|
function renderListPanel({ tab, entries, desktop }) {
|
|
1406
|
+
if (tab === "inbox") {
|
|
1407
|
+
return renderInboxPanel({ entries, desktop });
|
|
1408
|
+
}
|
|
1272
1409
|
if (tab === "timeline") {
|
|
1273
1410
|
return renderTimelinePanel({ entries, desktop });
|
|
1274
1411
|
}
|
|
1412
|
+
if (tab === "diff") {
|
|
1413
|
+
return renderDiffPanel({ entries, desktop });
|
|
1414
|
+
}
|
|
1275
1415
|
const meta = tabMeta(tab);
|
|
1276
|
-
const threadFilterHtml = tab === "completed" ? renderCompletedThreadDropdown() : "";
|
|
1277
1416
|
if (!desktop) {
|
|
1278
1417
|
return `
|
|
1279
1418
|
<div class="screen-shell screen-shell--mobile">
|
|
@@ -1281,7 +1420,6 @@ function renderListPanel({ tab, entries, desktop }) {
|
|
|
1281
1420
|
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1282
1421
|
<span class="count-chip">${entries.length}</span>
|
|
1283
1422
|
</div>
|
|
1284
|
-
${threadFilterHtml}
|
|
1285
1423
|
${
|
|
1286
1424
|
entries.length
|
|
1287
1425
|
? `<div class="card-list">
|
|
@@ -1303,7 +1441,6 @@ function renderListPanel({ tab, entries, desktop }) {
|
|
|
1303
1441
|
<span class="count-chip">${entries.length}</span>
|
|
1304
1442
|
</div>
|
|
1305
1443
|
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1306
|
-
${threadFilterHtml}
|
|
1307
1444
|
${
|
|
1308
1445
|
entries.length
|
|
1309
1446
|
? `<div class="card-list ${desktop ? "card-list--desktop" : ""}">
|
|
@@ -1315,6 +1452,96 @@ function renderListPanel({ tab, entries, desktop }) {
|
|
|
1315
1452
|
`;
|
|
1316
1453
|
}
|
|
1317
1454
|
|
|
1455
|
+
function renderInboxPanel({ entries, desktop }) {
|
|
1456
|
+
const meta = tabMeta("inbox");
|
|
1457
|
+
const subtabControls = renderInboxSubtabs();
|
|
1458
|
+
const threadFilterHtml = state.inboxSubtab === "completed" ? renderCompletedThreadDropdown() : "";
|
|
1459
|
+
const bodyHtml = entries.length
|
|
1460
|
+
? `<div class="card-list ${desktop ? "card-list--desktop" : ""}">
|
|
1461
|
+
${entries.map((entry) => renderItemCard(entry, "inbox", desktop)).join("")}
|
|
1462
|
+
</div>`
|
|
1463
|
+
: renderInboxEmptyState();
|
|
1464
|
+
|
|
1465
|
+
if (!desktop) {
|
|
1466
|
+
return `
|
|
1467
|
+
<div class="screen-shell screen-shell--mobile">
|
|
1468
|
+
<div class="screen-header screen-header--mobile">
|
|
1469
|
+
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1470
|
+
<span class="count-chip">${entries.length}</span>
|
|
1471
|
+
</div>
|
|
1472
|
+
${subtabControls}
|
|
1473
|
+
${threadFilterHtml}
|
|
1474
|
+
${bodyHtml}
|
|
1475
|
+
</div>
|
|
1476
|
+
`;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return `
|
|
1480
|
+
<div class="screen-shell">
|
|
1481
|
+
<div class="screen-header">
|
|
1482
|
+
<div>
|
|
1483
|
+
<p class="screen-eyebrow">${escapeHtml(meta.eyebrow)}</p>
|
|
1484
|
+
<h2 class="screen-title">${escapeHtml(meta.title)}</h2>
|
|
1485
|
+
</div>
|
|
1486
|
+
<span class="count-chip">${entries.length}</span>
|
|
1487
|
+
</div>
|
|
1488
|
+
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1489
|
+
${subtabControls}
|
|
1490
|
+
${threadFilterHtml}
|
|
1491
|
+
${bodyHtml}
|
|
1492
|
+
</div>
|
|
1493
|
+
`;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function renderInboxSubtabs() {
|
|
1497
|
+
const pendingCount = pendingInboxCount();
|
|
1498
|
+
return `
|
|
1499
|
+
<div class="inbox-subtabs" role="tablist" aria-label="${escapeHtml(tabMeta("inbox").title)}">
|
|
1500
|
+
${inboxSubtabOptions()
|
|
1501
|
+
.map(
|
|
1502
|
+
(option) => {
|
|
1503
|
+
const showPendingBadge = option.id === "pending" && pendingCount > 0;
|
|
1504
|
+
const badgeHtml = showPendingBadge
|
|
1505
|
+
? `<span class="inbox-subtabs__badge" aria-hidden="true">${pendingCount}</span>`
|
|
1506
|
+
: "";
|
|
1507
|
+
const ariaLabel = showPendingBadge ? `${option.label} (${pendingCount})` : option.label;
|
|
1508
|
+
return `
|
|
1509
|
+
<button
|
|
1510
|
+
type="button"
|
|
1511
|
+
class="inbox-subtabs__button ${state.inboxSubtab === option.id ? "is-active" : ""}"
|
|
1512
|
+
data-inbox-subtab="${escapeHtml(option.id)}"
|
|
1513
|
+
role="tab"
|
|
1514
|
+
aria-selected="${state.inboxSubtab === option.id ? "true" : "false"}"
|
|
1515
|
+
aria-label="${escapeHtml(ariaLabel)}"
|
|
1516
|
+
>
|
|
1517
|
+
<span class="inbox-subtabs__button-label">${escapeHtml(option.label)}</span>
|
|
1518
|
+
${badgeHtml}
|
|
1519
|
+
</button>
|
|
1520
|
+
`
|
|
1521
|
+
}
|
|
1522
|
+
)
|
|
1523
|
+
.join("")}
|
|
1524
|
+
</div>
|
|
1525
|
+
`;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
function inboxSubtabOptions() {
|
|
1529
|
+
return [
|
|
1530
|
+
{ id: "pending", label: L("inbox.subtab.pending") },
|
|
1531
|
+
{ id: "completed", label: L("inbox.subtab.completed") },
|
|
1532
|
+
];
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function renderInboxEmptyState() {
|
|
1536
|
+
const isCompletedView = state.inboxSubtab === "completed";
|
|
1537
|
+
return `
|
|
1538
|
+
<div class="empty-state">
|
|
1539
|
+
<p class="empty-state__title">${escapeHtml(L(isCompletedView ? "inbox.subtab.completed" : "inbox.subtab.pending"))}</p>
|
|
1540
|
+
<p class="muted">${escapeHtml(L(isCompletedView ? "empty.completed" : "empty.pending"))}</p>
|
|
1541
|
+
</div>
|
|
1542
|
+
`;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1318
1545
|
function renderItemCard(entry, sourceTab, desktop) {
|
|
1319
1546
|
if (entry.status === "completed" && entry.item.kind === "completion") {
|
|
1320
1547
|
return renderCompletedCompletionCard(entry, sourceTab);
|
|
@@ -1398,6 +1625,7 @@ function renderCompletedCompletionCard(entry, sourceTab) {
|
|
|
1398
1625
|
data-open-item-kind="${escapeHtml(item.kind)}"
|
|
1399
1626
|
data-open-item-token="${escapeHtml(item.token)}"
|
|
1400
1627
|
data-source-tab="${escapeHtml(sourceTab)}"
|
|
1628
|
+
data-source-subtab="completed"
|
|
1401
1629
|
>
|
|
1402
1630
|
<div class="item-card__header">
|
|
1403
1631
|
<div class="item-card__meta">
|
|
@@ -1453,12 +1681,47 @@ function renderTimelinePanel({ entries, desktop }) {
|
|
|
1453
1681
|
`;
|
|
1454
1682
|
}
|
|
1455
1683
|
|
|
1684
|
+
function renderDiffPanel({ entries, desktop }) {
|
|
1685
|
+
const meta = tabMeta("diff");
|
|
1686
|
+
const listClassName = desktop ? "diff-list diff-list--desktop" : "diff-list";
|
|
1687
|
+
const bodyHtml = entries.length
|
|
1688
|
+
? `<div class="${listClassName}">${entries.map((entry) => renderDiffEntry(entry)).join("")}</div>`
|
|
1689
|
+
: renderEmptyList("diff");
|
|
1690
|
+
|
|
1691
|
+
if (!desktop) {
|
|
1692
|
+
return `
|
|
1693
|
+
<div class="screen-shell screen-shell--mobile diff-shell diff-shell--mobile">
|
|
1694
|
+
<div class="screen-header screen-header--mobile">
|
|
1695
|
+
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1696
|
+
<span class="count-chip">${entries.length}</span>
|
|
1697
|
+
</div>
|
|
1698
|
+
${bodyHtml}
|
|
1699
|
+
</div>
|
|
1700
|
+
`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
return `
|
|
1704
|
+
<div class="screen-shell diff-shell">
|
|
1705
|
+
<div class="screen-header">
|
|
1706
|
+
<div>
|
|
1707
|
+
<p class="screen-eyebrow">${escapeHtml(meta.eyebrow)}</p>
|
|
1708
|
+
<h2 class="screen-title">${escapeHtml(meta.title)}</h2>
|
|
1709
|
+
</div>
|
|
1710
|
+
<span class="count-chip">${entries.length}</span>
|
|
1711
|
+
</div>
|
|
1712
|
+
<p class="screen-copy">${escapeHtml(meta.description)}</p>
|
|
1713
|
+
${bodyHtml}
|
|
1714
|
+
</div>
|
|
1715
|
+
`;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1456
1718
|
function renderTimelineThreadDropdown() {
|
|
1457
1719
|
const threads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
|
|
1458
1720
|
return renderThreadDropdown({
|
|
1459
1721
|
inputId: "timeline-thread-select",
|
|
1460
1722
|
dataAttribute: "data-timeline-thread-select",
|
|
1461
1723
|
selectedThreadId: state.timelineThreadFilter,
|
|
1724
|
+
controlsHtml: renderTimelineKindFilterControls(),
|
|
1462
1725
|
threads: threads.map((thread) => ({
|
|
1463
1726
|
id: thread.id,
|
|
1464
1727
|
label: dropdownThreadLabel(thread.id, thread.label || ""),
|
|
@@ -1475,7 +1738,16 @@ function renderCompletedThreadDropdown() {
|
|
|
1475
1738
|
});
|
|
1476
1739
|
}
|
|
1477
1740
|
|
|
1478
|
-
function
|
|
1741
|
+
function renderDiffThreadDropdown() {
|
|
1742
|
+
return renderThreadDropdown({
|
|
1743
|
+
inputId: "diff-thread-select",
|
|
1744
|
+
dataAttribute: "data-diff-thread-select",
|
|
1745
|
+
selectedThreadId: state.diffThreadFilter,
|
|
1746
|
+
threads: diffThreads(),
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function renderThreadDropdown({ inputId, dataAttribute, selectedThreadId, threads, controlsHtml = "" }) {
|
|
1479
1751
|
const options = [
|
|
1480
1752
|
{
|
|
1481
1753
|
id: "all",
|
|
@@ -1490,38 +1762,88 @@ function renderThreadDropdown({ inputId, dataAttribute, selectedThreadId, thread
|
|
|
1490
1762
|
return `
|
|
1491
1763
|
<div class="timeline-thread-filter">
|
|
1492
1764
|
<label class="timeline-thread-filter__label" for="${escapeHtml(inputId)}">${escapeHtml(L("timeline.filterLabel"))}</label>
|
|
1493
|
-
<div class="timeline-thread-
|
|
1494
|
-
<
|
|
1495
|
-
${
|
|
1496
|
-
|
|
1497
|
-
(
|
|
1498
|
-
|
|
1499
|
-
${escapeHtml(thread.
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1765
|
+
<div class="timeline-thread-filter__row">
|
|
1766
|
+
<div class="timeline-thread-select-wrap">
|
|
1767
|
+
<select id="${escapeHtml(inputId)}" class="timeline-thread-select" ${dataAttribute}>
|
|
1768
|
+
${options
|
|
1769
|
+
.map(
|
|
1770
|
+
(thread) => `
|
|
1771
|
+
<option value="${escapeHtml(thread.id)}" ${selectedThreadId === thread.id ? "selected" : ""}>
|
|
1772
|
+
${escapeHtml(thread.label)}
|
|
1773
|
+
</option>
|
|
1774
|
+
`
|
|
1775
|
+
)
|
|
1776
|
+
.join("")}
|
|
1777
|
+
</select>
|
|
1778
|
+
<span class="timeline-thread-select__chevron" aria-hidden="true">${renderIcon("chevron-down")}</span>
|
|
1779
|
+
</div>
|
|
1780
|
+
${controlsHtml}
|
|
1506
1781
|
</div>
|
|
1507
1782
|
</div>
|
|
1508
1783
|
`;
|
|
1509
1784
|
}
|
|
1510
1785
|
|
|
1786
|
+
function renderTimelineKindFilterControls() {
|
|
1787
|
+
const current = currentTimelineKindFilterOption();
|
|
1788
|
+
const options = timelineKindFilterOptions();
|
|
1789
|
+
return `
|
|
1790
|
+
<div class="timeline-kind-filter" data-timeline-kind-filter-root>
|
|
1791
|
+
<button
|
|
1792
|
+
type="button"
|
|
1793
|
+
class="timeline-kind-filter__button ${current.id !== "all" ? "is-active" : ""}"
|
|
1794
|
+
data-timeline-kind-filter-toggle
|
|
1795
|
+
aria-expanded="${state.timelineKindFilterOpen ? "true" : "false"}"
|
|
1796
|
+
aria-label="${escapeHtml(L("timeline.kindFilterButtonLabel"))}"
|
|
1797
|
+
>
|
|
1798
|
+
<span class="timeline-kind-filter__button-icon" aria-hidden="true">${renderIcon(current.icon)}</span>
|
|
1799
|
+
</button>
|
|
1800
|
+
${
|
|
1801
|
+
state.timelineKindFilterOpen
|
|
1802
|
+
? `
|
|
1803
|
+
<div class="timeline-kind-filter__popover" role="menu" aria-label="${escapeHtml(L("timeline.kindFilterLabel"))}">
|
|
1804
|
+
${options
|
|
1805
|
+
.map(
|
|
1806
|
+
(option) => `
|
|
1807
|
+
<button
|
|
1808
|
+
type="button"
|
|
1809
|
+
class="timeline-kind-filter__option ${option.id === current.id ? "is-selected" : ""}"
|
|
1810
|
+
data-timeline-kind-filter-option="${escapeHtml(option.id)}"
|
|
1811
|
+
role="menuitemradio"
|
|
1812
|
+
aria-checked="${option.id === current.id ? "true" : "false"}"
|
|
1813
|
+
>
|
|
1814
|
+
<span class="timeline-kind-filter__option-icon" aria-hidden="true">${renderIcon(option.icon)}</span>
|
|
1815
|
+
<span class="timeline-kind-filter__option-label">${escapeHtml(option.label)}</span>
|
|
1816
|
+
<span class="timeline-kind-filter__option-check" aria-hidden="true">${
|
|
1817
|
+
option.id === current.id ? renderIcon("check") : ""
|
|
1818
|
+
}</span>
|
|
1819
|
+
</button>
|
|
1820
|
+
`
|
|
1821
|
+
)
|
|
1822
|
+
.join("")}
|
|
1823
|
+
</div>
|
|
1824
|
+
`
|
|
1825
|
+
: ""
|
|
1826
|
+
}
|
|
1827
|
+
</div>
|
|
1828
|
+
`;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1511
1831
|
function renderTimelineEntry(entry, { desktop }) {
|
|
1512
1832
|
const item = entry.item;
|
|
1513
1833
|
const kindInfo = kindMeta(item.kind);
|
|
1514
1834
|
const kindClassName = escapeHtml(kindInfo.tone || "neutral");
|
|
1515
1835
|
const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
|
|
1516
1836
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || item.kind === "completion";
|
|
1837
|
+
const isFileEvent = item.kind === "file_event";
|
|
1517
1838
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
const secondaryText = isMessageLike ? "" : item.summary || fallbackSummaryForKind(item.kind, entry.status);
|
|
1839
|
+
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
1840
|
+
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent });
|
|
1841
|
+
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent });
|
|
1522
1842
|
const threadLabel = timelineEntryThreadLabel(item, isMessageLike);
|
|
1523
1843
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
1524
|
-
const statusLabel = isMessageLike
|
|
1844
|
+
const statusLabel = timelineEntryStatusLabel(item, isMessageLike);
|
|
1845
|
+
const fileEventFileSummary = isFileEvent ? timelineFileEventFileSummary(item) : "";
|
|
1846
|
+
const fileEventDiffStatsHtml = isFileEvent ? renderDiffEntryStatsHtml(item) : "";
|
|
1525
1847
|
|
|
1526
1848
|
return `
|
|
1527
1849
|
<button
|
|
@@ -1544,49 +1866,415 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
1544
1866
|
<div class="timeline-entry__body">
|
|
1545
1867
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
1546
1868
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
1869
|
+
${
|
|
1870
|
+
isFileEvent && fileEventFileSummary
|
|
1871
|
+
? `<p class="timeline-entry__file-summary" title="${escapeHtml(
|
|
1872
|
+
normalizeClientFileRefs(item.fileRefs).join("\n")
|
|
1873
|
+
)}">${escapeHtml(fileEventFileSummary)}</p>`
|
|
1874
|
+
: ""
|
|
1875
|
+
}
|
|
1876
|
+
${isFileEvent && fileEventDiffStatsHtml ? `<div class="timeline-entry__file-diff-stats diff-entry__stats">${fileEventDiffStatsHtml}</div>` : ""}
|
|
1547
1877
|
${renderTimelineEntryImageStrip(imageUrls)}
|
|
1878
|
+
${isFileEvent ? "" : renderTimelineEntryFileStrip(fileRefs)}
|
|
1548
1879
|
</div>
|
|
1549
1880
|
${statusLabel ? `<div class="timeline-entry__footer"><span class="timeline-entry__status">${escapeHtml(statusLabel)}</span></div>` : ""}
|
|
1550
1881
|
</button>
|
|
1551
1882
|
`;
|
|
1552
1883
|
}
|
|
1553
1884
|
|
|
1554
|
-
function
|
|
1555
|
-
|
|
1556
|
-
|
|
1885
|
+
function renderDiffEntry(entry) {
|
|
1886
|
+
const item = entry.item;
|
|
1887
|
+
const threadLabel = diffThreadCardTitle(item);
|
|
1888
|
+
const fileChipsHtml = renderDiffEntryFileChips(item);
|
|
1889
|
+
const fallbackSummary = fileChipsHtml ? "" : diffThreadSummaryLabel(item);
|
|
1890
|
+
const statsHtml = renderDiffEntryStatsHtml(item);
|
|
1891
|
+
const latestChangeSummary = diffThreadLatestChangeSummary(item);
|
|
1892
|
+
|
|
1893
|
+
return `
|
|
1894
|
+
<button
|
|
1895
|
+
class="diff-entry diff-entry--thread"
|
|
1896
|
+
data-open-item-kind="${escapeHtml(item.kind)}"
|
|
1897
|
+
data-open-item-token="${escapeHtml(item.token)}"
|
|
1898
|
+
data-source-tab="diff"
|
|
1899
|
+
>
|
|
1900
|
+
<div class="diff-entry__header">
|
|
1901
|
+
<p class="timeline-entry__thread diff-entry__thread">${escapeHtml(threadLabel)}</p>
|
|
1902
|
+
<span class="diff-entry__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>
|
|
1903
|
+
</div>
|
|
1904
|
+
<div class="diff-entry__body">
|
|
1905
|
+
${fileChipsHtml ? `<div class="diff-entry__files">${fileChipsHtml}</div>` : ""}
|
|
1906
|
+
${fallbackSummary ? `<p class="diff-entry__title">${escapeHtml(fallbackSummary)}</p>` : ""}
|
|
1907
|
+
${statsHtml ? `<div class="diff-entry__stats">${statsHtml}</div>` : ""}
|
|
1908
|
+
${latestChangeSummary ? `<p class="diff-entry__summary">${escapeHtml(latestChangeSummary)}</p>` : ""}
|
|
1909
|
+
</div>
|
|
1910
|
+
</button>
|
|
1911
|
+
`;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function timelineEntryStatusLabel(item, isMessageLike) {
|
|
1915
|
+
if (isMessageLike || item?.kind === "file_event") {
|
|
1916
|
+
return "";
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
const outcome = normalizeClientText(item?.outcome || "");
|
|
1920
|
+
switch (outcome) {
|
|
1921
|
+
case "pending":
|
|
1922
|
+
return L("common.actionNeeded");
|
|
1923
|
+
case "approved":
|
|
1924
|
+
return L("timeline.status.approved");
|
|
1925
|
+
case "rejected":
|
|
1926
|
+
return L("timeline.status.rejected");
|
|
1927
|
+
case "implemented":
|
|
1928
|
+
return L("timeline.status.implemented");
|
|
1929
|
+
case "dismissed":
|
|
1930
|
+
return L("timeline.status.dismissed");
|
|
1931
|
+
case "submitted":
|
|
1932
|
+
return L("timeline.status.submitted");
|
|
1933
|
+
default:
|
|
1934
|
+
break;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
if (item?.kind === "approval" || item?.kind === "plan" || item?.kind === "plan_ready" || item?.kind === "choice") {
|
|
1938
|
+
return L("common.actionNeeded");
|
|
1939
|
+
}
|
|
1940
|
+
return "";
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function renderTimelineEntryImageStrip(imageUrls) {
|
|
1944
|
+
if (!Array.isArray(imageUrls) || imageUrls.length === 0) {
|
|
1945
|
+
return "";
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
return `
|
|
1949
|
+
<div class="timeline-entry__images" aria-hidden="true">
|
|
1950
|
+
${imageUrls
|
|
1951
|
+
.slice(0, 4)
|
|
1952
|
+
.map(
|
|
1953
|
+
(imageUrl, index) => `
|
|
1954
|
+
<span class="timeline-entry__image-frame">
|
|
1955
|
+
<img
|
|
1956
|
+
class="timeline-entry__image"
|
|
1957
|
+
src="${escapeHtml(imageUrl)}"
|
|
1958
|
+
alt="${escapeHtml(L("detail.imageAlt", { index: index + 1 }))}"
|
|
1959
|
+
loading="lazy"
|
|
1960
|
+
>
|
|
1961
|
+
</span>
|
|
1962
|
+
`
|
|
1963
|
+
)
|
|
1964
|
+
.join("")}
|
|
1965
|
+
</div>
|
|
1966
|
+
`;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function renderTimelineEntryFileStrip(fileRefs) {
|
|
1970
|
+
if (!Array.isArray(fileRefs) || fileRefs.length === 0) {
|
|
1971
|
+
return "";
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
return `
|
|
1975
|
+
<div class="timeline-entry__files" aria-label="${escapeHtml(L("detail.filesTitle"))}">
|
|
1976
|
+
${fileRefs
|
|
1977
|
+
.slice(0, 4)
|
|
1978
|
+
.map(
|
|
1979
|
+
(fileRef) => `
|
|
1980
|
+
<span class="file-ref-chip" title="${escapeHtml(fileRef)}">
|
|
1981
|
+
<span class="file-ref-chip__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
1982
|
+
<span class="file-ref-chip__label">${escapeHtml(fileRefLabel(fileRef))}</span>
|
|
1983
|
+
</span>
|
|
1984
|
+
`
|
|
1985
|
+
)
|
|
1986
|
+
.join("")}
|
|
1987
|
+
</div>
|
|
1988
|
+
`;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function timelineEntryThreadLabel(item, isMessage) {
|
|
1992
|
+
const threadLabel = resolvedThreadLabel(item.threadId || "", item.threadLabel || "");
|
|
1993
|
+
return threadLabel || "";
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
1997
|
+
if (isMessageLike) {
|
|
1998
|
+
return item.summary || fallbackSummaryForKind(item.kind, status);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
if (isFileEvent) {
|
|
2002
|
+
return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
return timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: true }) || L("common.untitledItem");
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
2009
|
+
if (isMessageLike) {
|
|
2010
|
+
return "";
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
const summaryText = normalizeClientText(item.summary || fallbackSummaryForKind(item.kind, status));
|
|
2014
|
+
if (!summaryText || summaryText === normalizeClientText(primaryText || "")) {
|
|
2015
|
+
return "";
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
if (isFileEvent) {
|
|
2019
|
+
return "";
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const compactTitle = timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: false });
|
|
2023
|
+
return compactTitle ? summaryText : "";
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
function timelineDisplayTitleWithoutThread(item, { allowFallbackSummary = false } = {}) {
|
|
2027
|
+
const rawTitle = normalizeClientText(item?.title || "");
|
|
2028
|
+
const threadLabel = resolvedThreadLabel(item?.threadId || "", item?.threadLabel || "");
|
|
2029
|
+
if (!rawTitle) {
|
|
2030
|
+
return allowFallbackSummary ? normalizeClientText(item?.summary || "") : "";
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
let displayTitle = rawTitle;
|
|
2034
|
+
const removablePrefixes = timelineGeneratedTitlePrefixes();
|
|
2035
|
+
const hadGeneratedPrefix = removablePrefixes.some((prefix) => {
|
|
2036
|
+
const normalizedPrefix = normalizeClientText(prefix || "");
|
|
2037
|
+
return normalizedPrefix && rawTitle.startsWith(`${normalizedPrefix} | `);
|
|
2038
|
+
});
|
|
2039
|
+
|
|
2040
|
+
for (const prefix of removablePrefixes) {
|
|
2041
|
+
const normalizedPrefix = normalizeClientText(prefix || "");
|
|
2042
|
+
if (normalizedPrefix && displayTitle.startsWith(`${normalizedPrefix} | `)) {
|
|
2043
|
+
displayTitle = normalizeClientText(displayTitle.slice(normalizedPrefix.length + 3));
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (hadGeneratedPrefix) {
|
|
2049
|
+
if (!displayTitle || (threadLabel && displayTitle === threadLabel)) {
|
|
2050
|
+
return allowFallbackSummary ? normalizeClientText(item?.summary || "") : "";
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
if (threadLabel && displayTitle === threadLabel) {
|
|
2055
|
+
return allowFallbackSummary ? normalizeClientText(item?.summary || "") : "";
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
return displayTitle || (allowFallbackSummary ? normalizeClientText(item?.summary || "") : "");
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function timelineGeneratedTitlePrefixes() {
|
|
2062
|
+
return [
|
|
2063
|
+
kindMeta("approval").label,
|
|
2064
|
+
kindMeta("plan").label,
|
|
2065
|
+
kindMeta("choice").label,
|
|
2066
|
+
kindMeta("completion").label,
|
|
2067
|
+
kindMeta("user_message").label,
|
|
2068
|
+
kindMeta("assistant_commentary").label,
|
|
2069
|
+
kindMeta("assistant_final").label,
|
|
2070
|
+
L("common.fileEvent"),
|
|
2071
|
+
"Approval",
|
|
2072
|
+
"Plan",
|
|
2073
|
+
"Choice",
|
|
2074
|
+
"Completed",
|
|
2075
|
+
"User message",
|
|
2076
|
+
"Commentary",
|
|
2077
|
+
"Final answer",
|
|
2078
|
+
"Files",
|
|
2079
|
+
"承認",
|
|
2080
|
+
"プラン",
|
|
2081
|
+
"選択",
|
|
2082
|
+
"完了",
|
|
2083
|
+
"メッセージ",
|
|
2084
|
+
"途中経過",
|
|
2085
|
+
"最終回答",
|
|
2086
|
+
"ファイル",
|
|
2087
|
+
];
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
function fileEventDisplayLabel(fileEventType) {
|
|
2091
|
+
switch (normalizeClientText(fileEventType || "")) {
|
|
2092
|
+
case "read":
|
|
2093
|
+
return L("fileEvent.read");
|
|
2094
|
+
case "write":
|
|
2095
|
+
return L("fileEvent.write");
|
|
2096
|
+
case "create":
|
|
2097
|
+
return L("fileEvent.create");
|
|
2098
|
+
default:
|
|
2099
|
+
return "";
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
function fileEventTimelineCountLabel(item) {
|
|
2104
|
+
const fileEventType = normalizeClientText(item?.fileEventType || "");
|
|
2105
|
+
const count = normalizeClientFileRefs(item?.fileRefs).length;
|
|
2106
|
+
if (count <= 0) {
|
|
2107
|
+
return fileEventDisplayLabel(fileEventType) || L("common.fileEvent");
|
|
2108
|
+
}
|
|
2109
|
+
switch (fileEventType) {
|
|
2110
|
+
case "read":
|
|
2111
|
+
return L("fileEvent.timeline.read", { count });
|
|
2112
|
+
case "write":
|
|
2113
|
+
return L("fileEvent.timeline.write", { count });
|
|
2114
|
+
case "create":
|
|
2115
|
+
return L("fileEvent.timeline.create", { count });
|
|
2116
|
+
default:
|
|
2117
|
+
return L("common.fileEvent");
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
function diffThreadSummaryLabel(item) {
|
|
2122
|
+
const count = Math.max(0, Number(item?.changedFileCount) || 0);
|
|
2123
|
+
if (count <= 0) {
|
|
2124
|
+
return L("common.diff");
|
|
2125
|
+
}
|
|
2126
|
+
return L("diff.threadSummary", { count });
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
function diffThreadCardTitle(item) {
|
|
2130
|
+
return resolvedThreadLabel(item?.threadId || "", item?.threadLabel || "") || L("timeline.unknownThread");
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
function diffThreadCardSummary(item) {
|
|
2134
|
+
const parts = [];
|
|
2135
|
+
const filesLabel = diffThreadFilesSummary(item);
|
|
2136
|
+
if (filesLabel) {
|
|
2137
|
+
parts.push(filesLabel);
|
|
2138
|
+
} else {
|
|
2139
|
+
const summaryLabel = diffThreadSummaryLabel(item);
|
|
2140
|
+
if (summaryLabel && summaryLabel !== L("common.diff")) {
|
|
2141
|
+
parts.push(summaryLabel);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const statsLabel = diffEntryStatsLabel(item);
|
|
2145
|
+
if (statsLabel) {
|
|
2146
|
+
parts.push(statsLabel);
|
|
2147
|
+
}
|
|
2148
|
+
return parts.join(" • ");
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function diffThreadFilesSummary(item) {
|
|
2152
|
+
const labels = normalizeClientFileRefs(item?.fileRefs)
|
|
2153
|
+
.map((fileRef) => fileRefLabel(fileRef))
|
|
2154
|
+
.filter(Boolean);
|
|
2155
|
+
if (labels.length === 0) {
|
|
2156
|
+
return "";
|
|
2157
|
+
}
|
|
2158
|
+
const visibleLabels = labels.slice(0, 3);
|
|
2159
|
+
const hiddenCount = labels.length - visibleLabels.length;
|
|
2160
|
+
if (hiddenCount > 0) {
|
|
2161
|
+
visibleLabels.push(`+${hiddenCount}`);
|
|
2162
|
+
}
|
|
2163
|
+
return visibleLabels.join(", ");
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
function diffThreadLatestChangeSummary(item) {
|
|
2167
|
+
const prefix = L("diff.latestChange");
|
|
2168
|
+
const timestampLabel = formatDiffCardTimestamp(item?.latestChangedAtMs || item?.createdAtMs);
|
|
2169
|
+
if (timestampLabel) {
|
|
2170
|
+
return `${prefix}: ${timestampLabel}`;
|
|
2171
|
+
}
|
|
2172
|
+
if (Number(item?.latestChangedAtMs) > 0 || Number(item?.createdAtMs) > 0) {
|
|
2173
|
+
return L("diff.latestChangeFallback");
|
|
2174
|
+
}
|
|
2175
|
+
return "";
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
function renderDiffEntryFileChips(item) {
|
|
2179
|
+
const fileRefs = normalizeClientFileRefs(item?.fileRefs);
|
|
2180
|
+
if (fileRefs.length === 0) {
|
|
2181
|
+
return "";
|
|
2182
|
+
}
|
|
2183
|
+
const visibleRefs = fileRefs.slice(0, 4);
|
|
2184
|
+
const hiddenCount = fileRefs.length - visibleRefs.length;
|
|
2185
|
+
const chips = visibleRefs.map(
|
|
2186
|
+
(fileRef) => `
|
|
2187
|
+
<span class="file-ref-chip" title="${escapeHtml(fileRef)}">
|
|
2188
|
+
<span class="file-ref-chip__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
2189
|
+
<span class="file-ref-chip__label">${escapeHtml(fileRefLabel(fileRef))}</span>
|
|
2190
|
+
</span>
|
|
2191
|
+
`
|
|
2192
|
+
);
|
|
2193
|
+
if (hiddenCount > 0) {
|
|
2194
|
+
chips.push(`
|
|
2195
|
+
<span class="file-ref-chip file-ref-chip--count">
|
|
2196
|
+
<span class="file-ref-chip__label">+${hiddenCount}</span>
|
|
2197
|
+
</span>
|
|
2198
|
+
`);
|
|
2199
|
+
}
|
|
2200
|
+
return chips.join("");
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function renderDiffEntryStatsHtml(item) {
|
|
2204
|
+
const addedLines = Math.max(0, Number(item?.diffAddedLines ?? item?.addedLines) || 0);
|
|
2205
|
+
const removedLines = Math.max(0, Number(item?.diffRemovedLines ?? item?.removedLines) || 0);
|
|
2206
|
+
if (!addedLines && !removedLines) {
|
|
2207
|
+
return "";
|
|
2208
|
+
}
|
|
2209
|
+
const parts = [];
|
|
2210
|
+
if (addedLines) {
|
|
2211
|
+
parts.push(`<span class="diff-entry__stat diff-entry__stat--added">+${escapeHtml(String(addedLines))}</span>`);
|
|
2212
|
+
}
|
|
2213
|
+
if (removedLines) {
|
|
2214
|
+
parts.push(`<span class="diff-entry__stat diff-entry__stat--removed">-${escapeHtml(String(removedLines))}</span>`);
|
|
2215
|
+
}
|
|
2216
|
+
return parts.join('<span class="diff-entry__stats-separator">/</span>');
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
function formatDiffCardTimestamp(value) {
|
|
2220
|
+
const timestamp = Number(value) || 0;
|
|
2221
|
+
if (!timestamp) {
|
|
2222
|
+
return "";
|
|
2223
|
+
}
|
|
2224
|
+
try {
|
|
2225
|
+
return new Intl.DateTimeFormat(state.locale || DEFAULT_LOCALE, {
|
|
2226
|
+
month: "numeric",
|
|
2227
|
+
day: "numeric",
|
|
2228
|
+
hour: "numeric",
|
|
2229
|
+
minute: "2-digit",
|
|
2230
|
+
}).format(new Date(timestamp));
|
|
2231
|
+
} catch {
|
|
2232
|
+
return new Date(timestamp).toLocaleString();
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function diffThreadFileExpansionKey(token, fileRef) {
|
|
2237
|
+
return `${normalizeClientText(token || "")}:${normalizeClientText(fileRef || "")}`;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
function isDiffThreadFileExpanded(token, fileRef) {
|
|
2241
|
+
const key = diffThreadFileExpansionKey(token, fileRef);
|
|
2242
|
+
return state.diffThreadExpandedFiles?.[key] === true;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function toggleDiffThreadFileExpanded(token, fileRef) {
|
|
2246
|
+
const key = diffThreadFileExpansionKey(token, fileRef);
|
|
2247
|
+
if (!key || key === ":") {
|
|
2248
|
+
return;
|
|
1557
2249
|
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
.slice(0, 4)
|
|
1563
|
-
.map(
|
|
1564
|
-
(imageUrl, index) => `
|
|
1565
|
-
<span class="timeline-entry__image-frame">
|
|
1566
|
-
<img
|
|
1567
|
-
class="timeline-entry__image"
|
|
1568
|
-
src="${escapeHtml(imageUrl)}"
|
|
1569
|
-
alt="${escapeHtml(L("detail.imageAlt", { index: index + 1 }))}"
|
|
1570
|
-
loading="lazy"
|
|
1571
|
-
>
|
|
1572
|
-
</span>
|
|
1573
|
-
`
|
|
1574
|
-
)
|
|
1575
|
-
.join("")}
|
|
1576
|
-
</div>
|
|
1577
|
-
`;
|
|
2250
|
+
state.diffThreadExpandedFiles = {
|
|
2251
|
+
...(state.diffThreadExpandedFiles || {}),
|
|
2252
|
+
[key]: !isDiffThreadFileExpanded(token, fileRef),
|
|
2253
|
+
};
|
|
1578
2254
|
}
|
|
1579
2255
|
|
|
1580
|
-
function
|
|
1581
|
-
const
|
|
1582
|
-
|
|
2256
|
+
function timelineFileEventFileSummary(item) {
|
|
2257
|
+
const labels = normalizeClientFileRefs(item?.fileRefs)
|
|
2258
|
+
.map((fileRef) => fileRefLabel(fileRef))
|
|
2259
|
+
.filter(Boolean);
|
|
2260
|
+
if (labels.length === 0) {
|
|
1583
2261
|
return "";
|
|
1584
2262
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
2263
|
+
const visibleLabels = labels.slice(0, 3);
|
|
2264
|
+
const hiddenCount = labels.length - visibleLabels.length;
|
|
2265
|
+
if (hiddenCount > 0) {
|
|
2266
|
+
visibleLabels.push(`+${hiddenCount}`);
|
|
2267
|
+
}
|
|
2268
|
+
return visibleLabels.join(", ");
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
function diffEntryStatsLabel(item) {
|
|
2272
|
+
const addedLines = Math.max(0, Number(item?.diffAddedLines ?? item?.addedLines) || 0);
|
|
2273
|
+
const removedLines = Math.max(0, Number(item?.diffRemovedLines ?? item?.removedLines) || 0);
|
|
2274
|
+
if (!addedLines && !removedLines) {
|
|
2275
|
+
return "";
|
|
1587
2276
|
}
|
|
1588
|
-
|
|
1589
|
-
return title.includes(threadLabel) ? "" : threadLabel;
|
|
2277
|
+
return `+${addedLines} / -${removedLines}`;
|
|
1590
2278
|
}
|
|
1591
2279
|
|
|
1592
2280
|
function sanitizeThreadLabelForDisplay(label = "", threadId = "") {
|
|
@@ -2296,6 +2984,7 @@ function renderDetailContent(detail, { mobile }) {
|
|
|
2296
2984
|
function renderStandardDetailDesktop(detail) {
|
|
2297
2985
|
const kindInfo = kindMeta(detail.kind);
|
|
2298
2986
|
const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
|
|
2987
|
+
const plainIntro = renderDetailPlainIntro(detail);
|
|
2299
2988
|
return `
|
|
2300
2989
|
<div class="detail-shell">
|
|
2301
2990
|
${renderDetailMetaRow(detail, kindInfo)}
|
|
@@ -2303,10 +2992,19 @@ function renderStandardDetailDesktop(detail) {
|
|
|
2303
2992
|
${detail.readOnly ? "" : renderDetailLead(detail, kindInfo)}
|
|
2304
2993
|
${renderPreviousContextCard(detail)}
|
|
2305
2994
|
${renderInterruptedDetailNotice(detail)}
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2995
|
+
${
|
|
2996
|
+
plainIntro
|
|
2997
|
+
? plainIntro
|
|
2998
|
+
: `
|
|
2999
|
+
<section class="detail-card detail-card--body ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
|
|
3000
|
+
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
3001
|
+
</section>
|
|
3002
|
+
`
|
|
3003
|
+
}
|
|
2309
3004
|
${renderDetailImageGallery(detail)}
|
|
3005
|
+
${renderDetailDiffPanel(detail)}
|
|
3006
|
+
${renderDetailDiffThreadGroups(detail)}
|
|
3007
|
+
${renderDetailFileRefs(detail)}
|
|
2310
3008
|
${renderCompletionReplyComposer(detail)}
|
|
2311
3009
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [])}
|
|
2312
3010
|
</div>
|
|
@@ -2316,6 +3014,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
2316
3014
|
function renderStandardDetailMobile(detail) {
|
|
2317
3015
|
const kindInfo = kindMeta(detail.kind);
|
|
2318
3016
|
const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind) || detail.kind === "completion";
|
|
3017
|
+
const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
|
|
2319
3018
|
return `
|
|
2320
3019
|
<div class="mobile-detail-screen">
|
|
2321
3020
|
<div class="detail-shell detail-shell--mobile">
|
|
@@ -2323,11 +3022,20 @@ function renderStandardDetailMobile(detail) {
|
|
|
2323
3022
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
2324
3023
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
2325
3024
|
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
3025
|
+
${
|
|
3026
|
+
plainIntro
|
|
3027
|
+
? plainIntro
|
|
3028
|
+
: `
|
|
3029
|
+
<section class="detail-card detail-card--body detail-card--mobile ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
|
|
3030
|
+
${detail.readOnly ? "" : renderDetailLead(detail, kindInfo, { mobile: true })}
|
|
3031
|
+
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
3032
|
+
</section>
|
|
3033
|
+
`
|
|
3034
|
+
}
|
|
2330
3035
|
${renderDetailImageGallery(detail, { mobile: true })}
|
|
3036
|
+
${renderDetailDiffPanel(detail, { mobile: true })}
|
|
3037
|
+
${renderDetailDiffThreadGroups(detail, { mobile: true })}
|
|
3038
|
+
${renderDetailFileRefs(detail, { mobile: true })}
|
|
2331
3039
|
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
2332
3040
|
</div>
|
|
2333
3041
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
@@ -2336,6 +3044,20 @@ function renderStandardDetailMobile(detail) {
|
|
|
2336
3044
|
`;
|
|
2337
3045
|
}
|
|
2338
3046
|
|
|
3047
|
+
function renderDetailPlainIntro(detail, options = {}) {
|
|
3048
|
+
if (!["diff_thread", "file_event"].includes(detail?.kind || "")) {
|
|
3049
|
+
return "";
|
|
3050
|
+
}
|
|
3051
|
+
if (!detail?.messageHtml) {
|
|
3052
|
+
return "";
|
|
3053
|
+
}
|
|
3054
|
+
return `
|
|
3055
|
+
<div class="detail-page-copy ${options.mobile ? "detail-page-copy--mobile" : ""} markdown">
|
|
3056
|
+
${detail.messageHtml}
|
|
3057
|
+
</div>
|
|
3058
|
+
`;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
2339
3061
|
function renderDetailMetaRow(detail, kindInfo, options = {}) {
|
|
2340
3062
|
const timestampLabel = detail.createdAtMs ? formatTimelineTimestamp(detail.createdAtMs) : "";
|
|
2341
3063
|
const progressPill = options.progressLabel
|
|
@@ -2435,6 +3157,185 @@ function renderDetailImageGallery(detail, options = {}) {
|
|
|
2435
3157
|
`;
|
|
2436
3158
|
}
|
|
2437
3159
|
|
|
3160
|
+
function renderDetailFileRefs(detail, options = {}) {
|
|
3161
|
+
const fileRefs = normalizeClientFileRefs(detail?.fileRefs);
|
|
3162
|
+
if (fileRefs.length === 0) {
|
|
3163
|
+
return "";
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
return `
|
|
3167
|
+
<section class="detail-card detail-card--files ${options.mobile ? "detail-card--mobile" : ""}">
|
|
3168
|
+
<div class="detail-files-card__header">
|
|
3169
|
+
<span class="detail-files-card__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
3170
|
+
<span>${escapeHtml(L("detail.filesTitle"))}</span>
|
|
3171
|
+
</div>
|
|
3172
|
+
<div class="detail-file-grid">
|
|
3173
|
+
${fileRefs
|
|
3174
|
+
.map(
|
|
3175
|
+
(fileRef) => `
|
|
3176
|
+
<div class="detail-file-chip" title="${escapeHtml(fileRef)}">
|
|
3177
|
+
<span class="detail-file-chip__label">${escapeHtml(fileRefLabel(fileRef))}</span>
|
|
3178
|
+
<span class="detail-file-chip__path">${escapeHtml(fileRef)}</span>
|
|
3179
|
+
</div>
|
|
3180
|
+
`
|
|
3181
|
+
)
|
|
3182
|
+
.join("")}
|
|
3183
|
+
</div>
|
|
3184
|
+
</section>
|
|
3185
|
+
`;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
function renderDetailDiffPanel(detail, options = {}) {
|
|
3189
|
+
const detailKind = normalizeClientText(detail?.kind || "");
|
|
3190
|
+
if (!["file_event", "approval"].includes(detailKind)) {
|
|
3191
|
+
return "";
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
const fileEventType = normalizeClientText(detail?.fileEventType || "");
|
|
3195
|
+
if (detailKind === "file_event" && !["write", "create"].includes(fileEventType)) {
|
|
3196
|
+
return "";
|
|
3197
|
+
}
|
|
3198
|
+
if (
|
|
3199
|
+
detailKind === "approval" &&
|
|
3200
|
+
!String(detail?.diffText || "").trim() &&
|
|
3201
|
+
!detail?.diffAvailable &&
|
|
3202
|
+
normalizeClientFileRefs(detail?.fileRefs).length === 0
|
|
3203
|
+
) {
|
|
3204
|
+
return "";
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
const diffText = String(detail?.diffText || "").replace(/\r\n/g, "\n").trim();
|
|
3208
|
+
const statsHtml = renderDiffEntryStatsHtml(detail);
|
|
3209
|
+
|
|
3210
|
+
return `
|
|
3211
|
+
<section class="detail-card detail-card--diff ${options.mobile ? "detail-card--mobile" : ""}">
|
|
3212
|
+
<div class="detail-diff-card__header">
|
|
3213
|
+
<div class="detail-diff-card__title-wrap">
|
|
3214
|
+
<span class="detail-diff-card__icon" aria-hidden="true">${renderIcon("diff")}</span>
|
|
3215
|
+
<span>${escapeHtml(L("detail.diffTitle"))}</span>
|
|
3216
|
+
</div>
|
|
3217
|
+
${statsHtml ? `<span class="detail-diff-card__stats diff-entry__stats">${statsHtml}</span>` : ""}
|
|
3218
|
+
</div>
|
|
3219
|
+
${
|
|
3220
|
+
diffText
|
|
3221
|
+
? `<div class="detail-diff-viewer">${renderDiffLines(diffText)}</div>`
|
|
3222
|
+
: `<p class="detail-diff-card__notice">${escapeHtml(L("detail.diffUnavailable"))}</p>`
|
|
3223
|
+
}
|
|
3224
|
+
</section>
|
|
3225
|
+
`;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function renderDetailDiffThreadGroups(detail, options = {}) {
|
|
3229
|
+
if (detail?.kind !== "diff_thread") {
|
|
3230
|
+
return "";
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
const files = Array.isArray(detail?.files) ? detail.files.filter(Boolean) : [];
|
|
3234
|
+
if (files.length === 0) {
|
|
3235
|
+
return "";
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
return files
|
|
3239
|
+
.map((fileGroup) => renderDetailDiffThreadFileGroup(detail, fileGroup, options))
|
|
3240
|
+
.join("");
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function renderDetailDiffThreadFileGroup(detail, fileGroup, options = {}) {
|
|
3244
|
+
const fileRef = normalizeClientText(fileGroup?.fileRef || "");
|
|
3245
|
+
const fileLabel = normalizeClientText(fileGroup?.fileLabel || "") || fileRefLabel(fileRef) || L("common.unavailable");
|
|
3246
|
+
const statsHtml = renderDiffEntryStatsHtml(fileGroup);
|
|
3247
|
+
const sections = Array.isArray(fileGroup?.sections) ? fileGroup.sections.filter(Boolean) : [];
|
|
3248
|
+
const expanded = isDiffThreadFileExpanded(detail?.token, fileRef);
|
|
3249
|
+
|
|
3250
|
+
return `
|
|
3251
|
+
<section class="detail-card detail-card--diff-thread ${options.mobile ? "detail-card--mobile" : ""}">
|
|
3252
|
+
<button
|
|
3253
|
+
type="button"
|
|
3254
|
+
class="detail-diff-thread__header ${expanded ? "is-open" : ""}"
|
|
3255
|
+
data-diff-thread-file-toggle
|
|
3256
|
+
data-diff-thread-token="${escapeHtml(detail?.token || "")}"
|
|
3257
|
+
data-diff-thread-file="${escapeHtml(fileRef)}"
|
|
3258
|
+
>
|
|
3259
|
+
<div class="detail-diff-thread__title-wrap">
|
|
3260
|
+
<span class="detail-diff-thread__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
3261
|
+
<div class="detail-diff-thread__title-text">
|
|
3262
|
+
<span class="detail-diff-thread__label">${escapeHtml(fileLabel)}</span>
|
|
3263
|
+
${fileRef ? `<span class="detail-diff-thread__path">${escapeHtml(fileRef)}</span>` : ""}
|
|
3264
|
+
</div>
|
|
3265
|
+
</div>
|
|
3266
|
+
<div class="detail-diff-thread__header-right">
|
|
3267
|
+
${statsHtml ? `<span class="detail-diff-thread__stats diff-entry__stats">${statsHtml}</span>` : ""}
|
|
3268
|
+
<span class="detail-diff-thread__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>
|
|
3269
|
+
</div>
|
|
3270
|
+
</button>
|
|
3271
|
+
${
|
|
3272
|
+
expanded
|
|
3273
|
+
? `
|
|
3274
|
+
<div class="detail-diff-thread__sections">
|
|
3275
|
+
${sections.map((section) => renderDetailDiffThreadSection(section)).join("")}
|
|
3276
|
+
</div>
|
|
3277
|
+
`
|
|
3278
|
+
: ""
|
|
3279
|
+
}
|
|
3280
|
+
</section>
|
|
3281
|
+
`;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function renderDetailDiffThreadSection(section) {
|
|
3285
|
+
const sectionLabel = fileEventDisplayLabel(section?.fileEventType || "") || L("common.diff");
|
|
3286
|
+
const timestampLabel = section?.createdAtMs ? formatTimelineTimestamp(section.createdAtMs) : "";
|
|
3287
|
+
const statsHtml = renderDiffEntryStatsHtml(section);
|
|
3288
|
+
const diffText = String(section?.diffText || "").replace(/\r\n/g, "\n").trim();
|
|
3289
|
+
|
|
3290
|
+
return `
|
|
3291
|
+
<div class="detail-diff-thread__section">
|
|
3292
|
+
<div class="detail-diff-thread__section-meta">
|
|
3293
|
+
<span class="detail-diff-thread__section-label">${escapeHtml(sectionLabel)}</span>
|
|
3294
|
+
<div class="detail-diff-thread__section-right">
|
|
3295
|
+
${statsHtml ? `<span class="detail-diff-thread__section-stats diff-entry__stats">${statsHtml}</span>` : ""}
|
|
3296
|
+
${timestampLabel ? `<span class="detail-diff-thread__section-time">${escapeHtml(timestampLabel)}</span>` : ""}
|
|
3297
|
+
</div>
|
|
3298
|
+
</div>
|
|
3299
|
+
${
|
|
3300
|
+
diffText
|
|
3301
|
+
? `<div class="detail-diff-viewer">${renderDiffLines(diffText)}</div>`
|
|
3302
|
+
: `<p class="detail-diff-card__notice">${escapeHtml(L("detail.diffUnavailable"))}</p>`
|
|
3303
|
+
}
|
|
3304
|
+
</div>
|
|
3305
|
+
`;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function renderDiffLines(diffText) {
|
|
3309
|
+
return String(diffText || "")
|
|
3310
|
+
.replace(/\r\n/g, "\n")
|
|
3311
|
+
.split("\n")
|
|
3312
|
+
.map((line) => {
|
|
3313
|
+
const className = diffLineClassName(line);
|
|
3314
|
+
return `<div class="detail-diff-line ${className}">${escapeHtml(line || " ")}</div>`;
|
|
3315
|
+
})
|
|
3316
|
+
.join("");
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function diffLineClassName(line) {
|
|
3320
|
+
const text = String(line || "");
|
|
3321
|
+
if (text.startsWith("diff --git") || text.startsWith("index ") || text.startsWith("new file mode")) {
|
|
3322
|
+
return "detail-diff-line--meta";
|
|
3323
|
+
}
|
|
3324
|
+
if (text.startsWith("@@")) {
|
|
3325
|
+
return "detail-diff-line--hunk";
|
|
3326
|
+
}
|
|
3327
|
+
if (text.startsWith("+++ ") || text.startsWith("--- ")) {
|
|
3328
|
+
return "detail-diff-line--file";
|
|
3329
|
+
}
|
|
3330
|
+
if (text.startsWith("+")) {
|
|
3331
|
+
return "detail-diff-line--add";
|
|
3332
|
+
}
|
|
3333
|
+
if (text.startsWith("-")) {
|
|
3334
|
+
return "detail-diff-line--remove";
|
|
3335
|
+
}
|
|
3336
|
+
return "detail-diff-line--context";
|
|
3337
|
+
}
|
|
3338
|
+
|
|
2438
3339
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
2439
3340
|
if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
|
|
2440
3341
|
return "";
|
|
@@ -2985,14 +3886,19 @@ function renderBottomTabs() {
|
|
|
2985
3886
|
}
|
|
2986
3887
|
|
|
2987
3888
|
function renderTabButtons({ buttonClass, withIcons }) {
|
|
3889
|
+
const pendingCount = pendingInboxCount();
|
|
2988
3890
|
return tabs()
|
|
2989
3891
|
.map(
|
|
2990
|
-
(tab) =>
|
|
2991
|
-
|
|
2992
|
-
|
|
3892
|
+
(tab) => {
|
|
3893
|
+
const showAttentionDot = withIcons && buttonClass === "bottom-nav__button" && tab.id === "inbox" && pendingCount > 0;
|
|
3894
|
+
const ariaLabel = showAttentionDot ? `${tab.label} (${pendingCount})` : tab.label;
|
|
3895
|
+
return `
|
|
3896
|
+
<button class="${buttonClass} ${state.currentTab === tab.id ? "is-active" : ""}" data-tab="${escapeHtml(tab.id)}" aria-label="${escapeHtml(ariaLabel)}">
|
|
3897
|
+
${withIcons ? `<span class="tab-icon-wrap"><span class="tab-icon" aria-hidden="true">${renderIcon(tab.icon)}</span>${showAttentionDot ? `<span class="bottom-nav__attention-dot" aria-hidden="true"></span>` : ""}</span>` : ""}
|
|
2993
3898
|
<span class="tab-label">${escapeHtml(tab.label)}</span>
|
|
2994
3899
|
</button>
|
|
2995
3900
|
`
|
|
3901
|
+
}
|
|
2996
3902
|
)
|
|
2997
3903
|
.join("");
|
|
2998
3904
|
}
|
|
@@ -3060,6 +3966,27 @@ function bindShellInteractions() {
|
|
|
3060
3966
|
select.addEventListener("change", async () => {
|
|
3061
3967
|
clearThreadFilterInteraction();
|
|
3062
3968
|
state.timelineThreadFilter = select.value || "all";
|
|
3969
|
+
state.timelineKindFilterOpen = false;
|
|
3970
|
+
alignCurrentItemToVisibleEntries();
|
|
3971
|
+
await renderShell();
|
|
3972
|
+
});
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
for (const button of document.querySelectorAll("[data-timeline-kind-filter-toggle]")) {
|
|
3976
|
+
button.addEventListener("click", async (event) => {
|
|
3977
|
+
event.preventDefault();
|
|
3978
|
+
markThreadFilterInteraction();
|
|
3979
|
+
state.timelineKindFilterOpen = !state.timelineKindFilterOpen;
|
|
3980
|
+
await renderShell();
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
for (const button of document.querySelectorAll("[data-timeline-kind-filter-option]")) {
|
|
3985
|
+
button.addEventListener("click", async (event) => {
|
|
3986
|
+
event.preventDefault();
|
|
3987
|
+
clearThreadFilterInteraction();
|
|
3988
|
+
state.timelineKindFilter = button.dataset.timelineKindFilterOption || "all";
|
|
3989
|
+
state.timelineKindFilterOpen = false;
|
|
3063
3990
|
alignCurrentItemToVisibleEntries();
|
|
3064
3991
|
await renderShell();
|
|
3065
3992
|
});
|
|
@@ -3084,12 +4011,28 @@ function bindShellInteractions() {
|
|
|
3084
4011
|
});
|
|
3085
4012
|
}
|
|
3086
4013
|
|
|
4014
|
+
for (const button of document.querySelectorAll("[data-inbox-subtab]")) {
|
|
4015
|
+
button.addEventListener("click", async () => {
|
|
4016
|
+
const nextSubtab = button.dataset.inboxSubtab === "completed" ? "completed" : "pending";
|
|
4017
|
+
if (nextSubtab === state.inboxSubtab) {
|
|
4018
|
+
return;
|
|
4019
|
+
}
|
|
4020
|
+
state.inboxSubtab = nextSubtab;
|
|
4021
|
+
if (isDesktopLayout()) {
|
|
4022
|
+
alignCurrentItemToVisibleEntries();
|
|
4023
|
+
syncCurrentItemUrl(state.currentItem);
|
|
4024
|
+
}
|
|
4025
|
+
await renderShell();
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
|
|
3087
4029
|
for (const button of document.querySelectorAll("[data-open-item-kind][data-open-item-token]")) {
|
|
3088
4030
|
button.addEventListener("click", async () => {
|
|
3089
4031
|
openItem({
|
|
3090
4032
|
kind: button.dataset.openItemKind,
|
|
3091
4033
|
token: button.dataset.openItemToken,
|
|
3092
4034
|
sourceTab: button.dataset.sourceTab,
|
|
4035
|
+
sourceSubtab: button.dataset.sourceSubtab,
|
|
3093
4036
|
});
|
|
3094
4037
|
await renderShell();
|
|
3095
4038
|
});
|
|
@@ -3107,6 +4050,15 @@ function bindShellInteractions() {
|
|
|
3107
4050
|
});
|
|
3108
4051
|
}
|
|
3109
4052
|
|
|
4053
|
+
for (const button of document.querySelectorAll("[data-diff-thread-file-toggle]")) {
|
|
4054
|
+
button.addEventListener("click", async (event) => {
|
|
4055
|
+
event.preventDefault();
|
|
4056
|
+
event.stopPropagation();
|
|
4057
|
+
toggleDiffThreadFileExpanded(button.dataset.diffThreadToken || "", button.dataset.diffThreadFile || "");
|
|
4058
|
+
await renderShell();
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
|
|
3110
4062
|
for (const button of document.querySelectorAll("[data-back-to-list]")) {
|
|
3111
4063
|
button.addEventListener("click", async () => {
|
|
3112
4064
|
clearChoiceLocalDraftForItem(state.currentItem);
|
|
@@ -3550,6 +4502,7 @@ function closeSettingsSubpage() {
|
|
|
3550
4502
|
|
|
3551
4503
|
async function switchTab(tab) {
|
|
3552
4504
|
state.currentTab = tab;
|
|
4505
|
+
state.timelineKindFilterOpen = false;
|
|
3553
4506
|
state.pushNotice = "";
|
|
3554
4507
|
state.pushError = "";
|
|
3555
4508
|
state.settingsSubpage = "";
|
|
@@ -3566,10 +4519,14 @@ async function switchTab(tab) {
|
|
|
3566
4519
|
await renderShell();
|
|
3567
4520
|
}
|
|
3568
4521
|
|
|
3569
|
-
function openItem({ kind, token, sourceTab }) {
|
|
4522
|
+
function openItem({ kind, token, sourceTab, sourceSubtab }) {
|
|
3570
4523
|
const previousItem = state.currentItem ? { ...state.currentItem } : null;
|
|
3571
4524
|
clearPinnedDetailState();
|
|
3572
4525
|
const nextTab = sourceTab || tabForItemKind(kind, state.currentTab);
|
|
4526
|
+
if (nextTab === "inbox") {
|
|
4527
|
+
state.inboxSubtab = inboxSubtabForItemKind(kind, sourceSubtab);
|
|
4528
|
+
}
|
|
4529
|
+
state.timelineKindFilterOpen = false;
|
|
3573
4530
|
if (previousItem && (previousItem.kind !== kind || previousItem.token !== token)) {
|
|
3574
4531
|
clearChoiceLocalDraftForItem(previousItem);
|
|
3575
4532
|
}
|
|
@@ -3641,14 +4598,14 @@ function isSettingsSubpageOpen() {
|
|
|
3641
4598
|
|
|
3642
4599
|
function tabMeta(tab) {
|
|
3643
4600
|
switch (tab) {
|
|
3644
|
-
case "
|
|
4601
|
+
case "inbox":
|
|
3645
4602
|
return {
|
|
3646
|
-
id: "
|
|
3647
|
-
title: L("tab.
|
|
3648
|
-
label: L("tab.
|
|
4603
|
+
id: "inbox",
|
|
4604
|
+
title: L("tab.inbox.title"),
|
|
4605
|
+
label: L("tab.inbox.label"),
|
|
3649
4606
|
icon: "pending",
|
|
3650
|
-
eyebrow: L("tab.
|
|
3651
|
-
description: L("tab.
|
|
4607
|
+
eyebrow: L("tab.inbox.eyebrow"),
|
|
4608
|
+
description: L("tab.inbox.description"),
|
|
3652
4609
|
};
|
|
3653
4610
|
case "timeline":
|
|
3654
4611
|
return {
|
|
@@ -3659,14 +4616,14 @@ function tabMeta(tab) {
|
|
|
3659
4616
|
eyebrow: L("tab.timeline.eyebrow"),
|
|
3660
4617
|
description: L("tab.timeline.description"),
|
|
3661
4618
|
};
|
|
3662
|
-
case "
|
|
4619
|
+
case "diff":
|
|
3663
4620
|
return {
|
|
3664
|
-
id: "
|
|
3665
|
-
title: L("tab.
|
|
3666
|
-
label: L("tab.
|
|
3667
|
-
icon: "
|
|
3668
|
-
eyebrow: L("tab.
|
|
3669
|
-
description: L("tab.
|
|
4621
|
+
id: "diff",
|
|
4622
|
+
title: L("tab.code.title"),
|
|
4623
|
+
label: L("tab.code.label"),
|
|
4624
|
+
icon: "file-event",
|
|
4625
|
+
eyebrow: L("tab.code.eyebrow"),
|
|
4626
|
+
description: L("tab.code.description"),
|
|
3670
4627
|
};
|
|
3671
4628
|
case "settings":
|
|
3672
4629
|
return {
|
|
@@ -3684,26 +4641,43 @@ function tabMeta(tab) {
|
|
|
3684
4641
|
|
|
3685
4642
|
function tabs() {
|
|
3686
4643
|
return [
|
|
3687
|
-
tabMeta("
|
|
4644
|
+
tabMeta("inbox"),
|
|
3688
4645
|
tabMeta("timeline"),
|
|
3689
|
-
tabMeta("
|
|
4646
|
+
tabMeta("diff"),
|
|
3690
4647
|
tabMeta("settings"),
|
|
3691
4648
|
];
|
|
3692
4649
|
}
|
|
3693
4650
|
|
|
4651
|
+
function pendingInboxCount() {
|
|
4652
|
+
return Array.isArray(state.inbox?.pending) ? state.inbox.pending.length : 0;
|
|
4653
|
+
}
|
|
4654
|
+
|
|
3694
4655
|
function tabForItemKind(kind, fallback) {
|
|
4656
|
+
if (kind === "diff_thread") {
|
|
4657
|
+
return "diff";
|
|
4658
|
+
}
|
|
4659
|
+
if (kind === "file_event") {
|
|
4660
|
+
return "timeline";
|
|
4661
|
+
}
|
|
3695
4662
|
if (TIMELINE_MESSAGE_KINDS.has(kind)) {
|
|
3696
4663
|
return "timeline";
|
|
3697
4664
|
}
|
|
3698
4665
|
if (kind === "completion") {
|
|
3699
|
-
return "
|
|
4666
|
+
return "inbox";
|
|
3700
4667
|
}
|
|
3701
4668
|
if (fallback === "timeline") {
|
|
3702
4669
|
return "timeline";
|
|
3703
4670
|
}
|
|
3704
4671
|
return kind === "approval" || kind === "plan" || kind === "choice"
|
|
3705
|
-
? "
|
|
3706
|
-
: fallback || "
|
|
4672
|
+
? "inbox"
|
|
4673
|
+
: fallback || "inbox";
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
function inboxSubtabForItemKind(kind, sourceSubtab = "") {
|
|
4677
|
+
if (normalizeClientText(sourceSubtab || "") === "completed") {
|
|
4678
|
+
return "completed";
|
|
4679
|
+
}
|
|
4680
|
+
return kind === "completion" ? "completed" : "pending";
|
|
3707
4681
|
}
|
|
3708
4682
|
|
|
3709
4683
|
function kindMeta(kind) {
|
|
@@ -3723,6 +4697,10 @@ function kindMeta(kind) {
|
|
|
3723
4697
|
return { label: L("common.choice"), tone: "choice", icon: "choice" };
|
|
3724
4698
|
case "completion":
|
|
3725
4699
|
return { label: L("common.completion"), tone: "completion", icon: "completion-item" };
|
|
4700
|
+
case "diff_thread":
|
|
4701
|
+
return { label: L("common.diff"), tone: "neutral", icon: "diff" };
|
|
4702
|
+
case "file_event":
|
|
4703
|
+
return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
|
|
3726
4704
|
default:
|
|
3727
4705
|
return { label: L("common.item"), tone: "neutral", icon: "item" };
|
|
3728
4706
|
}
|
|
@@ -3736,6 +4714,12 @@ function renderTypePillContent(kindInfo) {
|
|
|
3736
4714
|
}
|
|
3737
4715
|
|
|
3738
4716
|
function itemIntentText(kind, status = "pending") {
|
|
4717
|
+
if (kind === "diff_thread") {
|
|
4718
|
+
return L("intent.diffThread");
|
|
4719
|
+
}
|
|
4720
|
+
if (kind === "file_event") {
|
|
4721
|
+
return L("intent.fileEvent");
|
|
4722
|
+
}
|
|
3739
4723
|
if (kind === "user_message") {
|
|
3740
4724
|
return L("intent.userMessage");
|
|
3741
4725
|
}
|
|
@@ -3763,6 +4747,12 @@ function itemIntentText(kind, status = "pending") {
|
|
|
3763
4747
|
}
|
|
3764
4748
|
|
|
3765
4749
|
function detailIntentText(detail) {
|
|
4750
|
+
if (detail.kind === "diff_thread") {
|
|
4751
|
+
return itemIntentText(detail.kind, "diff");
|
|
4752
|
+
}
|
|
4753
|
+
if (detail.kind === "file_event") {
|
|
4754
|
+
return itemIntentText(detail.kind, "timeline");
|
|
4755
|
+
}
|
|
3766
4756
|
if (TIMELINE_MESSAGE_KINDS.has(detail.kind)) {
|
|
3767
4757
|
return itemIntentText(detail.kind, "timeline");
|
|
3768
4758
|
}
|
|
@@ -3816,6 +4806,10 @@ function fallbackSummaryForKind(kind, status) {
|
|
|
3816
4806
|
return L("summary.completed");
|
|
3817
4807
|
}
|
|
3818
4808
|
switch (kind) {
|
|
4809
|
+
case "diff_thread":
|
|
4810
|
+
return L("summary.diffThread");
|
|
4811
|
+
case "file_event":
|
|
4812
|
+
return L("summary.fileEvent");
|
|
3819
4813
|
case "user_message":
|
|
3820
4814
|
return L("summary.userMessage");
|
|
3821
4815
|
case "assistant_commentary":
|
|
@@ -4018,6 +5012,10 @@ function renderIcon(name) {
|
|
|
4018
5012
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/><path d="m8.7 12.1 2 2.1 4.7-4.9"/></svg>`;
|
|
4019
5013
|
case "item":
|
|
4020
5014
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="15" rx="2.5"/><path d="M8.5 9h7"/><path d="M8.5 12h7"/><path d="M8.5 15h4.5"/></svg>`;
|
|
5015
|
+
case "file-event":
|
|
5016
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.8h5.9l4.3 4.3v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2v-12.3a2 2 0 0 1 2-2Z"/><path d="M13.9 3.8v4.3h4.3"/><path d="M9.2 13.1h5.6"/><path d="M9.2 16.2h4"/></svg>`;
|
|
5017
|
+
case "diff":
|
|
5018
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 5.5v13"/><path d="M4.8 8.2 7.5 5.5 10.2 8.2"/><path d="M16.5 18.5v-13"/><path d="m13.8 15.8 2.7 2.7 2.7-2.7"/><path d="M11.8 7.5h1.2"/><path d="M11 12h2.8"/><path d="M11.8 16.5h1.2"/></svg>`;
|
|
4021
5019
|
case "pending":
|
|
4022
5020
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v5"/><path d="M12 16v5"/><path d="M4.8 6.8l3.5 3.5"/><path d="M15.7 15.7l3.5 3.5"/><path d="M3 12h5"/><path d="M16 12h5"/><path d="M4.8 17.2l3.5-3.5"/><path d="M15.7 8.3l3.5-3.5"/></svg>`;
|
|
4023
5021
|
case "timeline":
|
|
@@ -4044,6 +5042,8 @@ function renderIcon(name) {
|
|
|
4044
5042
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10.4 13.6 8.3 15.7a3 3 0 0 1-4.2-4.2l2.8-2.8a3 3 0 0 1 4.2 0"/><path d="m13.6 10.4 2.1-2.1a3 3 0 1 1 4.2 4.2l-2.8 2.8a3 3 0 0 1-4.2 0"/><path d="m9.5 14.5 5-5"/></svg>`;
|
|
4045
5043
|
case "clip":
|
|
4046
5044
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 12.5 5.9-5.9a3 3 0 1 1 4.2 4.2l-7.7 7.7a5 5 0 1 1-7.1-7.1l8.1-8.1"/></svg>`;
|
|
5045
|
+
case "filter":
|
|
5046
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14"/><path d="M8 12h8"/><path d="M10.5 17h3"/></svg>`;
|
|
4047
5047
|
case "check":
|
|
4048
5048
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.8 12.5 3.2 3.2 7.2-7.4"/></svg>`;
|
|
4049
5049
|
case "back":
|
|
@@ -4151,7 +5151,29 @@ function handleServiceWorkerMessage(event) {
|
|
|
4151
5151
|
const type = event?.data?.type || "";
|
|
4152
5152
|
if (type === "pushsubscriptionchange") {
|
|
4153
5153
|
refreshPushStatus().then(renderCurrentSurface).catch(() => {});
|
|
5154
|
+
return;
|
|
5155
|
+
}
|
|
5156
|
+
if (type === "open-target-url" && event?.data?.url) {
|
|
5157
|
+
applyExternalTargetUrl(event.data.url).catch(() => {});
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
function handlePotentialExternalNavigation() {
|
|
5162
|
+
consumePendingNotificationIntent()
|
|
5163
|
+
.then((consumed) => {
|
|
5164
|
+
if (consumed) {
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
return applyExternalTargetUrl(window.location.href, { allowRefresh: false });
|
|
5168
|
+
})
|
|
5169
|
+
.catch(() => {});
|
|
5170
|
+
}
|
|
5171
|
+
|
|
5172
|
+
function handleDocumentVisibilityChange() {
|
|
5173
|
+
if (document.visibilityState !== "visible") {
|
|
5174
|
+
return;
|
|
4154
5175
|
}
|
|
5176
|
+
handlePotentialExternalNavigation();
|
|
4155
5177
|
}
|
|
4156
5178
|
|
|
4157
5179
|
async function apiGet(url) {
|
|
@@ -4250,11 +5272,124 @@ function normalizeClientText(value) {
|
|
|
4250
5272
|
return String(value ?? "").trim();
|
|
4251
5273
|
}
|
|
4252
5274
|
|
|
5275
|
+
function normalizeClientFileRefs(fileRefs) {
|
|
5276
|
+
if (!Array.isArray(fileRefs)) {
|
|
5277
|
+
return [];
|
|
5278
|
+
}
|
|
5279
|
+
const deduped = [];
|
|
5280
|
+
const seen = new Set();
|
|
5281
|
+
for (const fileRef of fileRefs) {
|
|
5282
|
+
const normalized = normalizeClientText(fileRef);
|
|
5283
|
+
if (!normalized || seen.has(normalized)) {
|
|
5284
|
+
continue;
|
|
5285
|
+
}
|
|
5286
|
+
seen.add(normalized);
|
|
5287
|
+
deduped.push(normalized);
|
|
5288
|
+
if (deduped.length >= 8) {
|
|
5289
|
+
break;
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
return deduped;
|
|
5293
|
+
}
|
|
5294
|
+
|
|
5295
|
+
function fileRefLabel(fileRef) {
|
|
5296
|
+
const normalized = normalizeClientText(fileRef);
|
|
5297
|
+
if (!normalized) {
|
|
5298
|
+
return "";
|
|
5299
|
+
}
|
|
5300
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
5301
|
+
return segments[segments.length - 1] || normalized;
|
|
5302
|
+
}
|
|
5303
|
+
|
|
4253
5304
|
function parseItemRef(value) {
|
|
4254
5305
|
const [kind, token] = String(value || "").split(":");
|
|
4255
5306
|
return kind && token ? { kind, token } : null;
|
|
4256
5307
|
}
|
|
4257
5308
|
|
|
5309
|
+
async function applyExternalTargetUrl(urlString, { allowRefresh = true } = {}) {
|
|
5310
|
+
if (!state.session?.authenticated) {
|
|
5311
|
+
return;
|
|
5312
|
+
}
|
|
5313
|
+
|
|
5314
|
+
let nextUrl;
|
|
5315
|
+
try {
|
|
5316
|
+
nextUrl = new URL(urlString, window.location.origin);
|
|
5317
|
+
} catch {
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
const itemRef = parseItemRef(nextUrl.searchParams.get("item"));
|
|
5322
|
+
if (!itemRef) {
|
|
5323
|
+
return;
|
|
5324
|
+
}
|
|
5325
|
+
|
|
5326
|
+
const sameItem =
|
|
5327
|
+
Boolean(state.currentItem) &&
|
|
5328
|
+
isSameItemRef(state.currentItem, itemRef) &&
|
|
5329
|
+
(isDesktopLayout() || state.detailOpen);
|
|
5330
|
+
if (sameItem) {
|
|
5331
|
+
if (allowRefresh) {
|
|
5332
|
+
await refreshAuthenticatedState();
|
|
5333
|
+
ensureCurrentSelection();
|
|
5334
|
+
await renderShell();
|
|
5335
|
+
}
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
openItem({
|
|
5340
|
+
kind: itemRef.kind,
|
|
5341
|
+
token: itemRef.token,
|
|
5342
|
+
sourceTab: tabForItemKind(itemRef.kind, state.currentTab),
|
|
5343
|
+
});
|
|
5344
|
+
if (isFastPathItemRef(itemRef)) {
|
|
5345
|
+
state.launchItemIntent = {
|
|
5346
|
+
...itemRef,
|
|
5347
|
+
status: "pending",
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
await renderShell();
|
|
5351
|
+
|
|
5352
|
+
if (!allowRefresh) {
|
|
5353
|
+
return;
|
|
5354
|
+
}
|
|
5355
|
+
await refreshAuthenticatedState();
|
|
5356
|
+
ensureCurrentSelection();
|
|
5357
|
+
await renderShell();
|
|
5358
|
+
}
|
|
5359
|
+
|
|
5360
|
+
async function consumePendingNotificationIntent() {
|
|
5361
|
+
if (!state.session?.authenticated || typeof caches === "undefined") {
|
|
5362
|
+
return false;
|
|
5363
|
+
}
|
|
5364
|
+
let cache;
|
|
5365
|
+
try {
|
|
5366
|
+
cache = await caches.open(NOTIFICATION_INTENT_CACHE);
|
|
5367
|
+
} catch {
|
|
5368
|
+
return false;
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
const request = new Request(NOTIFICATION_INTENT_PATH);
|
|
5372
|
+
const match = await cache.match(request).catch(() => null);
|
|
5373
|
+
if (!match) {
|
|
5374
|
+
return false;
|
|
5375
|
+
}
|
|
5376
|
+
|
|
5377
|
+
let payload = null;
|
|
5378
|
+
try {
|
|
5379
|
+
payload = await match.json();
|
|
5380
|
+
} catch {
|
|
5381
|
+
payload = null;
|
|
5382
|
+
}
|
|
5383
|
+
await cache.delete(request).catch(() => {});
|
|
5384
|
+
|
|
5385
|
+
const url = normalizeClientText(payload?.url || "");
|
|
5386
|
+
if (!url) {
|
|
5387
|
+
return false;
|
|
5388
|
+
}
|
|
5389
|
+
await applyExternalTargetUrl(url, { allowRefresh: true });
|
|
5390
|
+
return true;
|
|
5391
|
+
}
|
|
5392
|
+
|
|
4258
5393
|
function buildAppUrl(nextParams) {
|
|
4259
5394
|
const query = nextParams.toString();
|
|
4260
5395
|
return `/app${query ? `?${query}` : ""}`;
|