@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.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 +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +68 -12
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +82 -33
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +115 -0
- package/dist/cache-service.js +417 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +251 -24
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +90 -37
- package/dist/daemon.js.map +1 -1
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.js +22 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +46 -0
- package/dist/fleet-manager.js +287 -40
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/locale.js +14 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart.js +10 -4
- package/dist/quickstart.js.map +1 -1
- package/dist/restart-guard.d.ts +52 -0
- package/dist/restart-guard.js +109 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.js +6 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-activation.d.ts +47 -5
- package/dist/service-activation.js +156 -51
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +134 -12
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/settings-api.js +46 -5
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +34 -6
- package/dist/settings-change.js.map +1 -1
- package/dist/setup-host.js +2 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +89 -5
- package/dist/tmux-control.js.map +1 -1
- package/dist/topic-commands.js +14 -5
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +2 -2
- package/dist/ui/chat-store.js +30 -1
- package/dist/ui/chat-strings.js +22 -2
- package/dist/ui/chat-thread.js +47 -10
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/panel-chat.js +104 -18
- package/dist/ui/panel-fleet.js +26 -12
- package/dist/ui/panel-settings.js +12 -8
- package/dist/ui/preview-panel.js +202 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-strings.js +4 -2
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +6 -4
- package/dist/ui/shared/app.css +147 -3
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/signin.html +2 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +65 -7
- package/dist/update-install.js +126 -26
- package/dist/update-install.js.map +1 -1
- package/dist/web-api.d.ts +14 -0
- package/dist/web-api.js +65 -0
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.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/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +114 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +12 -4
- package/scripts/preinstall-guard.cjs +40 -21
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +1 -1
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -18,13 +18,14 @@ import { toast } from "/assets/ui-toast.js";
|
|
|
18
18
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
19
19
|
import { confirmedWrite } from "./settings-confirm.js";
|
|
20
20
|
import { OrgChart } from "./fleet-org.js";
|
|
21
|
+
import { CacheReport } from "./fleet-cache.js";
|
|
21
22
|
|
|
22
23
|
register("fleet", {
|
|
23
24
|
en: {
|
|
24
|
-
title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", config: "Config",
|
|
25
|
+
title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
|
|
25
26
|
newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
|
|
26
27
|
noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
|
|
27
|
-
noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams",
|
|
28
|
+
noTeams: "No teams yet", noTeamsHint: "Group instances for coordinated work.", countTasks: "{0} tasks", countSchedules: "{0} schedules", countTeams: "{0} teams", countTask: "1 task", countSchedule: "1 schedule", countTeam: "1 team",
|
|
28
29
|
claim: "Claim", done: "Done", delete: "Delete", on: "on", off: "off", cancel: "Cancel", create: "Create", creating: "Creating…", save: "Save changes",
|
|
29
30
|
taskTitle: "Title", taskDesc: "Description", priority: "Priority", assignee: "Assignee", unassigned: "Unassigned",
|
|
30
31
|
pNormal: "Normal", pLow: "Low", pHigh: "High", pUrgent: "Urgent",
|
|
@@ -41,10 +42,10 @@ register("fleet", {
|
|
|
41
42
|
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
42
43
|
},
|
|
43
44
|
"zh-TW": {
|
|
44
|
-
title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", config: "設定",
|
|
45
|
+
title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
|
|
45
46
|
newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
|
|
46
47
|
noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
|
|
47
|
-
noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team",
|
|
48
|
+
noTeams: "還沒有 team", noTeamsHint: "把 instance 分組一起工作。", countTasks: "{0} 個 task", countSchedules: "{0} 個排程", countTeams: "{0} 個 team", countTask: "1 個 task", countSchedule: "1 個排程", countTeam: "1 個 team",
|
|
48
49
|
claim: "認領", done: "完成", delete: "刪除", on: "開", off: "關", cancel: "取消", create: "建立", creating: "建立中…", save: "儲存變更",
|
|
49
50
|
taskTitle: "標題", taskDesc: "說明", priority: "優先順序", assignee: "負責人", unassigned: "未指派",
|
|
50
51
|
pNormal: "一般", pLow: "低", pHigh: "高", pUrgent: "緊急",
|
|
@@ -62,7 +63,7 @@ register("fleet", {
|
|
|
62
63
|
},
|
|
63
64
|
});
|
|
64
65
|
|
|
65
|
-
const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", config: "sliders" };
|
|
66
|
+
const ICONS = { tasks: "tasks", schedules: "clock", teams: "team", org: "org", cache: "chart", config: "sliders" };
|
|
66
67
|
const BACKENDS = ["claude-code", "codex", "opencode", "kiro-cli", "antigravity", "grok", "muse"];
|
|
67
68
|
|
|
68
69
|
async function api(method, path, body, lease) {
|
|
@@ -73,7 +74,7 @@ async function api(method, path, body, lease) {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
|
-
* Load `path` under the lease: `null` while the first load runs, `{ error }` on failure. A refresh (a new `version`,
|
|
77
|
+
* Load `path` under the lease: `null` while the first load runs, `{ error }` on failure (no answer, or an HTTP error). A refresh (a new `version`,
|
|
77
78
|
* after Claim or Create) keeps showing what is there until the new list arrives — nothing below it is torn down, so
|
|
78
79
|
* an open dialog and its draft survive (#1425 review). Only the latest request's answer is kept.
|
|
79
80
|
*/
|
|
@@ -86,7 +87,10 @@ function useLoad(lease, path, version) {
|
|
|
86
87
|
if (shownFor.current !== lease) { shownFor.current = lease; setState(null); }
|
|
87
88
|
(async () => {
|
|
88
89
|
try {
|
|
89
|
-
|
|
90
|
+
// A read the server refused (a 4xx/5xx with an { error } body) is a failure, not an empty list (alpha.2 sweep).
|
|
91
|
+
const r = await lease.fetch(path, { method: "GET", headers: { "Content-Type": "application/json" } });
|
|
92
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
93
|
+
const d = await r.json();
|
|
90
94
|
if (lease.current() && mine === seq.current) setState({ data: d });
|
|
91
95
|
} catch { if (lease.current() && mine === seq.current) setState({ error: true }); }
|
|
92
96
|
})();
|
|
@@ -98,12 +102,12 @@ export function FleetPanel({ route, navKey }) {
|
|
|
98
102
|
const lease = useLease(navKey);
|
|
99
103
|
const tab = route.tab;
|
|
100
104
|
useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
|
|
101
|
-
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, config: Config }[tab] || Tasks;
|
|
105
|
+
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
|
|
102
106
|
return html`<div class="panel p-fleet">
|
|
103
107
|
<${PanelHeader} title=${t("fleet.title")} />
|
|
104
108
|
<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" : ""}`}
|
|
105
109
|
aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
|
|
106
|
-
<div class="panel-body"><div class=${`col${tab === "org" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
110
|
+
<div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
107
111
|
</div>`;
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -126,7 +130,7 @@ function Tasks({ lease }) {
|
|
|
126
130
|
}
|
|
127
131
|
return html`<${Loaded} state=${state}>${(d) => {
|
|
128
132
|
const tasks = d.tasks || [];
|
|
129
|
-
return html`<${ListHead} count=${t("fleet.countTasks", tasks.length)} label=${t("fleet.newTask")} action=${{ onClick: () => setCreating(true) }} />
|
|
133
|
+
return html`<${ListHead} count=${tasks.length === 1 ? t("fleet.countTask") : t("fleet.countTasks", tasks.length)} label=${t("fleet.newTask")} action=${{ onClick: () => setCreating(true) }} />
|
|
130
134
|
${tasks.length ? html`<ul class="rows">${tasks.map(x => html`<li key=${x.id} class="row-item">
|
|
131
135
|
<span class=${`pill ${x.status}`}>${x.status}</span><span class="grow">${x.title}</span>
|
|
132
136
|
${x.assignee ? html`<a class="link" href=${chatPath(x.assignee)}>${x.assignee}</a>` : null}
|
|
@@ -150,7 +154,7 @@ function Schedules({ lease }) {
|
|
|
150
154
|
}
|
|
151
155
|
return html`<${Loaded} state=${state}>${(d) => {
|
|
152
156
|
const list = d.schedules || [];
|
|
153
|
-
return html`<${ListHead} count=${t("fleet.countSchedules", list.length)} label=${t("fleet.newSchedule")} action=${{ onClick: () => setCreating(true) }} />
|
|
157
|
+
return html`<${ListHead} count=${list.length === 1 ? t("fleet.countSchedule") : t("fleet.countSchedules", list.length)} label=${t("fleet.newSchedule")} action=${{ onClick: () => setCreating(true) }} />
|
|
154
158
|
${list.length ? html`<ul class="rows">${list.map(x => html`<li key=${x.id} class="row-item">
|
|
155
159
|
<span class=${`pill ${x.enabled ? "open" : "done"}`}>${x.enabled ? t("fleet.on") : t("fleet.off")}</span>
|
|
156
160
|
<span class="grow">${x.label || cronDesc(x.cron)}</span><span class="mono muted" title=${cronDesc(x.cron)}>${x.cron}</span>
|
|
@@ -174,7 +178,7 @@ function Teams({ lease }) {
|
|
|
174
178
|
}
|
|
175
179
|
return html`<${Loaded} state=${state}>${(d) => {
|
|
176
180
|
const entries = Object.entries(d.teams || {});
|
|
177
|
-
return html`<${ListHead} count=${t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${{ onClick: () => setCreating(true) }} />
|
|
181
|
+
return html`<${ListHead} count=${entries.length === 1 ? t("fleet.countTeam") : t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${{ onClick: () => setCreating(true) }} />
|
|
178
182
|
${entries.length ? html`<ul class="rows">${entries.map(([name, team]) => html`<li key=${name} class="row-item">
|
|
179
183
|
<span class="grow strong">${name}</span><span class="muted">${(team.members || []).join(", ")}</span>
|
|
180
184
|
<button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(name)}>${t("fleet.delete")}</button></li>`)}</ul>`
|
|
@@ -191,6 +195,16 @@ function Org({ lease }) {
|
|
|
191
195
|
return html`<${Loaded} state=${state}>${(org) => html`<${OrgChart} org=${org} app=${app} />`}</${Loaded}>`;
|
|
192
196
|
}
|
|
193
197
|
|
|
198
|
+
/** #1468: the prompt-cache analysis — one read per opening and per window chosen; Refresh reads again (fleet-cache.js). */
|
|
199
|
+
function Cache({ lease }) {
|
|
200
|
+
const [win, setWin] = useState("7d");
|
|
201
|
+
const [version, setVersion] = useState(0);
|
|
202
|
+
const state = useLoad(lease, `/ui/cache?window=${win}`, version);
|
|
203
|
+
return html`<${Loaded} state=${state}>${(report) => (report && Array.isArray(report.instances)
|
|
204
|
+
? html`<${CacheReport} report=${report} win=${win} setWin=${setWin} refresh=${() => setVersion((v) => v + 1)} />`
|
|
205
|
+
: html`<${ErrorState} onRetry=${renavigate} />`)}</${Loaded}>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
function Secret({ id, value, onInput, label }) {
|
|
195
209
|
const [shown, setShown] = useState(false);
|
|
196
210
|
return html`<div class="field-row"><input id=${id} type=${shown ? "text" : "password"} value=${value} onInput=${onInput} autocomplete="off" />
|
|
@@ -12,7 +12,7 @@ import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js"
|
|
|
12
12
|
import { t } from "/assets/app-i18n.js";
|
|
13
13
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
14
14
|
import { useLease } from "/assets/app-ctx.js";
|
|
15
|
-
import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
15
|
+
import { PanelHeader, PendingChanges, setTitle } from "/assets/app-shell.js";
|
|
16
16
|
import { navigate, setLeaveGuard } from "/assets/app-nav.js";
|
|
17
17
|
import { settingsPath, SETTINGS_SECTIONS } from "/assets/app-route.js";
|
|
18
18
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
@@ -66,7 +66,8 @@ async function loadAll(lease) {
|
|
|
66
66
|
const connections = await read(lease, "/api/settings/connections");
|
|
67
67
|
if (!lease.current()) return null;
|
|
68
68
|
out.connections = connections.ok && Array.isArray(connections.body) ? connections.body : [];
|
|
69
|
-
|
|
69
|
+
// Only where the fleet offers them (the schema says so): otherwise the route is a 404 and there is nothing to show.
|
|
70
|
+
const secrets = out.schema && out.schema.provider_secrets === false ? { ok: false } : await read(lease, "/api/settings/provider-secrets");
|
|
70
71
|
if (!lease.current()) return null;
|
|
71
72
|
out.providerOk = secrets.ok;
|
|
72
73
|
out.providerSecrets = secrets.ok && Array.isArray(secrets.body) ? secrets.body : [];
|
|
@@ -218,6 +219,7 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
218
219
|
${searchable ? html`<div class="s-toolbar"><label class="s-search"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("searchAll")}</span>
|
|
219
220
|
<input type="search" value=${search} placeholder=${tn("searchAll")} onInput=${(e) => setSearch(e.target.value)} /></label></div>` : null}
|
|
220
221
|
${op ? html`<${OperationCard} op=${op} schema=${data && data.schema} />` : null}
|
|
222
|
+
<${PendingChanges} inline=${true} />
|
|
221
223
|
${sectionBody}
|
|
222
224
|
</div></div>
|
|
223
225
|
${staged.size ? html`<${PendingBar} staged=${staged} busy=${opBusy(op)} onApply=${apply} onDiscard=${discard} />` : null}
|
|
@@ -329,7 +331,7 @@ function Agents({ ctx, search, openDialog }) {
|
|
|
329
331
|
setBusy((b) => new Map(b).set(name, action));
|
|
330
332
|
try { await work(); } finally { setBusy((b) => { const n = new Map(b); n.delete(name); return n; }); }
|
|
331
333
|
};
|
|
332
|
-
const head = html`<div class="list-head"><span>${tn("countAgents", Object.keys(insts).length)}</span>
|
|
334
|
+
const head = html`<div class="list-head"><span>${Object.keys(insts).length === 1 ? tn("countAgent") : tn("countAgents", Object.keys(insts).length)}</span>
|
|
333
335
|
<button type="button" class="btn btn-primary" onClick=${() => openDialog({ kind: "create" })}><${Icon} name="plus" size=${16} />${tn("newAgent")}</button></div>`;
|
|
334
336
|
if (!Object.keys(insts).length) return html`${head}<${Empty} icon="bot" title=${tn("noAgents")} />`;
|
|
335
337
|
if (!groups.length) return html`${head}<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
|
|
@@ -350,6 +352,7 @@ function Agents({ ctx, search, openDialog }) {
|
|
|
350
352
|
<span class=${dotClass(st)} title=${tn(STATUS_KEY[st] || "stStopped")} aria-hidden="true"></span>
|
|
351
353
|
<span class="s-name" title=${name}>${shortName(name, inst)}</span>
|
|
352
354
|
<span class="s-meta" title=${sum.backend}>${sum.model}</span>
|
|
355
|
+
<span class="s-br" aria-hidden="true"></span>
|
|
353
356
|
<span class="tag">${tn("effortTag", sum.effort)}</span>
|
|
354
357
|
${ch ? html`<span class=${`tag${ch.includes("persona") ? " persona" : ""}`}>${ch}</span>` : null}
|
|
355
358
|
<span class="sr-only">${tn(STATUS_KEY[st] || "stStopped")}</span>
|
|
@@ -370,7 +373,7 @@ function Bots({ ctx, search, openDialog }) {
|
|
|
370
373
|
const all = channelsOf(ctx.fleet);
|
|
371
374
|
const needle = search.trim().toLowerCase();
|
|
372
375
|
const rows = all.map((ch, i) => ({ ch, i })).filter(({ ch }) => !needle || [ch.id, ch.type, ch.group_id, ch.bot_token_env].some((v) => String(v ?? "").toLowerCase().includes(needle)));
|
|
373
|
-
const head = html`<div class="list-head"><span>${tn("countBots", all.length)}</span>
|
|
376
|
+
const head = html`<div class="list-head"><span>${all.length === 1 ? tn("countBot") : tn("countBots", all.length)}</span>
|
|
374
377
|
<button type="button" class="btn btn-primary" onClick=${() => openDialog({ kind: "newBot" })}><${Icon} name="plus" size=${16} />${tn("newBot")}</button></div>`;
|
|
375
378
|
return html`${head}
|
|
376
379
|
${!rows.length ? html`<${Empty} icon="plug" title=${needle ? tn("noAgentMatch", search.trim()) : tn("noBots")} />` : html`<div class="s-list">${rows.map(({ ch, i }) => {
|
|
@@ -464,7 +467,7 @@ function Classic({ ctx, search, openDialog }) {
|
|
|
464
467
|
.sort((a, b) => ((STATUS_ORDER[ctx.classicRuntime(a.instanceName).status] ?? 9) - (STATUS_ORDER[ctx.classicRuntime(b.instanceName).status] ?? 9))
|
|
465
468
|
|| String(a.name || "").localeCompare(String(b.name || "")));
|
|
466
469
|
if (!list.length) return html`<${Empty} icon="search" title=${tn("noAgentMatch", search.trim())} />`;
|
|
467
|
-
return html`<div class="list-head"><span>${tn("countRooms", rooms.length)}</span></div><div class="s-list">${list.map((c) => {
|
|
470
|
+
return html`<div class="list-head"><span>${rooms.length === 1 ? tn("countRoom") : tn("countRooms", rooms.length)}</span></div><div class="s-list">${list.map((c) => {
|
|
468
471
|
const rt = ctx.classicRuntime(c.instanceName);
|
|
469
472
|
const sum = effectiveSummary(ctx.live[c.instanceName] || {}, c, ctx.fleet.defaults || {}, (ctx.classic && ctx.classic.defaults) || {});
|
|
470
473
|
const adapter = c.adapterId || "discord";
|
|
@@ -472,6 +475,7 @@ function Classic({ ctx, search, openDialog }) {
|
|
|
472
475
|
<span class=${dotClass(rt.execution)} title=${tn(`exec_${rt.execution}`)} aria-hidden="true"></span>
|
|
473
476
|
<span class="s-name" title=${c.instanceName || ""}>${shortName(c.name || c.instanceName || "?", { display_name: c.display_name })}</span>
|
|
474
477
|
<span class="s-meta" title=${sum.backend}>${sum.model}</span>
|
|
478
|
+
<span class="s-br" aria-hidden="true"></span>
|
|
475
479
|
<span class="tag">${tn("effortTag", sum.effort)}</span>
|
|
476
480
|
<span class=${`tag${adapter !== "discord" ? " persona" : ""}`}>${adapter}</span>
|
|
477
481
|
${c.collab ? html`<span class="tag">collab</span>` : null}
|
|
@@ -666,10 +670,10 @@ function Developer({ ctx }) {
|
|
|
666
670
|
<div class="seg-inline" role="group" aria-label=${tn("format")}>
|
|
667
671
|
${["yaml", "json"].map((k) => html`<button key=${k} type="button" class=${`btn btn-sm${fmt === k ? " on" : ""}`} aria-pressed=${fmt === k ? "true" : "false"} onClick=${() => setFmt(k)}>${k.toUpperCase()}</button>`)}</div>
|
|
668
672
|
<button type="button" class="btn btn-sm" onClick=${() => setEditing(!editing)}>${editing ? tn("viewMode") : tn("editMode")}</button>
|
|
669
|
-
<button type="button" class="btn btn-sm" onClick=${copy}><${Icon} name="copy" size=${14}
|
|
670
|
-
<button type="button" class="btn btn-sm" onClick=${download}><${Icon} name="download" size=${14}
|
|
673
|
+
<button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
|
|
674
|
+
<button type="button" class="btn btn-sm" title="fleet.yaml" onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">fleet.yaml</span></button>
|
|
671
675
|
</div>
|
|
672
|
-
${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
676
|
+
${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck="false" wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
673
677
|
<div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
|
|
674
678
|
: html`<pre class="s-yaml">${text}</pre>`}
|
|
675
679
|
${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// #1481: an agent's HTML preview (#1306) in a panel beside the conversation — a split with a draggable divider on a
|
|
2
|
+
// wide screen, a full-screen sheet on a narrow one. Nothing about the preview itself changes here: the frame is the one
|
|
3
|
+
// preview.js makes (mountPreview: the preview origin, sandbox allow-scripts only), started by AgendPreview.start like a
|
|
4
|
+
// card's, only told to fill the panel. It runs on a click, as a card's does:
|
|
5
|
+
// - "Open in panel" on an idle card opens the panel with its Preview button — the panel runs nothing by itself;
|
|
6
|
+
// - on a card whose preview is running, the same button moves that run to the panel (stopped in the card, started in
|
|
7
|
+
// the panel: a moved frame reloads anyway);
|
|
8
|
+
// - a newer version of the block in a later reply is offered, never swapped in, and offered with Preview to click.
|
|
9
|
+
// Closing the panel or leaving the chat stops its frame; one preview per page still holds (start() stops any other).
|
|
10
|
+
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
11
|
+
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
13
|
+
import "./chat-render.js";
|
|
14
|
+
import "./preview.js";
|
|
15
|
+
|
|
16
|
+
const R = () => globalThis.AgendChatRender;
|
|
17
|
+
const P = () => globalThis.AgendPreview;
|
|
18
|
+
|
|
19
|
+
// ── The panel's state, one per page (the chat shows one instance at a time) ──
|
|
20
|
+
|
|
21
|
+
/** open: { key, instance, msgKey, n, code, sender, ts } | null; run: start it on mount (a moved run). */
|
|
22
|
+
let state = { open: null, run: 0 };
|
|
23
|
+
const subs = new Set();
|
|
24
|
+
function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
25
|
+
export const panelStore = { get: () => state, subscribe(fn) { subs.add(fn); return () => { subs.delete(fn); }; } };
|
|
26
|
+
|
|
27
|
+
/** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
|
|
28
|
+
export const frameKey = (key) => `panel:${key}`;
|
|
29
|
+
/** Show `spec` in the panel; `run`: start it there (a click that moves a running preview). The block shown before stops. */
|
|
30
|
+
export function openPanel(spec, { run = false } = {}) {
|
|
31
|
+
if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
|
|
32
|
+
set({ open: { ...spec }, run: run ? state.run + 1 : 0 });
|
|
33
|
+
}
|
|
34
|
+
/** Close the panel: its frame stops first. */
|
|
35
|
+
export function closePanel() {
|
|
36
|
+
if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
|
|
37
|
+
if (state.open) set({ open: null, run: 0 });
|
|
38
|
+
}
|
|
39
|
+
/** The card key the panel shows, or null. */
|
|
40
|
+
export const shownKey = () => (state.open ? state.open.key : null);
|
|
41
|
+
|
|
42
|
+
/** The text of a block's <title>, or "" — what says two blocks are versions of one thing. */
|
|
43
|
+
export function htmlTitle(code) {
|
|
44
|
+
const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(String(code || ""));
|
|
45
|
+
return m ? m[1].replace(/\s+/g, " ").trim() : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A newer version of the shown block: the last complete ```html block in an agent reply after the one it came from.
|
|
50
|
+
* When the shown block has a <title>, only a block with the same title counts (another page is not a new version);
|
|
51
|
+
* without one, the newest block does. `msgs` in order; `keyOf` makes a message's key.
|
|
52
|
+
*/
|
|
53
|
+
export function newerVersion(open, msgs, keyOf) {
|
|
54
|
+
if (!open || !Array.isArray(msgs)) return null;
|
|
55
|
+
const at = msgs.findIndex(x => keyOf(x) === open.msgKey);
|
|
56
|
+
if (at < 0) return null;
|
|
57
|
+
const title = htmlTitle(open.code);
|
|
58
|
+
for (let i = msgs.length - 1; i > at; i--) {
|
|
59
|
+
const x = msgs[i];
|
|
60
|
+
if (x.role !== "agent") continue;
|
|
61
|
+
const fences = R().htmlFences(x.text);
|
|
62
|
+
for (let n = fences.length - 1; n >= 0; n--) {
|
|
63
|
+
const f = fences[n];
|
|
64
|
+
if (!f.terminated || (title && htmlTitle(f.code) !== title)) continue;
|
|
65
|
+
const msgKey = keyOf(x);
|
|
66
|
+
return { key: `${msgKey}:f${n}`, instance: open.instance, msgKey, n, code: f.code, sender: x.sender, ts: x.ts };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── The panel's width: this device's choice (a convenience — the panel works without storage) ──
|
|
73
|
+
|
|
74
|
+
const WIDTH_KEY = "agend_preview_panel_w";
|
|
75
|
+
export const MIN_PANEL = 320, MIN_THREAD = 360, STEP = 32;
|
|
76
|
+
function storedWidth() { try { const v = Number(localStorage.getItem(WIDTH_KEY)); return Number.isFinite(v) && v > 0 ? v : null; } catch { return null; } }
|
|
77
|
+
function storeWidth(w) { try { localStorage.setItem(WIDTH_KEY, String(Math.round(w))); } catch { /* this page only */ } }
|
|
78
|
+
/** The width a panel may have in a split `total` px wide: at least MIN_PANEL, leaving the thread MIN_THREAD. */
|
|
79
|
+
export function clampWidth(w, total) {
|
|
80
|
+
const max = Math.max(MIN_PANEL, total - MIN_THREAD - 8);
|
|
81
|
+
return Math.round(Math.min(max, Math.max(MIN_PANEL, w)));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "");
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The panel, inside the chat's split (`split`: that element — the divider sets its width there, through CSSOM).
|
|
88
|
+
* props: name (the instance), msgs() → its messages, subscribe(fn) → unsubscribe (the chat store's), keyOf(x),
|
|
89
|
+
* reveal(msgKey) (the thread shows that message), download(code).
|
|
90
|
+
*/
|
|
91
|
+
export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, download }) {
|
|
92
|
+
const [st, setSt] = useState(state);
|
|
93
|
+
const [view, setView] = useState("preview");
|
|
94
|
+
const [run, setRun] = useState({ state: "idle", reason: "" });
|
|
95
|
+
const [newer, setNewer] = useState(null);
|
|
96
|
+
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
97
|
+
const open = st.open && st.open.instance === name ? st.open : null;
|
|
98
|
+
|
|
99
|
+
useEffect(() => panelStore.subscribe(setSt), []);
|
|
100
|
+
// This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
|
|
101
|
+
const [, choice] = useState(0);
|
|
102
|
+
useEffect(() => (P() ? P().onChange(() => choice(n => n + 1)) : undefined), []);
|
|
103
|
+
// Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
|
|
104
|
+
useEffect(() => () => closePanel(), []);
|
|
105
|
+
|
|
106
|
+
const ui = { state: (s, reason) => setRun({ state: s === "starting" || s === "running" ? s : "idle", reason: reason || "" }) };
|
|
107
|
+
const start = () => {
|
|
108
|
+
if (!open || !holder.current) return;
|
|
109
|
+
setView("preview");
|
|
110
|
+
P().start(frameKey(open.key), holder.current, open.code, ui, { fill: true });
|
|
111
|
+
};
|
|
112
|
+
const stop = () => { if (open) P().stop(frameKey(open.key), "stopped", ""); };
|
|
113
|
+
// A new block in the panel: what ran before is gone (openPanel stopped it); a moved run starts here.
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
setRun({ state: "idle", reason: "" });
|
|
116
|
+
setView("preview");
|
|
117
|
+
if (open && st.run) start();
|
|
118
|
+
// Focus goes to the panel's close control that is on screen (Back on a phone, × beside the thread).
|
|
119
|
+
if (panel.current) { const f = [...panel.current.querySelectorAll(".pv-panel-close")].find(b => b.offsetParent !== null); if (f) f.focus(); }
|
|
120
|
+
}, [open && open.key, st.run]);
|
|
121
|
+
// A later reply with a newer version of this block: offered, not swapped in.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!open) { setNewer(null); return undefined; }
|
|
124
|
+
const check = () => setNewer(newerVersion(open, msgs() || [], keyOf));
|
|
125
|
+
check();
|
|
126
|
+
return subscribe((instance, kind) => { if (instance === name && kind === "msgs") check(); });
|
|
127
|
+
}, [open && open.key, name]);
|
|
128
|
+
|
|
129
|
+
// The divider: drag, or the arrow keys, Home and End; the width is this device's.
|
|
130
|
+
const widthNow = useRef(MIN_PANEL);
|
|
131
|
+
const applyWidth = (w, save) => {
|
|
132
|
+
const sp = split.current; if (!sp) return;
|
|
133
|
+
const total = sp.clientWidth || 0;
|
|
134
|
+
const v = clampWidth(w, total);
|
|
135
|
+
widthNow.current = v;
|
|
136
|
+
sp.style.setProperty("--pv-w", `${v}px`); // CSSOM: the page's CSP allows no style attribute
|
|
137
|
+
if (divider.current) { divider.current.setAttribute("aria-valuenow", String(v)); divider.current.setAttribute("aria-valuemax", String(clampWidth(Infinity, total))); }
|
|
138
|
+
if (save) storeWidth(v);
|
|
139
|
+
};
|
|
140
|
+
useEffect(() => { if (open) applyWidth(storedWidth() ?? (split.current && split.current.clientWidth || 0) / 2, false); }, [!!open]);
|
|
141
|
+
const width = () => widthNow.current;
|
|
142
|
+
const setWidth = (w) => applyWidth(w, true);
|
|
143
|
+
const onKey = (e) => {
|
|
144
|
+
const k = e.key;
|
|
145
|
+
if (k === "ArrowLeft") setWidth(width() + STEP);
|
|
146
|
+
else if (k === "ArrowRight") setWidth(width() - STEP);
|
|
147
|
+
else if (k === "Home") setWidth(Infinity);
|
|
148
|
+
else if (k === "End") setWidth(0);
|
|
149
|
+
else return;
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
};
|
|
152
|
+
const onDown = (e) => {
|
|
153
|
+
const sp = split.current; if (!sp || e.button !== 0) return;
|
|
154
|
+
e.preventDefault();
|
|
155
|
+
const el = e.currentTarget;
|
|
156
|
+
try { el.setPointerCapture(e.pointerId); } catch { /* the move still works while over the divider */ }
|
|
157
|
+
sp.classList.add("resizing"); // frames take no pointer events meanwhile, or the drag would stop over one
|
|
158
|
+
const right = sp.getBoundingClientRect().right;
|
|
159
|
+
const move = (ev) => applyWidth(right - ev.clientX - 4, false);
|
|
160
|
+
const up = () => { sp.classList.remove("resizing"); storeWidth(widthNow.current); el.removeEventListener("pointermove", move); el.removeEventListener("pointerup", up); el.removeEventListener("pointercancel", up); };
|
|
161
|
+
el.addEventListener("pointermove", move);
|
|
162
|
+
el.addEventListener("pointerup", up);
|
|
163
|
+
el.addEventListener("pointercancel", up);
|
|
164
|
+
};
|
|
165
|
+
// Esc in the panel closes it (and is not the chat's "stop the reply").
|
|
166
|
+
const onPanelKey = (e) => { if (e.key === "Escape" && !e.isComposing) { e.preventDefault(); closePanel(); } };
|
|
167
|
+
|
|
168
|
+
if (!open) return null;
|
|
169
|
+
const going = run.state === "starting" || run.state === "running";
|
|
170
|
+
const a = P().availability();
|
|
171
|
+
const note = run.reason || (going ? (run.state === "starting" ? t("chat.pvStarting") : "") : a.ok ? t("chat.pvPanelIdle") : a.reason);
|
|
172
|
+
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
173
|
+
return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
174
|
+
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
|
175
|
+
<aside class="pv-panel" ref=${panel} aria-label=${t("chat.pvPanel")} onKeyDown=${onPanelKey}>
|
|
176
|
+
<div class="pv-panel-head">
|
|
177
|
+
<button type="button" class="icon-btn only-narrow pv-panel-close" title=${t("chat.pvBack")} aria-label=${t("chat.pvBack")} onClick=${closePanel}><${Icon} name="back" size=${18} /></button>
|
|
178
|
+
<div class="pv-panel-title"><strong>${t("chat.pvPanel")}</strong>
|
|
179
|
+
<button type="button" class="pv-source" title=${source} onClick=${() => { if (window.matchMedia && window.matchMedia("(max-width: 899px)").matches) closePanel(); reveal(open.msgKey); }}>${source}</button></div>
|
|
180
|
+
<button type="button" class="icon-btn only-wide pv-panel-close" title=${t("chat.pvClose")} aria-label=${t("chat.pvClose")} onClick=${closePanel}><${Icon} name="close" size=${18} /></button>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="pv-panel-bar">
|
|
183
|
+
<div class="seg-inline" role="group" aria-label=${t("chat.pvPanel")}>
|
|
184
|
+
<button type="button" class=${`btn btn-sm${view === "preview" ? " on" : ""}`} aria-pressed=${view === "preview" ? "true" : "false"} onClick=${() => setView("preview")}><${Icon} name="eye" size=${14} />${t("chat.pvPage")}</button>
|
|
185
|
+
<button type="button" class=${`btn btn-sm${view === "code" ? " on" : ""}`} aria-pressed=${view === "code" ? "true" : "false"} onClick=${() => setView("code")}><${Icon} name="code" size=${14} />${t("chat.pvCode")}</button>
|
|
186
|
+
</div>
|
|
187
|
+
<span class="grow"></span>
|
|
188
|
+
${going ? html`<button type="button" class="btn btn-sm pv-panel-reload" title=${t("chat.pvReload")} onClick=${() => { stop(); start(); }}><${Icon} name="restart" size=${14} /><span class="lbl">${t("chat.pvReload")}</span></button>
|
|
189
|
+
<button type="button" class="btn btn-sm pv-panel-stop" title=${t("chat.pvStop")} onClick=${stop}><${Icon} name="stop" size=${14} /><span class="lbl">${t("chat.pvStop")}</span></button>`
|
|
190
|
+
: html`<button type="button" class="btn btn-sm btn-primary pv-panel-run" title=${t("chat.pvPreview")} disabled=${!a.ok} onClick=${start}><${Icon} name="play" size=${14} /><span class="lbl">${t("chat.pvPreview")}</span></button>`}
|
|
191
|
+
<button type="button" class="btn btn-sm pv-panel-dl" title=${t("chat.pvDownload")} onClick=${() => download(open.code)}><${Icon} name="download" size=${14} /><span class="lbl">${t("chat.pvDownload")}</span></button>
|
|
192
|
+
</div>
|
|
193
|
+
${newer && newer.key !== open.key ? html`<div class="pv-newer" role="status"><span>${t("chat.pvNewer", timeOf(newer.ts))}</span>
|
|
194
|
+
<button type="button" class="btn btn-sm pv-newer-show" onClick=${() => openPanel(newer)}>${t("chat.pvNewerShow")}</button></div>` : null}
|
|
195
|
+
${going ? html`<div class="pv-banner">${P().BANNER}</div>` : null}
|
|
196
|
+
${note && view === "preview" ? html`<div class="pv-note">${note}</div>` : null}
|
|
197
|
+
<div class=${`pv-panel-body${view === "code" ? " show-code" : ""}`}>
|
|
198
|
+
<div class="pv-holder" ref=${holder}></div>
|
|
199
|
+
${view === "code" ? html`<pre class="pv-code"><code>${open.code}</code></pre>` : null}
|
|
200
|
+
</div>
|
|
201
|
+
</aside>`;
|
|
202
|
+
}
|
package/dist/ui/preview.js
CHANGED
|
@@ -128,8 +128,10 @@
|
|
|
128
128
|
/**
|
|
129
129
|
* Run `html` in `holder` (a node of the card) for `key`. `ui` is told what happened: ui.state(name, reason) with
|
|
130
130
|
* "starting" | "running" | "stopped" | "unavailable". One preview at a time: any other running one stops first.
|
|
131
|
+
* opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
|
|
132
|
+
* panel instead of by the frame's height messages, which it then ignores.
|
|
131
133
|
*/
|
|
132
|
-
function start(key, holder, html, ui) {
|
|
134
|
+
function start(key, holder, html, ui, opts) {
|
|
133
135
|
var a = availability();
|
|
134
136
|
if (!a.ok) { ui.state("unavailable", a.reason); return false; }
|
|
135
137
|
if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", "This HTML is over 1 MiB; it is not previewed."); return false; }
|
|
@@ -139,14 +141,15 @@
|
|
|
139
141
|
var card = {
|
|
140
142
|
key: key, iframe: iframe, ch: randomCh(), holder: holder, html: html, ui: ui, rendered: false,
|
|
141
143
|
height: LIMITS.minHeight, lastApplied: -Infinity, appliedInSecond: [], frozen: false, growth: [], pendingFrame: false,
|
|
142
|
-
readyTimer: null, watchdogTimer: null,
|
|
144
|
+
readyTimer: null, watchdogTimer: null, fill: !!(opts && opts.fill),
|
|
143
145
|
};
|
|
146
|
+
if (card.fill) iframe.classList.add("fill");
|
|
144
147
|
live.set(key, card);
|
|
145
148
|
card.readyTimer = root.setTimeout(function () {
|
|
146
149
|
if (live.get(key) === card && !card.rendered) stop(key, "unavailable", "Preview unavailable: the preview did not answer within 3 s. If an earlier preview froze, it may still be running — open the dashboard in a new tab. Over SSH, forward the preview port too.");
|
|
147
150
|
}, LIMITS.readyMs);
|
|
148
151
|
holder.appendChild(iframe);
|
|
149
|
-
setHeight(card, LIMITS.minHeight);
|
|
152
|
+
if (!card.fill) setHeight(card, LIMITS.minHeight);
|
|
150
153
|
ui.state("starting", "");
|
|
151
154
|
return true;
|
|
152
155
|
}
|
|
@@ -235,6 +238,7 @@
|
|
|
235
238
|
/** Height rules (design §4.2): clamp to [40, 4000]; ≤ 1 per animation frame and ≤ 10 per second; Δ < 2 px ignored;
|
|
236
239
|
* after 5 consecutive increases within 2 s the height freezes and the frame scrolls. */
|
|
237
240
|
function requestHeight(card, h) {
|
|
241
|
+
if (card.fill) return; // the panel sizes it (#1481)
|
|
238
242
|
var want = Math.max(LIMITS.minHeight, Math.min(LIMITS.maxHeight, h));
|
|
239
243
|
if (Math.abs(want - card.height) < LIMITS.minDelta) return;
|
|
240
244
|
var t = now();
|
|
@@ -62,7 +62,7 @@ register("settings", {
|
|
|
62
62
|
se_primaryServer: "primary",
|
|
63
63
|
se_externalEmojiHint: "Another server's emoji needs the bot's Use External Emojis permission in the channel it reacts in.",
|
|
64
64
|
se_unavailable: "unavailable",
|
|
65
|
-
se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react.",
|
|
65
|
+
se_hint: "Empty keeps the default. The preview is resolved by AgEnD exactly as the bot will react. Progress prefix is not a reaction: it starts the progress message.",
|
|
66
66
|
detection: "Detection",
|
|
67
67
|
startup: "Startup",
|
|
68
68
|
backendSection: "Backend",
|
|
@@ -217,6 +217,7 @@ register("settings", {
|
|
|
217
217
|
countAgents: "{0} agents",
|
|
218
218
|
countBots: "{0} connections",
|
|
219
219
|
countRooms: "{0} rooms",
|
|
220
|
+
countAgent: "1 agent", countBot: "1 connection", countRoom: "1 room",
|
|
220
221
|
newAgent: "New agent",
|
|
221
222
|
newBot: "New connection",
|
|
222
223
|
effortTag: "effort: {0}",
|
|
@@ -351,7 +352,7 @@ register("settings", {
|
|
|
351
352
|
se_primaryServer: "主伺服器",
|
|
352
353
|
se_externalEmojiHint: "使用其他伺服器的 emoji,bot 在要加反應的頻道需有「使用外部表情符號」權限。",
|
|
353
354
|
se_unavailable: "無法使用",
|
|
354
|
-
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react
|
|
355
|
+
se_hint: "留空即使用預設值。預覽由 AgEnD 依 bot 實際 react 的方式解析。進度前綴不是反應,而是進度訊息開頭的 emoji。",
|
|
355
356
|
title: "設定",
|
|
356
357
|
myAgents: "我的 Agent",
|
|
357
358
|
bots: "機器人",
|
|
@@ -540,6 +541,7 @@ register("settings", {
|
|
|
540
541
|
countAgents: "{0} 個 agent",
|
|
541
542
|
countBots: "{0} 個連線",
|
|
542
543
|
countRooms: "{0} 個房間",
|
|
544
|
+
countAgent: "1 個 agent", countBot: "1 個連線", countRoom: "1 個房間",
|
|
543
545
|
newAgent: "新增 agent",
|
|
544
546
|
newBot: "新增連線",
|
|
545
547
|
effortTag: "effort:{0}",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// #1408 §3: the app's routes on the client. The same table as the server's classifier (src/web-shell-routes.ts;
|
|
2
2
|
// a test runs both over one list of cases). Pure: no DOM, no history.
|
|
3
3
|
|
|
4
|
-
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "config"];
|
|
4
|
+
export const FLEET_TABS = ["tasks", "schedules", "teams", "org", "cache", "config"];
|
|
5
5
|
export const SETTINGS_SECTIONS = ["agents", "bots", "classic", "general", "advanced"];
|
|
6
6
|
const MAX_INSTANCE_NAME = 128;
|
|
7
7
|
const PATH_SEPARATOR_OR_CONTROL = /[/\\\u0000-\u001f\u007f]/;
|
|
@@ -245,14 +245,16 @@ const mmss = (ms) => { const s = Math.max(0, Math.ceil(ms / 1000)); return `${Ma
|
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* #1423: Settings writes an admin has to confirm, from whichever panel made them (settings-confirm.js publishes them to
|
|
248
|
-
* appStore.pendingChanges). What it asks, how to confirm it, the countdown, Withdraw — in any panel
|
|
248
|
+
* appStore.pendingChanges). What it asks, how to confirm it, the countdown, Withdraw — in any panel: a card in the
|
|
249
|
+
* corner, except in Settings itself, which shows the same cards in its column, right under the operation (`inline`),
|
|
250
|
+
* so they never cover it (alpha.2 sweep).
|
|
249
251
|
*/
|
|
250
|
-
function PendingChanges() {
|
|
252
|
+
export function PendingChanges({ inline = false } = {}) {
|
|
251
253
|
const { pendingChanges } = useStore(appStore);
|
|
252
254
|
const list = pendingChanges || [];
|
|
253
255
|
useTick(list.some(p => p.state === "pending"));
|
|
254
256
|
if (!list.length) return null;
|
|
255
|
-
return html`<div class
|
|
257
|
+
return html`<div class=${`pending-stack${inline ? " inline" : ""}`} role="region" aria-label=${t("app.pendingTitle")}>${list.map(p => html`<section key=${p.id} class=${`pending-card ${p.state}`} role="status">
|
|
256
258
|
<div class="pending-head"><${Icon} name=${p.state === "applied" ? "check" : p.state === "pending" || p.state === "applying" ? "clock" : "alert"} size=${16} />
|
|
257
259
|
<strong>${t(`app.pending_${p.state}`)}</strong>
|
|
258
260
|
${p.state === "pending" ? html`<span class="pending-left">${t("app.pendingLeft", mmss(p.deadline - NOW()))}</span>` : null}
|
|
@@ -336,7 +338,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
336
338
|
</main>
|
|
337
339
|
<${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
|
|
338
340
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
339
|
-
${viewOnly ? null : html`<${PendingChanges} />`}
|
|
341
|
+
${viewOnly || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
340
342
|
<${ConfirmHost} />
|
|
341
343
|
<${Toasts} />
|
|
342
344
|
</div>`;
|