viveworker 0.1.2 → 0.1.4
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/package.json +1 -1
- package/scripts/viveworker-bridge.mjs +1277 -63
- package/scripts/viveworker.mjs +2 -2
- package/web/app.css +242 -30
- package/web/app.js +357 -122
- package/web/i18n.js +15 -7
- package/web/sw.js +1 -1
package/web/app.js
CHANGED
|
@@ -7,6 +7,7 @@ const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
|
|
|
7
7
|
const TIMELINE_MESSAGE_KINDS = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
8
8
|
const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice", "completion"]);
|
|
9
9
|
const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
|
|
10
|
+
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
10
11
|
|
|
11
12
|
const state = {
|
|
12
13
|
session: null,
|
|
@@ -40,6 +41,7 @@ const state = {
|
|
|
40
41
|
pushError: "",
|
|
41
42
|
deviceNotice: "",
|
|
42
43
|
deviceError: "",
|
|
44
|
+
imageViewer: null,
|
|
43
45
|
serviceWorkerRegistration: null,
|
|
44
46
|
installGuideOpen: false,
|
|
45
47
|
logoutConfirmOpen: false,
|
|
@@ -89,7 +91,10 @@ async function boot() {
|
|
|
89
91
|
|
|
90
92
|
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
91
93
|
try {
|
|
92
|
-
await pair({
|
|
94
|
+
await pair({
|
|
95
|
+
token: initialPairToken,
|
|
96
|
+
temporary: shouldUseTemporaryBootstrapPairing(),
|
|
97
|
+
});
|
|
93
98
|
} catch (error) {
|
|
94
99
|
state.pairError = error.message || String(error);
|
|
95
100
|
}
|
|
@@ -532,7 +537,9 @@ function renderPair() {
|
|
|
532
537
|
|
|
533
538
|
async function pair(payload) {
|
|
534
539
|
const result = await apiPost("/api/session/pair", payload);
|
|
535
|
-
|
|
540
|
+
if (result?.temporaryPairing !== true) {
|
|
541
|
+
syncPairingTokenState("");
|
|
542
|
+
}
|
|
536
543
|
return result;
|
|
537
544
|
}
|
|
538
545
|
|
|
@@ -617,6 +624,7 @@ async function renderShell() {
|
|
|
617
624
|
${desktop ? renderDesktopWorkspace(detail) : renderMobileWorkspace(detail)}
|
|
618
625
|
</main>
|
|
619
626
|
${desktop || state.detailOpen || isSettingsSubpageOpen() ? "" : renderBottomTabs()}
|
|
627
|
+
${renderImageViewerModal()}
|
|
620
628
|
${renderInstallGuideModal()}
|
|
621
629
|
${renderLogoutConfirmModal()}
|
|
622
630
|
</div>
|
|
@@ -768,12 +776,7 @@ function normalizeReplyMode(value) {
|
|
|
768
776
|
return normalizeClientText(value).toLowerCase() === "plan" ? "plan" : "default";
|
|
769
777
|
}
|
|
770
778
|
|
|
771
|
-
const COMPLETION_REPLY_IMAGE_SUPPORT = false;
|
|
772
|
-
|
|
773
779
|
function normalizeCompletionReplyAttachment(value) {
|
|
774
|
-
if (!COMPLETION_REPLY_IMAGE_SUPPORT) {
|
|
775
|
-
return null;
|
|
776
|
-
}
|
|
777
780
|
if (!value || typeof value !== "object") {
|
|
778
781
|
return null;
|
|
779
782
|
}
|
|
@@ -795,9 +798,6 @@ function normalizeCompletionReplyAttachment(value) {
|
|
|
795
798
|
}
|
|
796
799
|
|
|
797
800
|
function createCompletionReplyAttachment(file) {
|
|
798
|
-
if (!COMPLETION_REPLY_IMAGE_SUPPORT) {
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
801
|
if (!(typeof File !== "undefined" && file instanceof File)) {
|
|
802
802
|
return null;
|
|
803
803
|
}
|
|
@@ -831,7 +831,7 @@ function getCompletionReplyDraft(token) {
|
|
|
831
831
|
return {
|
|
832
832
|
text: "",
|
|
833
833
|
sentText: "",
|
|
834
|
-
|
|
834
|
+
attachments: [],
|
|
835
835
|
mode: "default",
|
|
836
836
|
notice: "",
|
|
837
837
|
error: "",
|
|
@@ -846,7 +846,7 @@ function getCompletionReplyDraft(token) {
|
|
|
846
846
|
return {
|
|
847
847
|
text: String(draft.text ?? ""),
|
|
848
848
|
sentText: normalizeClientText(draft.sentText ?? ""),
|
|
849
|
-
|
|
849
|
+
attachments: normalizeCompletionReplyAttachments(draft.attachments ?? draft.attachment),
|
|
850
850
|
mode: normalizeReplyMode(draft.mode),
|
|
851
851
|
notice: normalizeClientText(draft.notice),
|
|
852
852
|
error: normalizeClientText(draft.error),
|
|
@@ -874,26 +874,44 @@ function normalizeCompletionReplyWarning(value) {
|
|
|
874
874
|
};
|
|
875
875
|
}
|
|
876
876
|
|
|
877
|
+
function normalizeCompletionReplyAttachments(values) {
|
|
878
|
+
const rawValues = Array.isArray(values)
|
|
879
|
+
? values
|
|
880
|
+
: values
|
|
881
|
+
? [values]
|
|
882
|
+
: [];
|
|
883
|
+
return rawValues
|
|
884
|
+
.map((value) => normalizeCompletionReplyAttachment(value))
|
|
885
|
+
.filter(Boolean);
|
|
886
|
+
}
|
|
887
|
+
|
|
877
888
|
function setCompletionReplyDraft(token, partialDraft) {
|
|
878
889
|
if (!token) {
|
|
879
890
|
return;
|
|
880
891
|
}
|
|
881
892
|
const previousStoredDraft = state.completionReplyDrafts?.[token] || {};
|
|
882
|
-
const
|
|
893
|
+
const previousAttachments = normalizeCompletionReplyAttachments(
|
|
894
|
+
previousStoredDraft.attachments ?? previousStoredDraft.attachment
|
|
895
|
+
);
|
|
883
896
|
const nextDraft = {
|
|
884
897
|
...getCompletionReplyDraft(token),
|
|
885
898
|
...(partialDraft || {}),
|
|
886
899
|
};
|
|
887
|
-
const
|
|
888
|
-
?
|
|
889
|
-
:
|
|
890
|
-
|
|
891
|
-
|
|
900
|
+
const nextAttachments = Object.prototype.hasOwnProperty.call(partialDraft || {}, "attachments")
|
|
901
|
+
? normalizeCompletionReplyAttachments(partialDraft?.attachments)
|
|
902
|
+
: Object.prototype.hasOwnProperty.call(partialDraft || {}, "attachment")
|
|
903
|
+
? normalizeCompletionReplyAttachments(partialDraft?.attachment)
|
|
904
|
+
: normalizeCompletionReplyAttachments(nextDraft.attachments);
|
|
905
|
+
const nextPreviewUrls = new Set(nextAttachments.map((attachment) => attachment.previewUrl).filter(Boolean));
|
|
906
|
+
for (const previousAttachment of previousAttachments) {
|
|
907
|
+
if (previousAttachment?.previewUrl && !nextPreviewUrls.has(previousAttachment.previewUrl)) {
|
|
908
|
+
releaseCompletionReplyAttachment(previousAttachment);
|
|
909
|
+
}
|
|
892
910
|
}
|
|
893
911
|
state.completionReplyDrafts[token] = {
|
|
894
912
|
text: String(nextDraft.text ?? ""),
|
|
895
913
|
sentText: normalizeClientText(nextDraft.sentText ?? ""),
|
|
896
|
-
|
|
914
|
+
attachments: nextAttachments,
|
|
897
915
|
mode: normalizeReplyMode(nextDraft.mode),
|
|
898
916
|
notice: normalizeClientText(nextDraft.notice),
|
|
899
917
|
error: normalizeClientText(nextDraft.error),
|
|
@@ -908,7 +926,11 @@ function clearCompletionReplyDraft(token) {
|
|
|
908
926
|
if (!token || !state.completionReplyDrafts?.[token]) {
|
|
909
927
|
return;
|
|
910
928
|
}
|
|
911
|
-
|
|
929
|
+
for (const attachment of normalizeCompletionReplyAttachments(
|
|
930
|
+
state.completionReplyDrafts[token]?.attachments ?? state.completionReplyDrafts[token]?.attachment
|
|
931
|
+
)) {
|
|
932
|
+
releaseCompletionReplyAttachment(attachment);
|
|
933
|
+
}
|
|
912
934
|
delete state.completionReplyDrafts[token];
|
|
913
935
|
}
|
|
914
936
|
|
|
@@ -1492,6 +1514,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
1492
1514
|
const kindClassName = escapeHtml(kindInfo.tone || "neutral");
|
|
1493
1515
|
const kindNameClass = escapeHtml(String(item.kind || "item").replace(/_/gu, "-"));
|
|
1494
1516
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || item.kind === "completion";
|
|
1517
|
+
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
1495
1518
|
const primaryText = isMessageLike
|
|
1496
1519
|
? item.summary || fallbackSummaryForKind(item.kind, entry.status)
|
|
1497
1520
|
: item.title || L("common.untitledItem");
|
|
@@ -1521,12 +1544,39 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
1521
1544
|
<div class="timeline-entry__body">
|
|
1522
1545
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
1523
1546
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
1547
|
+
${renderTimelineEntryImageStrip(imageUrls)}
|
|
1524
1548
|
</div>
|
|
1525
1549
|
${statusLabel ? `<div class="timeline-entry__footer"><span class="timeline-entry__status">${escapeHtml(statusLabel)}</span></div>` : ""}
|
|
1526
1550
|
</button>
|
|
1527
1551
|
`;
|
|
1528
1552
|
}
|
|
1529
1553
|
|
|
1554
|
+
function renderTimelineEntryImageStrip(imageUrls) {
|
|
1555
|
+
if (!Array.isArray(imageUrls) || imageUrls.length === 0) {
|
|
1556
|
+
return "";
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return `
|
|
1560
|
+
<div class="timeline-entry__images" aria-hidden="true">
|
|
1561
|
+
${imageUrls
|
|
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
|
+
`;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1530
1580
|
function timelineEntryThreadLabel(item, isMessage) {
|
|
1531
1581
|
const threadLabel = resolvedThreadLabel(item.threadId || "", item.threadLabel || "");
|
|
1532
1582
|
if (!threadLabel) {
|
|
@@ -1539,23 +1589,82 @@ function timelineEntryThreadLabel(item, isMessage) {
|
|
|
1539
1589
|
return title.includes(threadLabel) ? "" : threadLabel;
|
|
1540
1590
|
}
|
|
1541
1591
|
|
|
1592
|
+
function sanitizeThreadLabelForDisplay(label = "", threadId = "") {
|
|
1593
|
+
const normalizedLabel = normalizeClientText(label || "");
|
|
1594
|
+
if (!normalizedLabel) {
|
|
1595
|
+
return "";
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
const normalizedThreadId = normalizeClientText(threadId || "");
|
|
1599
|
+
if (normalizedThreadId && (normalizedLabel === normalizedThreadId || normalizedLabel === normalizedThreadId.slice(0, 8))) {
|
|
1600
|
+
return "";
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (/^[0-9a-f]{8}(?:-[0-9a-f]{4}){0,4}$/i.test(normalizedLabel)) {
|
|
1604
|
+
return "";
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
if (looksLikeGeneratedThreadTitle(normalizedLabel)) {
|
|
1608
|
+
return "";
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
return normalizedLabel;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function looksLikeGeneratedThreadTitle(label = "") {
|
|
1615
|
+
const normalizedLabel = normalizeClientText(label || "");
|
|
1616
|
+
if (!normalizedLabel.includes("|")) {
|
|
1617
|
+
return false;
|
|
1618
|
+
}
|
|
1619
|
+
const prefix = normalizeClientText(normalizedLabel.split("|", 1)[0] || "");
|
|
1620
|
+
if (!prefix) {
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
const titleKeys = [
|
|
1624
|
+
"server.title.userMessage",
|
|
1625
|
+
"server.title.assistantCommentary",
|
|
1626
|
+
"server.title.assistantFinal",
|
|
1627
|
+
"server.title.approval",
|
|
1628
|
+
"server.title.plan",
|
|
1629
|
+
"server.title.planReady",
|
|
1630
|
+
"server.title.choice",
|
|
1631
|
+
"server.title.choiceReadOnly",
|
|
1632
|
+
"server.title.complete",
|
|
1633
|
+
];
|
|
1634
|
+
return SUPPORTED_LOCALES.some((locale) => titleKeys.some((key) => t(locale, key) === prefix));
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1542
1637
|
function resolvedThreadLabel(threadId, explicitLabel = "") {
|
|
1543
|
-
const
|
|
1638
|
+
const normalizedThreadId = normalizeClientText(threadId || "");
|
|
1639
|
+
const normalizedLabel = sanitizeThreadLabelForDisplay(explicitLabel || "", normalizedThreadId);
|
|
1544
1640
|
if (normalizedLabel) {
|
|
1545
1641
|
return normalizedLabel;
|
|
1546
1642
|
}
|
|
1547
|
-
const normalizedThreadId = normalizeClientText(threadId || "");
|
|
1548
1643
|
if (!normalizedThreadId) {
|
|
1549
1644
|
return "";
|
|
1550
1645
|
}
|
|
1551
1646
|
const timelineThreads = Array.isArray(state.timeline?.threads) ? state.timeline.threads : [];
|
|
1552
1647
|
const matchingThread = timelineThreads.find((thread) => thread.id === normalizedThreadId);
|
|
1553
|
-
const fallbackLabel =
|
|
1648
|
+
const fallbackLabel = sanitizeThreadLabelForDisplay(matchingThread?.label || "", normalizedThreadId);
|
|
1554
1649
|
return fallbackLabel || "";
|
|
1555
1650
|
}
|
|
1556
1651
|
|
|
1652
|
+
function compactDropdownThreadLabel(label) {
|
|
1653
|
+
const normalized = normalizeClientText(label || "");
|
|
1654
|
+
if (!normalized) {
|
|
1655
|
+
return "";
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const glyphs = Array.from(normalized);
|
|
1659
|
+
if (glyphs.length <= 28) {
|
|
1660
|
+
return normalized;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
return `${glyphs.slice(0, 28).join("")}...`;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1557
1666
|
function dropdownThreadLabel(threadId, explicitLabel = "") {
|
|
1558
|
-
return resolvedThreadLabel(threadId, explicitLabel) || L("timeline.unknownThread");
|
|
1667
|
+
return compactDropdownThreadLabel(resolvedThreadLabel(threadId, explicitLabel)) || L("timeline.unknownThread");
|
|
1559
1668
|
}
|
|
1560
1669
|
|
|
1561
1670
|
function formatTimelineTimestamp(value) {
|
|
@@ -2193,9 +2302,11 @@ function renderStandardDetailDesktop(detail) {
|
|
|
2193
2302
|
<h2 class="detail-title detail-title--desktop">${escapeHtml(detailDisplayTitle(detail))}</h2>
|
|
2194
2303
|
${detail.readOnly ? "" : renderDetailLead(detail, kindInfo)}
|
|
2195
2304
|
${renderPreviousContextCard(detail)}
|
|
2305
|
+
${renderInterruptedDetailNotice(detail)}
|
|
2196
2306
|
<section class="detail-card detail-card--body ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
|
|
2197
2307
|
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
2198
2308
|
</section>
|
|
2309
|
+
${renderDetailImageGallery(detail)}
|
|
2199
2310
|
${renderCompletionReplyComposer(detail)}
|
|
2200
2311
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [])}
|
|
2201
2312
|
</div>
|
|
@@ -2211,10 +2322,12 @@ function renderStandardDetailMobile(detail) {
|
|
|
2211
2322
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
2212
2323
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
2213
2324
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
2325
|
+
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
2214
2326
|
<section class="detail-card detail-card--body detail-card--mobile ${spaciousBodyDetail ? "detail-card--message-body" : ""}">
|
|
2215
2327
|
${detail.readOnly ? "" : renderDetailLead(detail, kindInfo, { mobile: true })}
|
|
2216
2328
|
<div class="detail-body ${spaciousBodyDetail ? "detail-body--message " : ""}markdown">${detail.messageHtml || ""}</div>
|
|
2217
2329
|
</section>
|
|
2330
|
+
${renderDetailImageGallery(detail, { mobile: true })}
|
|
2218
2331
|
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
2219
2332
|
</div>
|
|
2220
2333
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
@@ -2250,9 +2363,24 @@ function renderDetailLead(detail, kindInfo, options = {}) {
|
|
|
2250
2363
|
`;
|
|
2251
2364
|
}
|
|
2252
2365
|
|
|
2366
|
+
function renderInterruptedDetailNotice(detail, options = {}) {
|
|
2367
|
+
const message = normalizeClientText(detail?.interruptNotice || "");
|
|
2368
|
+
if (!message) {
|
|
2369
|
+
return "";
|
|
2370
|
+
}
|
|
2371
|
+
return `
|
|
2372
|
+
<section class="detail-card detail-card--interrupt ${options.mobile ? "detail-card--mobile" : ""}">
|
|
2373
|
+
<p class="detail-interrupt-copy">
|
|
2374
|
+
<span class="detail-interrupt-copy__icon" aria-hidden="true">${renderIcon("pending")}</span>
|
|
2375
|
+
<span>${escapeHtml(message)}</span>
|
|
2376
|
+
</p>
|
|
2377
|
+
</section>
|
|
2378
|
+
`;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2253
2381
|
function renderPreviousContextCard(detail, options = {}) {
|
|
2254
2382
|
const context = detail?.previousContext;
|
|
2255
|
-
if (!context?.messageHtml
|
|
2383
|
+
if (!context?.messageHtml) {
|
|
2256
2384
|
return "";
|
|
2257
2385
|
}
|
|
2258
2386
|
|
|
@@ -2263,7 +2391,7 @@ function renderPreviousContextCard(detail, options = {}) {
|
|
|
2263
2391
|
<div class="detail-context-card__header">
|
|
2264
2392
|
<div class="detail-context-card__eyebrow">
|
|
2265
2393
|
<span class="detail-context-card__icon" aria-hidden="true">${renderIcon(contextKind.icon)}</span>
|
|
2266
|
-
<span>${escapeHtml(L("detail.previousMessage"))}</span>
|
|
2394
|
+
<span>${escapeHtml(context.label || L("detail.previousMessage"))}</span>
|
|
2267
2395
|
</div>
|
|
2268
2396
|
${timestampLabel ? `<span class="detail-context-card__time">${escapeHtml(timestampLabel)}</span>` : ""}
|
|
2269
2397
|
</div>
|
|
@@ -2273,6 +2401,40 @@ function renderPreviousContextCard(detail, options = {}) {
|
|
|
2273
2401
|
`;
|
|
2274
2402
|
}
|
|
2275
2403
|
|
|
2404
|
+
function renderDetailImageGallery(detail, options = {}) {
|
|
2405
|
+
const imageUrls = Array.isArray(detail?.imageUrls) ? detail.imageUrls.filter(Boolean) : [];
|
|
2406
|
+
if (imageUrls.length === 0) {
|
|
2407
|
+
return "";
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
return `
|
|
2411
|
+
<section class="detail-card detail-card--images ${options.mobile ? "detail-card--mobile" : ""}">
|
|
2412
|
+
<div class="detail-image-grid">
|
|
2413
|
+
${imageUrls
|
|
2414
|
+
.map((imageUrl, index) => {
|
|
2415
|
+
const altText = L("detail.imageAlt", { index: index + 1 });
|
|
2416
|
+
return `
|
|
2417
|
+
<button
|
|
2418
|
+
class="detail-image-link"
|
|
2419
|
+
type="button"
|
|
2420
|
+
data-open-image-viewer="${escapeHtml(imageUrl)}"
|
|
2421
|
+
data-image-alt="${escapeHtml(altText)}"
|
|
2422
|
+
>
|
|
2423
|
+
<img
|
|
2424
|
+
class="detail-image"
|
|
2425
|
+
src="${escapeHtml(imageUrl)}"
|
|
2426
|
+
alt="${escapeHtml(altText)}"
|
|
2427
|
+
loading="lazy"
|
|
2428
|
+
>
|
|
2429
|
+
</button>
|
|
2430
|
+
`;
|
|
2431
|
+
})
|
|
2432
|
+
.join("")}
|
|
2433
|
+
</div>
|
|
2434
|
+
</section>
|
|
2435
|
+
`;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2276
2438
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
2277
2439
|
if (detail.kind !== "completion" || detail.reply?.enabled !== true) {
|
|
2278
2440
|
return "";
|
|
@@ -2289,8 +2451,7 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
2289
2451
|
const warningTimestamp = draft.warning?.createdAtMs ? formatTimelineTimestamp(draft.warning.createdAtMs) : "";
|
|
2290
2452
|
const showCollapsedState =
|
|
2291
2453
|
draft.collapsedAfterSend && Boolean(draft.notice) && !draft.error && !draft.warning && !draft.sending;
|
|
2292
|
-
const
|
|
2293
|
-
const attachmentPreviewUrl = draft.attachment?.previewUrl ? escapeHtml(draft.attachment.previewUrl) : "";
|
|
2454
|
+
const attachments = Array.isArray(draft.attachments) ? draft.attachments : [];
|
|
2294
2455
|
|
|
2295
2456
|
return `
|
|
2296
2457
|
<section class="detail-card detail-card--reply ${options.mobile ? "detail-card--mobile" : ""}">
|
|
@@ -2347,86 +2508,93 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
2347
2508
|
<form class="reply-composer__form" data-completion-reply-form data-token="${escapeHtml(detail.token)}">
|
|
2348
2509
|
<label class="field reply-field">
|
|
2349
2510
|
<span class="field-label">${escapeHtml(L("reply.fieldLabel"))}</span>
|
|
2350
|
-
<
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2511
|
+
<div class="reply-field__shell">
|
|
2512
|
+
<textarea
|
|
2513
|
+
class="reply-field__input"
|
|
2514
|
+
name="text"
|
|
2515
|
+
rows="4"
|
|
2516
|
+
placeholder="${escapeHtml(L("reply.placeholder"))}"
|
|
2517
|
+
data-completion-reply-textarea
|
|
2518
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2519
|
+
>${escapeHtml(draft.text)}</textarea>
|
|
2520
|
+
<div class="reply-field__toolbar">
|
|
2521
|
+
${
|
|
2522
|
+
detail.reply?.supportsImages
|
|
2523
|
+
? `
|
|
2524
|
+
<label class="reply-attachment-trigger" aria-label="${escapeHtml(L(attachments.length ? "reply.imageAddMore" : "reply.imageAdd"))}">
|
|
2525
|
+
<input
|
|
2526
|
+
class="reply-attachment-trigger__input"
|
|
2527
|
+
type="file"
|
|
2528
|
+
accept="image/*"
|
|
2529
|
+
multiple
|
|
2530
|
+
data-reply-image-input
|
|
2531
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2532
|
+
>
|
|
2533
|
+
<span class="reply-attachment-trigger__icon" aria-hidden="true">${renderIcon("clip")}</span>
|
|
2534
|
+
${
|
|
2535
|
+
attachments.length
|
|
2536
|
+
? `<span class="reply-attachment-trigger__count">${escapeHtml(String(attachments.length))}</span>`
|
|
2537
|
+
: ""
|
|
2538
|
+
}
|
|
2539
|
+
</label>
|
|
2540
|
+
`
|
|
2541
|
+
: ""
|
|
2542
|
+
}
|
|
2543
|
+
${
|
|
2544
|
+
detail.reply?.supportsPlanMode
|
|
2545
|
+
? `
|
|
2546
|
+
<label class="reply-mode-toggle" data-reply-mode-switch>
|
|
2547
|
+
<span class="reply-mode-toggle__label">${escapeHtml(L("reply.mode.planLabel"))}</span>
|
|
2548
|
+
<input
|
|
2549
|
+
class="reply-mode-toggle__input"
|
|
2550
|
+
type="checkbox"
|
|
2551
|
+
${planMode ? "checked" : ""}
|
|
2552
|
+
data-reply-mode-toggle
|
|
2553
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2554
|
+
>
|
|
2555
|
+
<span class="reply-mode-toggle__track" aria-hidden="true">
|
|
2556
|
+
<span class="reply-mode-toggle__thumb"></span>
|
|
2557
|
+
</span>
|
|
2558
|
+
</label>
|
|
2559
|
+
`
|
|
2560
|
+
: ""
|
|
2561
|
+
}
|
|
2562
|
+
</div>
|
|
2563
|
+
</div>
|
|
2358
2564
|
</label>
|
|
2359
2565
|
${
|
|
2360
2566
|
detail.reply?.supportsImages
|
|
2361
2567
|
? `
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
draft.attachment
|
|
2393
|
-
? `
|
|
2394
|
-
<div class="reply-image-preview">
|
|
2395
|
-
<img class="reply-image-preview__image" src="${attachmentPreviewUrl}" alt="${attachmentName}">
|
|
2396
|
-
<div class="reply-image-preview__copy">
|
|
2397
|
-
<p class="reply-image-preview__name">${attachmentName}</p>
|
|
2398
|
-
<p class="reply-image-preview__meta">${escapeHtml(L("reply.imageAttached"))}</p>
|
|
2399
|
-
</div>
|
|
2400
|
-
</div>
|
|
2401
|
-
`
|
|
2402
|
-
: ""
|
|
2403
|
-
}
|
|
2404
|
-
</div>
|
|
2405
|
-
`
|
|
2406
|
-
: ""
|
|
2407
|
-
}
|
|
2408
|
-
${
|
|
2409
|
-
detail.reply?.supportsPlanMode
|
|
2410
|
-
? `
|
|
2411
|
-
<label class="reply-mode-switch" data-reply-mode-switch>
|
|
2412
|
-
<input
|
|
2413
|
-
class="reply-mode-switch__input"
|
|
2414
|
-
type="checkbox"
|
|
2415
|
-
${planMode ? "checked" : ""}
|
|
2416
|
-
data-reply-mode-toggle
|
|
2417
|
-
data-reply-token="${escapeHtml(detail.token)}"
|
|
2418
|
-
>
|
|
2419
|
-
<span class="reply-mode-switch__track" aria-hidden="true">
|
|
2420
|
-
<span class="reply-mode-switch__thumb"></span>
|
|
2421
|
-
</span>
|
|
2422
|
-
<span class="reply-mode-switch__copy">
|
|
2423
|
-
<span class="reply-mode-switch__title">
|
|
2424
|
-
<span>${escapeHtml(L("reply.mode.planLabel"))}</span>
|
|
2425
|
-
<span class="reply-mode-switch__state">${escapeHtml(L(planMode ? "reply.mode.on" : "reply.mode.off"))}</span>
|
|
2426
|
-
</span>
|
|
2427
|
-
<span class="reply-mode-switch__hint">${escapeHtml(L(planMode ? "reply.mode.planHint" : "reply.mode.defaultHint"))}</span>
|
|
2428
|
-
</span>
|
|
2429
|
-
</label>
|
|
2568
|
+
${
|
|
2569
|
+
attachments.length
|
|
2570
|
+
? `
|
|
2571
|
+
<div class="reply-image-preview-list">
|
|
2572
|
+
${attachments
|
|
2573
|
+
.map(
|
|
2574
|
+
(attachment, index) => `
|
|
2575
|
+
<div class="reply-image-preview">
|
|
2576
|
+
<img class="reply-image-preview__image" src="${escapeHtml(attachment.previewUrl || "")}" alt="${escapeHtml(attachment.name || "")}">
|
|
2577
|
+
<div class="reply-image-preview__copy">
|
|
2578
|
+
<p class="reply-image-preview__name">${escapeHtml(attachment.name || "")}</p>
|
|
2579
|
+
<p class="reply-image-preview__meta">${escapeHtml(L("reply.imageAttached"))}</p>
|
|
2580
|
+
</div>
|
|
2581
|
+
<button
|
|
2582
|
+
class="secondary secondary--compact"
|
|
2583
|
+
type="button"
|
|
2584
|
+
data-reply-image-remove
|
|
2585
|
+
data-reply-token="${escapeHtml(detail.token)}"
|
|
2586
|
+
data-reply-image-index="${index}"
|
|
2587
|
+
>
|
|
2588
|
+
${escapeHtml(L("reply.imageRemove"))}
|
|
2589
|
+
</button>
|
|
2590
|
+
</div>
|
|
2591
|
+
`
|
|
2592
|
+
)
|
|
2593
|
+
.join("")}
|
|
2594
|
+
</div>
|
|
2595
|
+
`
|
|
2596
|
+
: ""
|
|
2597
|
+
}
|
|
2430
2598
|
`
|
|
2431
2599
|
: ""
|
|
2432
2600
|
}
|
|
@@ -2748,6 +2916,26 @@ function renderInstallGuideModal() {
|
|
|
2748
2916
|
`;
|
|
2749
2917
|
}
|
|
2750
2918
|
|
|
2919
|
+
function renderImageViewerModal() {
|
|
2920
|
+
const imageViewer = state.imageViewer;
|
|
2921
|
+
if (!imageViewer?.url) {
|
|
2922
|
+
return "";
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
return `
|
|
2926
|
+
<div class="modal-backdrop modal-backdrop--image-viewer" data-close-image-viewer>
|
|
2927
|
+
<section class="image-viewer" role="dialog" aria-modal="true" aria-label="${escapeHtml(imageViewer.alt || L("common.detail"))}">
|
|
2928
|
+
<div class="image-viewer__chrome">
|
|
2929
|
+
<button class="secondary image-viewer__close" type="button" data-close-image-viewer>${escapeHtml(L("common.back"))}</button>
|
|
2930
|
+
</div>
|
|
2931
|
+
<div class="image-viewer__body">
|
|
2932
|
+
<img class="image-viewer__image" src="${escapeHtml(imageViewer.url)}" alt="${escapeHtml(imageViewer.alt || "")}">
|
|
2933
|
+
</div>
|
|
2934
|
+
</section>
|
|
2935
|
+
</div>
|
|
2936
|
+
`;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2751
2939
|
function renderLogoutConfirmModal() {
|
|
2752
2940
|
if (!state.logoutConfirmOpen || !state.session?.authenticated) {
|
|
2753
2941
|
return "";
|
|
@@ -2907,6 +3095,18 @@ function bindShellInteractions() {
|
|
|
2907
3095
|
});
|
|
2908
3096
|
}
|
|
2909
3097
|
|
|
3098
|
+
for (const button of document.querySelectorAll("[data-open-image-viewer]")) {
|
|
3099
|
+
button.addEventListener("click", async (event) => {
|
|
3100
|
+
event.preventDefault();
|
|
3101
|
+
event.stopPropagation();
|
|
3102
|
+
state.imageViewer = {
|
|
3103
|
+
url: button.dataset.openImageViewer || "",
|
|
3104
|
+
alt: button.dataset.imageAlt || "",
|
|
3105
|
+
};
|
|
3106
|
+
await renderShell();
|
|
3107
|
+
});
|
|
3108
|
+
}
|
|
3109
|
+
|
|
2910
3110
|
for (const button of document.querySelectorAll("[data-back-to-list]")) {
|
|
2911
3111
|
button.addEventListener("click", async () => {
|
|
2912
3112
|
clearChoiceLocalDraftForItem(state.currentItem);
|
|
@@ -2974,25 +3174,44 @@ function bindShellInteractions() {
|
|
|
2974
3174
|
for (const input of document.querySelectorAll("[data-reply-image-input][data-reply-token]")) {
|
|
2975
3175
|
input.addEventListener("change", async () => {
|
|
2976
3176
|
const token = input.dataset.replyToken || "";
|
|
2977
|
-
const
|
|
2978
|
-
const
|
|
2979
|
-
if (
|
|
3177
|
+
const files = Array.from(input.files || []);
|
|
3178
|
+
const nextAttachments = files.map((file) => createCompletionReplyAttachment(file)).filter(Boolean);
|
|
3179
|
+
if (files.length > 0 && nextAttachments.length !== files.length) {
|
|
2980
3180
|
setCompletionReplyDraft(token, {
|
|
2981
3181
|
error: L("error.completionReplyImageInvalidType"),
|
|
2982
3182
|
notice: "",
|
|
2983
3183
|
warning: null,
|
|
2984
3184
|
confirmOverride: false,
|
|
2985
3185
|
});
|
|
3186
|
+
input.value = "";
|
|
3187
|
+
await renderShell();
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const existingAttachments = getCompletionReplyDraft(token).attachments || [];
|
|
3191
|
+
const mergedAttachments = [...existingAttachments, ...nextAttachments].slice(0, MAX_COMPLETION_REPLY_IMAGE_COUNT);
|
|
3192
|
+
if (existingAttachments.length + nextAttachments.length > MAX_COMPLETION_REPLY_IMAGE_COUNT) {
|
|
3193
|
+
for (const attachment of nextAttachments.slice(Math.max(0, MAX_COMPLETION_REPLY_IMAGE_COUNT - existingAttachments.length))) {
|
|
3194
|
+
releaseCompletionReplyAttachment(attachment);
|
|
3195
|
+
}
|
|
3196
|
+
setCompletionReplyDraft(token, {
|
|
3197
|
+
error: L("error.completionReplyImageLimit", { count: MAX_COMPLETION_REPLY_IMAGE_COUNT }),
|
|
3198
|
+
notice: "",
|
|
3199
|
+
warning: null,
|
|
3200
|
+
confirmOverride: false,
|
|
3201
|
+
attachments: mergedAttachments,
|
|
3202
|
+
});
|
|
3203
|
+
input.value = "";
|
|
2986
3204
|
await renderShell();
|
|
2987
3205
|
return;
|
|
2988
3206
|
}
|
|
2989
3207
|
setCompletionReplyDraft(token, {
|
|
2990
|
-
|
|
3208
|
+
attachments: mergedAttachments,
|
|
2991
3209
|
notice: "",
|
|
2992
3210
|
error: "",
|
|
2993
3211
|
warning: null,
|
|
2994
3212
|
confirmOverride: false,
|
|
2995
3213
|
});
|
|
3214
|
+
input.value = "";
|
|
2996
3215
|
await renderShell();
|
|
2997
3216
|
});
|
|
2998
3217
|
}
|
|
@@ -3000,8 +3219,13 @@ function bindShellInteractions() {
|
|
|
3000
3219
|
for (const button of document.querySelectorAll("[data-reply-image-remove][data-reply-token]")) {
|
|
3001
3220
|
button.addEventListener("click", async () => {
|
|
3002
3221
|
const token = button.dataset.replyToken || "";
|
|
3222
|
+
const index = Number(button.dataset.replyImageIndex ?? "-1");
|
|
3223
|
+
const existingAttachments = getCompletionReplyDraft(token).attachments || [];
|
|
3003
3224
|
setCompletionReplyDraft(token, {
|
|
3004
|
-
|
|
3225
|
+
attachments:
|
|
3226
|
+
index >= 0
|
|
3227
|
+
? existingAttachments.filter((_, attachmentIndex) => attachmentIndex !== index)
|
|
3228
|
+
: [],
|
|
3005
3229
|
notice: "",
|
|
3006
3230
|
error: "",
|
|
3007
3231
|
warning: null,
|
|
@@ -3184,14 +3408,16 @@ function bindShellInteractions() {
|
|
|
3184
3408
|
requestBody.set("text", text);
|
|
3185
3409
|
requestBody.set("planMode", draft.mode === "plan" ? "true" : "false");
|
|
3186
3410
|
requestBody.set("force", draft.confirmOverride === true ? "true" : "false");
|
|
3187
|
-
|
|
3188
|
-
|
|
3411
|
+
for (const attachment of draft.attachments || []) {
|
|
3412
|
+
if (attachment?.file) {
|
|
3413
|
+
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
3414
|
+
}
|
|
3189
3415
|
}
|
|
3190
3416
|
await apiPost(`/api/items/completion/${encodeURIComponent(token)}/reply`, requestBody);
|
|
3191
3417
|
setCompletionReplyDraft(token, {
|
|
3192
3418
|
text: "",
|
|
3193
3419
|
sentText: text,
|
|
3194
|
-
|
|
3420
|
+
attachments: [],
|
|
3195
3421
|
mode: draft.mode,
|
|
3196
3422
|
sending: false,
|
|
3197
3423
|
error: "",
|
|
@@ -3206,7 +3432,7 @@ function bindShellInteractions() {
|
|
|
3206
3432
|
setCompletionReplyDraft(token, {
|
|
3207
3433
|
text,
|
|
3208
3434
|
sentText: "",
|
|
3209
|
-
|
|
3435
|
+
attachments: draft.attachments,
|
|
3210
3436
|
mode: draft.mode,
|
|
3211
3437
|
sending: false,
|
|
3212
3438
|
notice: "",
|
|
@@ -3221,7 +3447,7 @@ function bindShellInteractions() {
|
|
|
3221
3447
|
setCompletionReplyDraft(token, {
|
|
3222
3448
|
text,
|
|
3223
3449
|
sentText: "",
|
|
3224
|
-
|
|
3450
|
+
attachments: draft.attachments,
|
|
3225
3451
|
mode: draft.mode,
|
|
3226
3452
|
sending: false,
|
|
3227
3453
|
notice: "",
|
|
@@ -3240,6 +3466,16 @@ function bindShellInteractions() {
|
|
|
3240
3466
|
}
|
|
3241
3467
|
|
|
3242
3468
|
function bindSharedUi(renderFn) {
|
|
3469
|
+
for (const button of document.querySelectorAll("[data-close-image-viewer]")) {
|
|
3470
|
+
button.addEventListener("click", async (event) => {
|
|
3471
|
+
if (button.classList.contains("modal-backdrop") && event.target.closest(".image-viewer")) {
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
state.imageViewer = null;
|
|
3475
|
+
await renderFn();
|
|
3476
|
+
});
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3243
3479
|
for (const button of document.querySelectorAll("[data-install-guide-open]")) {
|
|
3244
3480
|
button.addEventListener("click", async () => {
|
|
3245
3481
|
state.installGuideOpen = true;
|
|
@@ -3806,6 +4042,8 @@ function renderIcon(name) {
|
|
|
3806
4042
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5c3.8 0 7 3.8 7 8.5s-3.2 8.5-7 8.5-7-3.8-7-8.5 3.2-8.5 7-8.5Z"/><path d="M5.8 9h12.4"/><path d="M5.8 15h12.4"/><path d="M12 3.8c1.9 2 3 4.9 3 8.2s-1.1 6.2-3 8.2c-1.9-2-3-4.9-3-8.2s1.1-6.2 3-8.2Z"/></svg>`;
|
|
3807
4043
|
case "link":
|
|
3808
4044
|
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
|
+
case "clip":
|
|
4046
|
+
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>`;
|
|
3809
4047
|
case "check":
|
|
3810
4048
|
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>`;
|
|
3811
4049
|
case "back":
|
|
@@ -3994,7 +4232,6 @@ function localizeApiError(value) {
|
|
|
3994
4232
|
"completion-reply-image-too-large": "error.completionReplyImageTooLarge",
|
|
3995
4233
|
"completion-reply-image-limit": "error.completionReplyImageLimit",
|
|
3996
4234
|
"completion-reply-image-invalid-upload": "error.completionReplyImageInvalidUpload",
|
|
3997
|
-
"completion-reply-image-disabled": "error.completionReplyImageDisabled",
|
|
3998
4235
|
"codex-ipc-not-connected": "error.codexIpcNotConnected",
|
|
3999
4236
|
"approval-not-found": "error.approvalNotFound",
|
|
4000
4237
|
"approval-already-handled": "error.approvalAlreadyHandled",
|
|
@@ -4069,7 +4306,7 @@ function syncPairingTokenState(pairToken) {
|
|
|
4069
4306
|
}
|
|
4070
4307
|
|
|
4071
4308
|
function desiredBootstrapPairingToken() {
|
|
4072
|
-
if (state.session?.authenticated) {
|
|
4309
|
+
if (state.session?.authenticated && !state.session?.temporaryPairing) {
|
|
4073
4310
|
return "";
|
|
4074
4311
|
}
|
|
4075
4312
|
return initialPairToken;
|
|
@@ -4079,15 +4316,13 @@ function shouldAutoPairFromBootstrapToken() {
|
|
|
4079
4316
|
if (!initialPairToken) {
|
|
4080
4317
|
return false;
|
|
4081
4318
|
}
|
|
4082
|
-
if (isStandaloneMode()) {
|
|
4083
|
-
return true;
|
|
4084
|
-
}
|
|
4085
|
-
if (isProbablySafari()) {
|
|
4086
|
-
return false;
|
|
4087
|
-
}
|
|
4088
4319
|
return true;
|
|
4089
4320
|
}
|
|
4090
4321
|
|
|
4322
|
+
function shouldUseTemporaryBootstrapPairing() {
|
|
4323
|
+
return Boolean(initialPairToken) && !isStandaloneMode() && isProbablySafari();
|
|
4324
|
+
}
|
|
4325
|
+
|
|
4091
4326
|
function urlBase64ToUint8Array(base64String) {
|
|
4092
4327
|
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
4093
4328
|
const normalized = `${base64String}${padding}`.replace(/-/gu, "+").replace(/_/gu, "/");
|