@songsid/agend 2.2.0-beta.3 → 2.2.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli.js +6 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/backend/claude-code.d.ts +39 -2
- package/dist/backend/claude-code.js +115 -31
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +33 -3
- package/dist/backend/credential-profile.js +107 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/types.d.ts +1 -1
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +13 -0
- package/dist/channel/adapters/discord.js +42 -0
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -1
- package/dist/channel/adapters/telegram.js +27 -2
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +22 -1
- package/dist/config-validator.js +4 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +96 -0
- package/dist/daemon.js +339 -16
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +18 -5
- package/dist/delivery-outbox.js +121 -14
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -0
- package/dist/fleet-manager.d.ts +59 -3
- package/dist/fleet-manager.js +357 -71
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +43 -10
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +73 -26
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +44 -18
- package/dist/locale.js.map +1 -1
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/outbound-handlers.d.ts +61 -0
- package/dist/outbound-handlers.js +100 -35
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +2 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +4 -3
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/settings-api.js +10 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/topic-commands.js +9 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +25 -0
- package/dist/ui/chat-thread.js +26 -8
- package/dist/ui/panel-chat.js +17 -9
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +51 -22
- package/dist/ui/panel-settings.js +3 -1
- package/dist/ui/preview-panel.js +48 -14
- package/dist/ui/preview.js +5 -1
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-store.js +3 -1
- package/dist/ui/shared/app.css +11 -7
- package/dist/ui/shared/instance-nav.js +32 -7
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/web-api.d.ts +3 -0
- package/dist/web-api.js +115 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/package.json +1 -1
package/dist/ui/panel-chat.js
CHANGED
|
@@ -28,7 +28,8 @@ import { confirmedWrite } from "./settings-confirm.js";
|
|
|
28
28
|
import { createChatStore } from "./chat-store.js";
|
|
29
29
|
import { createThread, msgKey } from "./chat-thread.js";
|
|
30
30
|
import { createEmojiIndex } from "./chat-emojis.js";
|
|
31
|
-
import {
|
|
31
|
+
import { createMentionIndex } from "./chat-mentions.js";
|
|
32
|
+
import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
|
|
32
33
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
33
34
|
import { installTour, refreshTourSpot } from "./chat-tour.js";
|
|
34
35
|
import { FirstRunCard } from "./first-run.js";
|
|
@@ -45,6 +46,7 @@ export let store = null;
|
|
|
45
46
|
let announceTimer = null;
|
|
46
47
|
/** #1630: which Discord server emoji this page may draw (created with the store, in boot). */
|
|
47
48
|
let emojiIndex = null;
|
|
49
|
+
let mentionIndex = null; // #1682: the names of the mentions shown
|
|
48
50
|
function announce(text) {
|
|
49
51
|
let el = document.getElementById("announcer");
|
|
50
52
|
if (!el) { el = document.createElement("div"); el.id = "announcer"; el.className = "sr-only"; el.setAttribute("role", "status"); el.setAttribute("aria-live", "polite"); el.setAttribute("aria-atomic", "true"); document.body.appendChild(el); }
|
|
@@ -56,11 +58,12 @@ function announce(text) {
|
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* This device's preview opt-in: asks once (the app's dialog) and says what it means; turning it off stops every
|
|
59
|
-
* running preview. Resolves to the choice in force.
|
|
61
|
+
* running preview. Resolves to the choice in force. commit(apply): runs `apply` (the change itself) — the panel's Allow
|
|
62
|
+
* wraps it to know which change of the choice is its own (#1704).
|
|
60
63
|
*/
|
|
61
|
-
export async function setPreviewOptIn(on) {
|
|
64
|
+
export async function setPreviewOptIn(on, commit = (apply) => apply()) {
|
|
62
65
|
if (on && !P().optedIn() && !(await confirmDialog({ message: t("chat.pvConfirm"), confirmLabel: t("chat.pvAllowButton") }))) on = false;
|
|
63
|
-
P().setOptIn(on);
|
|
66
|
+
commit(() => P().setOptIn(on));
|
|
64
67
|
// #1665: kept on the host for every device (only a signed-in page writes it; never the public link).
|
|
65
68
|
savePref("preview.optIn", P().optedIn());
|
|
66
69
|
return P().optedIn();
|
|
@@ -73,6 +76,7 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
73
76
|
fetch: (...a) => fetch(...a), toast, announce, t, setTimeout: (fn, ms) => setTimeout(fn, ms), confirm: confirmDialog, ...deps,
|
|
74
77
|
});
|
|
75
78
|
emojiIndex = createEmojiIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
|
|
79
|
+
mentionIndex = createMentionIndex((url) => (deps.fetch || fetch)(url, { credentials: "same-origin" }));
|
|
76
80
|
store.attach(stream);
|
|
77
81
|
// #1306: what the server said about previews for this load. The page-wide message and storage listeners this adds
|
|
78
82
|
// belong to the app, once (they are never per mount).
|
|
@@ -197,6 +201,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
|
197
201
|
try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
|
|
198
202
|
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
199
203
|
if (r && r.error) toast(r.error, false);
|
|
204
|
+
else if (r && r.held) toast(r.message);
|
|
200
205
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
201
206
|
};
|
|
202
207
|
const items = [
|
|
@@ -221,7 +226,7 @@ function ChatView({ name, inst, lease, navKey, exec, awaiting }) {
|
|
|
221
226
|
<${Dock} name=${name} inst=${inst} lease=${lease} exec=${exec} awaiting=${awaiting} />
|
|
222
227
|
</div>
|
|
223
228
|
<${PreviewPanel} name=${name} split=${split} msgs=${() => store.state.msgs[name] || []} subscribe=${(fn) => store.subscribe(fn)}
|
|
224
|
-
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${() => setPreviewOptIn(true)} />
|
|
229
|
+
keyOf=${msgKey} reveal=${(k) => thread.current && thread.current.reveal(k)} download=${downloadHtml} allow=${(commit) => setPreviewOptIn(true, commit)} />
|
|
225
230
|
</div>
|
|
226
231
|
<${LightboxHost} />
|
|
227
232
|
<div class="drop-overlay" aria-hidden="true"><div class="drop-card"><${Icon} name="attach" size=${32} /><span>${t("chat.dropHere")}</span></div></div>
|
|
@@ -240,14 +245,17 @@ function Thread({ name, th }) {
|
|
|
240
245
|
setPreviewOptIn, copyText, download: downloadHtml, toggleWrap,
|
|
241
246
|
clickReplyButton: (id, index) => store.clickReplyButton(name, id, index), // #1266
|
|
242
247
|
replyButtonBusy: (id) => store.state.rbBusy.has(id),
|
|
243
|
-
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o) }, // #1481
|
|
248
|
+
panel: { shown: shownKey, open: (spec, o) => openPanel(spec, o), version: panelVersion, claim: claimPanel, claims: panelClaims }, // #1481, #1704
|
|
244
249
|
openImage: openLightbox, // an image, full size, in the page
|
|
245
250
|
emojis: (id) => !!emojiIndex && emojiIndex.known(id), // #1630: the fleet's server emoji
|
|
251
|
+
mentions: (kind, id) => (mentionIndex ? mentionIndex.name(kind, id) : null), // #1682: mention names
|
|
246
252
|
});
|
|
247
253
|
th.current = thread;
|
|
248
254
|
// #1630: the server-emoji list is read once a message shows a tag; when it arrives, those messages are drawn again.
|
|
249
|
-
|
|
255
|
+
// #1682: the same for mention names — asked for the ids the messages carry; when names arrive, drawn again.
|
|
256
|
+
const wantEmojis = (msgs) => { if (emojiIndex && emojiIndex.wants(msgs)) emojiIndex.ensure(); if (mentionIndex) mentionIndex.ensure(msgs); };
|
|
250
257
|
const offEmojis = emojiIndex ? emojiIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
|
|
258
|
+
const offMentions = mentionIndex ? mentionIndex.subscribe(() => thread.render(store.state.msgs[name] || [])) : () => {};
|
|
251
259
|
thread.render(store.state.msgs[name] || [], { restore: store.state.scrollMemo[name] ?? null });
|
|
252
260
|
wantEmojis(store.state.msgs[name]);
|
|
253
261
|
const off = store.subscribe((instance, kind) => {
|
|
@@ -262,7 +270,7 @@ function Thread({ name, th }) {
|
|
|
262
270
|
sc.addEventListener("scroll", onScroll, { passive: true });
|
|
263
271
|
ls.addEventListener("click", thread.onClick);
|
|
264
272
|
return () => {
|
|
265
|
-
off(); offPv(); offPanel(); offEmojis();
|
|
273
|
+
off(); offPv(); offPanel(); offEmojis(); offMentions();
|
|
266
274
|
sc.removeEventListener("scroll", onScroll);
|
|
267
275
|
ls.removeEventListener("click", thread.onClick);
|
|
268
276
|
thread.dispose(); // the whole thread goes: every preview in it stops first
|
|
@@ -479,7 +487,7 @@ function DeleteDialog({ name, onClose }) {
|
|
|
479
487
|
<button type="button" class="btn btn-danger" disabled=${typed !== want || busy} onClick=${go}>${t("chat.deleteGo")}</button>`}>
|
|
480
488
|
<p>${t("chat.deleteHelp", name)}</p>
|
|
481
489
|
<label class="field"><span class="sr-only">${t("chat.deleteHelp", name)}</span>
|
|
482
|
-
<input type="text" value=${typed} autocomplete="off" spellcheck
|
|
490
|
+
<input type="text" value=${typed} autocomplete="off" spellcheck=${false} placeholder=${want} onInput=${(e) => setTyped(e.target.value)}
|
|
483
491
|
onKeyDown=${(e) => { if (e.key === "Enter") go(); }} /></label>
|
|
484
492
|
</${Dialog}>`;
|
|
485
493
|
}
|
package/dist/ui/panel-details.js
CHANGED
|
@@ -73,11 +73,13 @@ function DetailsView({ name, inst, navKey, exec, awaiting }) {
|
|
|
73
73
|
try { const res = await fetch(`/ui/${verb}/${encodeURIComponent(name)}`, { method: "POST", headers: { "Content-Type": "application/json" } }); r = await res.json(); }
|
|
74
74
|
catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
75
75
|
if (r && r.error) toast(r.error, false);
|
|
76
|
+
else if (r && r.held) toast(r.message);
|
|
76
77
|
else toast(t(verb === "start" ? "chat.started" : verb === "stop" ? "chat.stoppedInst" : "chat.restarted", name));
|
|
77
78
|
});
|
|
78
79
|
const pauseWake = (verb) => act(verb, async () => {
|
|
79
80
|
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${verb}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
|
|
80
81
|
if (!res.ok) toast((res.body && res.body.error) || t("chat.dActionFailed"), false);
|
|
82
|
+
else if (res.body && res.body.held) toast(res.body.message);
|
|
81
83
|
else toast(t(verb === "pause" ? "chat.dPaused" : "chat.dWoken", name));
|
|
82
84
|
});
|
|
83
85
|
// A ClassicBot room (classicBot.yaml) keeps Classic's split: its own Settings dialog, and only the actions Settings'
|
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -23,6 +23,8 @@ import { CacheReport } from "./fleet-cache.js";
|
|
|
23
23
|
|
|
24
24
|
register("fleet", {
|
|
25
25
|
en: {
|
|
26
|
+
hostMemory_elevated: "Host memory pressure: elevated — new agents start one at a time, at least 5 s apart",
|
|
27
|
+
hostMemory_critical: "Host memory pressure: critical — new agents wait for memory to recover",
|
|
26
28
|
title: "Fleet", tasks: "Tasks", schedules: "Schedules", teams: "Teams", org: "Org chart", cache: "Cache", config: "Config",
|
|
27
29
|
newTask: "New task", newSchedule: "New schedule", newTeam: "New team",
|
|
28
30
|
noTasks: "No tasks yet", noTasksHint: "Create one to coordinate fleet work.", noSchedules: "No schedules yet", noSchedulesHint: "Automate recurring work with cron.",
|
|
@@ -33,7 +35,8 @@ register("fleet", {
|
|
|
33
35
|
cron: "Cron expression", message: "Message", target: "Target instance", label: "Label", teamName: "Team name", members: "Members",
|
|
34
36
|
titleRequired: "Title required", scheduleFieldsRequired: "Cron, message, and target required", teamFieldsRequired: "Name and at least one member required",
|
|
35
37
|
deleteSchedule: "Delete this schedule?", deleteTeam: "Delete team “{0}”?", taskCreated: "Task created", scheduleCreated: "Schedule created", scheduleDeleted: "Schedule deleted",
|
|
36
|
-
teamCreated: "Team created", teamDeleted: "Team deleted",
|
|
38
|
+
teamCreated: "Team created", teamDeleted: "Team deleted", teamUpdated: "Team updated", edit: "Edit", editTeam: "Edit team",
|
|
39
|
+
teamNoRename: "Renaming isn't supported — create a new team and delete this one.", taskDone: "Task updated", configSaved: "Config saved", configRestart: " (restart the fleet to apply channel changes)",
|
|
37
40
|
channel: "Channel", type: "Type", tokenEnv: "Bot token env", groupId: "Group ID", show: "Show", hide: "Hide", access: "Access control", mode: "Mode", locked: "Locked", open: "Open",
|
|
38
41
|
allowedUsers: "Allowed users", allowedUsersNote: "Comma-separated user IDs", defaults: "Defaults", backend: "Backend", roots: "Project roots", addRoot: "Add root", removeRoot: "Remove",
|
|
39
42
|
saveNote: "Saving rewrites fleet.yaml (its comments are lost).",
|
|
@@ -43,6 +46,8 @@ register("fleet", {
|
|
|
43
46
|
fleetDefault: "Use the fleet default", notInstalled: "(not installed)", deprecated: "(deprecated)", instanceCreated: "Instance created",
|
|
44
47
|
},
|
|
45
48
|
"zh-TW": {
|
|
49
|
+
hostMemory_elevated: "主機記憶體壓力偏高:新的 agent 一次啟動一個,至少間隔 5 秒",
|
|
50
|
+
hostMemory_critical: "主機記憶體壓力嚴重:新的 agent 會等記憶體恢復再啟動",
|
|
46
51
|
title: "Fleet", tasks: "Tasks", schedules: "排程", teams: "Teams", org: "組織圖", cache: "快取", config: "設定",
|
|
47
52
|
newTask: "新增 task", newSchedule: "新增排程", newTeam: "新增 team",
|
|
48
53
|
noTasks: "還沒有 task", noTasksHint: "建立一個來協調 fleet 的工作。", noSchedules: "還沒有排程", noSchedulesHint: "用 cron 自動執行例行工作。",
|
|
@@ -53,7 +58,8 @@ register("fleet", {
|
|
|
53
58
|
cron: "Cron 表示式", message: "訊息", target: "目標 instance", label: "標籤", teamName: "Team 名稱", members: "成員",
|
|
54
59
|
titleRequired: "必須填寫標題", scheduleFieldsRequired: "必須填寫 Cron、訊息與目標 instance", teamFieldsRequired: "必須填寫名稱並選擇至少一名成員",
|
|
55
60
|
deleteSchedule: "確定刪除此排程嗎?", deleteTeam: "確定刪除 team「{0}」嗎?", taskCreated: "Task 已建立", scheduleCreated: "排程已建立", scheduleDeleted: "排程已刪除",
|
|
56
|
-
teamCreated: "Team 已建立", teamDeleted: "Team 已刪除",
|
|
61
|
+
teamCreated: "Team 已建立", teamDeleted: "Team 已刪除", teamUpdated: "Team 已更新", edit: "編輯", editTeam: "編輯 team",
|
|
62
|
+
teamNoRename: "不支援重新命名:請建立新的 team,再刪除這個。", taskDone: "Task 已更新", configSaved: "設定已儲存", configRestart: "(請重新啟動 fleet 以套用頻道變更)",
|
|
57
63
|
channel: "頻道", type: "類型", tokenEnv: "Bot token 環境變數", groupId: "Group ID", show: "顯示", hide: "隱藏", access: "存取控制", mode: "模式", locked: "鎖定", open: "開放",
|
|
58
64
|
allowedUsers: "允許的使用者", allowedUsersNote: "以逗號分隔的使用者 ID", defaults: "預設值", backend: "Backend", roots: "專案根目錄", addRoot: "新增根目錄", removeRoot: "移除",
|
|
59
65
|
saveNote: "儲存會改寫 fleet.yaml(其中的註解會遺失)。",
|
|
@@ -104,8 +110,11 @@ export function FleetPanel({ route, navKey }) {
|
|
|
104
110
|
const tab = route.tab;
|
|
105
111
|
useEffect(() => { setTitle(`${t("fleet.title")} · ${t(`fleet.${tab}`)}`); }, [tab, navKey]);
|
|
106
112
|
const Body = { tasks: Tasks, schedules: Schedules, teams: Teams, org: Org, cache: Cache, config: Config }[tab] || Tasks;
|
|
113
|
+
// #1685: host memory pressure slowing or holding new agents — shown here, where macOS's routine "elevated" is not a chat notice.
|
|
114
|
+
const { hostMemory } = useStore(appStore);
|
|
115
|
+
const sub = hostMemory ? html`<span class=${`status host-mem ${hostMemory}`} role="status"><span class=${`dot ${hostMemory === "critical" ? "bad" : "warn"}`} aria-hidden="true"></span>${t(`fleet.hostMemory_${hostMemory}`)}</span>` : null;
|
|
107
116
|
return html`<div class="panel p-fleet">
|
|
108
|
-
<${PanelHeader} title=${t("fleet.title")}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
|
|
117
|
+
<${PanelHeader} title=${t("fleet.title")} sub=${sub}><${HeaderTools} navKey=${navKey} /></${PanelHeader}>
|
|
109
118
|
<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" : ""}`}
|
|
110
119
|
aria-current=${k === tab ? "page" : undefined}><${Icon} name=${ICONS[k]} size=${16} /><span>${t(`fleet.${k}`)}</span></a>`)}</nav>
|
|
111
120
|
<div class="panel-body"><div class=${`col${tab === "org" || tab === "cache" ? " col-wide" : ""}`}><${Body} lease=${lease} /></div></div>
|
|
@@ -167,9 +176,13 @@ function Schedules({ lease }) {
|
|
|
167
176
|
${creating ? html`<${CreateSchedule} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
168
177
|
}
|
|
169
178
|
|
|
179
|
+
/** #1689: the public link changes no team (the server refuses it): it lists them only. */
|
|
180
|
+
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
181
|
+
|
|
170
182
|
function Teams({ lease }) {
|
|
171
183
|
const [version, setVersion] = useState(0);
|
|
172
184
|
const [creating, setCreating] = useState(false);
|
|
185
|
+
const [editing, setEditing] = useState(null); // #1689: { name, members, description } of the team being edited
|
|
173
186
|
const state = useLoad(lease, "/ui/teams", version);
|
|
174
187
|
async function del(name) {
|
|
175
188
|
if (!(await confirmDialog({ message: t("fleet.deleteTeam", name), confirmLabel: t("fleet.delete"), danger: true }))) return;
|
|
@@ -179,14 +192,17 @@ function Teams({ lease }) {
|
|
|
179
192
|
}
|
|
180
193
|
return html`<${Loaded} state=${state}>${(d) => {
|
|
181
194
|
const entries = Object.entries(d.teams || {});
|
|
182
|
-
|
|
195
|
+
const writable = !publicLink();
|
|
196
|
+
return html`<${ListHead} count=${entries.length === 1 ? t("fleet.countTeam") : t("fleet.countTeams", entries.length)} label=${t("fleet.newTeam")} action=${writable ? { onClick: () => setCreating(true) } : null} />
|
|
183
197
|
${entries.length ? html`<ul class="rows">${entries.map(([name, team]) => html`<li key=${name} class="row-item">
|
|
184
198
|
<span class="grow strong">${name}</span><span class="muted">${(team.members || []).join(", ")}</span>
|
|
185
|
-
|
|
199
|
+
${writable ? html`<button type="button" class="btn btn-ghost btn-sm team-edit" onClick=${() => setEditing({ name, members: [...(team.members || [])], description: team.description || "" })}>${t("fleet.edit")}</button>
|
|
200
|
+
<button type="button" class="btn btn-ghost btn-sm danger" onClick=${() => del(name)}>${t("fleet.delete")}</button>` : null}</li>`)}</ul>`
|
|
186
201
|
: html`<${Empty} icon="team" title=${t("fleet.noTeams")} hint=${t("fleet.noTeamsHint")} />`}
|
|
187
202
|
`;
|
|
188
203
|
}}</${Loaded}>
|
|
189
|
-
${creating ? html`<${CreateTeam} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}
|
|
204
|
+
${creating ? html`<${CreateTeam} onClose=${() => setCreating(false)} onDone=${() => setVersion(v => v + 1)} />` : null}
|
|
205
|
+
${editing ? html`<${CreateTeam} team=${editing} onClose=${() => setEditing(null)} onDone=${() => setVersion(v => v + 1)} />` : null}`;
|
|
190
206
|
}
|
|
191
207
|
|
|
192
208
|
/** #1389: the org chart — its structure read once per opening, its live state from the app store (fleet-org.js). */
|
|
@@ -286,7 +302,7 @@ export function FormDialog({ title, onClose, submit, children, ready = true }) {
|
|
|
286
302
|
// the dialog goes at once and the shell follows the request. From then on the dialog is done: the decision is
|
|
287
303
|
// said in a toast, and nothing here closes, refreshes or acts again.
|
|
288
304
|
try {
|
|
289
|
-
const res = await confirmedWrite(submit.path, { method: "POST", body, label: title,
|
|
305
|
+
const res = await confirmedWrite(submit.path, { method: submit.method || "POST", body, label: title,
|
|
290
306
|
onPending: () => { handedOver = true; toast(t("app.pendingSent")); if (lease.current()) { setBusy(false); onClose(); } } });
|
|
291
307
|
r = res.ok ? res.body || {} : { error: (res.body && res.body.error) || `HTTP ${res.status}` };
|
|
292
308
|
} catch (err) { r = { error: err.message }; }
|
|
@@ -299,7 +315,7 @@ export function FormDialog({ title, onClose, submit, children, ready = true }) {
|
|
|
299
315
|
}
|
|
300
316
|
return html`<${Dialog} title=${title} onClose=${onClose} busy=${busy}
|
|
301
317
|
actions=${html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${t("fleet.cancel")}</button>
|
|
302
|
-
<button type="button" class="btn btn-primary" onClick=${go} disabled=${busy || !ready}>${busy ? t("fleet.creating") : t("fleet.create")}</button>`}>
|
|
318
|
+
<button type="button" class="btn btn-primary" onClick=${go} disabled=${busy || !ready}>${busy ? t("fleet.creating") : submit.label || t("fleet.create")}</button>`}>
|
|
303
319
|
<form class="form" onSubmit=${(e) => { e.preventDefault(); go(); }}><fieldset class="form" disabled=${busy}>${children}</fieldset></form></${Dialog}>`;
|
|
304
320
|
}
|
|
305
321
|
const field = (label, input) => html`<label class="field"><span>${label}</span>${input}</label>`;
|
|
@@ -342,21 +358,34 @@ function CreateSchedule({ onClose, onDone }) {
|
|
|
342
358
|
</${FormDialog}>`;
|
|
343
359
|
}
|
|
344
360
|
|
|
345
|
-
|
|
361
|
+
/**
|
|
362
|
+
* New team, or (#1689) `team` given: edit that one — the same form, filled in from it: its members ticked (one no longer
|
|
363
|
+
* in the fleet listed too, so it can be taken out), its description; the name is not editable (a team is not renamed).
|
|
364
|
+
* Saved with PATCH /ui/teams/<name> (the update_team path on the host).
|
|
365
|
+
*/
|
|
366
|
+
function CreateTeam({ onClose, onDone, team = null }) {
|
|
346
367
|
const { instances } = useStore(appStore);
|
|
347
|
-
const [f, setF] = useState({ name: "", desc: "", members: [] });
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
368
|
+
const [f, setF] = useState(team ? { name: team.name, desc: team.description || "", members: [...team.members] } : { name: "", desc: "", members: [] });
|
|
369
|
+
// From the state as it is, not this render's copy: two ticks before the next render must both count.
|
|
370
|
+
const toggle = (n) => setF((cur) => ({ ...cur, members: cur.members.includes(n) ? cur.members.filter(x => x !== n) : [...cur.members, n] }));
|
|
371
|
+
const names = [...new Set([...instances.map(i => i.name), ...(team ? team.members : [])])];
|
|
372
|
+
const submit = team
|
|
373
|
+
? { path: `/ui/teams/${encodeURIComponent(team.name)}`, method: "PATCH", label: t("fleet.save"), done: t("fleet.teamUpdated"), after: onDone, collect: () => {
|
|
374
|
+
if (!f.members.length) { toast(t("fleet.teamFieldsRequired"), false); return null; }
|
|
375
|
+
return { members: f.members, description: f.desc.trim() };
|
|
376
|
+
} }
|
|
377
|
+
: { path: "/ui/teams", done: t("fleet.teamCreated"), after: onDone, collect: () => {
|
|
378
|
+
if (!f.name.trim() || !f.members.length) { toast(t("fleet.teamFieldsRequired"), false); return null; }
|
|
379
|
+
const body = { name: f.name.trim(), members: f.members };
|
|
380
|
+
if (f.desc.trim()) body.description = f.desc.trim();
|
|
381
|
+
return body;
|
|
382
|
+
} };
|
|
383
|
+
return html`<${FormDialog} title=${team ? t("fleet.editTeam") : t("fleet.newTeam")} onClose=${onClose} submit=${submit}>
|
|
384
|
+
${field(`${t("fleet.teamName")} *`, html`<input value=${f.name} readOnly=${!!team} aria-readonly=${team ? "true" : undefined} onInput=${(e) => { const v = e.target.value; setF((cur) => ({ ...cur, name: v })); }} />`)}
|
|
385
|
+
${team ? html`<p class="note">${t("fleet.teamNoRename")}</p>` : null}
|
|
386
|
+
${field(t("fleet.taskDesc"), html`<input value=${f.desc} onInput=${(e) => { const v = e.target.value; setF((cur) => ({ ...cur, desc: v })); }} />`)}
|
|
387
|
+
<fieldset class="field"><legend>${t("fleet.members")} *</legend><div class="checks">${names.map(n => html`<label key=${n} class="check">
|
|
388
|
+
<input type="checkbox" checked=${f.members.includes(n)} onChange=${() => toggle(n)} />${n}</label>`)}</div></fieldset>
|
|
360
389
|
</${FormDialog}>`;
|
|
361
390
|
}
|
|
362
391
|
|
|
@@ -372,11 +372,13 @@ function makeCtx(data, setData, stage, unstage, reload, reloadLive, alive) {
|
|
|
372
372
|
async pauseWake(name, action) {
|
|
373
373
|
const res = await api(`/api/settings/instances/${encodeURIComponent(name)}/${action}`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
|
|
374
374
|
if (!res.ok) toast((res.body && res.body.error) || tn("actionFailed", action), false);
|
|
375
|
+
else if (res.body && res.body.held) toast(res.body.message);
|
|
375
376
|
await reloadLive();
|
|
376
377
|
},
|
|
377
378
|
async startStop(name, running) {
|
|
378
379
|
const res = await api(running ? `/stop/${encodeURIComponent(name)}` : `/api/instance/${encodeURIComponent(name)}/start`, { method: "POST" }).catch(() => ({ ok: false, body: {} }));
|
|
379
380
|
if (!res.ok) toast(`${tn("actionFailed", running ? tn("stop") : tn("start"))}: ${(res.body && res.body.error) || res.status || ""}`, false);
|
|
381
|
+
else if (res.body && res.body.held) toast(res.body.message);
|
|
380
382
|
await new Promise((r) => setTimeout(r, 800));
|
|
381
383
|
await reloadLive();
|
|
382
384
|
},
|
|
@@ -847,7 +849,7 @@ function Developer({ ctx }) {
|
|
|
847
849
|
<button type="button" class="btn btn-sm" title=${tn("copy")} onClick=${copy}><${Icon} name="copy" size=${14} /><span class="lbl">${tn("copy")}</span></button>
|
|
848
850
|
<button type="button" class="btn btn-sm" title=${tn("devDownload")} onClick=${download}><${Icon} name="download" size=${14} /><span class="lbl">${tn("devDownload")}</span></button>
|
|
849
851
|
</div>
|
|
850
|
-
${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck
|
|
852
|
+
${editing ? html`<textarea class="s-yaml" aria-label=${tn("devEditor")} spellcheck=${false} wrap="off" value=${draft} onInput=${(e) => setDraft(e.target.value)}></textarea>
|
|
851
853
|
<div class="save-row"><button type="button" class="btn btn-primary" onClick=${save}>${tn("devApply")}</button><span class="note">${tn("devApplyHint")}</span></div>`
|
|
852
854
|
: html`<pre class="s-yaml">${text}</pre>`}
|
|
853
855
|
${msg ? html`<p class=${`feedback${msg.error ? " error" : ""}`} role="status">${msg.error || msg.text}</p>` : null}
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// #1481: an agent's HTML preview (#1306) in a panel beside the conversation — a split with a draggable divider on a
|
|
2
2
|
// wide screen, a full-screen sheet on a narrow one. Nothing about the preview itself changes here: the frame is the one
|
|
3
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
|
-
// -
|
|
4
|
+
// card's, only told to fill the panel. It runs on a person's click, as a card's does — and "Open in panel" is that click:
|
|
5
|
+
// - on an idle card it opens the panel and starts the preview there, as if Preview were clicked in the panel (#1704);
|
|
6
|
+
// a device that has not allowed previews gets the "Allow previews" offer instead, and allowing it starts the run;
|
|
7
|
+
// where previews cannot run (web.preview: false, a public link), the panel opens with nothing running;
|
|
6
8
|
// - on a card whose preview is running, the same button moves that run to the panel (stopped in the card, started in
|
|
7
9
|
// the panel: a moved frame reloads anyway);
|
|
8
|
-
// -
|
|
10
|
+
// - nothing runs by itself: not when the page loads, not when the panel is shown again, and a newer version of the
|
|
11
|
+
// block in a later reply is offered, never swapped in, and shown with Preview to click.
|
|
9
12
|
// Closing the panel or leaving the chat stops its frame; one preview per page still holds (start() stops any other).
|
|
10
13
|
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
11
14
|
import { t } from "/assets/app-i18n.js";
|
|
@@ -19,18 +22,29 @@ const P = () => globalThis.AgendPreview;
|
|
|
19
22
|
|
|
20
23
|
// ── The panel's state, one per page (the chat shows one instance at a time) ──
|
|
21
24
|
|
|
22
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* open: { key, instance, msgKey, n | att, code, sender, ts } | null (`att`: an attached .html file, #1306 Q4);
|
|
27
|
+
* run: start it on mount — the card's click, as a token no later open ever reuses (0: nothing to run).
|
|
28
|
+
*/
|
|
23
29
|
let state = { open: null, run: 0 };
|
|
30
|
+
let runs = 0, version = 0, claims = 0;
|
|
24
31
|
const subs = new Set();
|
|
25
|
-
function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
32
|
+
function set(next) { state = next; version++; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
26
33
|
export const panelStore = { get: () => state, subscribe(fn) { subs.add(fn); return () => { subs.delete(fn); }; } };
|
|
34
|
+
/** Moves on every open and close, and on every claim: what a card's click compares when its HTML arrives late. */
|
|
35
|
+
export const panelVersion = () => version;
|
|
36
|
+
/** A click that will show something in the panel (an attachment's HTML still to read): from now on it is the newest —
|
|
37
|
+
* any earlier click still reading is dropped when its answer comes. Returns the version the click owns. */
|
|
38
|
+
export function claimPanel() { claims++; return ++version; }
|
|
39
|
+
/** How many clicks have claimed: an inline Preview still reading asks only whether a newer click came (not a close). */
|
|
40
|
+
export const panelClaims = () => claims;
|
|
27
41
|
|
|
28
42
|
/** The frame's key in preview.js: its own, so the card that opened it keeps its own state. */
|
|
29
43
|
export const frameKey = (key) => `panel:${key}`;
|
|
30
|
-
/** Show `spec` in the panel; `run`: start it there (
|
|
44
|
+
/** Show `spec` in the panel; `run`: start it there (the card's "Open in panel" click). The block shown before stops. */
|
|
31
45
|
export function openPanel(spec, { run = false } = {}) {
|
|
32
46
|
if (state.open && P()) P().stop(frameKey(state.open.key), "stopped", "");
|
|
33
|
-
set({ open: { ...spec }, run: run ?
|
|
47
|
+
set({ open: { ...spec }, run: run ? ++runs : 0 });
|
|
34
48
|
}
|
|
35
49
|
/** Close the panel: its frame stops first. */
|
|
36
50
|
export function closePanel() {
|
|
@@ -105,7 +119,7 @@ const timeOf = (ts) => (ts ? new Date(ts).toLocaleTimeString([], { hour: "2-digi
|
|
|
105
119
|
* props: name (the instance), msgs() → its messages, subscribe(fn) → unsubscribe (the chat store's), keyOf(x),
|
|
106
120
|
* reveal(msgKey) (the thread shows that message), download(code).
|
|
107
121
|
*/
|
|
108
|
-
/** allow(): this device's opt-in through the chat's one path (setPreviewOptIn: the confirm, then setOptIn) → the choice. */
|
|
122
|
+
/** allow(commit): this device's opt-in through the chat's one path (setPreviewOptIn: the confirm, then commit(setOptIn)) → the choice. */
|
|
109
123
|
export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, download, allow }) {
|
|
110
124
|
const [st, setSt] = useState(state);
|
|
111
125
|
const [view, setView] = useState("preview");
|
|
@@ -113,13 +127,17 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
113
127
|
const [newer, setNewer] = useState(null);
|
|
114
128
|
const [newerNote, setNewerNote] = useState("");
|
|
115
129
|
const reading = useRef(null); // the newer version whose file is being read (one at a time)
|
|
130
|
+
const pending = useRef(0); // #1704: the card's click, waiting for this device's Allow (the st.run it came with)
|
|
131
|
+
const ownAllow = useRef(0); // while this panel's own Allow commits its choice: the click it answers (not cancelled by it)
|
|
116
132
|
const holder = useRef(null), divider = useRef(null), panel = useRef(null);
|
|
117
133
|
const open = st.open && st.open.instance === name ? st.open : null;
|
|
118
134
|
|
|
119
135
|
useEffect(() => panelStore.subscribe(setSt), []);
|
|
120
136
|
// This device's choice (here or in another tab) decides whether Preview is offered: re-read it when it changes.
|
|
137
|
+
// A click waiting for Allow is cancelled by any other change of that choice (Never, a card's menu, another tab):
|
|
138
|
+
// after it, running needs a new click — only this panel's own Allow runs the click it was offered for.
|
|
121
139
|
const [, choice] = useState(0);
|
|
122
|
-
useEffect(() => (P() ? P().onChange(() => choice(n => n + 1)) : undefined), []);
|
|
140
|
+
useEffect(() => (P() ? P().onChange(() => { if (pending.current && ownAllow.current !== pending.current) pending.current = 0; choice(n => n + 1); }) : undefined), []);
|
|
123
141
|
// Leaving the chat (another instance, another panel) closes the panel: its frame stops with the thread's.
|
|
124
142
|
useEffect(() => () => closePanel(), []);
|
|
125
143
|
|
|
@@ -130,11 +148,17 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
130
148
|
P().start(frameKey(open.key), holder.current, open.code, ui, { fill: true });
|
|
131
149
|
};
|
|
132
150
|
const stop = () => { if (open) P().stop(frameKey(open.key), "stopped", ""); };
|
|
133
|
-
// A new block in the panel: what ran before is gone (openPanel stopped it);
|
|
151
|
+
// A new block in the panel: what ran before is gone (openPanel stopped it); the card's click starts it here — or,
|
|
152
|
+
// where it cannot run, waits as `pending`: only the Allow below can use it (offered to a device that has not opted
|
|
153
|
+
// in), only while the panel shows what that click opened (st.run is a token no later open reuses), and any other
|
|
154
|
+
// change of this device's choice cancels it.
|
|
134
155
|
useEffect(() => {
|
|
135
156
|
setRun({ state: "idle", reason: "" });
|
|
136
157
|
setView("preview");
|
|
137
|
-
if (open && st.run)
|
|
158
|
+
if (open && st.run) {
|
|
159
|
+
if (P().availability().ok) start();
|
|
160
|
+
else pending.current = st.run;
|
|
161
|
+
}
|
|
138
162
|
// Focus goes to the panel's close control that is on screen (Back on a phone, × beside the thread).
|
|
139
163
|
if (panel.current) { const f = [...panel.current.querySelectorAll(".pv-panel-close")].find(b => b.offsetParent !== null); if (f) f.focus(); }
|
|
140
164
|
}, [open && open.key, st.run]);
|
|
@@ -204,11 +228,21 @@ export function PreviewPanel({ name, split, msgs, subscribe, keyOf, reveal, down
|
|
|
204
228
|
if (!open) return null;
|
|
205
229
|
const going = run.state === "starting" || run.state === "running";
|
|
206
230
|
const a = P().availability();
|
|
207
|
-
// #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm)
|
|
208
|
-
// is then enabled — still a click. Why a run stopped
|
|
231
|
+
// #1554: a device that has not allowed previews is offered the same "Allow previews" here (the same confirm). Allowing
|
|
232
|
+
// runs the block the card's click opened (#1704); otherwise Preview is then enabled — still a click. Why a run stopped
|
|
233
|
+
// (e.g. switched off in another tab) is still said first.
|
|
209
234
|
const askOptIn = !going && !a.ok && a.why === "optin" && typeof allow === "function";
|
|
210
235
|
const note = run.reason || (going ? (run.state === "starting" ? t("chat.pvStarting") : "") : a.ok ? t("chat.pvPanelIdle") : askOptIn ? t("chat.pvWhyOptinPanel") : a.reason);
|
|
211
|
-
|
|
236
|
+
// Allowed here: the click that opened the panel runs now — if it is still waiting (no other change of the choice
|
|
237
|
+
// cancelled it, even while the confirm was open: only the change this Allow commits is its own) and the panel still
|
|
238
|
+
// shows what it opened (the confirm is a wait: the panel may have moved on meanwhile).
|
|
239
|
+
const allowHere = async () => {
|
|
240
|
+
const want = pending.current;
|
|
241
|
+
const yes = await allow((apply) => { ownAllow.current = want; try { apply(); } finally { ownAllow.current = 0; } });
|
|
242
|
+
if (!yes) return;
|
|
243
|
+
setRun({ state: "idle", reason: "" });
|
|
244
|
+
if (want && pending.current === want && panelStore.get().run === want) start();
|
|
245
|
+
};
|
|
212
246
|
const source = t("chat.pvSource", open.sender || name, timeOf(open.ts));
|
|
213
247
|
return html`<div class="pv-divider" ref=${divider} role="separator" aria-orientation="vertical" tabindex="0" aria-label=${t("chat.pvResize")}
|
|
214
248
|
aria-valuemin=${String(MIN_PANEL)} onKeyDown=${onKey} onPointerDown=${onDown}></div>
|
package/dist/ui/preview.js
CHANGED
|
@@ -197,11 +197,15 @@
|
|
|
197
197
|
* opts.fill (#1481, the side panel): the frame fills its holder — the same frame from mountPreview, only sized by the
|
|
198
198
|
* panel instead of by the frame's height messages, which it then ignores.
|
|
199
199
|
*/
|
|
200
|
+
/** How many previews this page has started: a click whose HTML arrives late asks whether another started meanwhile. */
|
|
201
|
+
var starts = 0;
|
|
202
|
+
function startCount() { return starts; }
|
|
200
203
|
function start(key, holder, html, ui, opts) {
|
|
201
204
|
var a = availability();
|
|
202
205
|
if (!a.ok) { ui.state("unavailable", a.reason); return false; }
|
|
203
206
|
if (typeof html !== "string" || utf8Bytes(html) > LIMITS.maxBytes) { ui.state("unavailable", say("pvHtmlOver", "This HTML is over 1 MiB; it is not previewed.")); return false; }
|
|
204
207
|
stopAll("another");
|
|
208
|
+
starts++;
|
|
205
209
|
var doc = holder.ownerDocument;
|
|
206
210
|
var iframe = mountPreview(doc);
|
|
207
211
|
var card = {
|
|
@@ -330,7 +334,7 @@
|
|
|
330
334
|
|
|
331
335
|
return {
|
|
332
336
|
init: init, availability: availability, optedIn: optedIn, setOptIn: setOptIn, never: never, setNever: setNever, onChange: onChange, listeners: listeners, onStorage: onStorage,
|
|
333
|
-
start: start, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
|
|
337
|
+
start: start, startCount: startCount, stop: stop, stopAll: stopAll, stopIn: stopIn, running: running, liveCount: liveCount, liveFrame: liveFrame,
|
|
334
338
|
mountPreview: mountPreview, onMessage: onMessage, BANNER: BANNER, LIMITS: LIMITS,
|
|
335
339
|
/** The banner every running preview carries, in the page's language (#1554); BANNER is its English. */
|
|
336
340
|
banner: function () { return say("pvBanner", BANNER); },
|
|
@@ -31,7 +31,7 @@ export function TokenField({ id, platform, value, onInput, identity, busy, onVer
|
|
|
31
31
|
const [shown, setShown] = useState(false);
|
|
32
32
|
return html`<div class="field token-field"><label for=${id}>${tn("wizardToken")}</label>
|
|
33
33
|
<div class="token-row">
|
|
34
|
-
<input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck
|
|
34
|
+
<input id=${id} type=${shown ? "text" : "password"} autocomplete="off" autocapitalize="off" spellcheck=${false} disabled=${!!busy}
|
|
35
35
|
placeholder=${platform === "discord" ? "MTA…" : "123456:ABC-DEF…"} value=${value} onInput=${(e) => onInput(e.target.value.trim())} />
|
|
36
36
|
<button type="button" class="icon-btn" aria-pressed=${shown ? "true" : "false"} title=${shown ? tn("tokenHide") : tn("tokenShow")}
|
|
37
37
|
aria-label=${shown ? tn("tokenHide") : tn("tokenShow")} onClick=${() => setShown(!shown)}><${Icon} name="eye" size=${16} /></button>
|
|
@@ -38,7 +38,9 @@ export function applyStatus(d) {
|
|
|
38
38
|
appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
|
|
39
39
|
connections: typeof d.connections === "number" ? d.connections : null,
|
|
40
40
|
// #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
|
|
41
|
-
prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null
|
|
41
|
+
prefsRev: typeof d.prefs_rev === "number" ? d.prefs_rev : null,
|
|
42
|
+
// #1685: host memory pressure slowing or holding new agents ("elevated" | "critical"), else null.
|
|
43
|
+
hostMemory: d.host_memory === "elevated" || d.host_memory === "critical" ? d.host_memory : null });
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** One "activity" event: an instance started or stopped working. */
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -628,15 +628,16 @@ html.lb-open .scroller { overflow: hidden; }
|
|
|
628
628
|
.v-filter-box input::placeholder { color: var(--text-3); }
|
|
629
629
|
.v-filter-clear { width: 28px; height: 28px; }
|
|
630
630
|
.v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
|
|
631
|
-
/* alpha.2 (N1): status and CLI chips under the filter box — each a
|
|
631
|
+
/* alpha.2 (N1): status and CLI chips under the filter box — each a group of checkboxes under its header (#1700). */
|
|
632
632
|
.nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
|
|
633
633
|
.nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
|
|
634
|
-
|
|
635
|
-
.nav-facet
|
|
636
|
-
|
|
637
|
-
.nav-facet
|
|
638
|
-
.nav-facet
|
|
639
|
-
.nav-facet
|
|
634
|
+
/* #1700: a facet group's header — the whole row toggles its list; folded with a filter set, a short summary. */
|
|
635
|
+
.nav-facet-head { display: inline-flex; align-items: center; gap: var(--s-1); max-width: 100%; padding: 2px var(--s-2); border: 0; border-radius: var(--r-sm);
|
|
636
|
+
background: transparent; color: inherit; font: inherit; cursor: pointer; text-align: left; }
|
|
637
|
+
.nav-facet-head:hover { background: var(--hover); }
|
|
638
|
+
.nav-facet-head:focus-visible { outline: 2px solid var(--accent); }
|
|
639
|
+
.nav-facet-head .caret { color: var(--text-3); flex: none; }
|
|
640
|
+
.nav-facet-sum { min-width: 0; max-width: 12em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--accent); }
|
|
640
641
|
.nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
|
|
641
642
|
.nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
|
|
642
643
|
.nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
|
|
@@ -989,3 +990,6 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
989
990
|
}
|
|
990
991
|
/* #1630: a Discord server emoji in a chat message, the size of the text around it. */
|
|
991
992
|
.md img.emoji { display: inline-block; width: 1.375em; height: 1.375em; object-fit: contain; vertical-align: -0.3em; margin: 0 0.05em; }
|
|
993
|
+
/* #1682: a mention, as Discord draws one — accent text on a tint of it; the id (or the Telegram handle) on hover. */
|
|
994
|
+
.md .mention { display: inline; padding: 0 0.2em; border-radius: var(--r-sm); color: var(--accent); background: var(--accent-dim);
|
|
995
|
+
font-weight: var(--fw-medium); white-space: nowrap; }
|
|
@@ -207,6 +207,16 @@ export function moveFavorite(name, delta, known) {
|
|
|
207
207
|
cur.splice(to, 0, cur.splice(from, 1)[0]);
|
|
208
208
|
setFavorites(cur);
|
|
209
209
|
}
|
|
210
|
+
// ── #1700: the filter's facet groups, open or folded — per device (localStorage; blocked or full storage: this page only) ──
|
|
211
|
+
const FACETS_KEY = "agend_nav_facets";
|
|
212
|
+
function readFacetsOpen() {
|
|
213
|
+
try {
|
|
214
|
+
const v = JSON.parse(stored(FACETS_KEY) || "{}");
|
|
215
|
+
return { status: !!(v && v.status === true), cli: !!(v && v.cli === true) };
|
|
216
|
+
} catch { return { status: false, cli: false }; }
|
|
217
|
+
}
|
|
218
|
+
function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.stringify({ status: !!open.status, cli: !!open.cli })); } catch { /* this page only */ } }
|
|
219
|
+
|
|
210
220
|
/** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
|
|
211
221
|
const FAV_GROUP = "\u0000favorites";
|
|
212
222
|
function persistOrder(groupNames, instByGroup) {
|
|
@@ -299,6 +309,9 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
299
309
|
const filterRef = useRef(null);
|
|
300
310
|
const list = useRef(null);
|
|
301
311
|
const drag = useRef(null);
|
|
312
|
+
// #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
|
|
313
|
+
const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
|
|
314
|
+
const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
|
|
302
315
|
// #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
|
|
303
316
|
// full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
|
|
304
317
|
const [editing, setEditing] = useState(false);
|
|
@@ -334,7 +347,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
334
347
|
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
335
348
|
// their group.
|
|
336
349
|
const dragProps = (item) => shown.active ? {} : {
|
|
337
|
-
draggable:
|
|
350
|
+
draggable: true,
|
|
338
351
|
onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
|
|
339
352
|
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
340
353
|
onDragOver: (e) => {
|
|
@@ -360,11 +373,23 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
360
373
|
persistOrder(names, insts);
|
|
361
374
|
},
|
|
362
375
|
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
376
|
+
// #1700: a facet group (Status, CLI) folds under a real header button. Folded with a filter set, the header says so:
|
|
377
|
+
// one or two choices by name, more as "N selected" — a filter is never out of sight.
|
|
378
|
+
const chips = (key, values, label, text) => {
|
|
379
|
+
const open = !!facetOpen[key];
|
|
380
|
+
const sel = f[key];
|
|
381
|
+
const summary = !open && sel.length ? (sel.length <= 2 ? sel.map(text).join(", ") : tn("facetSelected", { n: sel.length })) : "";
|
|
382
|
+
const hint = tn(open ? "facetHide" : "facetShow", { group: label });
|
|
383
|
+
return html`<div class=${`nav-facet${open ? " open" : ""}`}>
|
|
384
|
+
<button type="button" class="nav-facet-head" aria-expanded=${open ? "true" : "false"} aria-controls=${`nav-facet-${key}`}
|
|
385
|
+
title=${summary ? `${hint} (${label} · ${summary})` : hint} onClick=${() => toggleFacet(key)}>
|
|
386
|
+
<${Icon} name="chevron" size=${12} cls=${open ? "caret" : "caret folded"} /><span class="nav-facet-label">${label}</span>
|
|
387
|
+
${summary ? html`<span class="nav-facet-sum">· ${summary}</span>` : sel.length ? html`<span class="nav-facet-n">${sel.length}</span>` : null}
|
|
388
|
+
</button>
|
|
389
|
+
${open ? html`<div id=${`nav-facet-${key}`} class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
|
|
390
|
+
<input type="checkbox" checked=${sel.includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>` : null}
|
|
391
|
+
</div>`;
|
|
392
|
+
};
|
|
368
393
|
const row = (name, g, i, count) => {
|
|
369
394
|
const it = byName.get(name);
|
|
370
395
|
const label = labelOf(it);
|
|
@@ -415,7 +440,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
415
440
|
</div>
|
|
416
441
|
<div class="v-filter">
|
|
417
442
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
418
|
-
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck
|
|
443
|
+
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
419
444
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
420
445
|
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
|
|
421
446
|
${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
@@ -15,6 +15,9 @@ register("view", {
|
|
|
15
15
|
"filterStatus": "Status",
|
|
16
16
|
"filterCli": "CLI",
|
|
17
17
|
"filterFacetsReset": "Clear",
|
|
18
|
+
"facetSelected": "{n} selected",
|
|
19
|
+
"facetShow": "Show {group} filters",
|
|
20
|
+
"facetHide": "Hide {group} filters",
|
|
18
21
|
"displayName": "Display name",
|
|
19
22
|
"role": "Role",
|
|
20
23
|
"description": "Description",
|
|
@@ -186,6 +189,9 @@ register("view", {
|
|
|
186
189
|
"filterStatus": "狀態",
|
|
187
190
|
"filterCli": "CLI",
|
|
188
191
|
"filterFacetsReset": "清除",
|
|
192
|
+
"facetSelected": "已選 {n} 個",
|
|
193
|
+
"facetShow": "顯示{group}篩選",
|
|
194
|
+
"facetHide": "隱藏{group}篩選",
|
|
189
195
|
"displayName": "顯示名稱",
|
|
190
196
|
"role": "角色",
|
|
191
197
|
"description": "描述",
|