@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.23
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 -2
- package/README.zh-TW.md +2 -2
- package/dist/agent-cli.js +4 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +1 -0
- package/dist/agent-endpoint.js +4 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +5 -0
- package/dist/agent-whats-new.js +16 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +7 -0
- package/dist/auth-api.js +8 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex-package.d.ts +30 -0
- package/dist/backend/codex-package.js +133 -0
- package/dist/backend/codex-package.js.map +1 -0
- package/dist/backend/codex.d.ts +2 -0
- package/dist/backend/codex.js +9 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -1
- package/dist/backend/kiro.js +102 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode-permission.js +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js +2 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/binary-probe.js +1 -1
- package/dist/bug-report-cli.js +1 -1
- package/dist/bundled-skills.d.ts +7 -3
- package/dist/bundled-skills.js +16 -7
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +12 -4
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +3 -1
- package/dist/channel/mcp-tools.js +36 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +54 -0
- package/dist/child-process.js.map +1 -0
- package/dist/cli.js +167 -182
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +1 -1
- package/dist/command-table.js +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +20 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +3 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +31 -8
- package/dist/daemon.js.map +1 -1
- package/dist/darwin-memory.js +1 -1
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +42 -0
- package/dist/doctor.js.map +1 -1
- package/dist/export-import.js +1 -1
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-level-config.d.ts +1 -1
- package/dist/fleet-level-config.js +2 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +45 -0
- package/dist/fleet-manager.js +301 -21
- package/dist/fleet-manager.js.map +1 -1
- package/dist/full-restart.d.ts +5 -5
- package/dist/full-restart.js +5 -5
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -3
- package/dist/general-knowledge/steering/core-rules.md +1 -1
- package/dist/instance-config-impact.js +10 -3
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +4 -4
- package/dist/locale.js +118 -4
- package/dist/locale.js.map +1 -1
- package/dist/login-flows.js +1 -1
- package/dist/login-shell.js +3 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/ls-rows.js +1 -1
- package/dist/needs-you-hub.d.ts +98 -1
- package/dist/needs-you-hub.js +313 -44
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +5 -2
- package/dist/needs-you-live.js +6 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +58 -3
- package/dist/needs-you.js +36 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-schemas.d.ts +3 -3
- package/dist/public-link-progress.d.ts +12 -1
- package/dist/public-link-progress.js +5 -10
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-tunnel-config.d.ts +5 -0
- package/dist/public-tunnel-config.js +45 -1
- package/dist/public-tunnel-config.js.map +1 -1
- package/dist/public-web-link.js +6 -5
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart.js +14 -8
- package/dist/quickstart.js.map +1 -1
- package/dist/resource-report.js +1 -1
- package/dist/scheduler/task-board.d.ts +41 -1
- package/dist/scheduler/task-board.js +150 -4
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.js +2 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +69 -0
- package/dist/service-installer.js +181 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +44 -0
- package/dist/service-owner.js +48 -0
- package/dist/service-owner.js.map +1 -0
- package/dist/settings-api.d.ts +4 -0
- package/dist/settings-api.js +21 -3
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +85 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.d.ts +2 -0
- package/dist/settings-http-confirmation.js +3 -2
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-request-capability.d.ts +4 -1
- package/dist/settings-request-capability.js +11 -3
- package/dist/settings-request-capability.js.map +1 -1
- package/dist/setup-host.js +1 -1
- package/dist/setup-wizard.js +1 -1
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-summary.d.ts +101 -0
- package/dist/task-board-summary.js +177 -0
- package/dist/task-board-summary.js.map +1 -0
- package/dist/task-board-waker.d.ts +8 -1
- package/dist/task-board-waker.js +15 -6
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.js +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +5 -1
- package/dist/tmux-exec-helper.js +13 -21
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.js +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -0
- package/dist/topic-commands.js +29 -18
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +5 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +2 -0
- package/dist/transcript-sources.js +4 -0
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js +3 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +1 -1
- package/dist/tunnel/cloudflared.js +51 -10
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +13 -0
- package/dist/tunnel/named-diagnostics.js +52 -0
- package/dist/tunnel/named-diagnostics.js.map +1 -0
- package/dist/tunnel/types.d.ts +2 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/ui/app-effects.js +10 -4
- package/dist/ui/chat-strings.js +3 -0
- package/dist/ui/chat-thread.js +14 -7
- package/dist/ui/effect-trial-offer.js +2 -31
- package/dist/ui/effects-waker.js +2 -70
- package/dist/ui/fleet-task-card.js +5 -5
- package/dist/ui/fleet-task-common.js +41 -7
- package/dist/ui/fleet-task-work.js +107 -0
- package/dist/ui/fleet-tasks.js +46 -12
- package/dist/ui/image-lightbox.js +39 -1
- package/dist/ui/image-zoom.js +242 -0
- package/dist/ui/panel-chat.js +24 -13
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +6 -2
- package/dist/ui/panel-needs.js +21 -6
- package/dist/ui/panel-settings.js +35 -13
- package/dist/ui/preview-panel.js +2 -1
- package/dist/ui/search-dialog.js +2 -1
- package/dist/ui/settings-appearance.js +33 -8
- package/dist/ui/settings-dialogs.js +33 -17
- package/dist/ui/settings-discord.js +6 -2
- package/dist/ui/settings-guests.js +2 -1
- package/dist/ui/settings-model.js +2 -11
- package/dist/ui/settings-mods.js +6 -3
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/settings-token.js +3 -1
- package/dist/ui/settings-web-authority.js +6 -3
- package/dist/ui/settings-wizard.js +13 -7
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-needs.js +2 -2
- package/dist/ui/shared/app-store.js +2 -2
- package/dist/ui/shared/app.css +80 -9
- package/dist/ui/shared/app.js +5 -1
- package/dist/ui/shared/effect-app-runtime.js +7 -5
- package/dist/ui/shared/effect-chat-runtime.js +2 -7
- package/dist/ui/shared/effect-cursor.js +3 -2
- package/dist/ui/shared/effect-display-metadata.js +39 -0
- package/dist/ui/shared/effect-registry.js +4 -2
- package/dist/ui/shared/effect-runtime-loader.js +3 -1
- package/dist/ui/shared/effect-stock-trust.js +1 -1
- package/dist/ui/shared/feature-flags.js +127 -0
- package/dist/ui/shared/identity-color.js +6 -3
- package/dist/ui/shared/instance-nav.js +3 -2
- package/dist/ui/shared/text-undo.js +210 -0
- package/dist/ui/shared/ui-dialog.js +4 -3
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/update-dispatch.js +1 -1
- package/dist/update-launch.d.ts +1 -1
- package/dist/update-launch.js +1 -2
- package/dist/update-launch.js.map +1 -1
- package/dist/usage/providers.js +1 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +152 -14
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +1 -0
- package/dist/web-appearance-tools.js +7 -3
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance.d.ts +4 -10
- package/dist/web-appearance.js +4 -3
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-chat-history.d.ts +14 -0
- package/dist/web-chat-history.js +26 -1
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-experimental-http.d.ts +2 -0
- package/dist/web-experimental-http.js +12 -0
- package/dist/web-experimental-http.js.map +1 -0
- package/dist/web-experimental.d.ts +11 -0
- package/dist/web-experimental.js +49 -0
- package/dist/web-experimental.js.map +1 -0
- package/dist/web-guest-access.d.ts +1 -0
- package/dist/web-guest-access.js +4 -2
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-store.d.ts +2 -0
- package/dist/web-mod-store.js +10 -0
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-preview.d.ts +2 -0
- package/dist/web-preview.js +2 -0
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +5 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +1 -0
- package/dist/web-settings-tools.js +14 -10
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal.js +1 -1
- package/dist/web-theme-store.d.ts +2 -2
- package/dist/web-theme-store.js +6 -3
- package/dist/web-theme-store.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/panel-fleet.js
CHANGED
|
@@ -276,6 +276,10 @@ function Config({ lease }) {
|
|
|
276
276
|
// Each save owns the form's busy state only while it is the latest: an older save that is decided later (it waited
|
|
277
277
|
// for an admin's confirmation) never unlocks the form under a newer one still on its way (#1453 review).
|
|
278
278
|
const saves = useRef(0);
|
|
279
|
+
// #2342: each project root's row keeps its own key — keyed by position, removing one shifted the texts below into
|
|
280
|
+
// the boxes above (each box's text rewritten, its undo history lost).
|
|
281
|
+
const rootKeys = useRef({ n: 0, list: [] });
|
|
282
|
+
const rootKey = (i) => { const k = rootKeys.current; while (k.list.length <= i) k.list.push(++k.n); return k.list[i]; };
|
|
279
283
|
useEffect(() => {
|
|
280
284
|
if (!state || !state.data) return;
|
|
281
285
|
const c = state.data, ch = c.channel || {}, acc = ch.access || {};
|
|
@@ -314,9 +318,9 @@ function Config({ lease }) {
|
|
|
314
318
|
<section class="card"><h3>${t("fleet.defaults")}</h3>
|
|
315
319
|
<div class="field"><label for="cfg-be">${t("fleet.backend")}</label><${BackendSelect} id="cfg-be" value=${form.backend} onChange=${(v) => setForm({ ...form, backend: v })} /></div></section>
|
|
316
320
|
<section class="card"><h3>${t("fleet.roots")}</h3>
|
|
317
|
-
${form.roots.map((r, i) => html`<div key=${i} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
|
|
321
|
+
${form.roots.map((r, i) => html`<div key=${rootKey(i)} class="field-row"><input value=${r} placeholder="/path/to/projects" aria-label=${t("fleet.roots")}
|
|
318
322
|
onInput=${(e) => { const roots = [...form.roots]; roots[i] = e.target.value; setForm({ ...form, roots }); }} />
|
|
319
|
-
<button type="button" class="icon-btn" aria-label=${t("fleet.removeRoot")} title=${t("fleet.removeRoot")} onClick=${() => setForm({ ...form, roots: form.roots.filter((_, j) => j !== i) })}><${Icon} name="close" size=${16} /></button></div>`)}
|
|
323
|
+
<button type="button" class="icon-btn" aria-label=${t("fleet.removeRoot")} title=${t("fleet.removeRoot")} onClick=${() => { rootKeys.current.list.splice(i, 1); setForm({ ...form, roots: form.roots.filter((_, j) => j !== i) }); }}><${Icon} name="close" size=${16} /></button></div>`)}
|
|
320
324
|
<button type="button" class="btn btn-ghost" onClick=${() => setForm({ ...form, roots: [...form.roots, ""] })}><${Icon} name="plus" size=${16} />${t("fleet.addRoot")}</button></section>
|
|
321
325
|
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${saving} onClick=${save}>${t("fleet.save")}</button><span class="note">${t("fleet.saveNote")}</span></div>`}</${Loaded}>`;
|
|
322
326
|
}
|
package/dist/ui/panel-needs.js
CHANGED
|
@@ -25,7 +25,8 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
|
|
|
25
25
|
register("needs", {
|
|
26
26
|
en: {
|
|
27
27
|
title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
|
|
28
|
-
open: "Open chat",
|
|
28
|
+
open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", signIn: "Sign in",
|
|
29
|
+
gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
|
|
29
30
|
ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
|
|
30
31
|
notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
|
|
31
32
|
notifyOff: "Get a desktop notification when something new needs you while this tab is in the background.",
|
|
@@ -37,7 +38,8 @@ register("needs", {
|
|
|
37
38
|
},
|
|
38
39
|
"zh-TW": {
|
|
39
40
|
title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
|
|
40
|
-
open: "開啟聊天",
|
|
41
|
+
open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", signIn: "登入",
|
|
42
|
+
gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
|
|
41
43
|
ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
|
|
42
44
|
notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
|
|
43
45
|
notifyOff: "分頁在背景時,有新的事等你處理就跳出桌面通知。",
|
|
@@ -84,9 +86,21 @@ const setClaim = (kind, key, value) => claims.set((c) => {
|
|
|
84
86
|
return { ...c, [kind]: next };
|
|
85
87
|
});
|
|
86
88
|
|
|
87
|
-
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send" };
|
|
89
|
+
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks" };
|
|
88
90
|
const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* An item's line. A task board card's is said here, in the page's language (#2246 step 3): its number and title, then
|
|
94
|
+
* its question or what it lacks; anything else is the server's own words.
|
|
95
|
+
*/
|
|
96
|
+
function detailOf(item) {
|
|
97
|
+
const c = item.type === "task" ? item.card : null;
|
|
98
|
+
if (!c || !c.title) return item.detail;
|
|
99
|
+
const lacks = c.gap ? [c.gap.unchecked ? tn("gapUnchecked", c.gap.unchecked) : "", c.gap.noResult ? tn("gapNoResult") : ""].filter(Boolean).join(tn("gapJoin")) : "";
|
|
100
|
+
const tail = c.question || (lacks ? tn("gap", lacks) : "");
|
|
101
|
+
return `${c.code} ${c.title}${tail ? ` — ${tail}` : ""}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
90
104
|
/** "3 min", "2 h": the age shown beside an item (refreshed by a display timer, never by a read). */
|
|
91
105
|
export function age(since, nowMs) {
|
|
92
106
|
const minutes = Math.max(0, Math.floor((nowMs - since) / 60_000));
|
|
@@ -129,7 +143,7 @@ export function NeedsPanel({ navKey }) {
|
|
|
129
143
|
<div data-trust="panel-content" class="panel-body"><div class="col">
|
|
130
144
|
${!items.length ? html`<${Empty} icon="check" title=${tn("empty")} hint=${tn(chatApp ? "emptyHint" : "emptyHintWeb")} />`
|
|
131
145
|
: groups.map((g) => html`<section key=${g.instance} class="n-group" aria-label=${g.instance}>
|
|
132
|
-
<h2 class="n-group-head"
|
|
146
|
+
<h2 class="n-group-head">${g.instance ? html`<a href=${chatPath(g.instance)} title=${g.instance}>${nameFor(g.instance)}</a>` : tn("noAssignee")}</h2>
|
|
133
147
|
${g.items.map((item) => html`<${NeedsItem} key=${item.id} item=${item} nowMs=${nowMs} owner=${owner} />`)}
|
|
134
148
|
</section>`)}
|
|
135
149
|
<${NotifyToggle} />
|
|
@@ -180,14 +194,15 @@ function NeedsItem({ item, nowMs, owner }) {
|
|
|
180
194
|
<span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
|
|
181
195
|
<div class="n-body">
|
|
182
196
|
<div class="n-title"><strong>${reasonText(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
|
|
183
|
-
${item
|
|
197
|
+
${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
|
|
184
198
|
<div class="n-actions">
|
|
185
199
|
${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
|
|
186
200
|
disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
|
|
187
201
|
${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
188
202
|
<${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
|
|
189
203
|
${item.reason === "auth_paused" && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
|
|
190
|
-
|
|
204
|
+
${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
|
|
205
|
+
${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn("open")}</a>` : null}
|
|
191
206
|
</div>
|
|
192
207
|
</div>
|
|
193
208
|
</article>`;
|
|
@@ -175,7 +175,7 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
175
175
|
const applied = (pendingChanges || []).filter((p) => p.state === "applied").map((p) => p.id).join(",");
|
|
176
176
|
useEffect(() => { if (applied) reload(); }, [applied]);
|
|
177
177
|
|
|
178
|
-
const stage = (key, change) => {
|
|
178
|
+
const stage = (key, change, quiet = false) => {
|
|
179
179
|
setStaged((s) => {
|
|
180
180
|
const m = new Map(s);
|
|
181
181
|
const prev = m.get(key);
|
|
@@ -186,7 +186,7 @@ export function SettingsPanel({ route, navKey }) {
|
|
|
186
186
|
m.set(key, { ...change, stageKey: key });
|
|
187
187
|
return m;
|
|
188
188
|
});
|
|
189
|
-
toast(tn("queued", change.label));
|
|
189
|
+
if (!quiet) toast(tn("queued", change.label));
|
|
190
190
|
};
|
|
191
191
|
const unstage = (key) => setStaged((s) => { if (!s.has(key)) return s; const m = new Map(s); m.delete(key); return m; });
|
|
192
192
|
const discard = () => { dropSecrets(staged); setStaged(new Map()); reload(); };
|
|
@@ -818,11 +818,21 @@ function WebSection({ ctx, staged }) {
|
|
|
818
818
|
terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
|
|
819
819
|
}), []);
|
|
820
820
|
const text = (v) => (v === null || v === undefined ? "" : String(v));
|
|
821
|
-
const [f, setF] = useState(() =>
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
821
|
+
const [f, setF] = useState(() => {
|
|
822
|
+
// Settings sections share one draft. Reopening Web must show it as well as preserve it in the Apply bar, so a
|
|
823
|
+
// subsequent field edit keeps earlier edits. The original disk values remain the diff/confirmation baseline.
|
|
824
|
+
const request = staged.get("web")?.request;
|
|
825
|
+
const pending = request?.method === "PUT" && request.url === "/api/settings/fleet/web" ? request.body || {} : {};
|
|
826
|
+
const pub = pending.public_link || {}, session = pending.session?.local || {}, terminal = pending.web_terminal || {};
|
|
827
|
+
const pick = (obj, key, fallback) => Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : fallback;
|
|
828
|
+
return {
|
|
829
|
+
echo: pick(pending, "echo_to_channel", original.echo), log: pick(pending, "log_tab", original.log), usagePub: pick(pending, "usage_report_on_public_link", original.usagePub),
|
|
830
|
+
pub: pick(pub, "allow_public", original.pub.allow_public), ttl: String(pick(pub, "ttl_minutes", original.pub.ttl_minutes)), protocol: pick(pub, "protocol", original.pub.protocol),
|
|
831
|
+
hostname: pick(pub, "hostname", original.pub.hostname), originPort: String(pick(pub, "origin_port", original.pub.origin_port)),
|
|
832
|
+
abs: text(pick(session, "absolute_hours", original.session.absolute_hours)), idle: text(pick(session, "idle_hours", original.session.idle_hours)),
|
|
833
|
+
termOn: pick(terminal, "enabled", original.terminal.enabled) !== false, termTtl: text(pick(terminal, "ttl_minutes", original.terminal.ttl_minutes)),
|
|
834
|
+
};
|
|
835
|
+
});
|
|
826
836
|
const [msg, setMsg] = useState(null);
|
|
827
837
|
const set = (k) => (v) => setF((x) => ({ ...x, [k]: v }));
|
|
828
838
|
// Empty = the default (null in the write: the key is removed from fleet.yaml).
|
|
@@ -840,9 +850,14 @@ function WebSection({ ctx, staged }) {
|
|
|
840
850
|
termTtl: value(f.termTtl) !== null && (!Number.isInteger(value(f.termTtl)) || value(f.termTtl) < 1 || value(f.termTtl) > 20) ? "webTerminalTtlRange" : "",
|
|
841
851
|
};
|
|
842
852
|
const ready = !overPublicLink && !Object.values(errors).some(Boolean);
|
|
843
|
-
const review = () => {
|
|
844
|
-
|
|
845
|
-
|
|
853
|
+
const review = (quiet = false) => {
|
|
854
|
+
// Invalid intermediate text must not leave an older valid draft available to Apply.
|
|
855
|
+
if (!ready) { if (staged.has("web")) ctx.unstage("web"); setMsg(null); return; }
|
|
856
|
+
// Keep an existing secret reference, including a custom one. Only a newly supplied hostname without any
|
|
857
|
+
// reference adopts the standard env key; mounting or editing an unrelated field never rewrites credentials.
|
|
858
|
+
const tokenEnv = f.hostname === original.pub.hostname ? original.pub.tunnel_token_env
|
|
859
|
+
: f.hostname ? original.pub.tunnel_token_env || "AGEND_CLOUDFLARE_TUNNEL_TOKEN" : "";
|
|
860
|
+
const publicPatch = changedFields({ allow_public: f.pub, ttl_minutes: Number(f.ttl), protocol: f.protocol, hostname: f.hostname, tunnel_token_env: tokenEnv, origin_port: Number(f.originPort) }, original.pub);
|
|
846
861
|
const sessionPatch = changedFields({ absolute_hours: value(f.abs), idle_hours: value(f.idle) }, original.session);
|
|
847
862
|
// The checkbox shows the default (on) for an unset key: only a real toggle is written; on again = back to the default.
|
|
848
863
|
const terminalPatch = changedFields({ enabled: f.termOn === (original.terminal.enabled !== false) ? original.terminal.enabled : f.termOn ? null : false,
|
|
@@ -865,10 +880,16 @@ function WebSection({ ctx, staged }) {
|
|
|
865
880
|
];
|
|
866
881
|
if (fields.length) {
|
|
867
882
|
ctx.stage("web", { label: tn("sec_web"), impact: batchImpact(schema, fields, (k) => impactOf(schema, k)),
|
|
868
|
-
request: { method: "PUT", url: "/api/settings/fleet/web", body } });
|
|
883
|
+
request: { method: "PUT", url: "/api/settings/fleet/web", body } }, quiet);
|
|
869
884
|
} else if (staged.has("web")) ctx.unstage("web");
|
|
870
|
-
setMsg(tn("readyApply"));
|
|
885
|
+
setMsg(fields.length ? tn("readyApply") : null);
|
|
871
886
|
};
|
|
887
|
+
// This form already has validation and a single shared Web draft. Valid edits join that draft immediately;
|
|
888
|
+
// Apply, its confirmations and the leave guard still own all HTTP writes. Quiet staging avoids a toast per key.
|
|
889
|
+
// Returning from another Settings section remounts this form: its unchanged initial fields must not erase the
|
|
890
|
+
// parent's already-staged Web draft. Only an actual field edit replaces or removes that draft.
|
|
891
|
+
const formMounted = useRef(false);
|
|
892
|
+
useEffect(() => { if (!formMounted.current) { formMounted.current = true; return; } review(true); }, [f]);
|
|
872
893
|
const shown = (path) => {
|
|
873
894
|
const [top, key] = path.split(".");
|
|
874
895
|
const v = (top === "web" ? web : term)[key];
|
|
@@ -920,7 +941,7 @@ function WebSection({ ctx, staged }) {
|
|
|
920
941
|
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
921
942
|
<dl class="s-web-ro">${WEB_READ_ONLY.map((k) => html`<div key=${k}><dt><code>${k}</code></dt><dd>${shown(k)}</dd></div>`)}</dl>
|
|
922
943
|
</section>
|
|
923
|
-
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${review}>${tn("reviewChanges")}</button>
|
|
944
|
+
<div class="save-row"><button type="button" class="btn btn-primary" disabled=${!ready} onClick=${() => review()}>${tn("reviewChanges")}</button>
|
|
924
945
|
${msg ? html`<span class="note" role="status">${msg}</span>` : null}</div>
|
|
925
946
|
</div>`;
|
|
926
947
|
}
|
|
@@ -1062,6 +1083,7 @@ function PublicTunnelToken() {
|
|
|
1062
1083
|
try {
|
|
1063
1084
|
const prepared = await api(`${base}/verify`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify(clear ? { clear: true } : { secret: value }) });
|
|
1064
1085
|
if (!lease.current()) return;
|
|
1086
|
+
if (!prepared.ok && prepared.body?.error_code === "invalid_tunnel_token") { setMessage(tn("namedTokenInvalid")); return; }
|
|
1065
1087
|
if (!prepared.ok) throw Error("refused");
|
|
1066
1088
|
const applied = await confirmedWrite(`${base}/apply`, { method: "POST", key, label: tn(clear ? "namedTokenClear" : "namedTokenSet"), body: { verification_id: prepared.body?.verification_id, idempotency_key: key } });
|
|
1067
1089
|
let body = applied.body || {};
|
package/dist/ui/preview-panel.js
CHANGED
|
@@ -16,7 +16,8 @@ import { Icon } from "/assets/ui-icons.js";
|
|
|
16
16
|
import "./chat-render.js";
|
|
17
17
|
import "./preview.js";
|
|
18
18
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
19
|
-
import {
|
|
19
|
+
import { experimentalComponent, useCanvasWorker } from "/assets/feature-flags.js";
|
|
20
|
+
const EffectTrialPanel = experimentalComponent(() => import("./effect-trial-panel.js"), m => m.EffectTrialPanel);
|
|
20
21
|
import { chatTime, onDayChange } from "./chat-time.js";
|
|
21
22
|
|
|
22
23
|
const R = () => globalThis.AgendChatRender;
|
package/dist/ui/search-dialog.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// agents, a task — opens here as the index's copy (GET /ui/search/context). Text only: no innerHTML, no style attribute.
|
|
6
6
|
import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
|
|
7
7
|
import { t, register } from "/assets/app-i18n.js";
|
|
8
|
+
import { recordEdit } from "/assets/text-undo.js";
|
|
8
9
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
9
10
|
import { navigate } from "/assets/app-nav.js";
|
|
10
11
|
import { chatPath, fleetPath } from "/assets/app-route.js";
|
|
@@ -286,7 +287,7 @@ export function SearchDialog({ preset = {}, onClose }) {
|
|
|
286
287
|
<div class="search-body" ref=${list}>
|
|
287
288
|
${detail ? html`<${Detail} detail=${detail} q=${q} label=${label} onBack=${() => { setDetail(null); input.current && input.current.focus(); }} onClose=${onClose} />`
|
|
288
289
|
: html`<${Results} q=${q} res=${res} groups=${groups} active=${active} setActive=${setActive} open=${open} label=${label} filtered=${activeFilters > 0}
|
|
289
|
-
recent=${recent} pick=${(s) => { setQ(s); input.current && input.current.focus(); }} clearRecent=${() => { forgetRecent(); setRecent([]); }} />`}
|
|
290
|
+
recent=${recent} pick=${(s) => { recordEdit(input.current); setQ(s); input.current && input.current.focus(); }} clearRecent=${() => { forgetRecent(); setRecent([]); }} />`}
|
|
290
291
|
</div>
|
|
291
292
|
<div class="search-foot" aria-hidden="true">${t("search.keys")}</div>
|
|
292
293
|
</div></dialog>`;
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
// versions, the pictures and fonts it may name, and this device's off switch. Settings itself is always drawn stock
|
|
3
3
|
// (owner-css.js removes the link here), so a stylesheet that hides things can always be fixed from this page.
|
|
4
4
|
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
5
|
-
import { t } from "/assets/app-i18n.js";
|
|
5
|
+
import { register, t } from "/assets/app-i18n.js";
|
|
6
|
+
import { recordEdit } from "/assets/text-undo.js";
|
|
6
7
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
7
8
|
import { ModsCard } from "./settings-mods.js";
|
|
8
|
-
import {
|
|
9
|
+
import { experimentalComponent, useCanvasWorker } from "/assets/feature-flags.js";
|
|
10
|
+
const EffectsCard = experimentalComponent(() => import("./settings-effects.js"), m => m.EffectsCard);
|
|
9
11
|
import { Icon } from "/assets/ui-icons.js";
|
|
10
12
|
import { toast } from "/assets/ui-toast.js";
|
|
11
13
|
import { ownerCssOffHere, ownerCssTrial, safeMode, setOwnerCssActive, setOwnerCssOffHere, startOwnerCssTrial } from "/assets/owner-css.js";
|
|
@@ -49,7 +51,26 @@ export function AppearanceSection() {
|
|
|
49
51
|
${group("groupLayout")}<${SidebarWidgetCard} />
|
|
50
52
|
${group("groupCss")}<${OwnerCssCard} /><${DeviceCard} />
|
|
51
53
|
${group("groupMods")}<${ModsCard} /><${EffectsCard} />
|
|
52
|
-
${group("groupAdvanced")}<${CardLimitsCard} /><${RestoreDefaultsCard} />`;
|
|
54
|
+
${group("groupAdvanced")}<${ExperimentalEffectsCard} /><${CardLimitsCard} /><${RestoreDefaultsCard} />`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
register("experimentalEffects", {
|
|
58
|
+
en: { title: "Experimental: Canvas Worker effects", help: "Enable uploaded Canvas Worker effects. Ambient effects are always available. Saved effects stay intact when this is off.", failed: "Could not change this setting. Try again." },
|
|
59
|
+
"zh-TW": { title: "實驗功能:Canvas Worker 特效", help: "啟用上傳的 Canvas Worker 特效。氛圍特效不受影響;關閉時保留已儲存的特效。", failed: "無法變更此設定,請重試。" },
|
|
60
|
+
});
|
|
61
|
+
function ExperimentalEffectsCard() {
|
|
62
|
+
const enabled = useCanvasWorker(), [writable, setWritable] = useState(false), [busy, setBusy] = useState(false), [failed, setFailed] = useState(false);
|
|
63
|
+
useEffect(() => { let live = true; Promise.resolve(globalThis.AgendAuth?.trialSession?.()).then(value => { if (live) setWritable(value?.localAdmin === true); }, () => {}); return () => { live = false; }; }, []);
|
|
64
|
+
if (!writable) return null;
|
|
65
|
+
const change = async value => {
|
|
66
|
+
setBusy(true); setFailed(false);
|
|
67
|
+
try {
|
|
68
|
+
const result = await confirmedWrite("/api/settings/fleet/web", { method: "PUT", body: { experimental: { canvas_worker: value } }, label: t("experimentalEffects.title") });
|
|
69
|
+
if (result.ok) appStore.set({ canvasWorker: value }); else setFailed(true);
|
|
70
|
+
} catch { setFailed(true); }
|
|
71
|
+
finally { setBusy(false); }
|
|
72
|
+
};
|
|
73
|
+
return html`<section class="card s-appearance s-experimental-effects"><label class="check"><input type="checkbox" checked=${enabled} disabled=${busy} onChange=${event => change(event.target.checked)} /><span>${t("experimentalEffects.title")}</span></label><p class="sub">${t("experimentalEffects.help")}</p>${failed ? html`<p role="alert">${t("experimentalEffects.failed")}</p>` : null}</section>`;
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/** #2056 B: a sample sidebar row in the host's default colour places, so the choice is seen rather than described. */
|
|
@@ -347,6 +368,7 @@ function SidebarWidgetCard() {
|
|
|
347
368
|
const app = useStore(appStore);
|
|
348
369
|
const saved = sidebarWidgetRaw();
|
|
349
370
|
const [text, setText] = useState(() => (saved ? JSON.stringify(saved, null, 2) : ""));
|
|
371
|
+
const area = useRef(null);
|
|
350
372
|
const [error, setError] = useState("");
|
|
351
373
|
let parsed = null, problem = "";
|
|
352
374
|
if (text.trim()) {
|
|
@@ -359,17 +381,17 @@ function SidebarWidgetCard() {
|
|
|
359
381
|
setSidebarWidget(parsed);
|
|
360
382
|
toast(tn("widgetSaved"));
|
|
361
383
|
};
|
|
362
|
-
const remove = () => { setSidebarWidget(null); setText(""); setError(""); toast(tn("widgetRemoved")); };
|
|
384
|
+
const remove = () => { setSidebarWidget(null); recordEdit(area.current); setText(""); setError(""); toast(tn("widgetRemoved")); };
|
|
363
385
|
return html`<section class="card s-appearance s-synced s-widget"><${CardTitle} title=${tn("widgetTitle")} topic="widgets" />
|
|
364
386
|
<p class="sub">${tn("widgetLead")}</p>
|
|
365
387
|
<label class="field"><span class="sr-only">${tn("widgetTitle")}</span>
|
|
366
|
-
<textarea class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
|
|
388
|
+
<textarea ref=${area} class="s-widget-text" rows="10" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text}
|
|
367
389
|
onInput=${(e) => { setText(e.target.value); setError(""); }}></textarea></label>
|
|
368
390
|
${error || problem ? html`<p class="err" role="alert">${error || problem}</p>` : null}
|
|
369
391
|
${preview ? html`<div class="s-widget-preview"><${AgentWidget} slot="sidebar" widget=${preview} binds=${fleetBinds(app.instances, app.exec, app.awaiting)} /></div>` : null}
|
|
370
392
|
<div class="s-widget-actions">
|
|
371
393
|
<button type="button" class="btn btn-primary" disabled=${!parsed || !!problem} onClick=${save}><${Icon} name="check" size=${16} />${tn("widgetSave")}</button>
|
|
372
|
-
${!text.trim() ? html`<button type="button" class="btn" onClick=${() => setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2))}>${tn("widgetExample")}</button>` : null}
|
|
394
|
+
${!text.trim() ? html`<button type="button" class="btn" onClick=${() => { recordEdit(area.current); setText(JSON.stringify(SIDEBAR_WIDGET_EXAMPLE, null, 2)); }}>${tn("widgetExample")}</button>` : null}
|
|
373
395
|
${saved ? html`<button type="button" class="btn" onClick=${remove}>${tn("widgetRemove")}</button>` : null}
|
|
374
396
|
</div>
|
|
375
397
|
</section>`;
|
|
@@ -378,6 +400,7 @@ function SidebarWidgetCard() {
|
|
|
378
400
|
function OwnerCssCard() {
|
|
379
401
|
const [state, setState] = useState(null);
|
|
380
402
|
const [text, setText] = useState("");
|
|
403
|
+
const area = useRef(null);
|
|
381
404
|
const [busy, setBusy] = useState(false);
|
|
382
405
|
const [error, setError] = useState("");
|
|
383
406
|
const trial = useStore(ownerCssTrial);
|
|
@@ -386,6 +409,8 @@ function OwnerCssCard() {
|
|
|
386
409
|
const r = await call("/ui/owner-css/state");
|
|
387
410
|
if (!r.ok || !r.body) { setState({ failed: true }); return; }
|
|
388
411
|
setState(r.body);
|
|
412
|
+
// #2342: a restored version replaces the editor's text as a step Ctrl+Z goes back from.
|
|
413
|
+
if (opts.undoable) recordEdit(area.current);
|
|
389
414
|
if (opts.text !== false) setText(r.body.text || "");
|
|
390
415
|
};
|
|
391
416
|
useEffect(() => { load(); }, []);
|
|
@@ -409,7 +434,7 @@ function OwnerCssCard() {
|
|
|
409
434
|
setBusy(false);
|
|
410
435
|
if (!r.ok) { setError(refusal(r.body)); return; }
|
|
411
436
|
setOwnerCssActive(r.body.active);
|
|
412
|
-
load();
|
|
437
|
+
load({ undoable: true });
|
|
413
438
|
};
|
|
414
439
|
const addAsset = async () => {
|
|
415
440
|
const f = file.current && file.current.files && file.current.files[0];
|
|
@@ -440,7 +465,7 @@ function OwnerCssCard() {
|
|
|
440
465
|
<p class="sub">${tn("cssLead")}</p>
|
|
441
466
|
<p class="s-css-status" role="status">${inForce}</p>
|
|
442
467
|
<label class="field"><span class="sr-only">${tn("cssTitle")}</span>
|
|
443
|
-
<textarea class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
|
|
468
|
+
<textarea ref=${area} class="s-css-text" rows="14" spellcheck=${false} autocapitalize="off" autocomplete="off" value=${text} disabled=${busy}
|
|
444
469
|
onInput=${(e) => setText(e.target.value)}></textarea></label>
|
|
445
470
|
${error ? html`<p class="err" role="alert">${error}</p>` : null}
|
|
446
471
|
<div class="s-css-actions">
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// provider key): each goes through confirmedWrite, so a change an admin must confirm waits for it (#1423).
|
|
5
5
|
import { html, useEffect, useMemo, useRef, useState } from "/assets/app-html.js";
|
|
6
6
|
import { t } from "/assets/app-i18n.js";
|
|
7
|
+
import { recordEdit } from "/assets/text-undo.js";
|
|
7
8
|
import { useLease } from "/assets/app-ctx.js";
|
|
8
9
|
import { Dialog } from "/assets/ui-dialog.js";
|
|
9
10
|
import { Icon } from "/assets/ui-icons.js";
|
|
@@ -16,11 +17,13 @@ import { startOperation } from "./settings-apply.js";
|
|
|
16
17
|
import "./settings-strings.js"; // its words, wherever a picker of this file is used (#2072: Fleet's create dialog too)
|
|
17
18
|
import { backendChoices, modelChoices, OTHER_MODEL, useBackendRows, useModelCatalog } from "./settings-catalog.js";
|
|
18
19
|
import {
|
|
19
|
-
ACCESS_MODES,
|
|
20
|
+
ACCESS_MODES, AGENT_MODES, CH_TYPES, HANG_TIMEOUT_DEFAULT_MINUTES, LOG_LEVELS, TOOL_PROGRESS, TOOL_SETS,
|
|
20
21
|
VISIBILITY_MODES, accessConfirmations, agentForm, agentPatch, backendOptions, changedFields, channelId, channelsOf,
|
|
21
|
-
chLabel, hasOwn, impactOf, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
|
|
22
|
+
chLabel, hasOwn, impactOf, newConnectionImpact, nonNegative, nonNegativeInteger, positive, replyGuardSupported, sameValue, visibilityDefault,
|
|
22
23
|
classicWebEchoEffective, worstImpact,
|
|
23
24
|
} from "./settings-model.js";
|
|
25
|
+
/** #2342: a field by id, for recordEdit before a pick or a reset writes it. */
|
|
26
|
+
const byId = (id) => (typeof document !== "undefined" ? document.getElementById(id) : null);
|
|
24
27
|
|
|
25
28
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
26
29
|
export const impactText = (kind) => tn(kind === "now" ? "impactNow" : kind === "fleet" ? "impactFleet" : "impactAgent");
|
|
@@ -103,7 +106,7 @@ export function ChipList({ items, onChange, placeholder, label }) {
|
|
|
103
106
|
|
|
104
107
|
const replyHint = (backend, mode, kiroUi) => (replyGuardSupported(backend, mode, kiroUi) ? tn("replyGuardSupported") : tn("replyGuardUnsupported", backend, mode));
|
|
105
108
|
|
|
106
|
-
/** The dialog every editor uses: Cancel / Stage change; the footer says what the
|
|
109
|
+
/** The dialog every editor uses: Cancel / Stage change; the footer says what the changes made so far will cost (#2346: only the fields that changed). */
|
|
107
110
|
function EditDialog({ title, subtitle, onClose, onStage, impacts, schema, children, ready = true, wide = true }) {
|
|
108
111
|
const kinds = [...new Set((impacts || []).map((f) => impactOf(schema, f)))];
|
|
109
112
|
return html`<${Dialog} title=${title} label=${subtitle ? `${title} — ${subtitle}` : title} onClose=${onClose} wide=${wide}
|
|
@@ -184,9 +187,9 @@ export function StatusEmojiEditor({ own, platform, channel, previewBody, onChang
|
|
|
184
187
|
<label for=${`se-${key}`}>${tn(`se_${key}`)}</label>
|
|
185
188
|
<input id=${`se-${key}`} type="text" autocomplete="off" value=${values[key] || ""} placeholder=${builtins[key] || ""} onInput=${(e) => set(key, e.target.value)} />
|
|
186
189
|
<button type="button" class="btn btn-sm" onClick=${() => setPicker(picker === key ? null : key)} aria-expanded=${picker === key ? "true" : "false"}>${tn("se_pick")}</button>
|
|
187
|
-
<button type="button" class="icon-btn" aria-label=${tn("se_useDefault")} title=${tn("se_useDefault")} onClick=${() => set(key, "")}><${Icon} name="close" size=${14} /></button>
|
|
190
|
+
<button type="button" class="icon-btn" aria-label=${tn("se_useDefault")} title=${tn("se_useDefault")} onClick=${() => { recordEdit(byId(`se-${key}`)); set(key, ""); }}><${Icon} name="close" size=${14} /></button>
|
|
188
191
|
${picker === key ? html`<${EmojiPicker} cat=${cat} keyName=${key} platform=${platform} channel=${channel}
|
|
189
|
-
onPick=${(v) => { set(key, v); setPicker(null); }} onClose=${() => setPicker(null)} />` : null}
|
|
192
|
+
onPick=${(v) => { recordEdit(byId(`se-${key}`)); set(key, v); setPicker(null); }} onClose=${() => setPicker(null)} />` : null}
|
|
190
193
|
</div>`)}</div>
|
|
191
194
|
<p class="note">${tn("se_hint")}</p>
|
|
192
195
|
</div>`;
|
|
@@ -241,16 +244,17 @@ export function AgentDialog({ name, inst, ctx, onClose }) {
|
|
|
241
244
|
};
|
|
242
245
|
const failoverWarn = !f.inherit.model_failover && !f.model_failover.trim() ? "failoverEmpty" : "";
|
|
243
246
|
const ready = !errors.auto && !errors.hang;
|
|
247
|
+
const pending = () => agentPatch({ ...f, status_emojis: emojis.value }, inst, defaults, visibilityBaseline, emojis.baseline);
|
|
244
248
|
const stage = () => {
|
|
245
249
|
if (!ready) return;
|
|
246
|
-
const patch =
|
|
250
|
+
const patch = pending();
|
|
247
251
|
if (Object.keys(patch).length) ctx.stageAgent(name, inst, patch);
|
|
248
252
|
onClose();
|
|
249
253
|
};
|
|
250
254
|
// What an override shows while it inherits: the default.
|
|
251
255
|
const shown = (k, own, dflt) => (f.inherit[k] ? dflt : own);
|
|
252
256
|
const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
|
|
253
|
-
return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${`ID: ${name}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${
|
|
257
|
+
return html`<${EditDialog} title=${ctx.shortName(name, inst)} subtitle=${`ID: ${name}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `instance.${k}`)} schema=${schema}>
|
|
254
258
|
<div class="grid2">
|
|
255
259
|
<div class="field">${L(tn("displayName"), "instance.display_name", "ag-dn")}
|
|
256
260
|
<${Override} inherit=${f.inherit.display_name} onInherit=${setInherit("display_name")}>
|
|
@@ -406,8 +410,8 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
406
410
|
setToken(""); setTokenId(null); setReplacing(false);
|
|
407
411
|
setTokenNote({ text: tn("tokenQueued") });
|
|
408
412
|
};
|
|
409
|
-
|
|
410
|
-
|
|
413
|
+
/** The connections with this dialog's edits, and which parts changed. */
|
|
414
|
+
const pending = () => {
|
|
411
415
|
const original = structuredClone(chs);
|
|
412
416
|
const next = structuredClone(original);
|
|
413
417
|
next[index] = { ...next[index], access: { ...(next[index].access || {}), mode, allowed_users: [...users] } };
|
|
@@ -418,6 +422,16 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
418
422
|
if (emojis.value) options.status_emojis = emojis.value; else delete options.status_emojis;
|
|
419
423
|
next[index] = { ...next[index], options };
|
|
420
424
|
}
|
|
425
|
+
return { next, accessChanged, emojisChanged };
|
|
426
|
+
};
|
|
427
|
+
const pendingImpacts = () => {
|
|
428
|
+
if (!ch) return [];
|
|
429
|
+
const p = pending();
|
|
430
|
+
return [...(p.accessChanged ? ["fleet.channels"] : []), ...(p.emojisChanged ? ["fleet.channel.options.status_emojis"] : [])];
|
|
431
|
+
};
|
|
432
|
+
const stage = () => {
|
|
433
|
+
if (!ch) return;
|
|
434
|
+
const { next, accessChanged, emojisChanged } = pending();
|
|
421
435
|
if (accessChanged || emojisChanged) {
|
|
422
436
|
ctx.stageChannels(`connections:${ch.id || chLabel(index)}`, next, {
|
|
423
437
|
label: tn(accessChanged && emojisChanged ? "updateAccessEmojis" : accessChanged ? "updateAccess" : "updateEmojis", ch.id || chLabel(index)),
|
|
@@ -445,7 +459,7 @@ export function BotDialog({ id, ctx, onClose }) {
|
|
|
445
459
|
};
|
|
446
460
|
const L = (text, field, htmlFor) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${htmlFor} />`;
|
|
447
461
|
if (!ch) return null;
|
|
448
|
-
return html`<${EditDialog} title=${type === "telegram" ? "Telegram" : "Discord"} subtitle=${id} onClose=${onClose} onStage=${stage} impacts=${
|
|
462
|
+
return html`<${EditDialog} title=${type === "telegram" ? "Telegram" : "Discord"} subtitle=${id} onClose=${onClose} onStage=${stage} impacts=${pendingImpacts()} schema=${schema}>
|
|
449
463
|
<div class="grid2">
|
|
450
464
|
<div class="field">${L(tn("accessMode"), "fleet.channel.access.mode", "bot-mode")}<${Select} id="bot-mode" value=${mode} onChange=${setMode} options=${ACCESS_MODES} />
|
|
451
465
|
<${Feedback} warning=${accessWarn} /></div>
|
|
@@ -503,6 +517,7 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
503
517
|
// The form's revision (#1529 review): any change to the token, platform, id or group moves it; a Verify or a plan
|
|
504
518
|
// that began before is dropped when it lands — never restoring an old token, naming it, or planning for it.
|
|
505
519
|
const rev = useRef(0);
|
|
520
|
+
// The fields keep what was typed (#2342: trimming the box would rewrite it, losing its undo); trimmed here.
|
|
506
521
|
const body = (x) => ({ platform: x.type, connection_only: true, admin_user_id: x.admin.trim(), ...(x.id.trim() ? { channel_id: x.id.trim() } : {}),
|
|
507
522
|
...(x.group.trim() ? (x.type === "discord" ? { guild_id: x.group.trim() } : { group_id: x.group.trim() }) : {}),
|
|
508
523
|
...(x.type === "discord" && x.general.trim() ? { general_channel_id: x.general.trim() } : {}) });
|
|
@@ -538,7 +553,7 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
538
553
|
setBusy(true);
|
|
539
554
|
const p = await planFor(x);
|
|
540
555
|
if (!p) { if (lease.current()) setBusy(false); return; }
|
|
541
|
-
const handed = startOperation([{ label: tn("addBotLabel", p.channel_id), impact:
|
|
556
|
+
const handed = startOperation([{ label: tn("addBotLabel", p.channel_id), impact: newConnectionImpact(ctx.schema),
|
|
542
557
|
request: { method: "POST", url: "/api/settings/quickstart/commit",
|
|
543
558
|
body: { ...body(x), channel_id: p.channel_id, token_env: p.token_env, token_env_generated: true, token: x.token }, sensitive: true } }]);
|
|
544
559
|
if (!handed) { setErr(tn("applyBusyLocal")); setBusy(false); return; }
|
|
@@ -560,9 +575,9 @@ export function NewBotDialog({ ctx, onClose }) {
|
|
|
560
575
|
invite=${f.identity && f.identity.invite} portal=${f.identity && f.identity.portal} guilds=${f.guilds} onGuilds=${(g) => setF((x) => ({ ...x, guilds: g }))}
|
|
561
576
|
guild=${f.group} onGuild=${set("group")} channel=${f.general} onChannel=${set("general")} />`
|
|
562
577
|
: html`<div class="field"><label for="nb-group">${tn("groupIdField")}</label>
|
|
563
|
-
<input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value
|
|
578
|
+
<input id="nb-group" type="text" value=${f.group} onInput=${(e) => set("group")(e.target.value)} /></div>`}
|
|
564
579
|
<div class="field"><label for="nb-user">${tn("wizardAdminUser")}</label>
|
|
565
|
-
<input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value
|
|
580
|
+
<input id="nb-user" type="text" required=${true} inputmode="numeric" pattern=${adminUserIdPattern(f.type)} value=${f.admin} onInput=${(e) => set("admin")(e.target.value)} /></div>
|
|
566
581
|
<${Drawer} title=${tn("advancedSection")}>
|
|
567
582
|
<div class="field"><label for="nb-id">${tn("connectionIdOptional")}</label><input id="nb-id" type="text" placeholder=${plan ? plan.channel_id : f.type}
|
|
568
583
|
value=${f.id} onInput=${(e) => set("id")(e.target.value)} onBlur=${() => { if (f.identity && f.identity.valid) planFor(f); }} /></div>
|
|
@@ -597,9 +612,7 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
597
612
|
const runtime = ctx.classicRuntime(c.instanceName);
|
|
598
613
|
const classicMode = fd.agent_mode ?? "mcp";
|
|
599
614
|
const lease = useLease("classic-dialog");
|
|
600
|
-
const
|
|
601
|
-
if (!ready) return;
|
|
602
|
-
const patch = changedFields({
|
|
615
|
+
const pending = () => changedFields({
|
|
603
616
|
backend: f.backend, model: f.model.trim() || null,
|
|
604
617
|
auto_pause_after: f.inherit.auto_pause_after ? null : Number(f.auto_pause_after),
|
|
605
618
|
tool_progress: f.inherit.tool_progress ? null : f.tool_progress,
|
|
@@ -612,13 +625,16 @@ export function ClassicDialog({ room, ctx, onClose }) {
|
|
|
612
625
|
reply_completion_guard: hasOwn(c, "reply_completion_guard") ? c.reply_completion_guard : null,
|
|
613
626
|
collab: !!c.collab, context_lines: c.context_lines ?? cd.context_lines ?? 5, web_echo: classicWebEchoEffective(c, fleet),
|
|
614
627
|
});
|
|
628
|
+
const stage = async () => {
|
|
629
|
+
if (!ready) return;
|
|
630
|
+
const patch = pending();
|
|
615
631
|
if (!Object.keys(patch).length) { onClose(); return; }
|
|
616
632
|
if (!lease.current()) return;
|
|
617
633
|
ctx.stageClassic(c, patch);
|
|
618
634
|
onClose();
|
|
619
635
|
};
|
|
620
636
|
const L = (text, field, id) => html`<${Label} text=${text} schema=${schema} field=${field} htmlFor=${id} />`;
|
|
621
|
-
return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${
|
|
637
|
+
return html`<${EditDialog} title=${c.name || c.instanceName} subtitle=${`ID: ${c.instanceName} · ${tn(`exec_${runtime.execution}`)}`} onClose=${onClose} onStage=${stage} ready=${ready} impacts=${Object.keys(pending()).map((k) => `classic.${k}`)} schema=${schema}>
|
|
622
638
|
<div class="grid2">
|
|
623
639
|
<div class="field">${L(tn("backend"), "classic.backend", "cl-be")}<${BackendSelect} id="cl-be" value=${f.backend} onChange=${set("backend")} /></div>
|
|
624
640
|
<div class="field">${L(tn("model"), "classic.model", "cl-model")}<${ModelSelect} id="cl-model" value=${f.model} onChange=${set("model")}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// B, nor after A again (an older A), nor once the page is gone.
|
|
10
10
|
import { html, useEffect, useRef, useState } from "/assets/app-html.js";
|
|
11
11
|
import { t } from "/assets/app-i18n.js";
|
|
12
|
+
import { useTypedText } from "/assets/text-undo.js";
|
|
12
13
|
import { api } from "./settings-confirm.js";
|
|
13
14
|
import { Select } from "./settings-dialogs.js";
|
|
14
15
|
|
|
@@ -23,6 +24,9 @@ const probe = (body) => api("/api/settings/quickstart/probe", { method: "POST",
|
|
|
23
24
|
*/
|
|
24
25
|
export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guilds, onGuilds, guild, onGuild, channel, onChannel }) {
|
|
25
26
|
const [polling, setPolling] = useState(null); // { timedOut } while waiting for the bot to join
|
|
27
|
+
// #2342: the typed IDs keep what was typed (a space, the undo history); the picks get them trimmed.
|
|
28
|
+
const [guildText, onGuildText] = useTypedText(guild);
|
|
29
|
+
const [channelText, onChannelText] = useTypedText(channel);
|
|
26
30
|
const [channels, setChannels] = useState(null); // null: not asked; { list } | { error }
|
|
27
31
|
const live = useRef(true);
|
|
28
32
|
const now = useRef({ token, guild, channel });
|
|
@@ -120,11 +124,11 @@ export function DiscordServerPicker({ idPrefix, token, bot, invite, portal, guil
|
|
|
120
124
|
<div class="field"><label for=${`${idPrefix}-guild`}>${tn("guildIdField")}</label>
|
|
121
125
|
${guilds && guilds.length
|
|
122
126
|
? html`<${Select} id=${`${idPrefix}-guild`} value=${guild} onChange=${(v) => pickGuild(v, true)} options=${["", ...guilds.map((g) => ({ value: g.id, label: `${g.name} (${g.id})` }))]} />`
|
|
123
|
-
: html`<input id=${`${idPrefix}-guild`} type="text" value=${
|
|
127
|
+
: html`<input id=${`${idPrefix}-guild`} type="text" value=${guildText} placeholder=${tn("discordNoServerYet")} onInput=${(e) => pickGuild(onGuildText(e.target.value), true)} />`}</div>
|
|
124
128
|
<div class="field"><label for=${`${idPrefix}-gen`}>${tn("wizardGeneralChannel")}</label>
|
|
125
129
|
${channels && channels.list && channels.list.length
|
|
126
130
|
? html`<${Select} id=${`${idPrefix}-gen`} value=${channel} onChange=${pickChannel} options=${["", ...channels.list.map((c) => ({ value: c.id, label: `#${c.name}` }))]} />`
|
|
127
|
-
: html`<input id=${`${idPrefix}-gen`} type="text" value=${
|
|
131
|
+
: html`<input id=${`${idPrefix}-gen`} type="text" value=${channelText} onInput=${(e) => pickChannel(onChannelText(e.target.value))} />`}
|
|
128
132
|
${channels && channels.error ? html`<p class="feedback warning">${channels.error}</p>` : null}</div>
|
|
129
133
|
</div>`;
|
|
130
134
|
}
|
|
@@ -8,6 +8,7 @@ import { t } from "/assets/app-i18n.js";
|
|
|
8
8
|
import { toast } from "/assets/ui-toast.js";
|
|
9
9
|
import { confirmDialog } from "/assets/ui-confirm.js";
|
|
10
10
|
import { api } from "./settings-confirm.js";
|
|
11
|
+
import { DEFAULT_SCHEMA, impactOf } from "./settings-model.js";
|
|
11
12
|
import "./settings-strings.js";
|
|
12
13
|
|
|
13
14
|
const tn = (k, ...v) => t(`settings.${k}`, ...v);
|
|
@@ -59,7 +60,7 @@ export function GuestsCard({ ctx, staged, disabled = false }) {
|
|
|
59
60
|
const setList = (key, k, list) => setF((x) => { const m = { ...x[key] }; if (list === undefined) delete m[k]; else m[k] = list; return { ...x, [key]: m }; });
|
|
60
61
|
const review = () => {
|
|
61
62
|
const body = guestsBody(original, f);
|
|
62
|
-
if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: "
|
|
63
|
+
if (body) ctx.stage("guests", { label: tn("guestsTitle"), impact: impactOf(ctx.schema || DEFAULT_SCHEMA, "web.guests"), request: { method: "PUT", url: "/api/settings/fleet/web", body: { guests: body } } });
|
|
63
64
|
else if (staged.has("guests")) ctx.unstage("guests");
|
|
64
65
|
setMsg(tn("readyApply"));
|
|
65
66
|
};
|
|
@@ -56,6 +56,8 @@ export const chLabel = (i) => (i === 0 ? "primary" : "persona");
|
|
|
56
56
|
// ── Impacts: the server's (/api/settings/schema, derived from HOT_INSTANCE_CONFIG_KEYS); this page never decides one ──
|
|
57
57
|
export const DEFAULT_SCHEMA = { impacts: {}, order: ["now", "instance", "fleet"] };
|
|
58
58
|
export const impactOf = (schema, field) => (schema.impacts && schema.impacts[field]) || "instance";
|
|
59
|
+
/** #2346: a new connection starts at once while another one runs (the schema says so); the first one needs AgEnD restarted. */
|
|
60
|
+
export const newConnectionImpact = (schema) => (schema && schema.connections_start_live === true ? "now" : impactOf(schema || DEFAULT_SCHEMA, "fleet.channels"));
|
|
59
61
|
/** A batch costs as much as its worst member, in the order the server ships. */
|
|
60
62
|
export function batchImpact(schema, keys, impactFor) {
|
|
61
63
|
const order = schema.order;
|
|
@@ -197,17 +199,6 @@ export function agentForm(inst, defaults) {
|
|
|
197
199
|
};
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
/** Every field the agent dialog shows, for its footer's impact summary. */
|
|
201
|
-
export const AGENT_FIELDS = [
|
|
202
|
-
"instance.display_name", "instance.description", "instance.backend", "instance.model", "instance.working_directory",
|
|
203
|
-
"instance.auto_pause_after", "instance.topic_id", "instance.hang_detector", "instance.agent_mode", "instance.tool_set",
|
|
204
|
-
"instance.tool_progress", "instance.cross_instance_visibility", "instance.reply_completion_guard", "instance.log_level",
|
|
205
|
-
"instance.lightweight", "instance.model_failover", "instance.systemPrompt", "instance.tags", "instance.general_topic",
|
|
206
|
-
"instance.status_emojis",
|
|
207
|
-
];
|
|
208
|
-
export const CLASSIC_FIELDS = ["classic.backend", "classic.model", "classic.auto_pause_after", "classic.tool_progress",
|
|
209
|
-
"classic.reply_completion_guard", "classic.collab", "classic.context_lines", "classic.web_echo"];
|
|
210
|
-
export const BOT_FIELDS = ["fleet.channel.access.mode", "fleet.channel.access.allowed_users", "fleet.channels", "fleet.channel.options.status_emojis"];
|
|
211
202
|
|
|
212
203
|
/** Validation messages (keys of the settings namespace), or "" when the value is fine. */
|
|
213
204
|
export const nonNegative = (v) => (!Number.isFinite(Number(v)) || String(v).trim() === "" || Number(v) < 0 ? "mustNonNegative" : "");
|