@songsid/agend 2.2.0-alpha.4 → 2.2.0-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth-api.d.ts +22 -0
- package/dist/auth-api.js +69 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/channel/adapters/discord.js +9 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/command-table.d.ts +8 -1
- package/dist/command-table.js +13 -4
- package/dist/command-table.js.map +1 -1
- package/dist/fleet-manager.d.ts +47 -0
- package/dist/fleet-manager.js +139 -5
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +10 -0
- package/dist/locale.js.map +1 -1
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/topic-commands.js +3 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-render.js +6 -0
- package/dist/ui/chat-strings.js +17 -0
- package/dist/ui/chat-thread.js +22 -3
- package/dist/ui/image-lightbox.js +104 -0
- package/dist/ui/panel-chat.js +20 -52
- 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 +2 -1
- package/dist/ui/preview.js +49 -0
- package/dist/ui/settings-request.js +9 -0
- package/dist/ui/shared/agend-auth.js +15 -7
- 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 +51 -1
- 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/signin.js +38 -1
- 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 +29 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +97 -6
- package/dist/web-chat-history.js +189 -12
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-chat-store.d.ts +45 -0
- package/dist/web-chat-store.js +192 -0
- package/dist/web-chat-store.js.map +1 -0
- package/dist/web-login.d.ts +2 -0
- package/dist/web-login.js +4 -0
- package/dist/web-login.js.map +1 -1
- package/dist/web-returning-device.d.ts +84 -0
- package/dist/web-returning-device.js +204 -0
- package/dist/web-returning-device.js.map +1 -0
- 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/dist/web-upload.d.ts +21 -0
- package/dist/web-upload.js +50 -1
- package/dist/web-upload.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
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;
|
|
@@ -66,6 +70,13 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
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.
|
|
68
72
|
if (P() && bootData) P().init(bootData, { text: (key, ...v) => { const k = `chat.${key}`, s = t(k, ...v); return s === k ? null : s; } });
|
|
73
|
+
// #1568: the banner's "I understand" lasts until the next sign-in — preview.js is told which sign-in this page is
|
|
74
|
+
// (the session handle, from the one /auth/session read); none (no session, or no AgendAuth): this tab only.
|
|
75
|
+
if (P() && typeof P().setBannerScope === "function") {
|
|
76
|
+
const auth = globalThis.AgendAuth;
|
|
77
|
+
if (auth && typeof auth.sessionHandle === "function") auth.sessionHandle().then((h) => P().setBannerScope(h), () => P().setBannerScope(null));
|
|
78
|
+
else P().setBannerScope(null);
|
|
79
|
+
}
|
|
69
80
|
addFooterItem("previews", PreviewOptIn);
|
|
70
81
|
addFooterItem("tour", TourButton);
|
|
71
82
|
installTour();
|
|
@@ -140,7 +151,7 @@ export function ChatPanel({ route, navKey }) {
|
|
|
140
151
|
}
|
|
141
152
|
if (!app.ready) return html`<div class="panel p-chat"><${PanelHeader} title=${name} /><div class="panel-body"><${Skeleton} lines=${5} /></div></div>`;
|
|
142
153
|
if (!inst) return html`<${NotFound} name=${name} />`;
|
|
143
|
-
return html`<${ChatView} key=${name} name=${name} inst=${inst} lease=${lease} exec=${app.exec[name]}
|
|
154
|
+
return html`<${ChatView} key=${name} name=${name} inst=${inst} lease=${lease} navKey=${navKey} exec=${app.exec[name]}
|
|
144
155
|
awaiting=${Object.prototype.hasOwnProperty.call(app.awaiting, name) ? app.awaiting[name] : null} />`;
|
|
145
156
|
}
|
|
146
157
|
|
|
@@ -152,8 +163,8 @@ function NotFound({ name }) {
|
|
|
152
163
|
action=${html`<button type="button" class="btn only-narrow" onClick=${openDrawer}>${t("chat.backToList")}</button>`} /></div></div>`;
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
156
|
-
const [dialog, setDialog] = useState(null); // "
|
|
166
|
+
function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
167
|
+
const [dialog, setDialog] = useState(null); // "delete" | null
|
|
157
168
|
const [wrap, setWrap] = useState(codeWrap);
|
|
158
169
|
const view = useRef(null), split = useRef(null), thread = useRef(null);
|
|
159
170
|
useEffect(() => { wrapListeners.add(setWrap); return () => wrapListeners.delete(setWrap); }, []);
|
|
@@ -191,7 +202,8 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
191
202
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
192
203
|
};
|
|
193
204
|
const items = [
|
|
194
|
-
|
|
205
|
+
// #1523 N2: Details is a page now (the Fleet side of this instance), not a dialog.
|
|
206
|
+
{ key: "details", label: t("chat.details"), icon: "info", onSelect: () => navigate(detailsPath(name)) },
|
|
195
207
|
running ? null : { key: "start", label: t("chat.start"), icon: "play", onSelect: () => action("start") },
|
|
196
208
|
running ? { key: "restart", label: t("chat.restart"), icon: "restart", onSelect: () => action("restart") } : null,
|
|
197
209
|
running ? { key: "stop", label: t("chat.stopInstance"), icon: "stop", onSelect: () => action("stop") } : null,
|
|
@@ -204,7 +216,7 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
204
216
|
${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}
|
|
205
217
|
${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}`;
|
|
206
218
|
return html`<div class=${`panel p-chat${wrap ? " wrap-code" : ""}`} ref=${view}>
|
|
207
|
-
<${PanelHeader} title=${name} sub=${sub}><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
|
|
219
|
+
<${PanelHeader} title=${name} sub=${sub} nav=${html`<${InstanceSwitch} name=${name} current="chat" />`}><${HeaderTools} navKey=${navKey} /><${Menu} items=${items} label=${t("app.more")} /></${PanelHeader}>
|
|
208
220
|
<div class="chat-split" ref=${split}>
|
|
209
221
|
<div class="chat-main">
|
|
210
222
|
<${Thread} name=${name} th=${thread} />
|
|
@@ -213,8 +225,8 @@ function ChatView({ name, inst, lease, exec, awaiting }) {
|
|
|
213
225
|
<${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
|
|
214
226
|
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
|
|
215
227
|
</div>
|
|
228
|
+
<${LightboxHost} />
|
|
216
229
|
<div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
|
|
217
|
-
${dialog === "details" ? html`<${DetailsDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
|
|
218
230
|
${dialog === "delete" ? html`<${DeleteDialog} name=${name} onClose=${() => setDialog(null)} />` : null}
|
|
219
231
|
</div>`;
|
|
220
232
|
}
|
|
@@ -231,6 +243,7 @@ function Thread({ name, th }) {
|
|
|
231
243
|
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
232
244
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
233
245
|
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
246
|
+
openImage: openLightbox, // an image, full size, in the page
|
|
234
247
|
});
|
|
235
248
|
th.current = thread;
|
|
236
249
|
thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
|
|
@@ -430,51 +443,6 @@ function FileChip({ f, i, name }) {
|
|
|
430
443
|
|
|
431
444
|
// ── Dialogs ──
|
|
432
445
|
|
|
433
|
-
function DetailsDialog({ name, onClose }) {
|
|
434
|
-
const lease = useLease(`details:${name}`);
|
|
435
|
-
const [d, setD] = useState(null);
|
|
436
|
-
const [failed, setFailed] = useState(false);
|
|
437
|
-
const body = useRef(null);
|
|
438
|
-
useEffect(() => {
|
|
439
|
-
(async () => {
|
|
440
|
-
try {
|
|
441
|
-
const r = await lease.fetch(`/ui/instance/${encodeURIComponent(name)}`, { headers: { "Content-Type": "application/json" } });
|
|
442
|
-
if (!lease.current()) return;
|
|
443
|
-
const j = await r.json();
|
|
444
|
-
if (!lease.current()) return;
|
|
445
|
-
setD(j);
|
|
446
|
-
} catch { if (lease.current()) setFailed(true); }
|
|
447
|
-
})();
|
|
448
|
-
}, [lease]);
|
|
449
|
-
// Bars are sized after they are drawn, through the CSSOM (never a style attribute).
|
|
450
|
-
useLayoutEffect(() => { if (body.current) for (const f of body.current.querySelectorAll(".progress-fill[data-pct]")) f.style.width = `${f.dataset.pct}%`; });
|
|
451
|
-
const src = (s) => (s === "instance" ? ` ${t("chat.configured")}` : s === "fleet-default" ? ` ${t("chat.fleetDefault")}` : "");
|
|
452
|
-
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>`;
|
|
453
|
-
const row = (k, v) => html`<div class="kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
|
|
454
|
-
let content;
|
|
455
|
-
if (failed) content = html`<${ErrorState} message=${t("chat.loadFailed")} />`;
|
|
456
|
-
else if (!d) content = html`<${Skeleton} lines=${6} />`;
|
|
457
|
-
else {
|
|
458
|
-
const sl = d.statusline || {}, cost = sl.cost?.total_cost_usd ?? 0, ctx = d.context_pct;
|
|
459
|
-
const rl5 = sl.rate_limits?.five_hour?.used_percentage ?? 0, rl7 = sl.rate_limits?.seven_day?.used_percentage ?? 0;
|
|
460
|
-
content = html`<section class="card"><h3>${t("chat.dInstance")}</h3>
|
|
461
|
-
${row(t("chat.dName"), d.name)}${row(t("chat.dStatus"), d.status)}${row(t("chat.dDisplay"), d.display_name || "--")}
|
|
462
|
-
${row(t("chat.dDescription"), d.description || "--")}${row(t("chat.dDirectory"), html`<span class="mono">${d.working_directory}</span>`)}</section>
|
|
463
|
-
<section class="card"><h3>${t("chat.dRuntime")}</h3>
|
|
464
|
-
${row(t("chat.dBackend"), d.backend || "--")}
|
|
465
|
-
${row(t("chat.dModel"), `${d.model || sl.model?.display_name || "--"}${d.model_source === "live" ? "" : src(d.model_source)}`)}
|
|
466
|
-
${d.effort ? row(t("chat.dEffort"), `${d.effort}${src(d.effort_source)}`) : null}
|
|
467
|
-
${row(t("chat.dCost"), `$${cost.toFixed(2)} ${t("chat.dCostNote")}`)}
|
|
468
|
-
${row(t("chat.dContext"), html`<span class="pct">${ctx != null ? `${Math.round(ctx)}%` : "--"}</span>${ctx != null ? bar(ctx, ctx > 80 ? "warn" : "") : null}`)}
|
|
469
|
-
${row(t("chat.dRate5"), html`<span class="pct">${Math.round(rl5)}%</span>${bar(rl5, rl5 > 90 ? "error" : rl5 > 70 ? "warn" : "")}`)}
|
|
470
|
-
${row(t("chat.dRate7"), html`<span class="pct">${Math.round(rl7)}%</span>${bar(rl7, rl7 > 90 ? "error" : rl7 > 70 ? "warn" : "")}`)}</section>
|
|
471
|
-
${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">
|
|
472
|
-
<span class="mono muted">${a.timestamp ? new Date(`${a.timestamp.replace(" ", "T")}Z`).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""}</span>
|
|
473
|
-
<span class="ev">${a.event}</span> ${a.summary || ""}</div>`)}</section>` : null}`;
|
|
474
|
-
}
|
|
475
|
-
return html`<${Dialog} title=${t("chat.details")} onClose=${onClose} wide=${true}><div ref=${body}>${content}</div></${Dialog}>`;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
446
|
function DeleteDialog({ name, onClose }) {
|
|
479
447
|
// The dialog's own lease: it ends when the dialog goes (closed, or the chat left for another page). The delete
|
|
480
448
|
// 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
|
@@ -232,7 +232,8 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
232
232
|
</div>
|
|
233
233
|
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
234
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}
|
|
235
|
-
${going ? html`<div class="pv-banner">${P().banner()}</
|
|
235
|
+
${going && !P().bannerAcked() ? html`<div class="pv-banner"><span class="pv-banner-text">${P().banner()}</span>
|
|
236
|
+
<button type="button" class="btn btn-sm btn-ghost pv-ack" title=${P().bannerAckTitle()} onClick=${() => P().ackBanner()}>${P().bannerAckLabel()}</button></div>` : null}
|
|
236
237
|
${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}
|
|
237
238
|
<div class=${`pv-panel-body${view === "code" ? " show-code" : ""}`}>
|
|
238
239
|
<div class="pv-holder" ref=${holder}></div>
|
package/dist/ui/preview.js
CHANGED
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
var LIMITS = { maxBytes: 1024 * 1024, minHeight: 40, maxHeight: 4000, readyMs: 3000, watchdogMs: 10000, resizePerSec: 10, minDelta: 2, growthFreeze: 5, growthWindowMs: 2000 };
|
|
24
24
|
var OPT_IN_KEY = "agend_html_preview"; // localStorage: "on" — this device allows previews
|
|
25
25
|
var NEVER_KEY = "agend_html_preview_never"; // sessionStorage: "1" — never preview on this device (this session)
|
|
26
|
+
// #1568: the banner was acknowledged ("I understand"). localStorage: the sign-in's session handle — hidden while the
|
|
27
|
+
// page belongs to that sign-in; sessionStorage: "1" — no sign-in session known, this tab only.
|
|
28
|
+
var BANNER_ACK_KEY = "agend_html_preview_banner_ack";
|
|
26
29
|
var BANNER = "Previews run the agent's HTML in an isolated frame. It cannot use your login, but it may be able to send data out. Only preview content you trust. A preview can slow or freeze this tab.";
|
|
27
30
|
|
|
28
31
|
var cfg = { dashboardOrigin: "", previewOrigin: "", boot: "", reason: "", reasonCode: "" };
|
|
@@ -74,6 +77,8 @@
|
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
function onStorage(event) {
|
|
80
|
+
// #1568: the banner acknowledged in another tab of this sign-in: hide it here too (nothing starts or stops).
|
|
81
|
+
if (event && event.key === BANNER_ACK_KEY) { notify(); return; }
|
|
77
82
|
if (event && event.key !== OPT_IN_KEY && event.key !== null) return; // key null: storage was cleared
|
|
78
83
|
if (!optedIn()) stopAll("off");
|
|
79
84
|
notify();
|
|
@@ -106,6 +111,46 @@
|
|
|
106
111
|
notify();
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// ── #1568: the banner, acknowledged until the next sign-in ────────
|
|
115
|
+
|
|
116
|
+
// The page's sign-in, as the page found it out (AgendAuth): undefined — not known yet; null — no sign-in session (a
|
|
117
|
+
// loopback page without one, a session that ended, a failed read); otherwise the session's handle.
|
|
118
|
+
var bannerScope;
|
|
119
|
+
// Pressed on this page: { scope } — the sign-in it was pressed under (undefined: before the page knew). Holds for
|
|
120
|
+
// this page and that sign-in whatever storage does (blocked, full); null: not pressed here.
|
|
121
|
+
var pressed = null;
|
|
122
|
+
function validHandle(h) { return typeof h === "string" && /^[0-9a-f]{16}$/.test(h); }
|
|
123
|
+
/**
|
|
124
|
+
* Whether the banner was acknowledged for this page's sign-in: the handle stored is this session's (a new sign-in
|
|
125
|
+
* has a new handle, so it shows again); with no sign-in session, for this tab only. Not known yet: shown.
|
|
126
|
+
*/
|
|
127
|
+
function bannerAcked() {
|
|
128
|
+
if (pressed && (pressed.scope === undefined || pressed.scope === bannerScope)) return true;
|
|
129
|
+
if (bannerScope === undefined) return false;
|
|
130
|
+
if (bannerScope === null) { var t = store("sessionStorage"); try { return !!t && t.getItem(BANNER_ACK_KEY) === "1"; } catch (e) { return false; } }
|
|
131
|
+
var s = store("localStorage");
|
|
132
|
+
try { return !!s && s.getItem(BANNER_ACK_KEY) === bannerScope; } catch (e) { return false; }
|
|
133
|
+
}
|
|
134
|
+
function writeBannerAck() {
|
|
135
|
+
if (bannerScope === undefined) return;
|
|
136
|
+
var kind = bannerScope === null ? "sessionStorage" : "localStorage";
|
|
137
|
+
var s = store(kind);
|
|
138
|
+
try { if (s) s.setItem(BANNER_ACK_KEY, bannerScope === null ? "1" : bannerScope); } catch (e) { /* this page only */ }
|
|
139
|
+
}
|
|
140
|
+
/** "I understand": the banner is hidden until the next sign-in (or, with no sign-in session, for this tab). */
|
|
141
|
+
function ackBanner() {
|
|
142
|
+
pressed = { scope: bannerScope };
|
|
143
|
+
writeBannerAck();
|
|
144
|
+
notify();
|
|
145
|
+
}
|
|
146
|
+
/** The page's sign-in: the session handle, or null when there is no sign-in session. Set once the page knows. */
|
|
147
|
+
function setBannerScope(handle) {
|
|
148
|
+
bannerScope = validHandle(handle) ? handle : null;
|
|
149
|
+
// Pressed before the page knew its sign-in: it was this one — recorded for it now.
|
|
150
|
+
if (pressed && pressed.scope === undefined) { pressed = { scope: bannerScope }; writeBannerAck(); }
|
|
151
|
+
notify();
|
|
152
|
+
}
|
|
153
|
+
|
|
109
154
|
/**
|
|
110
155
|
* Whether this page may run a preview at all, and why not: the server must have chosen a preview origin for this
|
|
111
156
|
* load, the browser must really be at the origin the server believed (a proxy that rewrites Host would otherwise get
|
|
@@ -289,5 +334,9 @@
|
|
|
289
334
|
mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
|
|
290
335
|
/** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
|
|
291
336
|
banner: function () { return say("pvBanner", BANNER); },
|
|
337
|
+
/** #1568: its "I understand", and whether it was pressed for this sign-in. The frame's border stays either way. */
|
|
338
|
+
bannerAckLabel: function () { return say("pvBannerAck", "I understand"); },
|
|
339
|
+
bannerAckTitle: function () { return say("pvBannerAckTitle", "Hide this notice until you sign in again"); },
|
|
340
|
+
bannerAcked: bannerAcked, ackBanner: ackBanner, setBannerScope: setBannerScope,
|
|
292
341
|
};
|
|
293
342
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// #1523 N2: another page asks Settings to open one instance's dialog ("Edit in Settings" on Details). One request,
|
|
2
|
+
// taken once by Settings when its data is in; it opens only what Settings itself knows (its own config, its own
|
|
3
|
+
// rules): an agent of fleet.yaml, or a ClassicBot room of classicBot.yaml (#1561 review: the split stays).
|
|
4
|
+
let wanted = null;
|
|
5
|
+
/** kind: "agent" | "classic". */
|
|
6
|
+
export function requestAgentSettings(name, kind = "agent") {
|
|
7
|
+
wanted = typeof name === "string" && name ? { name, kind: kind === "classic" ? "classic" : "agent" } : null;
|
|
8
|
+
}
|
|
9
|
+
export function takeAgentRequest() { const w = wanted; wanted = null; return w; }
|
|
@@ -26,18 +26,24 @@
|
|
|
26
26
|
try { window.localStorage.removeItem("agend_web_token"); } catch { /* no storage, nothing stored */ }
|
|
27
27
|
|
|
28
28
|
const nativeFetch = window.fetch.bind(window);
|
|
29
|
-
let
|
|
29
|
+
let sessionPending = null;
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/**
|
|
32
|
+
* This page's sign-in, read once from GET /auth/session: { csrf, handle } or null (no session, or the read failed —
|
|
33
|
+
* asked again next time). The CSRF value and the session handle (#1568: what "until the next sign-in" is keyed by)
|
|
34
|
+
* come from the one read.
|
|
35
|
+
*/
|
|
36
|
+
function session() {
|
|
37
|
+
if (!sessionPending) {
|
|
38
|
+
sessionPending = nativeFetch("/auth/session", { credentials: "same-origin", cache: "no-store" })
|
|
34
39
|
.then((r) => (r.ok ? r.json() : null))
|
|
35
|
-
.then((j) => (j && typeof j.csrf === "string" ? j.csrf : null))
|
|
40
|
+
.then((j) => (j && typeof j.csrf === "string" ? { csrf: j.csrf, handle: typeof j.handle === "string" ? j.handle : null } : null))
|
|
36
41
|
.catch(() => null)
|
|
37
|
-
.then((value) => { if (!value)
|
|
42
|
+
.then((value) => { if (!value) sessionPending = null; return value; });
|
|
38
43
|
}
|
|
39
|
-
return
|
|
44
|
+
return sessionPending;
|
|
40
45
|
}
|
|
46
|
+
function csrf() { return session().then((s) => (s ? s.csrf : null)); }
|
|
41
47
|
|
|
42
48
|
let bannerShown = false;
|
|
43
49
|
function sessionEndedBanner() {
|
|
@@ -77,6 +83,8 @@
|
|
|
77
83
|
|
|
78
84
|
window.AgendAuth = {
|
|
79
85
|
csrf,
|
|
86
|
+
/** #1568: the session handle of this page's sign-in, or null when there is none. */
|
|
87
|
+
sessionHandle() { return session().then((s) => (s && s.handle ? s.handle : null)); },
|
|
80
88
|
async signOut() {
|
|
81
89
|
const value = await csrf();
|
|
82
90
|
const r = await nativeFetch("/auth/logout", { method: "POST", credentials: "same-origin", headers: value ? { "X-Agend-CSRF": value } : {} });
|
|
@@ -42,7 +42,8 @@ register("app", {
|
|
|
42
42
|
collapse: "Hide the sidebar", expand: "Show the sidebar", openMenu: "Open the sidebar", closeMenu: "Close the sidebar",
|
|
43
43
|
theme: "Theme", themeSystem: "System", themeLight: "Light", themeDark: "Dark", language: "Language",
|
|
44
44
|
previews: "Allow HTML previews on this device", tour: "Tour", tourTitle: "Show the web chat tour again",
|
|
45
|
-
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat",
|
|
45
|
+
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
|
|
46
|
+
textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage",
|
|
46
47
|
statusRunning: "running", statusStopped: "stopped", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
47
48
|
connPolling: "Live updates paused — refreshing every 5 s", connDown: "Disconnected — trying again…", connConnecting: "Connecting…",
|
|
48
49
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
@@ -70,7 +71,8 @@ register("app", {
|
|
|
70
71
|
collapse: "隱藏側邊欄", expand: "顯示側邊欄", openMenu: "打開側邊欄", closeMenu: "關閉側邊欄",
|
|
71
72
|
theme: "主題", themeSystem: "跟隨系統", themeLight: "淺色", themeDark: "深色", language: "語言",
|
|
72
73
|
previews: "允許這台裝置預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
|
|
73
|
-
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天",
|
|
74
|
+
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
|
|
75
|
+
textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量",
|
|
74
76
|
statusRunning: "執行中", statusStopped: "已停止", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
75
77
|
connPolling: "即時更新暫停中——每 5 秒重新整理", connDown: "已斷線——正在重試…", connConnecting: "連線中…",
|
|
76
78
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
@@ -13,7 +13,7 @@ export function isSafeInstanceName(name) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* A path → the route it names: { panel: "chat" | "view", instance } | { panel: "fleet", tab } |
|
|
16
|
+
* A path → the route it names: { panel: "chat" | "view" | "details", instance } | { panel: "fleet", tab } |
|
|
17
17
|
* { panel: "settings", section } | null (not a page of the app: a full load). A malformed name is null too; the
|
|
18
18
|
* server answers it 400.
|
|
19
19
|
*/
|
|
@@ -21,7 +21,10 @@ export function parseRoute(pathname) {
|
|
|
21
21
|
if (pathname === "/ui") return { panel: "chat", instance: null };
|
|
22
22
|
if (pathname === "/ui/fleet") return { panel: "fleet", tab: "tasks" };
|
|
23
23
|
if (pathname === "/ui/needs") return { panel: "needs" };
|
|
24
|
-
|
|
24
|
+
// #1523 N2: one instance's Details (`agent` is no Fleet tab; a tab is one segment).
|
|
25
|
+
let m = /^\/ui\/fleet\/agent\/([^/]+)$/.exec(pathname);
|
|
26
|
+
if (m) return named("details", m[1]);
|
|
27
|
+
m = /^\/ui\/fleet\/([^/]+)$/.exec(pathname);
|
|
25
28
|
if (m) return FLEET_TABS.includes(m[1]) ? { panel: "fleet", tab: m[1] } : null;
|
|
26
29
|
m = /^\/ui\/chat\/([^/]+)$/.exec(pathname);
|
|
27
30
|
if (m) return named("chat", m[1]);
|
|
@@ -41,6 +44,7 @@ function named(panel, segment) {
|
|
|
41
44
|
|
|
42
45
|
export function chatPath(instance) { return `/ui/chat/${encodeURIComponent(instance)}`; }
|
|
43
46
|
export function viewPath(instance) { return instance ? `/view/${encodeURIComponent(instance)}` : "/view"; }
|
|
47
|
+
export function detailsPath(instance) { return `/ui/fleet/agent/${encodeURIComponent(instance)}`; }
|
|
44
48
|
export function fleetPath(tab) { return tab && tab !== "tasks" ? `/ui/fleet/${tab}` : "/ui/fleet"; }
|
|
45
49
|
export const NEEDS_PATH = "/ui/needs";
|
|
46
50
|
export function settingsPath(section) { return section && section !== "agents" ? `/settings/${section}` : "/settings"; }
|
|
@@ -50,6 +54,7 @@ export function routePath(route) {
|
|
|
50
54
|
if (route.panel === "settings") return settingsPath(route.section);
|
|
51
55
|
if (route.panel === "needs") return NEEDS_PATH;
|
|
52
56
|
if (route.panel === "view") return viewPath(route.instance);
|
|
57
|
+
if (route.panel === "details") return detailsPath(route.instance);
|
|
53
58
|
return route.instance ? chatPath(route.instance) : "/ui";
|
|
54
59
|
}
|
|
55
60
|
/** One string per route: a change of it is a new navigation (a new lease for the panel, §4). */
|