@songsid/agend 2.2.0-alpha.3 → 2.2.0-alpha.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/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/discord-general.d.ts +15 -0
- package/dist/discord-general.js +24 -0
- package/dist/discord-general.js.map +1 -0
- package/dist/fleet-manager.d.ts +9 -0
- package/dist/fleet-manager.js +22 -15
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +8 -2
- package/dist/locale.js.map +1 -1
- package/dist/public-link-progress.d.ts +3 -1
- package/dist/public-link-progress.js +15 -1
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-web-gateway.js +3 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +4 -2
- package/dist/public-web-link.js +10 -8
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js +4 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/topic-commands.js +4 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +25 -1
- package/dist/tunnel/cloudflared-install.js +181 -10
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/deb-extract.d.ts +9 -0
- package/dist/tunnel/deb-extract.js +94 -0
- package/dist/tunnel/deb-extract.js.map +1 -0
- package/dist/ui/app.html +3 -0
- package/dist/ui/chat-strings.js +50 -0
- package/dist/ui/chat-thread.js +16 -1
- package/dist/ui/icons/apple-touch-icon.png +0 -0
- package/dist/ui/icons/favicon.ico +0 -0
- package/dist/ui/icons/favicon.svg +8 -0
- package/dist/ui/image-lightbox.js +104 -0
- package/dist/ui/panel-chat.js +16 -54
- package/dist/ui/panel-details.js +146 -0
- package/dist/ui/panel-fleet.js +2 -1
- package/dist/ui/panel-settings.js +18 -0
- package/dist/ui/preview-panel.js +9 -4
- package/dist/ui/preview.js +36 -13
- package/dist/ui/settings-request.js +9 -0
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-route.js +7 -2
- package/dist/ui/shared/app-shell.js +22 -8
- package/dist/ui/shared/app.css +43 -0
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/header-tools.js +93 -0
- package/dist/ui/shared/instance-nav.js +7 -4
- package/dist/ui/shared/instance-switch.js +16 -0
- package/dist/ui/shared/panel-view.js +14 -153
- package/dist/ui/shared/read-stream.js +26 -0
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +125 -0
- package/dist/ui/shared/view-strings.js +2 -2
- package/dist/ui/signin.html +3 -0
- package/dist/update-install.js +3 -1
- package/dist/update-install.js.map +1 -1
- package/dist/web-api.d.ts +16 -0
- package/dist/web-api.js +31 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-icons.d.ts +21 -0
- package/dist/web-icons.js +38 -0
- package/dist/web-icons.js.map +1 -0
- package/dist/web-preview.d.ts +9 -0
- package/dist/web-preview.js +15 -9
- package/dist/web-preview.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -0
- package/dist/web-shell-routes.js +9 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/old-updater-note.cjs +48 -0
- package/launcher/postinstall.cjs +8 -1
- package/package.json +1 -1
- package/scripts/preinstall-guard.cjs +5 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<title>AgEnD — Ag</title>
|
|
3
|
+
<rect x="1" y="1" width="62" height="62" rx="12" fill="#e5eaf0" stroke="#617386" stroke-width="2"/>
|
|
4
|
+
<g transform="translate(0 -2)"><g fill="#14273d" fill-rule="evenodd">
|
|
5
|
+
<path d="M8.5 43 17 18h6l8.5 25H26l-2-6h-8l-2 6Zm9-10.5h5l-2.5-8Z"/>
|
|
6
|
+
<path transform="translate(2 0)" d="M49.5 27v18.8c0 5-3.1 7.2-8.2 7.2h-6.8v-4.8H41c2.6 0 3.7-.9 3.7-3.2v-3.3c-1.3 1.5-3.1 2.3-5.6 2.3-5.8 0-9-3.7-9-8.9 0-5.1 3.2-8.9 9-8.9 2.5 0 4.3.8 5.6 2.3V27Zm-9.7 3.8c-3 0-4.6 1.8-4.6 4.3s1.6 4.3 4.6 4.3 4.6-1.8 4.6-4.3-1.6-4.3-4.6-4.3Z"/>
|
|
7
|
+
</g></g>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// A chat image, full size, in the page: a dark overlay with the image fitted to the viewport, ✕ to close, a click on
|
|
2
|
+
// the dark area or Esc closes it too, and focus goes back to the thumbnail. "Open original" opens the same URL in a
|
|
3
|
+
// new tab (the thumbnail's link, unchanged: a ctrl-, meta-, shift- or middle-click on it still does what the browser
|
|
4
|
+
// does). Several images in one message: previous / next, and the arrow keys.
|
|
5
|
+
//
|
|
6
|
+
// The native <dialog> opened with showModal(): the browser traps focus and makes the rest inert. No style attribute
|
|
7
|
+
// (#1300): classes only. Only a file URL the chat itself rendered (/ui/file/<32 hex>) is ever shown.
|
|
8
|
+
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
9
|
+
import { t } from "/assets/app-i18n.js";
|
|
10
|
+
import { createStore, useStore } from "/assets/app-store.js";
|
|
11
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
12
|
+
|
|
13
|
+
const FILE_URL = /^\/ui\/file\/[0-9a-f]{32}$/;
|
|
14
|
+
/** What is shown: { items: [{ src, name }], index, opener } or null. One per page; the chat that shows it owns it. */
|
|
15
|
+
export const lightboxStore = createStore({ open: null });
|
|
16
|
+
let seq = 0;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The images of a message, from its thumbnails' links; `index` the one clicked. Anything that is not a chat file URL
|
|
20
|
+
* is left out (and nothing opens if the clicked one is not).
|
|
21
|
+
*/
|
|
22
|
+
export function openLightbox(links, clicked) {
|
|
23
|
+
const items = [];
|
|
24
|
+
let index = -1;
|
|
25
|
+
for (const a of links) {
|
|
26
|
+
const src = a.getAttribute("href") || "";
|
|
27
|
+
if (!FILE_URL.test(src)) continue;
|
|
28
|
+
if (a === clicked) index = items.length;
|
|
29
|
+
const img = a.querySelector("img");
|
|
30
|
+
items.push({ src, name: (img && img.getAttribute("alt")) || "" });
|
|
31
|
+
}
|
|
32
|
+
if (index < 0) return false;
|
|
33
|
+
lightboxStore.set({ open: { items, index, opener: clicked, seq: ++seq } });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Close it — the one way every close goes: ✕, the dark area, Esc, the dialog's own close (the browser's, a form with
|
|
38
|
+
* method=dialog), the chat leaving. The store is the truth; unmounting the dialog is the one cleanup (scroll unlocked,
|
|
39
|
+
* focus back on the thumbnail). `seq`: only that lightbox — a late close of one already replaced closes nothing.
|
|
40
|
+
*/
|
|
41
|
+
export function closeLightbox(seq) {
|
|
42
|
+
const open = lightboxStore.get().open;
|
|
43
|
+
if (open && (seq === undefined || open.seq === seq)) lightboxStore.set({ open: null });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Rendered by the chat; gone with it (another instance, another panel). */
|
|
47
|
+
export function LightboxHost() {
|
|
48
|
+
const { open } = useStore(lightboxStore);
|
|
49
|
+
useEffect(() => () => closeLightbox(), []);
|
|
50
|
+
return open ? html`<${ImageLightbox} key=${open.seq} items=${open.items} index=${open.index} opener=${open.opener} onClose=${() => closeLightbox(open.seq)} />` : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ImageLightbox({ items, index, opener, onClose }) {
|
|
54
|
+
const [at, setAt] = useState(index);
|
|
55
|
+
const ref = useRef(null);
|
|
56
|
+
const close = useRef(null);
|
|
57
|
+
const item = items[at] || items[0];
|
|
58
|
+
const many = items.length > 1;
|
|
59
|
+
const step = (d) => setAt((n) => (n + d + items.length) % items.length);
|
|
60
|
+
useLayoutEffect(() => {
|
|
61
|
+
const d = ref.current;
|
|
62
|
+
const root = typeof document !== "undefined" ? document.documentElement : null;
|
|
63
|
+
if (root) root.classList.add("lb-open"); // the chat behind it does not scroll
|
|
64
|
+
if (d && typeof d.showModal === "function" && !d.open) d.showModal();
|
|
65
|
+
if (close.current && typeof close.current.focus === "function") close.current.focus();
|
|
66
|
+
return () => {
|
|
67
|
+
if (root) root.classList.remove("lb-open");
|
|
68
|
+
if (d && d.open && typeof d.close === "function") d.close();
|
|
69
|
+
if (opener && typeof opener.focus === "function" && document.contains?.(opener)) opener.focus();
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const d = ref.current;
|
|
74
|
+
if (!d) return undefined;
|
|
75
|
+
// Esc: the browser fires "cancel"; closing goes through the store so its state stays the truth.
|
|
76
|
+
const onCancel = (e) => { e.preventDefault(); onClose(); };
|
|
77
|
+
// The dialog closed by itself — the browser (a close request it did not ask about) or a form with method=dialog:
|
|
78
|
+
// the same close as every other, so the store and the cleanup follow (#1558 review).
|
|
79
|
+
const onNativeClose = () => onClose();
|
|
80
|
+
// The dark area (the dialog itself, or the stage around the image) closes it; the image and the controls do not.
|
|
81
|
+
const onClick = (e) => { if (e.target === d || (e.target && e.target.classList && e.target.classList.contains("lb-stage"))) onClose(); };
|
|
82
|
+
const onKey = (e) => {
|
|
83
|
+
if (!many) return;
|
|
84
|
+
if (e.key === "ArrowRight") { e.preventDefault(); step(1); } else if (e.key === "ArrowLeft") { e.preventDefault(); step(-1); }
|
|
85
|
+
};
|
|
86
|
+
d.addEventListener("cancel", onCancel);
|
|
87
|
+
d.addEventListener("close", onNativeClose);
|
|
88
|
+
d.addEventListener("click", onClick);
|
|
89
|
+
d.addEventListener("keydown", onKey);
|
|
90
|
+
return () => { d.removeEventListener("cancel", onCancel); d.removeEventListener("close", onNativeClose); d.removeEventListener("click", onClick); d.removeEventListener("keydown", onKey); };
|
|
91
|
+
}, [onClose, many, items.length]);
|
|
92
|
+
const label = item.name ? t("chat.lbLabel", item.name) : t("chat.lbLabelNoName");
|
|
93
|
+
return html`<dialog ref=${ref} class="lb" role="dialog" aria-modal="true" aria-label=${label}>
|
|
94
|
+
<div class="lb-bar">
|
|
95
|
+
<span class="lb-name" title=${item.name}>${item.name}</span>
|
|
96
|
+
${many ? html`<span class="lb-count">${t("chat.lbCount", at + 1, items.length)}</span>` : null}
|
|
97
|
+
<a class="btn btn-sm lb-orig" href=${item.src} target="_blank" rel="noopener noreferrer"><${Icon} name="external" size=${14} /><span class="lbl">${t("chat.lbOpenOriginal")}</span></a>
|
|
98
|
+
<button type="button" class="icon-btn lb-close" ref=${close} aria-label=${t("chat.lbClose")} title=${t("chat.lbClose")} onClick=${onClose}><${Icon} name="close" /></button>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="lb-stage"><img class="lb-img" src=${item.src} alt=${item.name} /></div>
|
|
101
|
+
${many ? html`<button type="button" class="icon-btn lb-nav lb-prev" aria-label=${t("chat.lbPrev")} title=${t("chat.lbPrev")} onClick=${() => step(-1)}><${Icon} name="back" /></button>
|
|
102
|
+
<button type="button" class="icon-btn lb-nav lb-next" aria-label=${t("chat.lbNext")} title=${t("chat.lbNext")} onClick=${() => step(1)}><${Icon} name="forward" /></button>` : null}
|
|
103
|
+
</dialog>`;
|
|
104
|
+
}
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -11,10 +11,13 @@ import { t } from "/assets/app-i18n.js";
|
|
|
11
11
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
12
12
|
import { useLease } from "/assets/app-ctx.js";
|
|
13
13
|
import { PanelHeader, onPanelKey, setTitle, addFooterItem, statusClass, statusLabel, openDrawer, requestNewInstance } from "/assets/app-shell.js";
|
|
14
|
+
import { detailsPath } from "/assets/app-route.js";
|
|
15
|
+
import { InstanceSwitch } from "/assets/instance-switch.js";
|
|
16
|
+
import { HeaderTools } from "/assets/header-tools.js";
|
|
14
17
|
import { navigate } from "/assets/app-nav.js";
|
|
15
18
|
import { Menu } from "/assets/ui-menu.js";
|
|
16
19
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
17
|
-
import { Empty,
|
|
20
|
+
import { Empty, Skeleton } from "/assets/ui-states.js";
|
|
18
21
|
import { Icon } from "/assets/ui-icons.js";
|
|
19
22
|
import { toast } from "/assets/ui-toast.js";
|
|
20
23
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
@@ -26,6 +29,7 @@ import { PreviewPanel, openPanel, panelStore, shownKey } from "./preview-panel.j
|
|
|
26
29
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
27
30
|
import { installTour, refreshTourSpot, startTour } from "./chat-tour.js";
|
|
28
31
|
import { FirstRunCard } from "./first-run.js";
|
|
32
|
+
import { LightboxHost, openLightbox } from "./image-lightbox.js";
|
|
29
33
|
|
|
30
34
|
const R = () => globalThis.AgendChatRender;
|
|
31
35
|
const P = () => globalThis.AgendPreview;
|
|
@@ -64,7 +68,8 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
64
68
|
store.attach(stream);
|
|
65
69
|
// #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
|
|
66
70
|
// belong to the app, once (they are never per mount).
|
|
67
|
-
|
|
71
|
+
// #1554: its words in the page's language (chat.pv*); a key the dictionary does not have keeps preview.js's English.
|
|
72
|
+
if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
|
|
68
73
|
addFooterItem("previews", PreviewOptIn);
|
|
69
74
|
addFooterItem("tour", TourButton);
|
|
70
75
|
installTour();
|
|
@@ -139,7 +144,7 @@ export function ChatPanel({ route, navKey }) {
|
|
|
139
144
|
}
|
|
140
145
|
if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
|
|
141
146
|
if (!inst) return html`<${NotFound} name=${name} />`;
|
|
142
|
-
return html`<${ChatView} key=${name} name=${name} inst=${inst} lease=${lease} exec=${app.exec[name]}
|
|
147
|
+
return html`<${ChatView} key=${name} name=${name} inst=${inst} lease=${lease} navKey=${navKey} exec=${app.exec[name]}
|
|
143
148
|
awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, name) ? app.awaiting[name] : null} />`;
|
|
144
149
|
}
|
|
145
150
|
|
|
@@ -151,8 +156,8 @@ function NotFound({ name }) {
|
|
|
151
156
|
action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
|
|
152
157
|
}
|
|
153
158
|
|
|
154
|
-
function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
155
|
-
const [dialog, setDialog] = useState(null); // "
|
|
159
|
+
function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
160
|
+
const [dialog, setDialog] = useState(null); // "delete" | null
|
|
156
161
|
const [wrap, setWrap] = useState(codeWrap);
|
|
157
162
|
const view = useRef(null), split = useRef(null), thread = useRef(null);
|
|
158
163
|
useEffect(() => { wrapListeners.add(setWrap); return () => wrapListeners.delete(setWrap); }, []);
|
|
@@ -190,7 +195,8 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
190
195
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
191
196
|
};
|
|
192
197
|
const items = [
|
|
193
|
-
|
|
198
|
+
// #1523 N2: Details is a page now (the Fleet side of this instance), not a dialog.
|
|
199
|
+
{ key: "details", label: t("chat.details"), icon: "info", onSelect: () => navigate(detailsPath(name)) },
|
|
194
200
|
running ? null : { key: "start", label: t("chat.start"), icon: "play", onSelect: () => action("start") },
|
|
195
201
|
running ? { key: "restart", label: t("chat.restart"), icon: "restart", onSelect: () => action("restart") } : null,
|
|
196
202
|
running ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", onSelect: () => action("stop") } : null,
|
|
@@ -203,17 +209,17 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
203
209
|
${inst.model ? html`<button type="button" class="hd-chip" title=${t("chat.chipModel", inst.model)} aria-label=${t("chat.chipModel", inst.model)} onClick=${() => pick("model")}>${inst.model}</button>` : null}
|
|
204
210
|
${inst.effort ? html`<button type="button" class="hd-chip" title=${t("chat.chipEffort", inst.effort)} aria-label=${t("chat.chipEffort", inst.effort)} onClick=${() => pick("effort")}>${inst.effort}</button>` : null}`;
|
|
205
211
|
return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
|
|
206
|
-
<${PanelHeader} title=${name} sub=${sub}><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
|
|
212
|
+
<${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
|
|
207
213
|
<div class="chat-split" ref=${split}>
|
|
208
214
|
<div class="chat-main">
|
|
209
215
|
<${Thread} name=${name} th=${thread} />
|
|
210
216
|
<${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
|
|
211
217
|
</div>
|
|
212
218
|
<${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
|
|
213
|
-
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} />
|
|
219
|
+
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
|
|
214
220
|
</div>
|
|
221
|
+
<${LightboxHost} />
|
|
215
222
|
<div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
|
|
216
|
-
${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
|
|
217
223
|
${dialog === "delete" ? html`<${DeleteDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
|
|
218
224
|
</div>`;
|
|
219
225
|
}
|
|
@@ -230,6 +236,7 @@ function Thread({ name, th }) {
|
|
|
230
236
|
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
231
237
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
232
238
|
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
239
|
+
openImage: openLightbox, // an image, full size, in the page
|
|
233
240
|
});
|
|
234
241
|
th.current = thread;
|
|
235
242
|
thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
|
|
@@ -429,51 +436,6 @@ function FileChip({ f, i, name }) {
|
|
|
429
436
|
|
|
430
437
|
// ── Dialogs ──
|
|
431
438
|
|
|
432
|
-
function DetailsDialog({ name, onClose }) {
|
|
433
|
-
const lease = useLease(`details:${name}`);
|
|
434
|
-
const [d, setD] = useState(null);
|
|
435
|
-
const [failed, setFailed] = useState(false);
|
|
436
|
-
const body = useRef(null);
|
|
437
|
-
useEffect(() => {
|
|
438
|
-
(async () => {
|
|
439
|
-
try {
|
|
440
|
-
const r = await lease.fetch(`/ui/instance/${encodeURIComponent(name)}`, { headers: { "Content-Type": "application/json" } });
|
|
441
|
-
if (!lease.current()) return;
|
|
442
|
-
const j = await r.json();
|
|
443
|
-
if (!lease.current()) return;
|
|
444
|
-
setD(j);
|
|
445
|
-
} catch { if (lease.current()) setFailed(true); }
|
|
446
|
-
})();
|
|
447
|
-
}, [lease]);
|
|
448
|
-
// Bars are sized after they are drawn, through the CSSOM (never a style attribute).
|
|
449
|
-
useLayoutEffect(() => { if (body.current) for (const f of body.current.querySelectorAll(".progress-fill[data-pct]")) f.style.width = `${f.dataset.pct}%`; });
|
|
450
|
-
const src = (s) => (s === "instance" ? ` ${t("chat.configured")}` : s === "fleet-default" ? ` ${t("chat.fleetDefault")}` : "");
|
|
451
|
-
const bar = (pct, level) => html`<span class="progress-bar"><span class=${`progress-fill${level ? ` ${level}` : ""}`} data-pct=${Math.max(0, Math.min(Number(pct) || 0, 100))}></span></span>`;
|
|
452
|
-
const row = (k, v) => html`<div class="kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
|
|
453
|
-
let content;
|
|
454
|
-
if (failed) content = html`<${ErrorState} message=${t("chat.loadFailed")} />`;
|
|
455
|
-
else if (!d) content = html`<${Skeleton} lines=${6} />`;
|
|
456
|
-
else {
|
|
457
|
-
const sl = d.statusline || {}, cost = sl.cost?.total_cost_usd ?? 0, ctx = d.context_pct;
|
|
458
|
-
const rl5 = sl.rate_limits?.five_hour?.used_percentage ?? 0, rl7 = sl.rate_limits?.seven_day?.used_percentage ?? 0;
|
|
459
|
-
content = html`<section class="card"><h3>${t("chat.dInstance")}</h3>
|
|
460
|
-
${row(t("chat.dName"), d.name)}${row(t("chat.dStatus"), d.status)}${row(t("chat.dDisplay"), d.display_name || "--")}
|
|
461
|
-
${row(t("chat.dDescription"), d.description || "--")}${row(t("chat.dDirectory"), html`<span class="mono">${d.working_directory}</span>`)}</section>
|
|
462
|
-
<section class="card"><h3>${t("chat.dRuntime")}</h3>
|
|
463
|
-
${row(t("chat.dBackend"), d.backend || "--")}
|
|
464
|
-
${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
|
|
465
|
-
${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
|
|
466
|
-
${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
|
|
467
|
-
${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
|
|
468
|
-
${row(t("chat.dRate5"), html`<span class="pct">${Math.round(rl5)}%</span>${bar(rl5, rl5 > 90 ? "error" : rl5 > 70 ? "warn" : "")}`)}
|
|
469
|
-
${row(t("chat.dRate7"), html`<span class="pct">${Math.round(rl7)}%</span>${bar(rl7, rl7 > 90 ? "error" : rl7 > 70 ? "warn" : "")}`)}</section>
|
|
470
|
-
${d.recent_activity?.length ? html`<section class="card"><h3>${t("chat.dActivity")}</h3>${d.recent_activity.map((a, i) => html`<div key=${i} class="activity">
|
|
471
|
-
<span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
|
|
472
|
-
<span class="ev">${a.event}</span> ${a.summary || ""}</div>`)}</section>` : null}`;
|
|
473
|
-
}
|
|
474
|
-
return html`<${Dialog} title=${t("chat.details")} onClose=${onClose} wide=${true}><div ref=${body}>${content}</div></${Dialog}>`;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
439
|
function DeleteDialog({ name, onClose }) {
|
|
478
440
|
// The dialog's own lease: it ends when the dialog goes (closed, or the chat left for another page). The delete
|
|
479
441
|
// may still finish on the server, and says so, but a dialog that is gone never closes or navigates the page the
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// #1523 N2 (§3.3, Q1 = B): Details — "the Fleet side of one instance", at /ui/fleet/agent/<name>. Behind the session
|
|
2
|
+
// gate (/ui/js/). What it holds:
|
|
3
|
+
// - runtime and recent activity (what Chat's details dialog showed, now a page);
|
|
4
|
+
// - a read-only config summary — directory, binding, tags, description — with "Edit in Settings" (that agent's
|
|
5
|
+
// Settings dialog: Settings' own staged Apply and confirmations; nothing is edited here);
|
|
6
|
+
// - the instance's actions (⋯), carried over with their calls and confirmations, none new: start / restart / stop (Chat's
|
|
7
|
+
// ⋯), pause / wake (Settings' agent row). Deletion stays where it already lives (Chat's ⋯, Settings).
|
|
8
|
+
// One read of /ui/instance/<name> when it opens (#1374: nothing recurring); the live status is the app's.
|
|
9
|
+
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
10
|
+
import { t } from "/assets/app-i18n.js";
|
|
11
|
+
import { appStore, useStore } from "/assets/app-store.js";
|
|
12
|
+
import { useLease } from "/assets/app-ctx.js";
|
|
13
|
+
import { PanelHeader, setTitle, statusClass, statusLabel, openDrawer } from "/assets/app-shell.js";
|
|
14
|
+
import { navigate } from "/assets/app-nav.js";
|
|
15
|
+
import { settingsPath } from "/assets/app-route.js";
|
|
16
|
+
import { InstanceSwitch } from "/assets/instance-switch.js";
|
|
17
|
+
import { HeaderTools } from "/assets/header-tools.js";
|
|
18
|
+
import { Menu } from "/assets/ui-menu.js";
|
|
19
|
+
import { Empty, ErrorState, Skeleton } from "/assets/ui-states.js";
|
|
20
|
+
import { toast } from "/assets/ui-toast.js";
|
|
21
|
+
import "./chat-strings.js";
|
|
22
|
+
import { api } from "./settings-confirm.js";
|
|
23
|
+
import { requestAgentSettings } from "./settings-request.js";
|
|
24
|
+
|
|
25
|
+
export function DetailsPanel({ route, navKey }) {
|
|
26
|
+
const app = useStore(appStore);
|
|
27
|
+
const name = route.instance;
|
|
28
|
+
const inst = app.instances.find((i) => i.name === name) || null;
|
|
29
|
+
useEffect(() => { setTitle(`${name} · ${t("app.details")}`); }, [name, navKey]);
|
|
30
|
+
if (!app.ready) return html`<div class="panel p-details"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${6} /></div></div>`;
|
|
31
|
+
if (!inst) {
|
|
32
|
+
return html`<div class="panel p-details"><${PanelHeader} title=${name} />
|
|
33
|
+
<div class="panel-body center"><${Empty} icon="alert" title=${t("chat.notFound", name)} hint=${t("chat.notFoundHint")}
|
|
34
|
+
action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
|
|
35
|
+
}
|
|
36
|
+
return html`<${DetailsView} key=${name} name=${name} inst=${inst} navKey=${navKey} exec=${app.exec[name]}
|
|
37
|
+
awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, name) ? app.awaiting[name] : null} />`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function DetailsView({ name, inst, navKey, exec, awaiting }) {
|
|
41
|
+
const lease = useLease(`details:${name}`);
|
|
42
|
+
const [d, setD] = useState(null);
|
|
43
|
+
const [failed, setFailed] = useState(false);
|
|
44
|
+
const [busy, setBusy] = useState("");
|
|
45
|
+
const body = useRef(null);
|
|
46
|
+
// Each read is numbered: only the latest one (on this page) may set what is shown — a Retry still on its way never
|
|
47
|
+
// overwrites a newer answer, failed or not (#1561 review).
|
|
48
|
+
const reads = useRef(0);
|
|
49
|
+
const load = async () => {
|
|
50
|
+
const mine = ++reads.current;
|
|
51
|
+
const current = () => lease.current() && reads.current === mine;
|
|
52
|
+
try {
|
|
53
|
+
const r = await lease.fetch(`/ui/instance/${encodeURIComponent(name)}`, { headers: { "Content-Type": "application/json" } });
|
|
54
|
+
if (!current()) return;
|
|
55
|
+
const j = await r.json();
|
|
56
|
+
if (!current()) return;
|
|
57
|
+
if (!r.ok) { setFailed(true); return; }
|
|
58
|
+
setFailed(false); setD(j);
|
|
59
|
+
} catch { if (current()) setFailed(true); }
|
|
60
|
+
};
|
|
61
|
+
useEffect(() => { load(); }, [lease]);
|
|
62
|
+
// Bars are sized after they are drawn, through the CSSOM (never a style attribute, #1300).
|
|
63
|
+
useLayoutEffect(() => { if (body.current) for (const f of body.current.querySelectorAll(".progress-fill[data-pct]")) f.style.width = `${f.dataset.pct}%`; });
|
|
64
|
+
|
|
65
|
+
// ── Actions: the existing calls, one at a time ──
|
|
66
|
+
const act = async (key, work) => {
|
|
67
|
+
if (busy) return;
|
|
68
|
+
setBusy(key);
|
|
69
|
+
try { await work(); } finally { if (lease.current()) setBusy(""); }
|
|
70
|
+
};
|
|
71
|
+
const lifecycle = (verb) => act(verb, async () => {
|
|
72
|
+
let r;
|
|
73
|
+
try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
|
|
74
|
+
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
75
|
+
if (r && r.error) toast(r.error, false);
|
|
76
|
+
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
77
|
+
});
|
|
78
|
+
const pauseWake = (verb) => act(verb, async () => {
|
|
79
|
+
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${verb}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
|
|
80
|
+
if (!res.ok) toast((res.body && res.body.error) || t("chat.dActionFailed"), false);
|
|
81
|
+
else toast(t(verb === "pause" ? "chat.dPaused" : "chat.dWoken", name));
|
|
82
|
+
});
|
|
83
|
+
// A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'
|
|
84
|
+
// Classic rows have (pause / wake) — never the fleet agent's (#1561 review).
|
|
85
|
+
const classic = !!(d && d.kind === "classic");
|
|
86
|
+
const editInSettings = () => { requestAgentSettings(name, classic ? "classic" : "agent"); navigate(settingsPath(classic ? "classic" : undefined)); };
|
|
87
|
+
const st = inst.status;
|
|
88
|
+
// Until its read is in, the page does not know which kind it is: no actions yet (only Edit is offered once known).
|
|
89
|
+
const items = !d ? [] : classic ? [
|
|
90
|
+
{ key: "edit", label: t("chat.dEditInSettings"), icon: "settings", onSelect: editInSettings },
|
|
91
|
+
st === "running" ? { key: "pause", label: t("chat.dPause"), icon: "pause", disabled: !!busy, onSelect: () => pauseWake("pause") } : null,
|
|
92
|
+
st === "paused" ? { key: "wake", label: t("chat.dWake"), icon: "play", disabled: !!busy, onSelect: () => pauseWake("wake") } : null,
|
|
93
|
+
] : [
|
|
94
|
+
{ key: "edit", label: t("chat.dEditInSettings"), icon: "settings", onSelect: editInSettings },
|
|
95
|
+
st === "running" ? null : st === "paused" ? null : { key: "start", label: t("chat.start"), icon: "play", disabled: !!busy, onSelect: () => lifecycle("start") },
|
|
96
|
+
st === "running" ? { key: "restart", label: t("chat.restart"), icon: "restart", disabled: !!busy, onSelect: () => lifecycle("restart") } : null,
|
|
97
|
+
st === "running" ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", disabled: !!busy, onSelect: () => lifecycle("stop") } : null,
|
|
98
|
+
st === "running" ? { key: "pause", label: t("chat.dPause"), icon: "pause", disabled: !!busy, onSelect: () => pauseWake("pause") } : null,
|
|
99
|
+
st === "paused" ? { key: "wake", label: t("chat.dWake"), icon: "play", disabled: !!busy, onSelect: () => pauseWake("wake") } : null,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const cls = statusClass(inst, exec, awaiting);
|
|
103
|
+
const sub = html`<span class="status"><span class=${`dot ${cls}`} aria-hidden="true"></span>${statusLabel(inst, exec, awaiting)}</span>`;
|
|
104
|
+
const src = (s) => (s === "instance" ? ` ${t("chat.configured")}` : s === "fleet-default" ? ` ${t("chat.fleetDefault")}` : "");
|
|
105
|
+
const bar = (pct, level) => html`<span class="progress-bar"><span class=${`progress-fill${level ? ` ${level}` : ""}`} data-pct=${Math.max(0, Math.min(Number(pct) || 0, 100))}></span></span>`;
|
|
106
|
+
const row = (k, v) => html`<div class="kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
|
|
107
|
+
let content;
|
|
108
|
+
if (failed) content = html`<${ErrorState} message=${t("chat.loadFailed")} onRetry=${load} />`;
|
|
109
|
+
else if (!d) content = html`<${Skeleton} lines=${6} />`;
|
|
110
|
+
else {
|
|
111
|
+
const sl = d.statusline || {}, cost = sl.cost?.total_cost_usd ?? 0, ctx = d.context_pct;
|
|
112
|
+
const rl5 = sl.rate_limits?.five_hour?.used_percentage ?? 0, rl7 = sl.rate_limits?.seven_day?.used_percentage ?? 0;
|
|
113
|
+
// Where it is bound, as the fleet resolves it: no channel_id is the first connection (said so); "web only" only
|
|
114
|
+
// when the fleet has no connection at all (#1561 review).
|
|
115
|
+
const b = d.binding || {};
|
|
116
|
+
const conn = b.channel_id ? (b.implicit ? t("chat.dBindingDefault", b.channel_id) : b.channel_id) : null;
|
|
117
|
+
const binding = d.room ? t("chat.dBindingRoom", d.room.name, d.room.adapter_id || t("chat.dPrimary"))
|
|
118
|
+
: !conn ? t("chat.dBindingNone")
|
|
119
|
+
: b.general_topic ? t("chat.dBindingGeneral", conn)
|
|
120
|
+
: b.topic_id ? t("chat.dBindingTopic", conn, b.topic_id)
|
|
121
|
+
: t("chat.dBindingNoTopic", conn);
|
|
122
|
+
content = html`<section class="card d-config"><h3>${t("chat.dConfig")}</h3>
|
|
123
|
+
${row(t("chat.dDisplay"), d.display_name || "--")}
|
|
124
|
+
${row(t("chat.dDescription"), d.description || "--")}
|
|
125
|
+
${d.working_directory ? row(t("chat.dDirectory"), html`<span class="mono">${d.working_directory}</span>`) : null}
|
|
126
|
+
${row(t("chat.dBinding"), binding)}
|
|
127
|
+
${row(t("chat.dTags"), Array.isArray(d.tags) && d.tags.length ? html`<span class="d-tags">${d.tags.map((x) => html`<span key=${x} class="tag">${x}</span>`)}</span>` : "--")}
|
|
128
|
+
<div class="d-edit"><span class="note">${t("chat.dReadOnly")}</span>
|
|
129
|
+
<button type="button" class="btn btn-sm d-edit-settings" onClick=${editInSettings}>${t("chat.dEditInSettings")}</button></div></section>
|
|
130
|
+
<section class="card"><h3>${t("chat.dRuntime")}</h3>
|
|
131
|
+
${row(t("chat.dBackend"), d.backend || "--")}
|
|
132
|
+
${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
|
|
133
|
+
${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
|
|
134
|
+
${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
|
|
135
|
+
${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
|
|
136
|
+
${row(t("chat.dRate5"), html`<span class="pct">${Math.round(rl5)}%</span>${bar(rl5, rl5 > 90 ? "error" : rl5 > 70 ? "warn" : "")}`)}
|
|
137
|
+
${row(t("chat.dRate7"), html`<span class="pct">${Math.round(rl7)}%</span>${bar(rl7, rl7 > 90 ? "error" : rl7 > 70 ? "warn" : "")}`)}</section>
|
|
138
|
+
<section class="card"><h3>${t("chat.dActivity")}</h3>${d.recent_activity?.length ? d.recent_activity.map((a, i) => html`<div key=${i} class="activity">
|
|
139
|
+
<span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
|
|
140
|
+
<span class="ev">${a.event}</span> ${a.summary || ""}</div>`) : html`<p class="note">${t("chat.dNoActivity")}</p>`}</section>`;
|
|
141
|
+
}
|
|
142
|
+
return html`<div class="panel p-details">
|
|
143
|
+
<${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="details" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
|
|
144
|
+
<div class="panel-body"><div class="col" ref=${body}>${busy ? html`<p class="note" role="status">${t("chat.dWorking")}</p>` : null}${content}</div></div>
|
|
145
|
+
</div>`;
|
|
146
|
+
}
|
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -9,6 +9,7 @@ import { t, register } from "/assets/app-i18n.js";
|
|
|
9
9
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
10
10
|
import { useLease } from "/assets/app-ctx.js";
|
|
11
11
|
import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
12
|
+
import { HeaderTools } from "/assets/header-tools.js";
|
|
12
13
|
import { renavigate } from "/assets/app-nav.js";
|
|
13
14
|
import { chatPath, fleetPath, FLEET_TABS } from "/assets/app-route.js";
|
|
14
15
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
@@ -104,7 +105,7 @@ export function FleetPanel({ route, navKey }) {
|
|
|
104
105
|
useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
|
|
105
106
|
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
|
|
106
107
|
return html`<div class="panel p-fleet">
|
|
107
|
-
<${PanelHeader} title=${t("fleet.title")}
|
|
108
|
+
<${PanelHeader} title=${t("fleet.title")}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
|
|
108
109
|
<nav class="seg" aria-label=${t("fleet.title")}>${FLEET_TABS.map(k => html`<a key=${k} href=${fleetPath(k)} class=${`seg-item${k === tab ? " active" : ""}`}
|
|
109
110
|
aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
|
|
110
111
|
<div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
import { SetupWizard } from "./settings-wizard.js";
|
|
37
37
|
import { CreateInstanceDialog } from "./panel-fleet.js";
|
|
38
38
|
import { FirstRunCard, takeWizardRequest } from "./first-run.js";
|
|
39
|
+
import { takeAgentRequest } from "./settings-request.js";
|
|
39
40
|
|
|
40
41
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
41
42
|
const ICONS = { agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
|
|
@@ -200,6 +201,23 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
200
201
|
const ctx = data && !data.error ? makeCtx(data, setData, stage, unstage, reload, reloadLive, () => mount.current()) : null;
|
|
201
202
|
// #1519 P7: "Connect a chat app" on the first-run card elsewhere came here for the wizard: it opens once the data is in.
|
|
202
203
|
useEffect(() => { if (ctx && takeWizardRequest()) setDialog({ kind: "wizard", key: navKey }); }, [!!ctx, navKey]);
|
|
204
|
+
// #1523 N2: "Edit in Settings" on an instance's Details: that agent's dialog, once the data is in — only an agent this
|
|
205
|
+
// configuration has (own key), with Settings' own staged Apply and confirmations.
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (!ctx) return;
|
|
208
|
+
const want = takeAgentRequest();
|
|
209
|
+
if (!want) return;
|
|
210
|
+
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
211
|
+
if (want.kind === "classic") {
|
|
212
|
+
// A ClassicBot room: its own dialog (ClassicDialog), found by its instance among this config's rooms.
|
|
213
|
+
const rooms = (ctx.classic && ctx.classic.channels) || {};
|
|
214
|
+
const key = Object.keys(rooms).find((k) => own(rooms, k) && rooms[k] && rooms[k].instanceName === want.name);
|
|
215
|
+
if (key) setDialog({ kind: "classic", room: { key, ...rooms[key] }, key: navKey });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const insts = (ctx.fleet && ctx.fleet.instances) || {};
|
|
219
|
+
if (own(insts, want.name)) setDialog({ kind: "agent", name: want.name, inst: insts[want.name], key: navKey });
|
|
220
|
+
}, [!!ctx, navKey]);
|
|
203
221
|
const sectionBody = !data ? html`<${Skeleton} lines=${6} />`
|
|
204
222
|
: data.error ? html`<${ErrorState} message=${tn("configLoadFailed")} onRetry=${reload} />`
|
|
205
223
|
: section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -105,7 +105,8 @@ const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digi
|
|
|
105
105
|
* props: name (the instance), msgs() → its messages, subscribe(fn) → unsubscribe (the chat store's), keyOf(x),
|
|
106
106
|
* reveal(msgKey) (the thread shows that message), download(code).
|
|
107
107
|
*/
|
|
108
|
-
|
|
108
|
+
/** allow(): this device's opt-in through the chat's one path (setPreviewOptIn: the confirm, then setOptIn) → the choice. */
|
|
109
|
+
export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, download, allow }) {
|
|
109
110
|
const [st, setSt] = useState(state);
|
|
110
111
|
const [view, setView] = useState("preview");
|
|
111
112
|
const [run, setRun] = useState({ state: "idle", reason: "" });
|
|
@@ -203,7 +204,11 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
203
204
|
if (!open) return null;
|
|
204
205
|
const going = run.state === "starting" || run.state === "running";
|
|
205
206
|
const a = P().availability();
|
|
206
|
-
|
|
207
|
+
// #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm); Preview
|
|
208
|
+
// is then enabled — still a click. Why a run stopped (e.g. switched off in another tab) is still said first.
|
|
209
|
+
const askOptIn = !going && !a.ok && a.why === "optin" && typeof allow === "function";
|
|
210
|
+
const note = run.reason || (going ? (run.state === "starting" ? t("chat.pvStarting") : "") : a.ok ? t("chat.pvPanelIdle") : askOptIn ? t("chat.pvWhyOptinPanel") : a.reason);
|
|
211
|
+
const allowHere = async () => { if (await allow()) setRun({ state: "idle", reason: "" }); };
|
|
207
212
|
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
208
213
|
return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
209
214
|
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
@@ -227,8 +232,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
227
232
|
</div>
|
|
228
233
|
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
229
234
|
<button type="button" class="btn btn-sm pv-newer-show" onClick=${showNewer}>${t("chat.pvNewerShow")}</button>${newerNote ? html`<span class="pv-newer-note">${newerNote}</span>` : null}</div>` : null}
|
|
230
|
-
${going ? html`<div class="pv-banner">${P().
|
|
231
|
-
${note && view === "preview" ? html`<div class="pv-note">${note}</div>` : null}
|
|
235
|
+
${going ? html`<div class="pv-banner">${P().banner()}</div>` : null}
|
|
236
|
+
${note && view === "preview" ? html`<div class="pv-note">${note}${askOptIn ? html` <button type="button" class="btn btn-sm pv-panel-allow" onClick=${allowHere}>${t("chat.pvAllowButton")}</button>` : null}</div>` : null}
|
|
232
237
|
<div class=${`pv-panel-body${view === "code" ? " show-code" : ""}`}>
|
|
233
238
|
<div class="pv-holder" ref=${holder}></div>
|
|
234
239
|
${view === "code" ? html`<pre class="pv-code"><code>${open.code}</code></pre>` : null}
|