@songsid/agend 2.2.0-alpha.9 → 2.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +10 -7
- package/dist/agent-endpoint.js +21 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +141 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +2 -2
- package/dist/auth-api.js +2 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +14 -2
- package/dist/backend/kiro.js +84 -9
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +4 -2
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +13 -1
- package/dist/channel/adapters/discord.js +34 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +11 -1
- package/dist/channel/adapters/telegram.js +28 -2
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +23 -12
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +7 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +13 -1
- package/dist/cli.js +55 -16
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +48 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +27 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +36 -0
- package/dist/daemon.js +157 -6
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +6 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +48 -0
- package/dist/fleet-manager.js +275 -6
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +3 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +10 -7
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +36 -26
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.js +7 -4
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +8 -0
- package/dist/outbound-schemas.js +15 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +3 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/service-activation.js +4 -3
- package/dist/service-activation.js.map +1 -1
- package/dist/settings-api.js +71 -21
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +70 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +26 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/transcript-monitor.d.ts +8 -0
- package/dist/transcript-monitor.js +26 -2
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +54 -0
- package/dist/transcript-sources.js +188 -27
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-render.js +12 -0
- package/dist/ui/chat-thread.js +1 -1
- package/dist/ui/panel-chat.js +13 -3
- package/dist/ui/panel-needs.js +5 -2
- package/dist/ui/panel-settings.js +117 -26
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +38 -2
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app.css +29 -1
- package/dist/ui/shared/instance-nav.js +77 -8
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/tokens.css +9 -0
- package/dist/ui/shared/view-strings.js +2 -2
- package/dist/ui/signin.html +1 -1
- package/dist/update-install.d.ts +15 -1
- package/dist/update-install.js +6 -0
- package/dist/update-install.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +33 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +11 -0
- package/dist/web-api.js +54 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +5 -3
- package/dist/web-auth.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-session.d.ts +36 -1
- package/dist/web-session.js +52 -5
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +21 -0
- package/dist/web-settings-patch.js +74 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
|
@@ -43,7 +43,7 @@ import { startTour } from "./chat-tour.js";
|
|
|
43
43
|
import "./chat-strings.js";
|
|
44
44
|
|
|
45
45
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
46
|
-
const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", advanced: "code" };
|
|
46
|
+
const ICONS = { device: "user", agents: "bot", bots: "plug", classic: "room", general: "sliders", web: "globe", advanced: "code" };
|
|
47
47
|
const RELEASES = "https://github.com/songsid/AgEnD/releases";
|
|
48
48
|
|
|
49
49
|
/** GET under the panel's lease → { ok, status, body }. */
|
|
@@ -229,7 +229,8 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
229
229
|
: section === "agents" ? html`<${Agents} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
230
230
|
: section === "bots" ? html`<${Bots} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
231
231
|
: section === "classic" ? html`<${Classic} ctx=${ctx} search=${search} openDialog=${(d) => setDialog({ ...d, key: navKey })} />`
|
|
232
|
-
: section === "general" ? html`<${General} key=${data.loadedAt} ctx=${ctx}
|
|
232
|
+
: section === "general" ? html`<${General} key=${data.loadedAt} ctx=${ctx} />`
|
|
233
|
+
: section === "web" ? html`<${WebSection} key=${data.loadedAt} ctx=${ctx} staged=${staged} />`
|
|
233
234
|
: html`<${Developer} ctx=${ctx} />`;
|
|
234
235
|
// A dialog belongs to the navigation that opened it (its draft names one object, never another).
|
|
235
236
|
const open = dialog && dialog.key === navKey ? dialog : null;
|
|
@@ -570,17 +571,13 @@ function Classic({ ctx, search, openDialog }) {
|
|
|
570
571
|
* ClassicBot defaults, web) — each only with what differs from what this form showed, so changing one hot default
|
|
571
572
|
* never also stages untouched settings as a fleet restart.
|
|
572
573
|
*/
|
|
573
|
-
function General({ ctx
|
|
574
|
+
function General({ ctx }) {
|
|
574
575
|
const { schema } = ctx;
|
|
575
576
|
const d = ctx.fleet.defaults || {}, c = (ctx.classic && ctx.classic.defaults) || {};
|
|
576
577
|
const hang = d.hang_detector || {}, startup = d.startup || {};
|
|
577
578
|
const primary = channelsOf(ctx.fleet)[0];
|
|
578
|
-
const
|
|
579
|
-
const originalPublic = useMemo(() => ({ allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" }), []);
|
|
580
|
-
const originalEcho = web.echo_to_channel ?? true;
|
|
581
|
-
const original = useMemo(() => ({ c: structuredClone(c), web: structuredClone(ctx.fleet.web) }), []);
|
|
579
|
+
const original = useMemo(() => ({ c: structuredClone(c) }), []);
|
|
582
580
|
const [f, setF] = useState(() => ({
|
|
583
|
-
echo: originalEcho, pub: originalPublic.allow_public, ttl: String(originalPublic.ttl_minutes), protocol: originalPublic.protocol,
|
|
584
581
|
backend: d.backend || "claude-code", model: d.model || "", locale: d.locale || "", auto: String(d.auto_pause_after ?? 0),
|
|
585
582
|
agent_mode: d.agent_mode || "mcp", tool_set: d.tool_set || "full", tool_progress: d.tool_progress ?? "off",
|
|
586
583
|
visibility: visibilityDefault(d), reply: d.reply_completion_guard ?? true, log_level: d.log_level || "info",
|
|
@@ -598,7 +595,6 @@ function General({ ctx, staged }) {
|
|
|
598
595
|
auto: nonNegative(f.auto), hang: positive(f.hang),
|
|
599
596
|
concurrency: !Number.isInteger(num(f.concurrency)) || num(f.concurrency) < 1 || num(f.concurrency) > 20 ? "concurrencyRange" : "",
|
|
600
597
|
stagger: !Number.isFinite(num(f.stagger)) || f.stagger.trim() === "" || num(f.stagger) < 0 || num(f.stagger) > 30000 ? "staggerRange" : "",
|
|
601
|
-
ttl: !Number.isInteger(num(f.ttl)) || num(f.ttl) < 1 || num(f.ttl) > 480 ? "publicTtl" : "",
|
|
602
598
|
};
|
|
603
599
|
const warnings = { concurrency: num(f.concurrency) > 4 ? "concurrencyWarning" : "", stagger: num(f.stagger) < 100 ? "staggerWarning" : "" };
|
|
604
600
|
const ready = !Object.values(errors).some(Boolean);
|
|
@@ -638,29 +634,12 @@ function General({ ctx, staged }) {
|
|
|
638
634
|
ctx.stage("defaults:classic", { label: tn("updateClassicDefaults"), impact: hotOnly ? "now" : "fleet",
|
|
639
635
|
request: { method: "PUT", url: "/api/settings/classic/defaults", body: classicPatch } });
|
|
640
636
|
}
|
|
641
|
-
const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: num(f.ttl), protocol: f.protocol }, originalPublic);
|
|
642
|
-
const webPatch = { ...(f.echo !== originalEcho ? { echo_to_channel: f.echo } : {}), ...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}) };
|
|
643
|
-
if (Object.keys(webPatch).length) {
|
|
644
|
-
ctx.stage("web:echo", { label: tn("publicLink"),
|
|
645
|
-
impact: batchImpact(schema, [...(webPatch.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []), ...Object.keys(publicPatch).map((k) => `web.public_link.${k}`)], (k) => impactOf(schema, k)),
|
|
646
|
-
request: { method: "PUT", url: "/api/settings/fleet/web", body: webPatch } });
|
|
647
|
-
} else if (staged.has("web:echo")) ctx.unstage("web:echo");
|
|
648
637
|
setMsg(tn("readyApply"));
|
|
649
638
|
};
|
|
650
639
|
const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
|
|
651
640
|
const fleetReply = replyGuardSupported(f.backend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.backend, f.agent_mode);
|
|
652
641
|
const classicReply = replyGuardSupported(f.classicBackend, f.agent_mode, d.kiro_ui) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", f.classicBackend, f.agent_mode);
|
|
653
642
|
return html`<div class="form s-general">
|
|
654
|
-
<section class="card"><h3>${tn("webChat")}</h3>
|
|
655
|
-
<label class="check"><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
|
|
656
|
-
<p class="note">${tn("webEchoHint")}</p>
|
|
657
|
-
<label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
|
|
658
|
-
<p class="note">${tn("publicLinkHint")}</p>
|
|
659
|
-
<div class="grid2">
|
|
660
|
-
<div class="field"><label for="publicWebTtl">${tn("publicTtl")}</label><input id="publicWebTtl" type="number" min="1" max="480" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
|
|
661
|
-
<div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
|
|
662
|
-
</div>
|
|
663
|
-
</section>
|
|
664
643
|
<section class="card"><h3>${tn("defaultsTitle")}</h3>
|
|
665
644
|
<div class="grid2">
|
|
666
645
|
<div class="field">${L(tn("language"), "defaults.locale", "g-locale")}<${Select} id="g-locale" value=${f.locale} onChange=${set("locale")}
|
|
@@ -714,6 +693,118 @@ function General({ ctx, staged }) {
|
|
|
714
693
|
</div>`;
|
|
715
694
|
}
|
|
716
695
|
|
|
696
|
+
/**
|
|
697
|
+
* #1620: Settings → Web — the dashboard's own settings, in one write (PUT /api/settings/fleet/web). Editable: the local
|
|
698
|
+
* session limits, the browser terminal (on/off, its minutes), web echo and the public link's menu; everything else
|
|
699
|
+
* under web: / web_terminal: is listed read-only (it decides what the dashboard listens on and exposes: fleet.yaml, then
|
|
700
|
+
* agend reload). The server judges each change: loosening one waits for a fleet admin in General, tightening applies at
|
|
701
|
+
* once. Over
|
|
702
|
+
* the temporary public link none of it can be changed — the form says so and stays disabled (the server refuses too).
|
|
703
|
+
*/
|
|
704
|
+
export const WEB_READ_ONLY = [
|
|
705
|
+
"web.allowed_hosts", "web.view_access", "web.notify_login", "web.preview", "web.preview_port", "web.preview_origin",
|
|
706
|
+
"web.usage_panel", "web.provider_secrets", "web_terminal.bind", "web_terminal.tunnel",
|
|
707
|
+
];
|
|
708
|
+
function WebSection({ ctx, staged }) {
|
|
709
|
+
const { schema } = ctx;
|
|
710
|
+
const web = ctx.fleet.web || {}, local = (web.session && web.session.local) || {}, term = ctx.fleet.web_terminal || {};
|
|
711
|
+
const overPublicLink = globalThis.document?.body?.dataset?.publicLink === "1";
|
|
712
|
+
const original = useMemo(() => ({
|
|
713
|
+
echo: web.echo_to_channel ?? true,
|
|
714
|
+
pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2" },
|
|
715
|
+
session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
|
|
716
|
+
terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
|
|
717
|
+
}), []);
|
|
718
|
+
const text = (v) => (v === null || v === undefined ? "" : String(v));
|
|
719
|
+
const [f, setF] = useState(() => ({
|
|
720
|
+
echo: original.echo, pub: original.pub.allow_public, ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol,
|
|
721
|
+
abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours),
|
|
722
|
+
termOn: original.terminal.enabled !== false, termTtl: text(original.terminal.ttl_minutes),
|
|
723
|
+
}));
|
|
724
|
+
const [msg, setMsg] = useState(null);
|
|
725
|
+
const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
|
|
726
|
+
// Empty = the default (null in the write: the key is removed from fleet.yaml).
|
|
727
|
+
const value = (v) => (v.trim() === "" ? null : Number(v));
|
|
728
|
+
const hours = (v) => { const n = value(v); return n !== null && (!Number.isFinite(n) || n < 1 || n > 720) ? "sessionRange" : ""; };
|
|
729
|
+
const abs = value(f.abs) ?? 12, idle = value(f.idle) ?? 2;
|
|
730
|
+
const errors = {
|
|
731
|
+
ttl: !Number.isInteger(Number(f.ttl)) || Number(f.ttl) < 1 || Number(f.ttl) > 480 ? "publicTtl" : "",
|
|
732
|
+
abs: hours(f.abs),
|
|
733
|
+
idle: hours(f.idle) || (!hours(f.abs) && idle > abs ? "sessionIdleOver" : ""),
|
|
734
|
+
termTtl: value(f.termTtl) !== null && (!Number.isInteger(value(f.termTtl)) || value(f.termTtl) < 1 || value(f.termTtl) > 20) ? "webTerminalTtlRange" : "",
|
|
735
|
+
};
|
|
736
|
+
const ready = !overPublicLink && !Object.values(errors).some(Boolean);
|
|
737
|
+
const review = () => {
|
|
738
|
+
if (!ready) return;
|
|
739
|
+
const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol }, original.pub);
|
|
740
|
+
const sessionPatch = changedFields({ absolute_hours: value(f.abs), idle_hours: value(f.idle) }, original.session);
|
|
741
|
+
// The checkbox shows the default (on) for an unset key: only a real toggle is written; on again = back to the default.
|
|
742
|
+
const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
|
|
743
|
+
ttl_minutes: value(f.termTtl) }, original.terminal);
|
|
744
|
+
const body = {
|
|
745
|
+
...(f.echo !== original.echo ? { echo_to_channel: f.echo } : {}),
|
|
746
|
+
...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
|
|
747
|
+
...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
|
|
748
|
+
...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
|
|
749
|
+
};
|
|
750
|
+
const fields = [
|
|
751
|
+
...(body.echo_to_channel !== undefined ? ["web.echo_to_channel"] : []),
|
|
752
|
+
...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
|
|
753
|
+
...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
|
|
754
|
+
...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
|
|
755
|
+
];
|
|
756
|
+
if (fields.length) {
|
|
757
|
+
ctx.stage("web", { label: tn("sec_web"), impact: batchImpact(schema, fields, (k) => impactOf(schema, k)),
|
|
758
|
+
request: { method: "PUT", url: "/api/settings/fleet/web", body } });
|
|
759
|
+
} else if (staged.has("web")) ctx.unstage("web");
|
|
760
|
+
setMsg(tn("readyApply"));
|
|
761
|
+
};
|
|
762
|
+
const shown = (path) => {
|
|
763
|
+
const [top, key] = path.split(".");
|
|
764
|
+
const v = (top === "web" ? web : term)[key];
|
|
765
|
+
return v === undefined ? tn("notSet") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
766
|
+
};
|
|
767
|
+
return html`<div class="form s-web">
|
|
768
|
+
<p class="sub">${tn("webLead")}</p>
|
|
769
|
+
${overPublicLink ? html`<p class="note" role="status" id="webPublicRefused">${tn("webPublicRefused")}</p>` : null}
|
|
770
|
+
<fieldset class="s-web-fields" disabled=${overPublicLink}>
|
|
771
|
+
<section class="card"><h3>${tn("webSessionTitle")}</h3>
|
|
772
|
+
<p class="note">${tn("webSessionHint")}</p>
|
|
773
|
+
<div class="grid2">
|
|
774
|
+
<div class="field"><label for="webSessionAbsolute">${tn("sessionAbsolute")}</label><input id="webSessionAbsolute" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 12)}
|
|
775
|
+
class=${errors.abs ? "invalid" : ""} value=${f.abs} onInput=${(e) => set("abs")(e.target.value)} /><${Feedback} error=${errors.abs} /></div>
|
|
776
|
+
<div class="field"><label for="webSessionIdle">${tn("sessionIdle")}</label><input id="webSessionIdle" type="number" min="1" max="720" step="any" placeholder=${tn("defaultHours", 2)}
|
|
777
|
+
class=${errors.idle ? "invalid" : ""} value=${f.idle} onInput=${(e) => set("idle")(e.target.value)} /><${Feedback} error=${errors.idle} /></div>
|
|
778
|
+
</div>
|
|
779
|
+
</section>
|
|
780
|
+
<section class="card"><h3>${tn("webTerminalTitle")}</h3>
|
|
781
|
+
<p class="note">${tn("webTerminalHint")}</p>
|
|
782
|
+
<label class="check"><input id="webTerminalEnabled" type="checkbox" checked=${f.termOn} onChange=${(e) => set("termOn")(e.target.checked)} /><span>${tn("webTerminalEnabled")}</span><${Impact} schema=${schema} field="web_terminal.enabled" /></label>
|
|
783
|
+
<div class="grid2">
|
|
784
|
+
<div class="field"><label for="webTerminalTtl">${tn("webTerminalTtl")}</label><input id="webTerminalTtl" type="number" min="1" max="20" step="1" placeholder=${tn("defaultHours", 10)}
|
|
785
|
+
class=${errors.termTtl ? "invalid" : ""} value=${f.termTtl} onInput=${(e) => set("termTtl")(e.target.value)} /><${Feedback} error=${errors.termTtl} /></div>
|
|
786
|
+
</div>
|
|
787
|
+
</section>
|
|
788
|
+
<section class="card"><h3>${tn("webChat")}</h3>
|
|
789
|
+
<label class="check"><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
|
|
790
|
+
<p class="note">${tn("webEchoHint")}</p>
|
|
791
|
+
<label class="check"><input id="publicWebLink" type="checkbox" checked=${f.pub} onChange=${(e) => set("pub")(e.target.checked)} /><span>${tn("publicLink")}</span><${Impact} schema=${schema} field="web.public_link.allow_public" /></label>
|
|
792
|
+
<p class="note">${tn("publicLinkHint")}</p>
|
|
793
|
+
<div class="grid2">
|
|
794
|
+
<div class="field"><label for="publicWebTtl">${tn("publicTtl")}</label><input id="publicWebTtl" type="number" min="1" max="480" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
|
|
795
|
+
<div class="field"><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
|
|
796
|
+
</div>
|
|
797
|
+
</section>
|
|
798
|
+
</fieldset>
|
|
799
|
+
<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
|
|
800
|
+
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
801
|
+
<dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
|
|
802
|
+
</section>
|
|
803
|
+
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${review}>${tn("reviewChanges")}</button>
|
|
804
|
+
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
805
|
+
</div>`;
|
|
806
|
+
}
|
|
807
|
+
|
|
717
808
|
/** Developer: the fleet model as YAML or JSON, to read, copy, download — or edit and apply as one operation. */
|
|
718
809
|
function Developer({ ctx }) {
|
|
719
810
|
const [fmt, setFmt] = useState("yaml");
|
|
@@ -108,8 +108,13 @@ export function orderedStatusMap(map, keys) {
|
|
|
108
108
|
for (const key of keys) { const v = map && map[key]; if (typeof v === "string" && v.trim()) out[key] = v.trim(); }
|
|
109
109
|
return Object.keys(out).length ? out : null;
|
|
110
110
|
}
|
|
111
|
-
//
|
|
112
|
-
|
|
111
|
+
// #1630: from the dashboard's own route (/ui/img/emoji/<id>). An emoji the fleet's servers do not have is not served
|
|
112
|
+
// there: then its :name: as text, never a broken image.
|
|
113
|
+
const EmojiImg = ({ url, alt }) => {
|
|
114
|
+
const [broken, setBroken] = useState("");
|
|
115
|
+
return broken === url ? html`<span class="se-alt">${alt}</span>`
|
|
116
|
+
: html`<img src=${url} alt=${alt} referrerpolicy="no-referrer" loading="lazy" onError=${() => setBroken(url)} />`;
|
|
117
|
+
};
|
|
113
118
|
|
|
114
119
|
/**
|
|
115
120
|
* One status_emojis map. `platform` and `channel` say where it applies; `previewBody(map)` is what the preview resolves.
|
|
@@ -24,8 +24,9 @@ export function changedFields(next, baseline) {
|
|
|
24
24
|
/** #1302: cross-instance notices. Unset, or a value the fleet does not know, means full. */
|
|
25
25
|
export const visibilityDefault = (defaults) => (VISIBILITY_MODES.includes(defaults && defaults.cross_instance_visibility) ? defaults.cross_instance_visibility : "full");
|
|
26
26
|
|
|
27
|
+
/** The backends whose turn end the reply guard can see (backend replyCompletionGuard: claude-code, codex and muse since #1510, kiro legacy/tui), with the MCP server it needs. */
|
|
27
28
|
export const replyGuardSupported = (backend, mode, kiroUi = "legacy") =>
|
|
28
|
-
mode === "mcp" && (
|
|
29
|
+
mode === "mcp" && (["claude-code", "codex", "muse"].includes(backend) || (backend === "kiro-cli" && ["legacy", "tui"].includes(kiroUi || "legacy")));
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* The backend picker's options. A current value it does not offer (the removed gemini-cli, #1280 review) stays first
|
|
@@ -75,7 +75,7 @@ register("settings", {
|
|
|
75
75
|
crossInstanceVisibility: "Bot-to-bot messages in topics",
|
|
76
76
|
crossInstanceVisibilityHint: "full: the whole message · summary: one line · hidden: nothing. Messages are always delivered; the Mirror Topic still shows them.",
|
|
77
77
|
replyCompletionGuard: "Require a delivered reply",
|
|
78
|
-
replyGuardSupported: "Available for Claude Code and Kiro legacy/TUI in MCP mode.",
|
|
78
|
+
replyGuardSupported: "Available for Claude Code, Codex, Muse and Kiro legacy/TUI in MCP mode.",
|
|
79
79
|
replyGuardUnsupported: "Stored but inactive: {0} in {1} mode does not support reply-drop recovery.",
|
|
80
80
|
logLevel: "Log level",
|
|
81
81
|
concurrency: "Startup concurrency",
|
|
@@ -277,6 +277,24 @@ register("settings", {
|
|
|
277
277
|
keyRollbackFailed: "Rollback failed",
|
|
278
278
|
accessTitle: "Access",
|
|
279
279
|
webChat: "Web chat",
|
|
280
|
+
sec_web: "Web",
|
|
281
|
+
webLead: "How the web dashboard behaves. A change that loosens access (longer sessions, the browser terminal back on or for longer, the public link) waits for a fleet admin to confirm it in General; one that tightens it applies at once.",
|
|
282
|
+
webPublicRefused: "Web settings can't be changed over a temporary public link. Open the dashboard at its own address to change them.",
|
|
283
|
+
webSessionTitle: "Sign-in sessions",
|
|
284
|
+
webSessionHint: "For sign-ins at the dashboard's own address (this computer, the LAN, a tailnet). A temporary public link always keeps 4 h / 30 min. Shortening ends the sessions it now outlives; lengthening applies to new sign-ins and to an existing one's idle time on its next use. Empty = the default.",
|
|
285
|
+
sessionAbsolute: "Session length, hours (1–720)",
|
|
286
|
+
sessionIdle: "Sign out after unused for, hours (1–720)",
|
|
287
|
+
sessionRange: "Must be a number of hours from 1 to 720",
|
|
288
|
+
sessionIdleOver: "Must not exceed the session length",
|
|
289
|
+
defaultHours: "{0} (default)",
|
|
290
|
+
webTerminalTitle: "Browser terminal",
|
|
291
|
+
webTerminalHint: "The token-gated terminal /login opens to sign a CLI in. Off: /login refuses. Applies to the next /login.",
|
|
292
|
+
webTerminalEnabled: "Allow the browser terminal",
|
|
293
|
+
webTerminalTtl: "Terminal session minutes (1–20)",
|
|
294
|
+
webTerminalTtlRange: "Must be a whole number from 1 to 20",
|
|
295
|
+
webReadOnlyTitle: "Set in fleet.yaml",
|
|
296
|
+
webReadOnlyLead: "Read-only here: these decide what the dashboard listens on and exposes. Edit fleet.yaml, then run agend reload (web.preview_port and web.preview_origin take a restart of AgEnD).",
|
|
297
|
+
notSet: "not set (default)",
|
|
280
298
|
defaultsTitle: "Defaults",
|
|
281
299
|
classicBackend: "ClassicBot default backend",
|
|
282
300
|
whoUsesClassic: "Who can use ClassicBot",
|
|
@@ -420,7 +438,7 @@ register("settings", {
|
|
|
420
438
|
crossInstanceVisibility: "Agent 之間的訊息在主題中",
|
|
421
439
|
crossInstanceVisibilityHint: "full:完整訊息 · summary:一行摘要 · hidden:不顯示。訊息一律照常送達,Mirror Topic 也照常顯示。",
|
|
422
440
|
replyCompletionGuard: "要求送出回覆",
|
|
423
|
-
replyGuardSupported: "Claude Code 與 Kiro legacy/TUI 的 MCP 模式可用。",
|
|
441
|
+
replyGuardSupported: "Claude Code、Codex、Muse 與 Kiro legacy/TUI 的 MCP 模式可用。",
|
|
424
442
|
replyGuardUnsupported: "設定會保留,但不會生效:{0} 的 {1} 模式不支援回覆遺失補救。",
|
|
425
443
|
logLevel: "Log 等級",
|
|
426
444
|
concurrency: "啟動併發數",
|
|
@@ -622,6 +640,24 @@ register("settings", {
|
|
|
622
640
|
keyRollbackFailed: "還原失敗",
|
|
623
641
|
accessTitle: "存取",
|
|
624
642
|
webChat: "網頁聊天",
|
|
643
|
+
sec_web: "網頁",
|
|
644
|
+
webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在 General 確認;收緊的變更會立即套用。",
|
|
645
|
+
webPublicRefused: "無法透過臨時公開連結變更網頁設定。請從儀表板本身的網址開啟後再變更。",
|
|
646
|
+
webSessionTitle: "登入 session",
|
|
647
|
+
webSessionHint: "適用於從儀表板本身網址登入的 session(本機、區域網路、tailnet)。臨時公開連結固定為 4 小時 / 30 分鐘。縮短會讓已超過新限制的 session 結束;延長會套用到新的登入,以及既有 session 下次使用時的閒置時間。留空 = 預設值。",
|
|
648
|
+
sessionAbsolute: "Session 時長,小時(1–720)",
|
|
649
|
+
sessionIdle: "閒置多久後登出,小時(1–720)",
|
|
650
|
+
sessionRange: "必須是 1 到 720 之間的小時數",
|
|
651
|
+
sessionIdleOver: "不能超過 session 時長",
|
|
652
|
+
defaultHours: "{0}(預設)",
|
|
653
|
+
webTerminalTitle: "瀏覽器終端機",
|
|
654
|
+
webTerminalHint: "/login 用來登入 CLI 的 token 保護終端機。關閉時 /login 會拒絕。下一次 /login 起生效。",
|
|
655
|
+
webTerminalEnabled: "允許瀏覽器終端機",
|
|
656
|
+
webTerminalTtl: "終端機 session 分鐘數(1–20)",
|
|
657
|
+
webTerminalTtlRange: "必須是 1 到 20 之間的整數",
|
|
658
|
+
webReadOnlyTitle: "在 fleet.yaml 設定",
|
|
659
|
+
webReadOnlyLead: "這裡僅供檢視:這些設定決定儀表板監聽和公開的內容。請編輯 fleet.yaml 後執行 agend reload(web.preview_port 與 web.preview_origin 需要重新啟動 AgEnD)。",
|
|
660
|
+
notSet: "未設定(預設)",
|
|
625
661
|
defaultsTitle: "預設值",
|
|
626
662
|
classicBackend: "ClassicBot 預設 backend",
|
|
627
663
|
whoUsesClassic: "誰可以使用 ClassicBot",
|
|
@@ -44,7 +44,7 @@ register("app", {
|
|
|
44
44
|
previews: "Allow HTML previews on this device", tour: "Tour", tourTitle: "Show the web chat tour again",
|
|
45
45
|
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", instanceViews: "Views of {0}",
|
|
46
46
|
textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage",
|
|
47
|
-
statusRunning: "running", statusStopped: "stopped", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
47
|
+
statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
48
48
|
connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
|
|
49
49
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
50
50
|
skip: "Skip to the conversation", loadFailed: "This could not be loaded.", retry: "Try again", loading: "Loading…",
|
|
@@ -73,7 +73,7 @@ register("app", {
|
|
|
73
73
|
previews: "允許這台裝置預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
|
|
74
74
|
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", instanceViews: "{0} 的檢視",
|
|
75
75
|
textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "剛好填滿", usage: "用量",
|
|
76
|
-
statusRunning: "執行中", statusStopped: "已停止", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
76
|
+
statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
77
77
|
connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
|
|
78
78
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|
|
79
79
|
skip: "跳到對話", loadFailed: "無法載入。", retry: "再試一次", loading: "載入中…",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
4
|
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
5
|
-
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "advanced"]; // #1604: device first
|
|
5
|
+
export const SETTINGS_SECTIONS = ["device", "agents", "bots", "classic", "general", "web", "advanced"]; // #1604: device first
|
|
6
6
|
const MAX_INSTANCE_NAME = 128;
|
|
7
7
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
8
8
|
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -12,6 +12,10 @@ h1, h2, h3, p, ul, ol, figure, fieldset { margin: 0; padding: 0; }
|
|
|
12
12
|
fieldset { border: 0; min-width: 0; }
|
|
13
13
|
ul, ol { list-style: none; }
|
|
14
14
|
button, input, select, textarea { font: inherit; color: inherit; }
|
|
15
|
+
/* #1615: and a field with no size of its own, inside small text, is still never under 16px on a touch screen or the phone
|
|
16
|
+
layout (iOS zooms on focusing one; #1611's tokens only reach a field sized from them). After the reset above, as weak
|
|
17
|
+
as it (an element selector: any rule of a field's own still wins), keeping a larger inherited size. */
|
|
18
|
+
@media (pointer: coarse), (max-width: 899px) { input, select, textarea { font-size: max(16px, 1em); } }
|
|
15
19
|
a { color: inherit; text-decoration: none; }
|
|
16
20
|
[hidden] { display: none !important; }
|
|
17
21
|
:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
@@ -99,6 +103,22 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
99
103
|
.inst:hover { background: var(--surface-2); }
|
|
100
104
|
.inst.active { background: var(--surface-2); }
|
|
101
105
|
.inst .dot { margin-top: 8px; }
|
|
106
|
+
/* #1630: the avatar (a picture, an emoji or the first character), its status dot, and an optional ring in its bot's colour (--ring, set through the CSSOM). */
|
|
107
|
+
.av { position: relative; flex-shrink: 0; width: 28px; height: 28px; border-radius: 50%; background: var(--surface-2); color: var(--text-2);
|
|
108
|
+
display: inline-flex; align-items: center; justify-content: center; font-size: var(--fs-sm); font-weight: var(--fw-semibold); line-height: 1; }
|
|
109
|
+
.av img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
|
|
110
|
+
.av.av-emoji { background: transparent; font-size: 18px; font-weight: normal; }
|
|
111
|
+
.av.av-emoji img { width: 22px; height: 22px; border-radius: 0; object-fit: contain; }
|
|
112
|
+
.av.ring { box-shadow: 0 0 0 2px var(--ring, transparent); }
|
|
113
|
+
.av-text { overflow: hidden; }
|
|
114
|
+
.av .av-dot { position: absolute; right: -2px; bottom: -2px; margin: 0; box-shadow: 0 0 0 2px var(--bg-sidebar); }
|
|
115
|
+
.inst.active .av .av-dot, .inst:hover .av .av-dot { box-shadow: 0 0 0 2px var(--surface-2); }
|
|
116
|
+
.dot.hollow { background: transparent; border: 1.5px solid var(--text-3); }
|
|
117
|
+
/* On a grey avatar a grey dot needs more contrast: stopped is a stronger grey, paused a clear ring around the sidebar colour. */
|
|
118
|
+
.av .av-dot.off { background: var(--text-2); }
|
|
119
|
+
.av .av-dot.hollow { box-sizing: border-box; width: 10px; height: 10px; background: var(--bg-sidebar); border: 2.5px solid var(--text-2); }
|
|
120
|
+
.inst .av { margin-top: 1px; }
|
|
121
|
+
.inst-id { font-family: var(--font-mono); }
|
|
102
122
|
.inst-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
103
123
|
.inst-name { font-size: var(--fs-sm); line-height: var(--lh-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
104
124
|
.inst.active .inst-name { font-weight: var(--fw-semibold); }
|
|
@@ -124,6 +144,13 @@ html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
|
124
144
|
.s-device { display: flex; flex-direction: column; gap: var(--s-3); }
|
|
125
145
|
.s-device .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
|
|
126
146
|
.s-device-row { display: flex; gap: var(--s-2); }
|
|
147
|
+
/* #1620: Settings → Web — the lead, and the read-only list of what is set in fleet.yaml. */
|
|
148
|
+
.s-web .sub { margin: 0; color: var(--text-3); font-size: var(--fs-sm); }
|
|
149
|
+
.s-web-fields:disabled select { opacity: 0.6; } /* over the public link: dimmed like the inputs (.field input:disabled) */
|
|
150
|
+
.s-web-ro { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--s-1) var(--s-3); margin: 0; font-size: var(--fs-sm); }
|
|
151
|
+
.s-web-ro > div { display: contents; }
|
|
152
|
+
.s-web-ro dt { color: var(--text-3); }
|
|
153
|
+
.s-web-ro dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
127
154
|
.prefs { display: flex; gap: var(--s-2); padding: var(--s-1) var(--s-3); }
|
|
128
155
|
.pref { display: flex; align-items: center; gap: var(--s-1); color: var(--text-2); flex: 1; min-width: 0; }
|
|
129
156
|
.pref select { flex: 1; min-width: 0; height: 30px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--text-2); font-size: var(--fs-xs); cursor: pointer; }
|
|
@@ -518,7 +545,6 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
518
545
|
.thread { padding: var(--s-3) var(--s-3) var(--s-2); gap: var(--s-5); }
|
|
519
546
|
.col { padding: var(--s-3) var(--s-3) var(--s-8); }
|
|
520
547
|
.dock { padding: 0 max(var(--s-2), env(safe-area-inset-right)) var(--s-2) max(var(--s-2), env(safe-area-inset-left)); }
|
|
521
|
-
.composer textarea { font-size: 16px; } /* under 16px, iOS Safari zooms the page on focus */
|
|
522
548
|
.hint { display: none; }
|
|
523
549
|
.msg.user { max-width: 88%; }
|
|
524
550
|
/* Code wraps on a phone: a long line scrolled sideways inside a scrolling page is unreadable (rough edge 9). */
|
|
@@ -931,3 +957,5 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
931
957
|
.cmd-palette .cmd-desc { white-space: normal; flex-basis: 100%; }
|
|
932
958
|
.cmd-choices .btn, .quick-actions .btn { min-height: var(--tap); }
|
|
933
959
|
}
|
|
960
|
+
/* #1630: a Discord server emoji in a chat message, the size of the text around it. */
|
|
961
|
+
.md img.emoji { display: inline-block; width: 1.375em; height: 1.375em; object-fit: contain; vertical-align: -0.3em; margin: 0 0.05em; }
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// - a row opens the view you are in (View → View, everywhere else → Chat);
|
|
8
8
|
// - the list is never remounted on navigation, and only its own scrollTop follows the active row (#1515).
|
|
9
9
|
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
|
-
import { html, useLayoutEffect, useRef } from "./app-html.js";
|
|
10
|
+
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
12
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
13
13
|
import { chatPath, detailsPath, viewPath } from "./app-route.js";
|
|
@@ -177,11 +177,76 @@ function persistOrder(groupNames, instByGroup) {
|
|
|
177
177
|
navStore.set({ order: { groups: new Map(groupNames.map((g, i) => [g, i])), insts: new Map(rows.filter((r) => r.item_type === "instance").map((r) => [r.item_name, r.sort_index])) } });
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
// ── #1630: what stands for an instance (design: the comment on #1630) ──
|
|
181
|
+
// The server decides (the status frame's `avatar` / `accent`); the page only draws it. The anonymous View roster has
|
|
182
|
+
// neither: its rows get the first character. A picture is only ever a same-origin /ui/img/ path — never a platform's
|
|
183
|
+
// CDN (CSP img-src stays 'self'); one that fails to load falls back to the first character, never a broken image.
|
|
184
|
+
|
|
185
|
+
const GRAPHEMES = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
|
|
186
|
+
/** The first user-perceived character, upper-cased where that means anything. */
|
|
187
|
+
export function firstCharacter(text) {
|
|
188
|
+
const s = String(text == null ? "" : text).trim();
|
|
189
|
+
if (!s) return "?";
|
|
190
|
+
if (GRAPHEMES) for (const { segment } of GRAPHEMES.segment(s)) return segment.toLocaleUpperCase();
|
|
191
|
+
return Array.from(s)[0].toLocaleUpperCase();
|
|
192
|
+
}
|
|
193
|
+
/** What a row is called: the display name when it has one, else the instance name (shown on hover and in Details). */
|
|
194
|
+
export function labelOf(it) {
|
|
195
|
+
const d = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
|
|
196
|
+
return d || nameOf(it);
|
|
197
|
+
}
|
|
198
|
+
// One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
|
|
199
|
+
// %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
|
|
200
|
+
const sameOrigin = (src) => typeof src === "string"
|
|
201
|
+
&& /^\/ui\/img\/(bot|avatar|emoji)\/(?!(?:\.|%2[eE]){1,2}(?:\?|$))[^/\\?#\s]+(\?v=[0-9A-Za-z%._-]*)?$/.test(src);
|
|
202
|
+
/** The avatar to draw: the server's, if it is one the page may load; else the first character. */
|
|
203
|
+
export function avatarOf(it) {
|
|
204
|
+
const a = it && it.avatar;
|
|
205
|
+
if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
|
|
206
|
+
if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
|
|
207
|
+
if (a && a.kind === "emoji" && typeof a.text === "string" && a.text.trim()) return { kind: "emoji", text: a.text.trim() };
|
|
208
|
+
return { kind: "initial", text: a && a.kind === "initial" && typeof a.text === "string" && a.text ? a.text : firstCharacter(labelOf(it)) };
|
|
209
|
+
}
|
|
210
|
+
const ACCENT = /^#[0-9a-f]{6}$/i;
|
|
211
|
+
/** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
|
|
212
|
+
export function dotState(it, exec, awaiting) {
|
|
213
|
+
if (awaiting != null) return "needs";
|
|
214
|
+
if (!it || it.status === "crashed") return "crashed";
|
|
215
|
+
if (it.status === "paused") return "paused";
|
|
216
|
+
if (it.status !== "running") return "stopped";
|
|
217
|
+
return exec === "stuck" ? "stuck" : exec === "working" ? "working" : "idle";
|
|
218
|
+
}
|
|
219
|
+
const DOT = {
|
|
220
|
+
needs: ["warn", () => t("app.needsYou")], crashed: ["bad", () => t("app.statusCrashed")], paused: ["off hollow", () => t("app.statusPaused")],
|
|
221
|
+
stopped: ["off", () => t("app.statusStopped")], stuck: ["warn", () => t("app.statusStuck")], working: ["busy", () => t("app.statusWorking")],
|
|
222
|
+
idle: ["ok", () => t("app.statusIdle")],
|
|
223
|
+
};
|
|
224
|
+
export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
|
|
225
|
+
|
|
226
|
+
/** An instance's avatar with its status dot. The ring (its bot's colour) is a CSS custom property set through the CSSOM. */
|
|
227
|
+
export function InstanceAvatar({ it, state }) {
|
|
228
|
+
const a = avatarOf(it);
|
|
229
|
+
const [broken, setBroken] = useState("");
|
|
230
|
+
const el = useRef(null);
|
|
231
|
+
const accent = typeof it.accent === "string" && ACCENT.test(it.accent) && a.kind === "image" ? it.accent : "";
|
|
232
|
+
useLayoutEffect(() => {
|
|
233
|
+
const node = el.current;
|
|
234
|
+
if (!node || !node.style) return;
|
|
235
|
+
if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
|
|
236
|
+
}, [accent]);
|
|
237
|
+
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
238
|
+
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(labelOf(it));
|
|
239
|
+
const [cls] = DOT[state] || DOT.stopped;
|
|
240
|
+
const label = dotLabel(state);
|
|
241
|
+
return html`<span ref=${el} class=${`av${accent ? " ring" : ""}${a.kind === "emoji" ? " av-emoji" : ""}`}>
|
|
242
|
+
${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
|
|
243
|
+
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span></span>`;
|
|
244
|
+
}
|
|
245
|
+
|
|
180
246
|
// ── The list ──
|
|
181
247
|
|
|
182
248
|
const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
|
|
183
249
|
: facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
|
|
184
|
-
const dotClass = (facet, exec) => (facet === "needs" ? "warn" : facet === "crashed" ? "bad" : facet === "stopped" ? "off" : exec === "stuck" ? "warn" : facet === "working" ? "busy" : "ok");
|
|
185
250
|
|
|
186
251
|
/**
|
|
187
252
|
* The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
|
|
@@ -207,6 +272,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
207
272
|
const f = nav.filter;
|
|
208
273
|
const shown = filterSidebar(groupNames, instByGroup, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
209
274
|
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
275
|
+
// #1630: a display name two rows share would tell them apart from nothing — only then does the row keep its name too.
|
|
276
|
+
const labelCount = new Map();
|
|
277
|
+
for (const it of items) { const l = labelOf(it); labelCount.set(l, (labelCount.get(l) || 0) + 1); }
|
|
210
278
|
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
211
279
|
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
212
280
|
const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
|
|
@@ -257,17 +325,18 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
257
325
|
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
258
326
|
${folded ? null : html`<ul class="inst-list">${names.map((name) => {
|
|
259
327
|
const it = byName.get(name);
|
|
260
|
-
const
|
|
328
|
+
const label = labelOf(it);
|
|
329
|
+
// The instance name under a display name only when another row has the same display name.
|
|
330
|
+
const twin = label !== name && (labelCount.get(label) || 0) > 1 ? name : "";
|
|
261
331
|
const facet = facetOf(name);
|
|
262
332
|
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
263
333
|
const on = name === active;
|
|
264
334
|
return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
265
335
|
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
|
|
266
|
-
|
|
267
|
-
<span class="inst-text"><span class="inst-name">${
|
|
268
|
-
${
|
|
269
|
-
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
|
|
270
|
-
<span class="sr-only">${statusText(facet)}</span></span>
|
|
336
|
+
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} />
|
|
337
|
+
<span class="inst-text"><span class="inst-name">${label}</span>
|
|
338
|
+
${twin || facet === "needs" ? html`<span class="inst-sub">${twin ? html`<span class="inst-alias inst-id">${twin}</span>` : null}
|
|
339
|
+
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}</span>
|
|
271
340
|
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
272
341
|
<span class=${`cli-icon cli-${KNOWN_BACKENDS.has(it.backend) ? it.backend : "other"}`} title=${backendLabel(it.backend)} aria-label=${tn("tooltipBackend", { value: backendLabel(it.backend) })}></span>
|
|
273
342
|
</a></li>`;
|
package/dist/ui/shared/signin.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// (a SameSite=Strict cookie is not sent on a navigation that came from a chat app), probe the session
|
|
5
5
|
// from *here* — a same-site request does carry the cookie — and carry on to the page that was asked
|
|
6
6
|
// for; (2) otherwise take the one-time code and exchange it. A browser that signed in before with a code from
|
|
7
|
-
// /
|
|
7
|
+
// /web (a returning device, #1570) is also offered "Send me a new code": the server sends it privately to the
|
|
8
8
|
// person who received that earlier code — the page never learns who that is.
|
|
9
9
|
(() => {
|
|
10
10
|
"use strict";
|
|
@@ -13,24 +13,24 @@
|
|
|
13
13
|
en: {
|
|
14
14
|
checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter the one-time code from your chat channel.",
|
|
15
15
|
label: "One-time code", submit: "Sign in", working: "Signing in…",
|
|
16
|
-
hint: "Send /
|
|
17
|
-
refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /
|
|
16
|
+
hint: "Send /web to your AgEnD bot, or run “agend web --code” on the host. A code works once and expires in 5 minutes.",
|
|
17
|
+
refused: "That code did not work — it may be wrong, expired or already used. Ask for a new one with /web.",
|
|
18
18
|
paused: "Too many wrong codes — sign-in is paused for a few minutes.",
|
|
19
19
|
network: "Could not reach AgEnD. Check the connection and try again.",
|
|
20
20
|
resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
|
|
21
21
|
resend_wait: "A code was asked for a moment ago — wait a little and try again.",
|
|
22
|
-
resend_refused: "A new code could not be sent. Ask for one with /
|
|
22
|
+
resend_refused: "A new code could not be sent. Ask for one with /web.",
|
|
23
23
|
},
|
|
24
24
|
"zh-TW": {
|
|
25
25
|
checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "請輸入聊天頻道給你的一次性登入碼。",
|
|
26
26
|
label: "一次性登入碼", submit: "登入", working: "登入中…",
|
|
27
|
-
hint: "在 AgEnD bot 傳 /
|
|
28
|
-
refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /
|
|
27
|
+
hint: "在 AgEnD bot 傳 /web,或在主機執行「agend web --code」。登入碼只能用一次,5 分鐘內有效。",
|
|
28
|
+
refused: "登入碼無效:可能輸入錯誤、已過期或已使用。請用 /web 再要一個。",
|
|
29
29
|
paused: "錯誤次數過多,登入暫停幾分鐘。",
|
|
30
30
|
network: "連不上 AgEnD,請檢查連線後再試。",
|
|
31
31
|
resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
|
|
32
32
|
resend_wait: "剛剛才要過登入碼,請稍候再試。",
|
|
33
|
-
resend_refused: "無法傳送新的登入碼。請用 /
|
|
33
|
+
resend_refused: "無法傳送新的登入碼。請用 /web 再要一個。",
|
|
34
34
|
},
|
|
35
35
|
};
|
|
36
36
|
const lang = /^zh/i.test(navigator.language || "") ? "zh-TW" : "en";
|
|
@@ -151,3 +151,12 @@
|
|
|
151
151
|
@media (prefers-reduced-motion: reduce) {
|
|
152
152
|
:root { --t-fast: 0ms; --t-med: 0ms; }
|
|
153
153
|
}
|
|
154
|
+
|
|
155
|
+
/* #1611: on a touch screen or the phone layout, no text field is under 16px — iOS Safari zooms the page in when one under
|
|
156
|
+
16px gets the focus, and never zooms back. One rule for every page that loads this file (the app, the sign-in page):
|
|
157
|
+
the small type sizes are 16px ON the controls themselves, so every control sized from them (the filter, search,
|
|
158
|
+
settings fields, selects, the composer) computes to 16px, and a larger one (the sign-in code, 22px) stays as it is.
|
|
159
|
+
Pinch zoom is left alone (no maximum-scale / user-scalable in the viewport); the desktop look does not change. */
|
|
160
|
+
@media (pointer: coarse), (max-width: 899px) {
|
|
161
|
+
input, select, textarea { --fs-xs: 16px; --fs-sm: 16px; --fs-md: 16px; }
|
|
162
|
+
}
|
|
@@ -18,7 +18,7 @@ register("view", {
|
|
|
18
18
|
"displayName": "Display name",
|
|
19
19
|
"role": "Role",
|
|
20
20
|
"description": "Description",
|
|
21
|
-
"saveHint": "Saving needs a signed-in session. Send /
|
|
21
|
+
"saveHint": "Saving needs a signed-in session. Send /web to your AgEnD bot for a sign-in code.",
|
|
22
22
|
"helpTitle": "📖 Help",
|
|
23
23
|
"help1": "Pick an instance in the list: its live terminal shows here.",
|
|
24
24
|
"help2": "Click a group's name to fold it or open it again.",
|
|
@@ -180,7 +180,7 @@ register("view", {
|
|
|
180
180
|
"displayName": "顯示名稱",
|
|
181
181
|
"role": "角色",
|
|
182
182
|
"description": "描述",
|
|
183
|
-
"saveHint": "儲存需要已登入的 session。請在 AgEnD bot 傳 /
|
|
183
|
+
"saveHint": "儲存需要已登入的 session。請在 AgEnD bot 傳 /web 取得登入碼。",
|
|
184
184
|
"helpTitle": "📖 使用說明",
|
|
185
185
|
"help1": "在清單裡選一個 instance:它的即時終端畫面會顯示在這裡。",
|
|
186
186
|
"help2": "點群組名稱可以收合或展開。",
|
package/dist/ui/signin.html
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
<button id="go" type="submit" data-i18n="submit">Sign in</button>
|
|
50
50
|
<button id="resend" type="button" class="secondary" hidden data-i18n="resend">Send me a new code</button>
|
|
51
51
|
<div id="msg" role="alert" aria-live="polite"></div>
|
|
52
|
-
<div class="hint" data-i18n="hint">Send /
|
|
52
|
+
<div class="hint" data-i18n="hint">Send /web to your AgEnD bot, or run “<code>agend web --code</code>” on the host. A code works once and expires in 5 minutes.</div>
|
|
53
53
|
</form>
|
|
54
54
|
<noscript>This page needs JavaScript to sign you in.</noscript>
|
|
55
55
|
</main>
|