viveworker 0.1.4 → 0.1.5
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 +241 -17
- package/web/app.css +179 -0
- package/web/app.js +370 -18
- package/web/i18n.js +28 -0
- package/web/sw.js +52 -5
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viveworker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Local iPhone companion for Codex Desktop approvals, plan checks, questions, and notifications on your LAN.",
|
|
5
5
|
"author": "Yuta Hoshino <hoshino.lireneo@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -267,6 +267,187 @@ function withNotificationIcon(kind, title) {
|
|
|
267
267
|
return prefix ? `${prefix} ${title}` : title;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function normalizeTimelineOutcome(value) {
|
|
271
|
+
const normalized = cleanText(value || "").toLowerCase();
|
|
272
|
+
return ["pending", "approved", "rejected", "implemented", "dismissed", "submitted"].includes(normalized)
|
|
273
|
+
? normalized
|
|
274
|
+
: "";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function inferTimelineOutcome(kind, summary = "", messageText = "") {
|
|
278
|
+
const normalizedKind = cleanText(kind || "");
|
|
279
|
+
if (!normalizedKind) {
|
|
280
|
+
return "";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const candidates = [cleanText(summary || ""), cleanText(messageText || "")].filter(Boolean);
|
|
284
|
+
if (candidates.length === 0) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const startsWithAny = (keys) =>
|
|
289
|
+
candidates.some((text) => SUPPORTED_LOCALES.some((locale) => keys.some((key) => text.startsWith(t(locale, key)))));
|
|
290
|
+
|
|
291
|
+
if (normalizedKind === "approval") {
|
|
292
|
+
if (startsWithAny(["server.message.approvalAccepted"])) {
|
|
293
|
+
return "approved";
|
|
294
|
+
}
|
|
295
|
+
if (startsWithAny(["server.message.approvalRejected"])) {
|
|
296
|
+
return "rejected";
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (normalizedKind === "plan") {
|
|
302
|
+
if (startsWithAny(["server.message.planImplemented"])) {
|
|
303
|
+
return "implemented";
|
|
304
|
+
}
|
|
305
|
+
if (startsWithAny(["server.message.planDismissed"])) {
|
|
306
|
+
return "dismissed";
|
|
307
|
+
}
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (normalizedKind === "choice") {
|
|
312
|
+
if (
|
|
313
|
+
startsWithAny([
|
|
314
|
+
"server.message.choiceSubmitted",
|
|
315
|
+
"server.message.choiceSubmittedTest",
|
|
316
|
+
"server.message.choiceSummarySubmitted",
|
|
317
|
+
"server.message.choiceSummaryReceivedTest",
|
|
318
|
+
])
|
|
319
|
+
) {
|
|
320
|
+
return "submitted";
|
|
321
|
+
}
|
|
322
|
+
return "";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return "";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function normalizeTimelineFileRefs(rawFileRefs) {
|
|
329
|
+
if (!Array.isArray(rawFileRefs)) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const deduped = [];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
for (const rawRef of rawFileRefs) {
|
|
336
|
+
const normalized = cleanTimelineFileRef(rawRef);
|
|
337
|
+
if (!normalized || seen.has(normalized)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
seen.add(normalized);
|
|
341
|
+
deduped.push(normalized);
|
|
342
|
+
if (deduped.length >= 8) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return deduped;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function cleanTimelineFileRef(value) {
|
|
350
|
+
let normalized = cleanText(value || "");
|
|
351
|
+
if (!normalized) {
|
|
352
|
+
return "";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
normalized = normalized
|
|
356
|
+
.replace(/^[`"'([{<]+/u, "")
|
|
357
|
+
.replace(/[)`"'\]}>.,:;!?]+$/u, "")
|
|
358
|
+
.replace(/[#:]L?\d+(?::\d+)?$/u, "");
|
|
359
|
+
|
|
360
|
+
if (!normalized || /^https?:\/\//iu.test(normalized)) {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (normalized.startsWith("/")) {
|
|
365
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
366
|
+
if (segments.length >= 2 && looksLikeFileRefBasename(segments[segments.length - 1])) {
|
|
367
|
+
return normalized;
|
|
368
|
+
}
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (normalized.includes("/")) {
|
|
373
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
374
|
+
if (
|
|
375
|
+
segments.length >= 2 &&
|
|
376
|
+
segments.every((segment) => /^[A-Za-z0-9._-]+$/u.test(segment)) &&
|
|
377
|
+
looksLikeFileRefBasename(segments[segments.length - 1])
|
|
378
|
+
) {
|
|
379
|
+
return normalized;
|
|
380
|
+
}
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return looksLikeFileRefBasename(normalized) ? normalized : "";
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function looksLikeFileRefBasename(value) {
|
|
388
|
+
const normalized = cleanText(value || "");
|
|
389
|
+
if (!normalized || /[\s\\]/u.test(normalized)) {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
if (
|
|
393
|
+
[
|
|
394
|
+
".env",
|
|
395
|
+
".env.example",
|
|
396
|
+
".gitignore",
|
|
397
|
+
"Dockerfile",
|
|
398
|
+
"LICENSE",
|
|
399
|
+
"Makefile",
|
|
400
|
+
"README.md",
|
|
401
|
+
"package-lock.json",
|
|
402
|
+
"package.json",
|
|
403
|
+
"pnpm-lock.yaml",
|
|
404
|
+
"tsconfig.json",
|
|
405
|
+
"vite.config.ts",
|
|
406
|
+
].includes(normalized)
|
|
407
|
+
) {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
return /^(?:\.[A-Za-z0-9_-]+(?:\.[A-Za-z][A-Za-z0-9_-]{0,9})?|[A-Za-z0-9_-][A-Za-z0-9._-]*\.[A-Za-z][A-Za-z0-9_-]{0,9})$/u.test(
|
|
411
|
+
normalized
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function extractTimelineFileRefs(messageText = "") {
|
|
416
|
+
const sourceText = String(messageText || "");
|
|
417
|
+
if (!sourceText) {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const refs = [];
|
|
422
|
+
const pushCandidate = (candidate) => {
|
|
423
|
+
const normalized = cleanTimelineFileRef(candidate);
|
|
424
|
+
if (normalized) {
|
|
425
|
+
refs.push(normalized);
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const collectCandidates = (text) => {
|
|
429
|
+
for (const candidate of String(text || "").split(/\s+/u)) {
|
|
430
|
+
pushCandidate(candidate);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
for (const match of sourceText.matchAll(/\[[^\]]+\]\((\/[^)\s]+)\)/gu)) {
|
|
435
|
+
pushCandidate(match[1]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
for (const match of sourceText.matchAll(/`([^`\n]+)`/gu)) {
|
|
439
|
+
collectCandidates(match[1]);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
collectCandidates(
|
|
443
|
+
sourceText
|
|
444
|
+
.replace(/`[^`\n]+`/gu, " ")
|
|
445
|
+
.replace(/\[[^\]]+\]\(([^)]+)\)/gu, " ")
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return normalizeTimelineFileRefs(refs);
|
|
449
|
+
}
|
|
450
|
+
|
|
270
451
|
function handleSignal() {
|
|
271
452
|
runtime.stopping = true;
|
|
272
453
|
}
|
|
@@ -304,11 +485,14 @@ function normalizeHistoryItem(raw) {
|
|
|
304
485
|
const kind = cleanText(raw.kind ?? "");
|
|
305
486
|
const title = cleanText(raw.title ?? "");
|
|
306
487
|
const messageText = normalizeTimelineMessageText(raw.messageText ?? "");
|
|
488
|
+
const summary = normalizeNotificationText(raw.summary ?? "") || formatNotificationBody(messageText, 100) || "";
|
|
307
489
|
const createdAtMs = Number(raw.createdAtMs) || Date.now();
|
|
308
490
|
if (!stableId || !historyKinds.has(kind) || !title) {
|
|
309
491
|
return null;
|
|
310
492
|
}
|
|
311
493
|
|
|
494
|
+
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
495
|
+
|
|
312
496
|
return {
|
|
313
497
|
stableId,
|
|
314
498
|
token: cleanText(raw.token ?? "") || historyToken(stableId),
|
|
@@ -316,9 +500,11 @@ function normalizeHistoryItem(raw) {
|
|
|
316
500
|
threadId: cleanText(raw.threadId ?? extractConversationIdFromStableId(stableId) ?? ""),
|
|
317
501
|
title,
|
|
318
502
|
threadLabel: cleanText(raw.threadLabel ?? ""),
|
|
319
|
-
summary
|
|
503
|
+
summary,
|
|
320
504
|
messageText,
|
|
321
505
|
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
506
|
+
fileRefs: normalizeTimelineFileRefs(raw.fileRefs ?? extractTimelineFileRefs(messageText)),
|
|
507
|
+
outcome,
|
|
322
508
|
createdAtMs,
|
|
323
509
|
readOnly: raw.readOnly !== false,
|
|
324
510
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -400,6 +586,7 @@ function normalizeTimelineEntry(raw) {
|
|
|
400
586
|
"";
|
|
401
587
|
const threadLabel = cleanText(raw.threadLabel ?? "");
|
|
402
588
|
const title = cleanText(raw.title ?? "") || threadLabel || kindTitle(DEFAULT_LOCALE, kind);
|
|
589
|
+
const outcome = normalizeTimelineOutcome(raw.outcome ?? "") || inferTimelineOutcome(kind, summary, messageText);
|
|
403
590
|
|
|
404
591
|
return {
|
|
405
592
|
stableId,
|
|
@@ -411,6 +598,8 @@ function normalizeTimelineEntry(raw) {
|
|
|
411
598
|
summary,
|
|
412
599
|
messageText,
|
|
413
600
|
imagePaths: normalizeTimelineImagePaths(raw.imagePaths ?? raw.localImagePaths ?? []),
|
|
601
|
+
fileRefs: normalizeTimelineFileRefs(raw.fileRefs ?? extractTimelineFileRefs(messageText)),
|
|
602
|
+
outcome,
|
|
414
603
|
createdAtMs,
|
|
415
604
|
readOnly: raw.readOnly !== false,
|
|
416
605
|
primaryLabel: cleanText(raw.primaryLabel ?? "") || "詳細",
|
|
@@ -498,26 +687,48 @@ function recordHistoryItem({ config, runtime, state, item }) {
|
|
|
498
687
|
return changed;
|
|
499
688
|
}
|
|
500
689
|
|
|
501
|
-
function recordActionHistoryItem({
|
|
502
|
-
|
|
690
|
+
function recordActionHistoryItem({
|
|
691
|
+
config,
|
|
692
|
+
runtime,
|
|
693
|
+
state,
|
|
694
|
+
kind,
|
|
695
|
+
stableId,
|
|
696
|
+
token,
|
|
697
|
+
title,
|
|
698
|
+
threadLabel = "",
|
|
699
|
+
messageText,
|
|
700
|
+
summary,
|
|
701
|
+
outcome = "",
|
|
702
|
+
}) {
|
|
703
|
+
const item = {
|
|
704
|
+
stableId,
|
|
705
|
+
token,
|
|
706
|
+
kind,
|
|
707
|
+
title,
|
|
708
|
+
threadId: cleanText(extractConversationIdFromStableId(stableId) ?? ""),
|
|
709
|
+
threadLabel,
|
|
710
|
+
summary,
|
|
711
|
+
messageText,
|
|
712
|
+
outcome,
|
|
713
|
+
createdAtMs: Date.now(),
|
|
714
|
+
readOnly: true,
|
|
715
|
+
primaryLabel: "詳細",
|
|
716
|
+
tone: "secondary",
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
const historyChanged = recordHistoryItem({
|
|
503
720
|
config,
|
|
504
721
|
runtime,
|
|
505
722
|
state,
|
|
506
|
-
item
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
summary,
|
|
514
|
-
messageText,
|
|
515
|
-
createdAtMs: Date.now(),
|
|
516
|
-
readOnly: true,
|
|
517
|
-
primaryLabel: "詳細",
|
|
518
|
-
tone: "secondary",
|
|
519
|
-
},
|
|
723
|
+
item,
|
|
724
|
+
});
|
|
725
|
+
const timelineChanged = recordTimelineEntry({
|
|
726
|
+
config,
|
|
727
|
+
runtime,
|
|
728
|
+
state,
|
|
729
|
+
entry: item,
|
|
520
730
|
});
|
|
731
|
+
return historyChanged || timelineChanged;
|
|
521
732
|
}
|
|
522
733
|
|
|
523
734
|
function pendingApprovalStableId(approval) {
|
|
@@ -5685,6 +5896,7 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
5685
5896
|
const items = normalizeHistoryItems(state.recentHistoryItems ?? runtime.recentHistoryItems, config.maxHistoryItems);
|
|
5686
5897
|
runtime.recentHistoryItems = items;
|
|
5687
5898
|
return items
|
|
5899
|
+
.filter((item) => cleanText(item?.kind || "") === "completion")
|
|
5688
5900
|
.slice()
|
|
5689
5901
|
.sort((left, right) => Number(right.createdAtMs ?? 0) - Number(left.createdAtMs ?? 0))
|
|
5690
5902
|
.map((item) => ({
|
|
@@ -5694,6 +5906,7 @@ function buildCompletedInboxItems(runtime, state, config, locale) {
|
|
|
5694
5906
|
threadLabel: item.threadLabel || "",
|
|
5695
5907
|
title: item.threadLabel ? formatTitle(kindTitle(locale, item.kind), item.threadLabel) : item.title,
|
|
5696
5908
|
summary: item.summary,
|
|
5909
|
+
fileRefs: normalizeTimelineFileRefs(item.fileRefs ?? []),
|
|
5697
5910
|
primaryLabel: t(locale, "server.action.detail"),
|
|
5698
5911
|
createdAtMs: item.createdAtMs,
|
|
5699
5912
|
}));
|
|
@@ -5724,6 +5937,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5724
5937
|
title: formatLocalizedTitle(locale, "server.title.approval", approval.threadLabel),
|
|
5725
5938
|
summary: formatNotificationBody(approval.messageText, 180) || approval.messageText,
|
|
5726
5939
|
messageText: approval.messageText,
|
|
5940
|
+
outcome: "pending",
|
|
5727
5941
|
createdAtMs: Number(approval.createdAtMs) || now,
|
|
5728
5942
|
})
|
|
5729
5943
|
);
|
|
@@ -5746,6 +5960,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5746
5960
|
title: formatLocalizedTitle(locale, "server.title.plan", planRequest.threadLabel),
|
|
5747
5961
|
summary: formatNotificationBody(planRequest.messageText, 180) || planRequest.messageText,
|
|
5748
5962
|
messageText: planRequest.messageText,
|
|
5963
|
+
outcome: "pending",
|
|
5749
5964
|
createdAtMs: Number(planRequest.createdAtMs) || now,
|
|
5750
5965
|
})
|
|
5751
5966
|
);
|
|
@@ -5772,6 +5987,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5772
5987
|
),
|
|
5773
5988
|
summary: userInputRequest.notificationText || formatNotificationBody(userInputRequest.messageText, 180),
|
|
5774
5989
|
messageText: userInputRequest.messageText,
|
|
5990
|
+
outcome: "pending",
|
|
5775
5991
|
createdAtMs: Number(userInputRequest.createdAtMs) || now,
|
|
5776
5992
|
})
|
|
5777
5993
|
);
|
|
@@ -5791,6 +6007,7 @@ function buildOperationalTimelineEntries(runtime, state, config, locale) {
|
|
|
5791
6007
|
title: historyItem.threadLabel ? formatTitle(kindTitle(locale, historyItem.kind), historyItem.threadLabel) : historyItem.title,
|
|
5792
6008
|
summary: historyItem.summary,
|
|
5793
6009
|
messageText: historyItem.messageText,
|
|
6010
|
+
outcome: historyItem.outcome,
|
|
5794
6011
|
createdAtMs: historyItem.createdAtMs,
|
|
5795
6012
|
})
|
|
5796
6013
|
);
|
|
@@ -5872,6 +6089,8 @@ function buildTimelineResponse(runtime, state, config, locale) {
|
|
|
5872
6089
|
threadLabel: entry.threadLabel,
|
|
5873
6090
|
summary: entry.summary,
|
|
5874
6091
|
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
6092
|
+
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
6093
|
+
outcome: entry.outcome || "",
|
|
5875
6094
|
createdAtMs: entry.createdAtMs,
|
|
5876
6095
|
}));
|
|
5877
6096
|
|
|
@@ -6120,6 +6339,7 @@ function buildHistoryDetail(item, locale, runtime = null) {
|
|
|
6120
6339
|
threadLabel: item.threadLabel || "",
|
|
6121
6340
|
createdAtMs: Number(item.createdAtMs) || 0,
|
|
6122
6341
|
messageHtml: renderMessageHtml(item.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6342
|
+
fileRefs: normalizeTimelineFileRefs(item.fileRefs ?? []),
|
|
6123
6343
|
interruptNotice: interruptedDetailNotice(item.messageText, locale),
|
|
6124
6344
|
readOnly: true,
|
|
6125
6345
|
reply: replyEnabled
|
|
@@ -6155,6 +6375,7 @@ function buildTimelineMessageDetail(entry, locale, runtime = null) {
|
|
|
6155
6375
|
createdAtMs: Number(entry.createdAtMs) || 0,
|
|
6156
6376
|
messageHtml: renderMessageHtml(entry.messageText, `<p>${escapeHtml(t(locale, "detail.detailUnavailable"))}</p>`),
|
|
6157
6377
|
imageUrls: buildTimelineEntryImageUrls(entry),
|
|
6378
|
+
fileRefs: normalizeTimelineFileRefs(entry.fileRefs ?? []),
|
|
6158
6379
|
previousContext: buildInterruptedTimelineContext(runtime, entry, locale),
|
|
6159
6380
|
interruptNotice: interruptedDetailNotice(entry.messageText, locale),
|
|
6160
6381
|
readOnly: true,
|
|
@@ -6234,6 +6455,7 @@ async function submitGenericUserInputDecision({ config, runtime, state, userInpu
|
|
|
6234
6455
|
summary: userInputRequest.testRequest
|
|
6235
6456
|
? t(config.defaultLocale, "server.message.choiceSummaryReceivedTest")
|
|
6236
6457
|
: t(config.defaultLocale, "server.message.choiceSummarySubmitted"),
|
|
6458
|
+
outcome: "submitted",
|
|
6237
6459
|
}) || stateChanged;
|
|
6238
6460
|
if (stateChanged) {
|
|
6239
6461
|
await saveState(config.stateFile, state);
|
|
@@ -6659,6 +6881,7 @@ async function handlePlanDecision({ config, runtime, state, planRequest, decisio
|
|
|
6659
6881
|
title: planRequest.title,
|
|
6660
6882
|
messageText: `${planDecisionMessage(decision, config.defaultLocale)}\n\n${planRequest.messageText}`,
|
|
6661
6883
|
summary: planDecisionMessage(decision, config.defaultLocale),
|
|
6884
|
+
outcome: decision === "implement" ? "implemented" : "dismissed",
|
|
6662
6885
|
}) || stateChanged;
|
|
6663
6886
|
if (stateChanged) {
|
|
6664
6887
|
await saveState(config.stateFile, state);
|
|
@@ -6682,6 +6905,7 @@ async function handleNativeApprovalDecision({ config, runtime, state, approval,
|
|
|
6682
6905
|
title: approval.title,
|
|
6683
6906
|
messageText: `${approvalDecisionMessage(decision, config.defaultLocale)}\n\n${approval.messageText}`,
|
|
6684
6907
|
summary: approvalDecisionMessage(decision, config.defaultLocale),
|
|
6908
|
+
outcome: decision === "accept" ? "approved" : "rejected",
|
|
6685
6909
|
});
|
|
6686
6910
|
if (stateChanged) {
|
|
6687
6911
|
await saveState(config.stateFile, state);
|
package/web/app.css
CHANGED
|
@@ -974,6 +974,13 @@ code {
|
|
|
974
974
|
gap: 0.38rem;
|
|
975
975
|
}
|
|
976
976
|
|
|
977
|
+
.timeline-thread-filter__row {
|
|
978
|
+
display: grid;
|
|
979
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
980
|
+
gap: 0.6rem;
|
|
981
|
+
align-items: start;
|
|
982
|
+
}
|
|
983
|
+
|
|
977
984
|
.timeline-thread-filter__label {
|
|
978
985
|
color: var(--muted);
|
|
979
986
|
font-size: 0.8rem;
|
|
@@ -1009,6 +1016,92 @@ code {
|
|
|
1009
1016
|
pointer-events: none;
|
|
1010
1017
|
}
|
|
1011
1018
|
|
|
1019
|
+
.timeline-kind-filter {
|
|
1020
|
+
position: relative;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
.timeline-kind-filter__button {
|
|
1024
|
+
width: 2.72rem;
|
|
1025
|
+
min-width: 2.72rem;
|
|
1026
|
+
min-height: 2.72rem;
|
|
1027
|
+
display: inline-flex;
|
|
1028
|
+
align-items: center;
|
|
1029
|
+
justify-content: center;
|
|
1030
|
+
border-radius: 15px;
|
|
1031
|
+
border: 1px solid rgba(156, 181, 197, 0.14);
|
|
1032
|
+
background: rgba(255, 255, 255, 0.05);
|
|
1033
|
+
color: rgba(236, 248, 255, 0.78);
|
|
1034
|
+
box-shadow: var(--shadow-card);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.timeline-kind-filter__button.is-active {
|
|
1038
|
+
border-color: rgba(104, 211, 164, 0.34);
|
|
1039
|
+
background: rgba(31, 83, 63, 0.46);
|
|
1040
|
+
color: rgba(224, 255, 240, 0.96);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.timeline-kind-filter__button-icon,
|
|
1044
|
+
.timeline-kind-filter__option-icon,
|
|
1045
|
+
.timeline-kind-filter__option-check {
|
|
1046
|
+
width: 1rem;
|
|
1047
|
+
height: 1rem;
|
|
1048
|
+
display: inline-flex;
|
|
1049
|
+
align-items: center;
|
|
1050
|
+
justify-content: center;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.timeline-kind-filter__button-icon svg,
|
|
1054
|
+
.timeline-kind-filter__option-icon svg,
|
|
1055
|
+
.timeline-kind-filter__option-check svg {
|
|
1056
|
+
width: 100%;
|
|
1057
|
+
height: 100%;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.timeline-kind-filter__popover {
|
|
1061
|
+
position: absolute;
|
|
1062
|
+
right: 0;
|
|
1063
|
+
top: calc(100% + 0.46rem);
|
|
1064
|
+
z-index: 14;
|
|
1065
|
+
width: min(15rem, calc(100vw - 2.25rem));
|
|
1066
|
+
display: grid;
|
|
1067
|
+
gap: 0.22rem;
|
|
1068
|
+
padding: 0.4rem;
|
|
1069
|
+
border: 1px solid rgba(156, 181, 197, 0.16);
|
|
1070
|
+
border-radius: 18px;
|
|
1071
|
+
background: rgba(15, 23, 31, 0.94);
|
|
1072
|
+
box-shadow: 0 24px 46px rgba(5, 9, 14, 0.34);
|
|
1073
|
+
-webkit-backdrop-filter: blur(24px) saturate(1.08);
|
|
1074
|
+
backdrop-filter: blur(24px) saturate(1.08);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.timeline-kind-filter__option {
|
|
1078
|
+
width: 100%;
|
|
1079
|
+
display: grid;
|
|
1080
|
+
grid-template-columns: auto 1fr auto;
|
|
1081
|
+
gap: 0.62rem;
|
|
1082
|
+
align-items: center;
|
|
1083
|
+
padding: 0.74rem 0.78rem;
|
|
1084
|
+
border: 0;
|
|
1085
|
+
border-radius: 14px;
|
|
1086
|
+
background: transparent;
|
|
1087
|
+
color: var(--text);
|
|
1088
|
+
text-align: left;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.timeline-kind-filter__option.is-selected {
|
|
1092
|
+
background: rgba(255, 255, 255, 0.06);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.timeline-kind-filter__option-label {
|
|
1096
|
+
font-size: 0.9rem;
|
|
1097
|
+
font-weight: 600;
|
|
1098
|
+
line-height: 1.3;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.timeline-kind-filter__option-check {
|
|
1102
|
+
color: rgba(104, 211, 164, 0.94);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1012
1105
|
.timeline-list {
|
|
1013
1106
|
display: grid;
|
|
1014
1107
|
gap: 0.72rem;
|
|
@@ -1090,6 +1183,42 @@ code {
|
|
|
1090
1183
|
margin-top: 0.08rem;
|
|
1091
1184
|
}
|
|
1092
1185
|
|
|
1186
|
+
.timeline-entry__files {
|
|
1187
|
+
display: flex;
|
|
1188
|
+
flex-wrap: wrap;
|
|
1189
|
+
gap: 0.42rem;
|
|
1190
|
+
margin-top: 0.14rem;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.file-ref-chip {
|
|
1194
|
+
display: inline-flex;
|
|
1195
|
+
align-items: center;
|
|
1196
|
+
gap: 0.34rem;
|
|
1197
|
+
min-width: 0;
|
|
1198
|
+
padding: 0.4rem 0.56rem;
|
|
1199
|
+
border-radius: 999px;
|
|
1200
|
+
background: rgba(16, 24, 31, 0.72);
|
|
1201
|
+
border: 1px solid rgba(156, 181, 197, 0.12);
|
|
1202
|
+
color: var(--muted);
|
|
1203
|
+
font-size: 0.76rem;
|
|
1204
|
+
line-height: 1.2;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.file-ref-chip__icon {
|
|
1208
|
+
width: 0.82rem;
|
|
1209
|
+
height: 0.82rem;
|
|
1210
|
+
color: rgba(202, 220, 233, 0.72);
|
|
1211
|
+
flex: 0 0 auto;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.file-ref-chip__label {
|
|
1215
|
+
min-width: 0;
|
|
1216
|
+
overflow: hidden;
|
|
1217
|
+
text-overflow: ellipsis;
|
|
1218
|
+
white-space: nowrap;
|
|
1219
|
+
max-width: 13.5rem;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1093
1222
|
.timeline-entry__image-frame {
|
|
1094
1223
|
display: block;
|
|
1095
1224
|
border-radius: 14px;
|
|
@@ -1463,6 +1592,12 @@ code {
|
|
|
1463
1592
|
padding: 0.5rem;
|
|
1464
1593
|
}
|
|
1465
1594
|
|
|
1595
|
+
.detail-card--files {
|
|
1596
|
+
margin-top: 0.24rem;
|
|
1597
|
+
gap: 0.62rem;
|
|
1598
|
+
padding: 0.72rem;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1466
1601
|
.detail-card--reply {
|
|
1467
1602
|
margin-top: 0.32rem;
|
|
1468
1603
|
gap: 0.72rem;
|
|
@@ -1475,6 +1610,50 @@ code {
|
|
|
1475
1610
|
width: 100%;
|
|
1476
1611
|
}
|
|
1477
1612
|
|
|
1613
|
+
.detail-files-card__header {
|
|
1614
|
+
display: inline-flex;
|
|
1615
|
+
align-items: center;
|
|
1616
|
+
gap: 0.42rem;
|
|
1617
|
+
font-size: 0.84rem;
|
|
1618
|
+
color: var(--muted);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
.detail-files-card__icon {
|
|
1622
|
+
width: 0.92rem;
|
|
1623
|
+
height: 0.92rem;
|
|
1624
|
+
color: rgba(202, 220, 233, 0.72);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
.detail-file-grid {
|
|
1628
|
+
display: grid;
|
|
1629
|
+
gap: 0.52rem;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
.detail-file-chip {
|
|
1633
|
+
display: grid;
|
|
1634
|
+
gap: 0.14rem;
|
|
1635
|
+
padding: 0.68rem 0.78rem;
|
|
1636
|
+
border-radius: 16px;
|
|
1637
|
+
background:
|
|
1638
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)),
|
|
1639
|
+
rgba(10, 17, 22, 0.76);
|
|
1640
|
+
border: 1px solid rgba(156, 181, 197, 0.12);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
.detail-file-chip__label {
|
|
1644
|
+
font-size: 0.88rem;
|
|
1645
|
+
line-height: 1.35;
|
|
1646
|
+
color: var(--text);
|
|
1647
|
+
overflow-wrap: anywhere;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
.detail-file-chip__path {
|
|
1651
|
+
font-size: 0.76rem;
|
|
1652
|
+
line-height: 1.45;
|
|
1653
|
+
color: var(--muted);
|
|
1654
|
+
overflow-wrap: anywhere;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1478
1657
|
.detail-image-link {
|
|
1479
1658
|
display: block;
|
|
1480
1659
|
padding: 0;
|
package/web/app.js
CHANGED
|
@@ -8,6 +8,8 @@ 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,
|
|
@@ -21,6 +23,8 @@ const state = {
|
|
|
21
23
|
detailLoadingItem: null,
|
|
22
24
|
detailOpen: false,
|
|
23
25
|
timelineThreadFilter: "all",
|
|
26
|
+
timelineKindFilter: "all",
|
|
27
|
+
timelineKindFilterOpen: false,
|
|
24
28
|
completedThreadFilter: "all",
|
|
25
29
|
settingsSubpage: "",
|
|
26
30
|
settingsScrollState: null,
|
|
@@ -86,6 +90,9 @@ async function boot() {
|
|
|
86
90
|
await registerServiceWorker();
|
|
87
91
|
navigator.serviceWorker?.addEventListener("message", handleServiceWorkerMessage);
|
|
88
92
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
93
|
+
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
94
|
+
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
95
|
+
document.addEventListener("visibilitychange", handleDocumentVisibilityChange);
|
|
89
96
|
|
|
90
97
|
await refreshSession();
|
|
91
98
|
|
|
@@ -121,6 +128,7 @@ async function boot() {
|
|
|
121
128
|
return;
|
|
122
129
|
}
|
|
123
130
|
|
|
131
|
+
await consumePendingNotificationIntent();
|
|
124
132
|
await syncDetectedLocalePreference();
|
|
125
133
|
await refreshAuthenticatedState();
|
|
126
134
|
ensureCurrentSelection();
|
|
@@ -130,6 +138,10 @@ async function boot() {
|
|
|
130
138
|
if (!state.session?.authenticated) {
|
|
131
139
|
return;
|
|
132
140
|
}
|
|
141
|
+
const consumedNotificationIntent = await consumePendingNotificationIntent();
|
|
142
|
+
if (consumedNotificationIntent) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
133
145
|
await refreshAuthenticatedState();
|
|
134
146
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
135
147
|
await renderShell();
|
|
@@ -314,6 +326,7 @@ async function refreshInbox() {
|
|
|
314
326
|
async function refreshTimeline() {
|
|
315
327
|
state.timeline = await apiGet("/api/timeline");
|
|
316
328
|
syncTimelineThreadFilter();
|
|
329
|
+
syncTimelineKindFilter();
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
async function refreshDevices() {
|
|
@@ -420,10 +433,14 @@ function filteredTimelineEntries() {
|
|
|
420
433
|
if (!entries.length) {
|
|
421
434
|
return [];
|
|
422
435
|
}
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
let filtered = entries;
|
|
437
|
+
if (state.timelineThreadFilter && state.timelineThreadFilter !== "all") {
|
|
438
|
+
filtered = filtered.filter((entry) => entry.threadId === state.timelineThreadFilter);
|
|
439
|
+
}
|
|
440
|
+
if (!state.timelineKindFilter || state.timelineKindFilter === "all") {
|
|
441
|
+
return filtered;
|
|
425
442
|
}
|
|
426
|
-
return
|
|
443
|
+
return filtered.filter((entry) => timelineEntryMatchesKindFilter(entry, state.timelineKindFilter));
|
|
427
444
|
}
|
|
428
445
|
|
|
429
446
|
function filteredCompletedEntries() {
|
|
@@ -448,6 +465,49 @@ function syncTimelineThreadFilter() {
|
|
|
448
465
|
}
|
|
449
466
|
}
|
|
450
467
|
|
|
468
|
+
function syncTimelineKindFilter() {
|
|
469
|
+
const validIds = new Set(timelineKindFilterOptions().map((option) => option.id));
|
|
470
|
+
if (!state.timelineKindFilter || !validIds.has(state.timelineKindFilter)) {
|
|
471
|
+
state.timelineKindFilter = "all";
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function timelineKindFilterOptions() {
|
|
476
|
+
return [
|
|
477
|
+
{ id: "all", label: L("timeline.kindFilter.all"), icon: "filter" },
|
|
478
|
+
{ id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
|
|
479
|
+
{ id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
|
|
480
|
+
{ id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
|
|
481
|
+
{ id: "choices", label: L("timeline.kindFilter.choices"), icon: "choice" },
|
|
482
|
+
{ id: "completions", label: L("timeline.kindFilter.completions"), icon: "completion-item" },
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function currentTimelineKindFilterOption() {
|
|
487
|
+
return (
|
|
488
|
+
timelineKindFilterOptions().find((option) => option.id === state.timelineKindFilter) ||
|
|
489
|
+
timelineKindFilterOptions()[0]
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
494
|
+
const kind = normalizeClientText(entry?.kind || entry?.item?.kind || "");
|
|
495
|
+
switch (filterId) {
|
|
496
|
+
case "messages":
|
|
497
|
+
return TIMELINE_MESSAGE_KINDS.has(kind);
|
|
498
|
+
case "approvals":
|
|
499
|
+
return kind === "approval";
|
|
500
|
+
case "plans":
|
|
501
|
+
return kind === "plan" || kind === "plan_ready";
|
|
502
|
+
case "choices":
|
|
503
|
+
return kind === "choice";
|
|
504
|
+
case "completions":
|
|
505
|
+
return kind === "completion";
|
|
506
|
+
default:
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
451
511
|
function completedThreads() {
|
|
452
512
|
const items = Array.isArray(state.inbox?.completed) ? state.inbox.completed : [];
|
|
453
513
|
if (!items.length) {
|
|
@@ -1459,6 +1519,7 @@ function renderTimelineThreadDropdown() {
|
|
|
1459
1519
|
inputId: "timeline-thread-select",
|
|
1460
1520
|
dataAttribute: "data-timeline-thread-select",
|
|
1461
1521
|
selectedThreadId: state.timelineThreadFilter,
|
|
1522
|
+
controlsHtml: renderTimelineKindFilterControls(),
|
|
1462
1523
|
threads: threads.map((thread) => ({
|
|
1463
1524
|
id: thread.id,
|
|
1464
1525
|
label: dropdownThreadLabel(thread.id, thread.label || ""),
|
|
@@ -1475,7 +1536,7 @@ function renderCompletedThreadDropdown() {
|
|
|
1475
1536
|
});
|
|
1476
1537
|
}
|
|
1477
1538
|
|
|
1478
|
-
function renderThreadDropdown({ inputId, dataAttribute, selectedThreadId, threads }) {
|
|
1539
|
+
function renderThreadDropdown({ inputId, dataAttribute, selectedThreadId, threads, controlsHtml = "" }) {
|
|
1479
1540
|
const options = [
|
|
1480
1541
|
{
|
|
1481
1542
|
id: "all",
|
|
@@ -1490,24 +1551,72 @@ function renderThreadDropdown({ inputId, dataAttribute, selectedThreadId, thread
|
|
|
1490
1551
|
return `
|
|
1491
1552
|
<div class="timeline-thread-filter">
|
|
1492
1553
|
<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
|
-
|
|
1554
|
+
<div class="timeline-thread-filter__row">
|
|
1555
|
+
<div class="timeline-thread-select-wrap">
|
|
1556
|
+
<select id="${escapeHtml(inputId)}" class="timeline-thread-select" ${dataAttribute}>
|
|
1557
|
+
${options
|
|
1558
|
+
.map(
|
|
1559
|
+
(thread) => `
|
|
1560
|
+
<option value="${escapeHtml(thread.id)}" ${selectedThreadId === thread.id ? "selected" : ""}>
|
|
1561
|
+
${escapeHtml(thread.label)}
|
|
1562
|
+
</option>
|
|
1563
|
+
`
|
|
1564
|
+
)
|
|
1565
|
+
.join("")}
|
|
1566
|
+
</select>
|
|
1567
|
+
<span class="timeline-thread-select__chevron" aria-hidden="true">${renderIcon("chevron-down")}</span>
|
|
1568
|
+
</div>
|
|
1569
|
+
${controlsHtml}
|
|
1506
1570
|
</div>
|
|
1507
1571
|
</div>
|
|
1508
1572
|
`;
|
|
1509
1573
|
}
|
|
1510
1574
|
|
|
1575
|
+
function renderTimelineKindFilterControls() {
|
|
1576
|
+
const current = currentTimelineKindFilterOption();
|
|
1577
|
+
const options = timelineKindFilterOptions();
|
|
1578
|
+
return `
|
|
1579
|
+
<div class="timeline-kind-filter" data-timeline-kind-filter-root>
|
|
1580
|
+
<button
|
|
1581
|
+
type="button"
|
|
1582
|
+
class="timeline-kind-filter__button ${current.id !== "all" ? "is-active" : ""}"
|
|
1583
|
+
data-timeline-kind-filter-toggle
|
|
1584
|
+
aria-expanded="${state.timelineKindFilterOpen ? "true" : "false"}"
|
|
1585
|
+
aria-label="${escapeHtml(L("timeline.kindFilterButtonLabel"))}"
|
|
1586
|
+
>
|
|
1587
|
+
<span class="timeline-kind-filter__button-icon" aria-hidden="true">${renderIcon(current.icon)}</span>
|
|
1588
|
+
</button>
|
|
1589
|
+
${
|
|
1590
|
+
state.timelineKindFilterOpen
|
|
1591
|
+
? `
|
|
1592
|
+
<div class="timeline-kind-filter__popover" role="menu" aria-label="${escapeHtml(L("timeline.kindFilterLabel"))}">
|
|
1593
|
+
${options
|
|
1594
|
+
.map(
|
|
1595
|
+
(option) => `
|
|
1596
|
+
<button
|
|
1597
|
+
type="button"
|
|
1598
|
+
class="timeline-kind-filter__option ${option.id === current.id ? "is-selected" : ""}"
|
|
1599
|
+
data-timeline-kind-filter-option="${escapeHtml(option.id)}"
|
|
1600
|
+
role="menuitemradio"
|
|
1601
|
+
aria-checked="${option.id === current.id ? "true" : "false"}"
|
|
1602
|
+
>
|
|
1603
|
+
<span class="timeline-kind-filter__option-icon" aria-hidden="true">${renderIcon(option.icon)}</span>
|
|
1604
|
+
<span class="timeline-kind-filter__option-label">${escapeHtml(option.label)}</span>
|
|
1605
|
+
<span class="timeline-kind-filter__option-check" aria-hidden="true">${
|
|
1606
|
+
option.id === current.id ? renderIcon("check") : ""
|
|
1607
|
+
}</span>
|
|
1608
|
+
</button>
|
|
1609
|
+
`
|
|
1610
|
+
)
|
|
1611
|
+
.join("")}
|
|
1612
|
+
</div>
|
|
1613
|
+
`
|
|
1614
|
+
: ""
|
|
1615
|
+
}
|
|
1616
|
+
</div>
|
|
1617
|
+
`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1511
1620
|
function renderTimelineEntry(entry, { desktop }) {
|
|
1512
1621
|
const item = entry.item;
|
|
1513
1622
|
const kindInfo = kindMeta(item.kind);
|
|
@@ -1515,13 +1624,14 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
1515
1624
|
const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
|
|
1516
1625
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || item.kind === "completion";
|
|
1517
1626
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
1627
|
+
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
1518
1628
|
const primaryText = isMessageLike
|
|
1519
1629
|
? item.summary || fallbackSummaryForKind(item.kind, entry.status)
|
|
1520
1630
|
: item.title || L("common.untitledItem");
|
|
1521
1631
|
const secondaryText = isMessageLike ? "" : item.summary || fallbackSummaryForKind(item.kind, entry.status);
|
|
1522
1632
|
const threadLabel = timelineEntryThreadLabel(item, isMessageLike);
|
|
1523
1633
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
1524
|
-
const statusLabel = isMessageLike
|
|
1634
|
+
const statusLabel = timelineEntryStatusLabel(item, isMessageLike);
|
|
1525
1635
|
|
|
1526
1636
|
return `
|
|
1527
1637
|
<button
|
|
@@ -1545,12 +1655,42 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
1545
1655
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
1546
1656
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
1547
1657
|
${renderTimelineEntryImageStrip(imageUrls)}
|
|
1658
|
+
${renderTimelineEntryFileStrip(fileRefs)}
|
|
1548
1659
|
</div>
|
|
1549
1660
|
${statusLabel ? `<div class="timeline-entry__footer"><span class="timeline-entry__status">${escapeHtml(statusLabel)}</span></div>` : ""}
|
|
1550
1661
|
</button>
|
|
1551
1662
|
`;
|
|
1552
1663
|
}
|
|
1553
1664
|
|
|
1665
|
+
function timelineEntryStatusLabel(item, isMessageLike) {
|
|
1666
|
+
if (isMessageLike) {
|
|
1667
|
+
return "";
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
const outcome = normalizeClientText(item?.outcome || "");
|
|
1671
|
+
switch (outcome) {
|
|
1672
|
+
case "pending":
|
|
1673
|
+
return L("common.actionNeeded");
|
|
1674
|
+
case "approved":
|
|
1675
|
+
return L("timeline.status.approved");
|
|
1676
|
+
case "rejected":
|
|
1677
|
+
return L("timeline.status.rejected");
|
|
1678
|
+
case "implemented":
|
|
1679
|
+
return L("timeline.status.implemented");
|
|
1680
|
+
case "dismissed":
|
|
1681
|
+
return L("timeline.status.dismissed");
|
|
1682
|
+
case "submitted":
|
|
1683
|
+
return L("timeline.status.submitted");
|
|
1684
|
+
default:
|
|
1685
|
+
break;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
if (item?.kind === "approval" || item?.kind === "plan" || item?.kind === "plan_ready" || item?.kind === "choice") {
|
|
1689
|
+
return L("common.actionNeeded");
|
|
1690
|
+
}
|
|
1691
|
+
return "";
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1554
1694
|
function renderTimelineEntryImageStrip(imageUrls) {
|
|
1555
1695
|
if (!Array.isArray(imageUrls) || imageUrls.length === 0) {
|
|
1556
1696
|
return "";
|
|
@@ -1577,6 +1717,28 @@ function renderTimelineEntryImageStrip(imageUrls) {
|
|
|
1577
1717
|
`;
|
|
1578
1718
|
}
|
|
1579
1719
|
|
|
1720
|
+
function renderTimelineEntryFileStrip(fileRefs) {
|
|
1721
|
+
if (!Array.isArray(fileRefs) || fileRefs.length === 0) {
|
|
1722
|
+
return "";
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
return `
|
|
1726
|
+
<div class="timeline-entry__files" aria-label="${escapeHtml(L("detail.filesTitle"))}">
|
|
1727
|
+
${fileRefs
|
|
1728
|
+
.slice(0, 4)
|
|
1729
|
+
.map(
|
|
1730
|
+
(fileRef) => `
|
|
1731
|
+
<span class="file-ref-chip" title="${escapeHtml(fileRef)}">
|
|
1732
|
+
<span class="file-ref-chip__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
1733
|
+
<span class="file-ref-chip__label">${escapeHtml(fileRefLabel(fileRef))}</span>
|
|
1734
|
+
</span>
|
|
1735
|
+
`
|
|
1736
|
+
)
|
|
1737
|
+
.join("")}
|
|
1738
|
+
</div>
|
|
1739
|
+
`;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1580
1742
|
function timelineEntryThreadLabel(item, isMessage) {
|
|
1581
1743
|
const threadLabel = resolvedThreadLabel(item.threadId || "", item.threadLabel || "");
|
|
1582
1744
|
if (!threadLabel) {
|
|
@@ -2307,6 +2469,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
2307
2469
|
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
2308
2470
|
</section>
|
|
2309
2471
|
${renderDetailImageGallery(detail)}
|
|
2472
|
+
${renderDetailFileRefs(detail)}
|
|
2310
2473
|
${renderCompletionReplyComposer(detail)}
|
|
2311
2474
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [])}
|
|
2312
2475
|
</div>
|
|
@@ -2328,6 +2491,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
2328
2491
|
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
2329
2492
|
</section>
|
|
2330
2493
|
${renderDetailImageGallery(detail, { mobile: true })}
|
|
2494
|
+
${renderDetailFileRefs(detail, { mobile: true })}
|
|
2331
2495
|
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
2332
2496
|
</div>
|
|
2333
2497
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
@@ -2435,6 +2599,34 @@ function renderDetailImageGallery(detail, options = {}) {
|
|
|
2435
2599
|
`;
|
|
2436
2600
|
}
|
|
2437
2601
|
|
|
2602
|
+
function renderDetailFileRefs(detail, options = {}) {
|
|
2603
|
+
const fileRefs = normalizeClientFileRefs(detail?.fileRefs);
|
|
2604
|
+
if (fileRefs.length === 0) {
|
|
2605
|
+
return "";
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
return `
|
|
2609
|
+
<section class="detail-card detail-card--files ${options.mobile ? "detail-card--mobile" : ""}">
|
|
2610
|
+
<div class="detail-files-card__header">
|
|
2611
|
+
<span class="detail-files-card__icon" aria-hidden="true">${renderIcon("item")}</span>
|
|
2612
|
+
<span>${escapeHtml(L("detail.filesTitle"))}</span>
|
|
2613
|
+
</div>
|
|
2614
|
+
<div class="detail-file-grid">
|
|
2615
|
+
${fileRefs
|
|
2616
|
+
.map(
|
|
2617
|
+
(fileRef) => `
|
|
2618
|
+
<div class="detail-file-chip" title="${escapeHtml(fileRef)}">
|
|
2619
|
+
<span class="detail-file-chip__label">${escapeHtml(fileRefLabel(fileRef))}</span>
|
|
2620
|
+
<span class="detail-file-chip__path">${escapeHtml(fileRef)}</span>
|
|
2621
|
+
</div>
|
|
2622
|
+
`
|
|
2623
|
+
)
|
|
2624
|
+
.join("")}
|
|
2625
|
+
</div>
|
|
2626
|
+
</section>
|
|
2627
|
+
`;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2438
2630
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
2439
2631
|
if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
|
|
2440
2632
|
return "";
|
|
@@ -3060,6 +3252,27 @@ function bindShellInteractions() {
|
|
|
3060
3252
|
select.addEventListener("change", async () => {
|
|
3061
3253
|
clearThreadFilterInteraction();
|
|
3062
3254
|
state.timelineThreadFilter = select.value || "all";
|
|
3255
|
+
state.timelineKindFilterOpen = false;
|
|
3256
|
+
alignCurrentItemToVisibleEntries();
|
|
3257
|
+
await renderShell();
|
|
3258
|
+
});
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
for (const button of document.querySelectorAll("[data-timeline-kind-filter-toggle]")) {
|
|
3262
|
+
button.addEventListener("click", async (event) => {
|
|
3263
|
+
event.preventDefault();
|
|
3264
|
+
markThreadFilterInteraction();
|
|
3265
|
+
state.timelineKindFilterOpen = !state.timelineKindFilterOpen;
|
|
3266
|
+
await renderShell();
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
for (const button of document.querySelectorAll("[data-timeline-kind-filter-option]")) {
|
|
3271
|
+
button.addEventListener("click", async (event) => {
|
|
3272
|
+
event.preventDefault();
|
|
3273
|
+
clearThreadFilterInteraction();
|
|
3274
|
+
state.timelineKindFilter = button.dataset.timelineKindFilterOption || "all";
|
|
3275
|
+
state.timelineKindFilterOpen = false;
|
|
3063
3276
|
alignCurrentItemToVisibleEntries();
|
|
3064
3277
|
await renderShell();
|
|
3065
3278
|
});
|
|
@@ -3550,6 +3763,7 @@ function closeSettingsSubpage() {
|
|
|
3550
3763
|
|
|
3551
3764
|
async function switchTab(tab) {
|
|
3552
3765
|
state.currentTab = tab;
|
|
3766
|
+
state.timelineKindFilterOpen = false;
|
|
3553
3767
|
state.pushNotice = "";
|
|
3554
3768
|
state.pushError = "";
|
|
3555
3769
|
state.settingsSubpage = "";
|
|
@@ -3570,6 +3784,7 @@ function openItem({ kind, token, sourceTab }) {
|
|
|
3570
3784
|
const previousItem = state.currentItem ? { ...state.currentItem } : null;
|
|
3571
3785
|
clearPinnedDetailState();
|
|
3572
3786
|
const nextTab = sourceTab || tabForItemKind(kind, state.currentTab);
|
|
3787
|
+
state.timelineKindFilterOpen = false;
|
|
3573
3788
|
if (previousItem && (previousItem.kind !== kind || previousItem.token !== token)) {
|
|
3574
3789
|
clearChoiceLocalDraftForItem(previousItem);
|
|
3575
3790
|
}
|
|
@@ -4044,6 +4259,8 @@ function renderIcon(name) {
|
|
|
4044
4259
|
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
4260
|
case "clip":
|
|
4046
4261
|
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>`;
|
|
4262
|
+
case "filter":
|
|
4263
|
+
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
4264
|
case "check":
|
|
4048
4265
|
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
4266
|
case "back":
|
|
@@ -4151,9 +4368,31 @@ function handleServiceWorkerMessage(event) {
|
|
|
4151
4368
|
const type = event?.data?.type || "";
|
|
4152
4369
|
if (type === "pushsubscriptionchange") {
|
|
4153
4370
|
refreshPushStatus().then(renderCurrentSurface).catch(() => {});
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
if (type === "open-target-url" && event?.data?.url) {
|
|
4374
|
+
applyExternalTargetUrl(event.data.url).catch(() => {});
|
|
4154
4375
|
}
|
|
4155
4376
|
}
|
|
4156
4377
|
|
|
4378
|
+
function handlePotentialExternalNavigation() {
|
|
4379
|
+
consumePendingNotificationIntent()
|
|
4380
|
+
.then((consumed) => {
|
|
4381
|
+
if (consumed) {
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
return applyExternalTargetUrl(window.location.href, { allowRefresh: false });
|
|
4385
|
+
})
|
|
4386
|
+
.catch(() => {});
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
function handleDocumentVisibilityChange() {
|
|
4390
|
+
if (document.visibilityState !== "visible") {
|
|
4391
|
+
return;
|
|
4392
|
+
}
|
|
4393
|
+
handlePotentialExternalNavigation();
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4157
4396
|
async function apiGet(url) {
|
|
4158
4397
|
const response = await fetch(url, {
|
|
4159
4398
|
credentials: "same-origin",
|
|
@@ -4250,11 +4489,124 @@ function normalizeClientText(value) {
|
|
|
4250
4489
|
return String(value ?? "").trim();
|
|
4251
4490
|
}
|
|
4252
4491
|
|
|
4492
|
+
function normalizeClientFileRefs(fileRefs) {
|
|
4493
|
+
if (!Array.isArray(fileRefs)) {
|
|
4494
|
+
return [];
|
|
4495
|
+
}
|
|
4496
|
+
const deduped = [];
|
|
4497
|
+
const seen = new Set();
|
|
4498
|
+
for (const fileRef of fileRefs) {
|
|
4499
|
+
const normalized = normalizeClientText(fileRef);
|
|
4500
|
+
if (!normalized || seen.has(normalized)) {
|
|
4501
|
+
continue;
|
|
4502
|
+
}
|
|
4503
|
+
seen.add(normalized);
|
|
4504
|
+
deduped.push(normalized);
|
|
4505
|
+
if (deduped.length >= 8) {
|
|
4506
|
+
break;
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
return deduped;
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
function fileRefLabel(fileRef) {
|
|
4513
|
+
const normalized = normalizeClientText(fileRef);
|
|
4514
|
+
if (!normalized) {
|
|
4515
|
+
return "";
|
|
4516
|
+
}
|
|
4517
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
4518
|
+
return segments[segments.length - 1] || normalized;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4253
4521
|
function parseItemRef(value) {
|
|
4254
4522
|
const [kind, token] = String(value || "").split(":");
|
|
4255
4523
|
return kind && token ? { kind, token } : null;
|
|
4256
4524
|
}
|
|
4257
4525
|
|
|
4526
|
+
async function applyExternalTargetUrl(urlString, { allowRefresh = true } = {}) {
|
|
4527
|
+
if (!state.session?.authenticated) {
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
let nextUrl;
|
|
4532
|
+
try {
|
|
4533
|
+
nextUrl = new URL(urlString, window.location.origin);
|
|
4534
|
+
} catch {
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
|
|
4538
|
+
const itemRef = parseItemRef(nextUrl.searchParams.get("item"));
|
|
4539
|
+
if (!itemRef) {
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4542
|
+
|
|
4543
|
+
const sameItem =
|
|
4544
|
+
Boolean(state.currentItem) &&
|
|
4545
|
+
isSameItemRef(state.currentItem, itemRef) &&
|
|
4546
|
+
(isDesktopLayout() || state.detailOpen);
|
|
4547
|
+
if (sameItem) {
|
|
4548
|
+
if (allowRefresh) {
|
|
4549
|
+
await refreshAuthenticatedState();
|
|
4550
|
+
ensureCurrentSelection();
|
|
4551
|
+
await renderShell();
|
|
4552
|
+
}
|
|
4553
|
+
return;
|
|
4554
|
+
}
|
|
4555
|
+
|
|
4556
|
+
openItem({
|
|
4557
|
+
kind: itemRef.kind,
|
|
4558
|
+
token: itemRef.token,
|
|
4559
|
+
sourceTab: tabForItemKind(itemRef.kind, state.currentTab),
|
|
4560
|
+
});
|
|
4561
|
+
if (isFastPathItemRef(itemRef)) {
|
|
4562
|
+
state.launchItemIntent = {
|
|
4563
|
+
...itemRef,
|
|
4564
|
+
status: "pending",
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
await renderShell();
|
|
4568
|
+
|
|
4569
|
+
if (!allowRefresh) {
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4572
|
+
await refreshAuthenticatedState();
|
|
4573
|
+
ensureCurrentSelection();
|
|
4574
|
+
await renderShell();
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4577
|
+
async function consumePendingNotificationIntent() {
|
|
4578
|
+
if (!state.session?.authenticated || typeof caches === "undefined") {
|
|
4579
|
+
return false;
|
|
4580
|
+
}
|
|
4581
|
+
let cache;
|
|
4582
|
+
try {
|
|
4583
|
+
cache = await caches.open(NOTIFICATION_INTENT_CACHE);
|
|
4584
|
+
} catch {
|
|
4585
|
+
return false;
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
const request = new Request(NOTIFICATION_INTENT_PATH);
|
|
4589
|
+
const match = await cache.match(request).catch(() => null);
|
|
4590
|
+
if (!match) {
|
|
4591
|
+
return false;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
let payload = null;
|
|
4595
|
+
try {
|
|
4596
|
+
payload = await match.json();
|
|
4597
|
+
} catch {
|
|
4598
|
+
payload = null;
|
|
4599
|
+
}
|
|
4600
|
+
await cache.delete(request).catch(() => {});
|
|
4601
|
+
|
|
4602
|
+
const url = normalizeClientText(payload?.url || "");
|
|
4603
|
+
if (!url) {
|
|
4604
|
+
return false;
|
|
4605
|
+
}
|
|
4606
|
+
await applyExternalTargetUrl(url, { allowRefresh: true });
|
|
4607
|
+
return true;
|
|
4608
|
+
}
|
|
4609
|
+
|
|
4258
4610
|
function buildAppUrl(nextParams) {
|
|
4259
4611
|
const query = nextParams.toString();
|
|
4260
4612
|
return `/app${query ? `?${query}` : ""}`;
|
package/web/i18n.js
CHANGED
|
@@ -108,6 +108,7 @@ const translations = {
|
|
|
108
108
|
"detail.previousMessage": "Previous message",
|
|
109
109
|
"detail.interruptedTask": "Interrupted task",
|
|
110
110
|
"detail.imageAlt": ({ index }) => `Attached image ${index}`,
|
|
111
|
+
"detail.filesTitle": "Files",
|
|
111
112
|
"detail.turnAbortedNotice": "This task was interrupted.",
|
|
112
113
|
"detail.macOnlyChoice": "Please answer this type of input on the Mac.",
|
|
113
114
|
"detail.detailUnavailable": "Detail not available.",
|
|
@@ -167,6 +168,19 @@ const translations = {
|
|
|
167
168
|
"timeline.allThreads": "All",
|
|
168
169
|
"timeline.unknownThread": "Unknown thread",
|
|
169
170
|
"timeline.filterLabel": "Thread filter",
|
|
171
|
+
"timeline.status.approved": "Approved",
|
|
172
|
+
"timeline.status.rejected": "Rejected",
|
|
173
|
+
"timeline.status.implemented": "Started",
|
|
174
|
+
"timeline.status.dismissed": "Dismissed",
|
|
175
|
+
"timeline.status.submitted": "Sent",
|
|
176
|
+
"timeline.kindFilterLabel": "Event filter",
|
|
177
|
+
"timeline.kindFilterButtonLabel": "Filter timeline events",
|
|
178
|
+
"timeline.kindFilter.all": "All events",
|
|
179
|
+
"timeline.kindFilter.messages": "Messages",
|
|
180
|
+
"timeline.kindFilter.approvals": "Approvals",
|
|
181
|
+
"timeline.kindFilter.plans": "Plans",
|
|
182
|
+
"timeline.kindFilter.choices": "Choices",
|
|
183
|
+
"timeline.kindFilter.completions": "Completed",
|
|
170
184
|
"settings.intro": "Check pairing, language, install status, and Web Push health for this iPhone.",
|
|
171
185
|
"settings.section.overview": "Quick setup",
|
|
172
186
|
"settings.section.notifications": "Notifications",
|
|
@@ -565,6 +579,7 @@ const translations = {
|
|
|
565
579
|
"detail.previousMessage": "ひとつ前のメッセージ",
|
|
566
580
|
"detail.interruptedTask": "中断されたタスク",
|
|
567
581
|
"detail.imageAlt": ({ index }) => `添付画像 ${index}`,
|
|
582
|
+
"detail.filesTitle": "関連ファイル",
|
|
568
583
|
"detail.turnAbortedNotice": "タスクを中断しました。",
|
|
569
584
|
"detail.macOnlyChoice": "この種類の入力は Mac で回答してください。",
|
|
570
585
|
"detail.detailUnavailable": "詳細は利用できません。",
|
|
@@ -624,6 +639,19 @@ const translations = {
|
|
|
624
639
|
"timeline.allThreads": "すべて",
|
|
625
640
|
"timeline.unknownThread": "不明なスレッド",
|
|
626
641
|
"timeline.filterLabel": "スレッドフィルター",
|
|
642
|
+
"timeline.status.approved": "承認済み",
|
|
643
|
+
"timeline.status.rejected": "拒否済み",
|
|
644
|
+
"timeline.status.implemented": "実装開始",
|
|
645
|
+
"timeline.status.dismissed": "見送り",
|
|
646
|
+
"timeline.status.submitted": "送信済み",
|
|
647
|
+
"timeline.kindFilterLabel": "イベントフィルター",
|
|
648
|
+
"timeline.kindFilterButtonLabel": "タイムラインのイベントを絞り込む",
|
|
649
|
+
"timeline.kindFilter.all": "すべてのイベント",
|
|
650
|
+
"timeline.kindFilter.messages": "メッセージ",
|
|
651
|
+
"timeline.kindFilter.approvals": "承認",
|
|
652
|
+
"timeline.kindFilter.plans": "プラン",
|
|
653
|
+
"timeline.kindFilter.choices": "選択",
|
|
654
|
+
"timeline.kindFilter.completions": "完了",
|
|
627
655
|
"settings.intro": "この iPhone のペアリング、言語、インストール状況、Web Push の状態を確認できます。",
|
|
628
656
|
"settings.section.overview": "クイック確認",
|
|
629
657
|
"settings.section.notifications": "通知",
|
package/web/sw.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const CACHE_NAME = "viveworker-
|
|
1
|
+
const CACHE_NAME = "viveworker-v7";
|
|
2
|
+
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
3
|
+
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
2
4
|
const APP_ASSETS = ["/app.css", "/app.js", "/i18n.js"];
|
|
3
5
|
const APP_ROUTES = new Set(["/", "/app", "/app/"]);
|
|
4
6
|
const CACHED_PATHS = new Set(APP_ASSETS);
|
|
@@ -74,6 +76,7 @@ self.addEventListener("push", (event) => {
|
|
|
74
76
|
});
|
|
75
77
|
|
|
76
78
|
self.addEventListener("notificationclick", (event) => {
|
|
79
|
+
event.preventDefault?.();
|
|
77
80
|
event.notification.close();
|
|
78
81
|
const targetUrl = event.notification?.data?.url || "/app";
|
|
79
82
|
event.waitUntil(openTargetWindow(targetUrl));
|
|
@@ -85,20 +88,26 @@ self.addEventListener("pushsubscriptionchange", (event) => {
|
|
|
85
88
|
|
|
86
89
|
async function openTargetWindow(targetUrl) {
|
|
87
90
|
const target = new URL(targetUrl, self.location.origin);
|
|
91
|
+
await persistNotificationIntent(target.toString());
|
|
88
92
|
const clients = await self.clients.matchAll({
|
|
89
93
|
type: "window",
|
|
90
94
|
includeUncontrolled: true,
|
|
91
95
|
});
|
|
96
|
+
broadcastTargetUrl(target.toString(), clients);
|
|
92
97
|
|
|
93
98
|
const preferredClients = clients
|
|
94
99
|
.slice()
|
|
95
|
-
.sort((left, right) => scoreClient(right
|
|
100
|
+
.sort((left, right) => scoreClient(right) - scoreClient(left));
|
|
96
101
|
|
|
97
102
|
for (const client of preferredClients) {
|
|
98
103
|
if (typeof client.focus === "function") {
|
|
99
104
|
if (typeof client.navigate === "function") {
|
|
100
|
-
await client.navigate(target.toString());
|
|
105
|
+
await client.navigate(target.toString()).catch(() => {});
|
|
101
106
|
}
|
|
107
|
+
client.postMessage({
|
|
108
|
+
type: "open-target-url",
|
|
109
|
+
url: target.toString(),
|
|
110
|
+
});
|
|
102
111
|
await client.focus();
|
|
103
112
|
return;
|
|
104
113
|
}
|
|
@@ -109,9 +118,9 @@ async function openTargetWindow(targetUrl) {
|
|
|
109
118
|
}
|
|
110
119
|
}
|
|
111
120
|
|
|
112
|
-
function scoreClient(
|
|
121
|
+
function scoreClient(client) {
|
|
113
122
|
try {
|
|
114
|
-
const url = new URL(
|
|
123
|
+
const url = new URL(client?.url || "");
|
|
115
124
|
let score = 0;
|
|
116
125
|
if (APP_ROUTES.has(url.pathname)) {
|
|
117
126
|
score += 20;
|
|
@@ -119,12 +128,50 @@ function scoreClient(urlString) {
|
|
|
119
128
|
if (url.pathname === "/app" || url.pathname === "/app/") {
|
|
120
129
|
score += 5;
|
|
121
130
|
}
|
|
131
|
+
if (client?.focused) {
|
|
132
|
+
score += 4;
|
|
133
|
+
}
|
|
134
|
+
if (client?.visibilityState === "visible") {
|
|
135
|
+
score += 2;
|
|
136
|
+
}
|
|
122
137
|
return score;
|
|
123
138
|
} catch {
|
|
124
139
|
return 0;
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
|
|
143
|
+
function broadcastTargetUrl(url, clients) {
|
|
144
|
+
for (const client of clients) {
|
|
145
|
+
client.postMessage({
|
|
146
|
+
type: "open-target-url",
|
|
147
|
+
url,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function persistNotificationIntent(url) {
|
|
153
|
+
try {
|
|
154
|
+
const cache = await caches.open(NOTIFICATION_INTENT_CACHE);
|
|
155
|
+
const request = new Request(NOTIFICATION_INTENT_PATH);
|
|
156
|
+
const response = new Response(
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
url,
|
|
159
|
+
nonce: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
160
|
+
createdAtMs: Date.now(),
|
|
161
|
+
}),
|
|
162
|
+
{
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"Cache-Control": "no-store",
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
await cache.put(request, response);
|
|
170
|
+
} catch {
|
|
171
|
+
// Best-effort fallback for iOS warm-start notification routing.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
128
175
|
async function notifyClients(type) {
|
|
129
176
|
const clients = await self.clients.matchAll({
|
|
130
177
|
type: "window",
|