@songsid/agend 2.3.3-beta.4 → 2.3.3-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-cli.js +13 -112
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -1
- package/dist/agent-endpoint.js +1 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +3 -6
- package/dist/agent-whats-new.js +7 -18
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +0 -7
- package/dist/auth-api.js +1 -15
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +4 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +106 -15
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +57 -11
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +35 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-skills.d.ts +4 -6
- package/dist/bundled-skills.js +10 -14
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +1 -3
- package/dist/channel/mcp-tools.js +4 -58
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +6 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +17 -3
- package/dist/daemon.js +76 -107
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-live.d.ts +1 -1
- package/dist/dashboard-live.js +3 -3
- package/dist/dashboard-live.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +1 -1
- package/dist/dashboard-permissions.js +1 -3
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-manager.d.ts +31 -44
- package/dist/fleet-manager.js +94 -227
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +26 -63
- package/dist/instance-lifecycle.d.ts +6 -0
- package/dist/instance-lifecycle.js +20 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +14 -4
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +19 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +3 -42
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +2 -0
- package/dist/public-web-link.js +5 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/search/search-docs.d.ts +1 -1
- package/dist/search/search-docs.js +2 -2
- package/dist/search/search-docs.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +14 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +3 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +17 -3
- package/dist/settings-confirmation.js +8 -5
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-http-confirmation.js +31 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tool-permissions.js +3 -10
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +27 -7
- package/dist/topic-commands.js +94 -36
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/ui/chat-ambient.js +4 -4
- package/dist/ui/chat-commands.js +3 -2
- package/dist/ui/chat-store.js +4 -1
- package/dist/ui/chat-strings.js +12 -46
- package/dist/ui/chat-thread.js +51 -59
- package/dist/ui/fleet-office-skin.js +37 -14
- package/dist/ui/fleet-office.js +31 -7
- package/dist/ui/html-attachment.js +22 -8
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/preview-panel.js +12 -21
- package/dist/ui/preview.js +10 -3
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +1 -3
- package/dist/ui/settings-confirm.js +18 -0
- package/dist/ui/settings-guests.js +13 -4
- package/dist/ui/settings-learn.js +1 -1
- package/dist/ui/settings-mods.js +5 -12
- package/dist/ui/settings-registry.js +0 -1
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app-i18n.js +13 -2
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +47 -9
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +19 -36
- package/dist/ui/shared/app.js +5 -4
- package/dist/ui/shared/instance-nav.js +9 -9
- package/dist/ui/shared/instance-switch.js +8 -2
- package/dist/ui/shared/panel-view.js +22 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/usage/usage-api.d.ts +3 -0
- package/dist/usage/usage-api.js +5 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +2 -0
- package/dist/view-api.js +64 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +9 -17
- package/dist/web-api.js +255 -371
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +0 -2
- package/dist/web-appearance-tools.js +2 -5
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +0 -6
- package/dist/web-appearance-view.js +0 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +14 -7
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +1 -17
- package/dist/web-asset-parts.js +1 -15
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -7
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +3 -7
- package/dist/web-chat-history.js +10 -17
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +9 -1
- package/dist/web-commands.js +13 -0
- package/dist/web-commands.js.map +1 -1
- package/dist/web-experimental.d.ts +2 -11
- package/dist/web-experimental.js +8 -36
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.d.ts +40 -2
- package/dist/web-guest-access.js +50 -14
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +2 -45
- package/dist/web-mod-bundle.js +15 -63
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +3 -48
- package/dist/web-mod-store.js +30 -97
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +13 -0
- package/dist/web-prefs.js +38 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -13
- package/dist/web-preview.js +12 -23
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +11 -3
- package/dist/web-session.js +22 -11
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -3
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +0 -13
- package/dist/web-settings-tools.js +12 -84
- package/dist/web-settings-tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -672
- package/dist/ui/app-effects.js +0 -251
- package/dist/ui/chat-effect-posters.js +0 -42
- package/dist/ui/chat-effects.js +0 -167
- package/dist/ui/effect-trial-controller.js +0 -124
- package/dist/ui/effect-trial-offer.js +0 -2
- package/dist/ui/effect-trial-panel.js +0 -40
- package/dist/ui/effects-waker.js +0 -2
- package/dist/ui/settings-effects.js +0 -202
- package/dist/ui/settings-owner-effects.js +0 -82
- package/dist/ui/shared/effect-app-runtime.js +0 -299
- package/dist/ui/shared/effect-baseline.js +0 -64
- package/dist/ui/shared/effect-chat-runtime.js +0 -162
- package/dist/ui/shared/effect-cursor.js +0 -126
- package/dist/ui/shared/effect-display-metadata.js +0 -39
- package/dist/ui/shared/effect-frame.js +0 -146
- package/dist/ui/shared/effect-geometry.js +0 -137
- package/dist/ui/shared/effect-host-style.js +0 -10
- package/dist/ui/shared/effect-owner-regions.js +0 -74
- package/dist/ui/shared/effect-owner-source.js +0 -122
- package/dist/ui/shared/effect-policy.js +0 -151
- package/dist/ui/shared/effect-poster-reader.js +0 -98
- package/dist/ui/shared/effect-posters.js +0 -117
- package/dist/ui/shared/effect-profiler.js +0 -184
- package/dist/ui/shared/effect-profiles.js +0 -65
- package/dist/ui/shared/effect-registry.js +0 -545
- package/dist/ui/shared/effect-runtime-loader.js +0 -236
- package/dist/ui/shared/effect-safety.js +0 -153
- package/dist/ui/shared/effect-stock-snapshot.js +0 -43
- package/dist/ui/shared/effect-stock-trust.js +0 -217
- package/dist/ui/shared/effect-trial-shadow.js +0 -62
- package/dist/ui/shared/effect-trial-source.js +0 -103
- package/dist/ui/shared/effect-trial-stage.js +0 -170
- package/dist/ui/shared/effect-trial.css +0 -40
- package/dist/ui/shared/feature-flags.js +0 -127
- package/dist/ui/shared/owner-effect-prefs.js +0 -117
- package/dist/web-effect-bootstrap.d.ts +0 -12
- package/dist/web-effect-bootstrap.js +0 -49
- package/dist/web-effect-bootstrap.js.map +0 -1
- package/dist/web-effect-bundle.d.ts +0 -28
- package/dist/web-effect-bundle.js +0 -171
- package/dist/web-effect-bundle.js.map +0 -1
- package/dist/web-effect-canonical.d.ts +0 -8
- package/dist/web-effect-canonical.js +0 -27
- package/dist/web-effect-canonical.js.map +0 -1
- package/dist/web-effect-library.d.ts +0 -114
- package/dist/web-effect-library.js +0 -530
- package/dist/web-effect-library.js.map +0 -1
- package/dist/web-effect-playback.d.ts +0 -30
- package/dist/web-effect-playback.js +0 -71
- package/dist/web-effect-playback.js.map +0 -1
- package/dist/web-effect-poster.d.ts +0 -8
- package/dist/web-effect-poster.js +0 -52
- package/dist/web-effect-poster.js.map +0 -1
- package/dist/web-effect-profiles.d.ts +0 -118
- package/dist/web-effect-profiles.js +0 -181
- package/dist/web-effect-profiles.js.map +0 -1
- package/dist/web-effect-service.d.ts +0 -93
- package/dist/web-effect-service.js +0 -335
- package/dist/web-effect-service.js.map +0 -1
- package/dist/web-effect-tools.d.ts +0 -48
- package/dist/web-effect-tools.js +0 -129
- package/dist/web-effect-tools.js.map +0 -1
- package/dist/web-effect-trials.d.ts +0 -86
- package/dist/web-effect-trials.js +0 -204
- package/dist/web-effect-trials.js.map +0 -1
- package/dist/web-effect-upload.d.ts +0 -27
- package/dist/web-effect-upload.js +0 -185
- package/dist/web-effect-upload.js.map +0 -1
- package/dist/web-effect-worker.d.ts +0 -8
- package/dist/web-effect-worker.js +0 -170
- package/dist/web-effect-worker.js.map +0 -1
- package/dist/web-effect.d.ts +0 -103
- package/dist/web-effect.js +0 -153
- package/dist/web-effect.js.map +0 -1
- package/dist/web-experimental-http.d.ts +0 -2
- package/dist/web-experimental-http.js +0 -12
- package/dist/web-experimental-http.js.map +0 -1
- package/dist/web-mod-agent.d.ts +0 -50
- package/dist/web-mod-agent.js +0 -207
- package/dist/web-mod-agent.js.map +0 -1
package/dist/ui/settings-mods.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// parts the person ticks are applied, all or nothing. Custom CSS from a file is never ticked by default. Installed
|
|
5
5
|
// mods can be downloaded again or uninstalled (what they applied stays: Reset and the CSS controls work on it).
|
|
6
6
|
// Export builds a file from what is in force now.
|
|
7
|
-
import { canvasWorkerEnabled, useCanvasWorker } from "/assets/feature-flags.js";
|
|
8
7
|
import { html, useEffect, useLayoutEffect, useRef, useState } from "/assets/app-html.js";
|
|
9
8
|
import { t } from "/assets/app-i18n.js";
|
|
10
9
|
import { appStore, useStore } from "/assets/app-store.js";
|
|
@@ -48,7 +47,6 @@ export function defaultChoice(plan) {
|
|
|
48
47
|
for (const it of plan.items) {
|
|
49
48
|
if (it.part === "theme" || it.part === "identity" || it.part === "ambient") c[it.part] = true;
|
|
50
49
|
else if (it.part === "widget") c.widgets[it.slot] = true;
|
|
51
|
-
else if (it.part === "effect" && canvasWorkerEnabled()) c.effects = true;
|
|
52
50
|
// ownerCss stays false: "read it before turning it on" (r4 §4).
|
|
53
51
|
}
|
|
54
52
|
return c;
|
|
@@ -62,7 +60,7 @@ function Swatch({ color }) {
|
|
|
62
60
|
return html`<span class="s-mod-sw"><span class="s-mod-sw-chip" ref=${ref} aria-hidden="true"></span><code>${color}</code></span>`;
|
|
63
61
|
}
|
|
64
62
|
/** Every field of the checked theme, as it will be applied — nothing left unseen (#1897 review). */
|
|
65
|
-
export function ThemeDetail({ theme, pictures
|
|
63
|
+
export function ThemeDetail({ theme, pictures }) {
|
|
66
64
|
const th = theme || {};
|
|
67
65
|
const row = (k, v) => html`<div class="s-mod-kv"><span class="k">${k}</span><span class="v">${v}</span></div>`;
|
|
68
66
|
const pal = (mode) => {
|
|
@@ -83,7 +81,6 @@ export function ThemeDetail({ theme, pictures, effects }) {
|
|
|
83
81
|
${th.background ? pic(th.background.image, tn("modBackground")) : null}
|
|
84
82
|
${th.background ? row(tn("modBackgroundFit"), `${th.background.fit} · ${Math.round(th.background.opacity * 100)}%`) : null}
|
|
85
83
|
${th.avatar ? pic(th.avatar, tn("modAvatar")) : null}
|
|
86
|
-
${(canvasWorkerEnabled() ? effects || [] : []).map((e, i) => html`<div key=${i}>${row(tn("modPlacesEffect"), html`<span><code>${e.effect}</code>${e.id ? html` <span class="muted">${e.id.slice(0, 8)}</span>` : null}${Object.keys(e.params || {}).length ? html` · <code>${JSON.stringify(e.params)}</code>` : null}</span>`)}</div>`)}
|
|
87
84
|
<details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify(th, null, 2)}</pre></details>
|
|
88
85
|
</div>`;
|
|
89
86
|
}
|
|
@@ -92,7 +89,7 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
|
|
|
92
89
|
const tick = (label, on, set) => html`<label class="check s-mod-tick"><input type="checkbox" checked=${on} onChange=${(e) => set(e.target.checked)} /><span>${label}</span></label>`;
|
|
93
90
|
if (it.part === "theme") {
|
|
94
91
|
return html`<li class="s-mod-item">${tick(tn("modPartTheme"), choice.theme, (v) => setChoice({ ...choice, theme: v }))}
|
|
95
|
-
<div class="s-mod-detail"><${ThemeDetail} theme=${it.theme} pictures=${it.pictures}
|
|
92
|
+
<div class="s-mod-detail"><${ThemeDetail} theme=${it.theme} pictures=${it.pictures} /></div></li>`;
|
|
96
93
|
}
|
|
97
94
|
if (it.part === "identity") {
|
|
98
95
|
return html`<li class="s-mod-item">${tick(tn("modPartIdentity"), choice.identity, (v) => setChoice({ ...choice, identity: v }))}
|
|
@@ -116,11 +113,8 @@ function PlanItem({ it, choice, setChoice, binds, fleet }) {
|
|
|
116
113
|
<div class="s-mod-detail">${one("loop")}${one("click")}
|
|
117
114
|
<details class="s-mod-raw"><summary>${tn("modAllFields")}</summary><pre class="s-mod-css-text">${JSON.stringify({ loop: it.loop, click: it.click }, null, 2)}</pre></details></div></li>`;
|
|
118
115
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return html`<li class="s-mod-item">${tick(tn("modPartEffect", it.title), choice.effects, (v) => setChoice({ ...choice, effects: v }))}
|
|
122
|
-
<div class="s-mod-detail"><span>${kib(it.bytes)} · ${tn("modRegions", it.regions.join(", "))}</span>${it.wants.length ? html`<span>${tn("modWants", it.wants.join(", "))}</span>` : null}</div></li>`;
|
|
123
|
-
}
|
|
116
|
+
// #2799: a Canvas Worker effect entry (the server refuses the file that carries one) is not offered.
|
|
117
|
+
if (it.part === "effect") return null;
|
|
124
118
|
if (it.part === "ownerCss") {
|
|
125
119
|
return html`<li class="s-mod-item s-mod-css">${tick(tn("modPartCss"), choice.ownerCss, (v) => setChoice({ ...choice, ownerCss: v }))}
|
|
126
120
|
<p class="s-mod-warn">${tn("modCssWarn")}</p>
|
|
@@ -141,7 +135,7 @@ function Plan({ staged, onDone }) {
|
|
|
141
135
|
const names = (app.instances || []).map((i) => i.name);
|
|
142
136
|
const install = async () => {
|
|
143
137
|
setBusy(true); setError("");
|
|
144
|
-
const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects:
|
|
138
|
+
const r = await postJson("/ui/mods/commit", { stage: staged.stage, plan_hash: staged.plan_hash, include: { ...choice, effects: false }, ...(toInstance ? { target } : {}) });
|
|
145
139
|
setBusy(false);
|
|
146
140
|
// The stage this plan shows is gone or no longer this plan (replaced on another device, expired): read again —
|
|
147
141
|
// the page then shows whatever is staged now, with its own, fresh choices (keyed by stage).
|
|
@@ -209,7 +203,6 @@ function ExportForm() {
|
|
|
209
203
|
}
|
|
210
204
|
|
|
211
205
|
export function ModsCard() {
|
|
212
|
-
useCanvasWorker();
|
|
213
206
|
const [state, setState] = useState(null);
|
|
214
207
|
const [busy, setBusy] = useState(false);
|
|
215
208
|
const [error, setError] = useState("");
|
|
@@ -28,7 +28,6 @@ const ROWS = [
|
|
|
28
28
|
["look.widget", "look", ".s-widget", "settings.widgetTitle", "widget.sidebar", ["sidebar widget", "widget", "json"], ["側邊欄小工具", "小工具"]],
|
|
29
29
|
["look.css", "look", ".s-owner-css", "settings.cssTitle", null, ["custom css", "stylesheet", "style", "css"], ["自訂 CSS", "樣式表", "樣式"]],
|
|
30
30
|
["look.mods", "look", ".s-mods", "settings.modsTitle", null, ["mod", "mods", "import", "add-on", "agendmod"], ["mod", "匯入", "擴充"]],
|
|
31
|
-
["look.effects", "look", ".s-effects", null, null, ["effects", "canvas", "ambient", "petals", "click stars"], ["特效", "效果", "花瓣", "氛圍"], L("Effects", "效果")],
|
|
32
31
|
["look.restore", "look", ".s-restore", "settings.restoreTitle", null, ["restore defaults", "reset", "default"], ["還原預設值", "重設", "預設"]],
|
|
33
32
|
// Fleet · Agents
|
|
34
33
|
["agents.new", "agents", "#newAgentBtn", "settings.newAgent", null, ["new agent", "create", "add agent", "instance"], ["新增 agent", "建立", "新 agent"]],
|
|
@@ -393,16 +393,14 @@ register("settings", {
|
|
|
393
393
|
modPartWidget_chat_header: "Chat header widget",
|
|
394
394
|
modPartWidget_details: "Details page widget",
|
|
395
395
|
modPartWidget_sidebar: "Your sidebar widget",
|
|
396
|
-
modPartEffect: "Effect: {0}",
|
|
397
396
|
modPartCss: "Custom CSS (on your screens only)",
|
|
398
397
|
modCssWarn: "Custom CSS from this file — read it before turning it on.",
|
|
399
398
|
modCssAssets: "{0} files for it ({1})",
|
|
400
399
|
modAccent: "Accent", modFont: "Font", modPictures: "{0} pictures ({1})", modPlaces: "places {0} effects",
|
|
401
400
|
modColorsLight: "Light colours", modColorsDark: "Dark colours", modBubble: "Bubbles", modWorkingLabel: "Working label",
|
|
402
401
|
modBackground: "Background picture", modBackgroundFit: "Background fit and opacity", modAvatar: "Picture (face)",
|
|
403
|
-
|
|
402
|
+
modAllFields: "Every field, as it will be applied",
|
|
404
403
|
modStageGone: "That import was replaced or ran out of time. Showing what is staged now.",
|
|
405
|
-
modRegions: "draws on: {0}", modWants: "hears: {0}",
|
|
406
404
|
modTarget: "Apply to",
|
|
407
405
|
modTargetPick: "Choose an agent…",
|
|
408
406
|
modTargetNeeded: "Choose the agent this look goes to.",
|
|
@@ -541,6 +539,8 @@ register("settings", {
|
|
|
541
539
|
grantsChoose: "Choose", grantsChosen: "Chosen", grantsMaySee: "{0} may see:", grantsClassicRooms: "ClassicBot rooms", grantsWroteHere: "wrote here", grantsWroteTo: "Wrote to: {0}",
|
|
542
540
|
grantsLastSeen: "last seen {0}", grantsRecent: "Recent changes", grantsRecentNone: "No change to anyone's access yet.", grantsAuditDirect: "applied at once", grantsAuditConfirmed: "confirmed in General", grantsAuditEnded: "{0} session(s) ended", grantsAuditCodeSent: "sign-in code sent", grantsAuditCodeNot: "sign-in code not sent ({0})", grantsAuditDmFailed: "the DM did not go through", grantsAuditWait: "asked again within a minute", grantsAuditAdminLimit: "the admin's limit of codes was reached",
|
|
543
541
|
grantsEditAgents: "Edit agents", grantsRemove: "Remove access", grantsUndoRemove: "Keep access",
|
|
542
|
+
grantsLevel: "What {0} may do", grantsLevelChat: "Chat", grantsLevelTrusted: "Trusted",
|
|
543
|
+
grantsLevelTrustedHint: "Trusted adds watching these agents' screens (read-only) and searching their chats. The screen shows everything the agent's terminal does: other people's messages to it, commands and their output, which can include secrets. Chat and search stay limited to what this person may see.", grantsAuditLevel: "→ {0}",
|
|
544
544
|
grantsWillBeRemoved: "Access ends when you save.", grantsNew: "New — granted when you save", grantsNoAgents: "Tick at least one agent, or remove this person.",
|
|
545
545
|
grantsSend: "Send sign-in code", grantsSendAgain: "Send again in {0}", grantsSendSaveFirst: "Save first: a code goes only to saved access.",
|
|
546
546
|
grantsSent: "Code sent {0} · expires {1}", grantsSentPublic: "It works on the open public link too.", grantsSending: "Sending…",
|
|
@@ -1047,16 +1047,14 @@ register("settings", {
|
|
|
1047
1047
|
modPartWidget_chat_header: "聊天標頭小工具",
|
|
1048
1048
|
modPartWidget_details: "詳細資料頁小工具",
|
|
1049
1049
|
modPartWidget_sidebar: "你的側邊欄小工具",
|
|
1050
|
-
modPartEffect: "效果:{0}",
|
|
1051
1050
|
modPartCss: "自訂 CSS(只套用在你的畫面)",
|
|
1052
1051
|
modCssWarn: "這是檔案裡的自訂 CSS——開啟前請先讀過。",
|
|
1053
1052
|
modCssAssets: "附帶 {0} 個檔案({1})",
|
|
1054
1053
|
modAccent: "強調色", modFont: "字型", modPictures: "{0} 張圖片({1})", modPlaces: "放置 {0} 個效果",
|
|
1055
1054
|
modColorsLight: "淺色模式顏色", modColorsDark: "深色模式顏色", modBubble: "對話泡泡", modWorkingLabel: "工作中文字",
|
|
1056
1055
|
modBackground: "背景圖片", modBackgroundFit: "背景填滿方式與不透明度", modAvatar: "圖片(頭像)",
|
|
1057
|
-
|
|
1056
|
+
modAllFields: "所有欄位(套用時的樣子)",
|
|
1058
1057
|
modStageGone: "那次匯入已被取代或已逾時。以下顯示目前暫存的內容。",
|
|
1059
|
-
modRegions: "繪製於:{0}", modWants: "接收:{0}",
|
|
1060
1058
|
modTarget: "套用到",
|
|
1061
1059
|
modTargetPick: "選擇一個 agent…",
|
|
1062
1060
|
modTargetNeeded: "請選擇這套外觀要套用到哪個 agent。",
|
|
@@ -1194,6 +1192,8 @@ register("settings", {
|
|
|
1194
1192
|
grantsChoose: "選擇", grantsChosen: "已選擇", grantsMaySee: "{0} 可以看到:", grantsClassicRooms: "ClassicBot 聊天室", grantsWroteHere: "傳過訊息", grantsWroteTo: "傳過訊息給:{0}",
|
|
1195
1193
|
grantsLastSeen: "最後出現 {0}", grantsRecent: "最近的變更", grantsRecentNone: "還沒有人的存取權變更過。", grantsAuditDirect: "立即生效", grantsAuditConfirmed: "在 General 確認後生效", grantsAuditEnded: "結束了 {0} 個登入", grantsAuditCodeSent: "已寄出登入碼", grantsAuditCodeNot: "登入碼未寄出({0})", grantsAuditDmFailed: "私訊沒有送達", grantsAuditWait: "一分鐘內重複要求", grantsAuditAdminLimit: "已達管理員的登入碼上限",
|
|
1196
1194
|
grantsEditAgents: "編輯 agent", grantsRemove: "移除存取權", grantsUndoRemove: "保留存取權",
|
|
1195
|
+
grantsLevel: "{0} 可以做什麼", grantsLevelChat: "聊天", grantsLevelTrusted: "信任",
|
|
1196
|
+
grantsLevelTrustedHint: "信任多了:觀看這些 agent 的畫面(唯讀),以及搜尋它們的聊天。畫面會顯示 agent 終端上的一切:別人傳給它的訊息、指令和它們的輸出,其中可能有機密。聊天和搜尋仍只限這個人看得到的內容。", grantsAuditLevel: "→ {0}",
|
|
1197
1197
|
grantsWillBeRemoved: "儲存後存取權就會結束。", grantsNew: "新增 —— 儲存後授權", grantsNoAgents: "至少勾選一個 agent,或移除這個人。",
|
|
1198
1198
|
grantsSend: "傳送登入碼", grantsSendAgain: "{0} 後可再傳", grantsSendSaveFirst: "請先儲存:登入碼只會傳給已儲存的授權。",
|
|
1199
1199
|
grantsSent: "{0} 已傳送 · {1} 到期", grantsSentPublic: "也可以用在目前開啟的公開連結上。", grantsSending: "傳送中…",
|
|
@@ -89,7 +89,13 @@ register("app", {
|
|
|
89
89
|
pendingWeb: "Confirmed by your account (this browser is web-verified). General is told.",
|
|
90
90
|
pending_applied: "Confirmed and applied", pending_rejected: "Not applied", pending_expired: "Expired — nothing was changed",
|
|
91
91
|
pending_stale: "Not applied — settings changed meanwhile", pending_failed: "Confirmed, but applying it failed",
|
|
92
|
-
|
|
92
|
+
pendingLeft: "{0} left", withdraw: "Withdraw",
|
|
93
|
+
// #2868: where to confirm it, said plainly; the host's command folded away unless it is the only way.
|
|
94
|
+
pendingPosting: "Sending the confirm card to General…", pendingChatWhere: "To apply it, press Confirm on the card in General on {0}.",
|
|
95
|
+
pendingHostOnly: "No General can take the confirm card. Confirm it on the host: {0}", pendingAdvanced: "Advanced: confirm on the host",
|
|
96
|
+
pendingSelf: "This browser is verified, so you can also apply it here.", pendingApplyNow: "Apply now",
|
|
97
|
+
pendingApplyRefused: "This browser can't apply it here now — confirm it in General.",
|
|
98
|
+
pendingAuthorize: "Authorize this browser to apply changes like this here next time, without General.",
|
|
93
99
|
opWriting: "Applying settings… {0}/{1}", opWaiting: "Settings: waiting for confirmation in chat", opApplying: "Applying settings…",
|
|
94
100
|
opDone: "Settings applied", opRestart: "Settings saved — AgEnD needs a restart to use them", opPartial: "Some settings were not applied",
|
|
95
101
|
opFailed: "Settings were not applied", openSettings: "Open Settings",
|
|
@@ -131,7 +137,12 @@ register("app", {
|
|
|
131
137
|
pendingWeb: "由你的帳號確認(這個瀏覽器已通過網頁驗證),也會通知 General。",
|
|
132
138
|
pending_applied: "已確認並套用", pending_rejected: "未套用", pending_expired: "已逾時——沒有任何變更",
|
|
133
139
|
pending_stale: "未套用——設定在這期間已變動", pending_failed: "已確認,但套用失敗",
|
|
134
|
-
|
|
140
|
+
pendingLeft: "剩 {0}", withdraw: "撤回",
|
|
141
|
+
pendingPosting: "正在把確認卡送到 General…", pendingChatWhere: "要套用,請到 {0} 的 General,在確認卡上按「確認」。",
|
|
142
|
+
pendingHostOnly: "沒有可以接收確認卡的 General。請在主機上確認:{0}", pendingAdvanced: "進階:在主機上確認",
|
|
143
|
+
pendingSelf: "這個瀏覽器已通過驗證,也可以直接在這裡套用。", pendingApplyNow: "直接套用",
|
|
144
|
+
pendingApplyRefused: "這個瀏覽器現在無法直接套用,請到 General 確認。",
|
|
145
|
+
pendingAuthorize: "授權這個瀏覽器,之後這類變更可以直接套用,不必到 General。",
|
|
135
146
|
opWriting: "正在套用設定… {0}/{1}", opWaiting: "設定:等待在聊天中確認", opApplying: "正在套用設定…",
|
|
136
147
|
opDone: "設定已套用", opRestart: "設定已儲存——需要重新啟動 AgEnD 才會生效", opPartial: "部分設定沒有套用",
|
|
137
148
|
opFailed: "設定沒有套用", openSettings: "開啟設定",
|
|
@@ -91,15 +91,6 @@ let holding = 0;
|
|
|
91
91
|
const held = new Map(); // key → value chosen in Settings, shown here, not saved yet
|
|
92
92
|
const heldFrom = new Map(); // key → its cached form before it was held (a choice back to it is no change)
|
|
93
93
|
const heldListeners = new Set();
|
|
94
|
-
// #2193: whether the signed-in person has a personal effect slot chosen (their fx.* keys: in the page's data-prefs and
|
|
95
|
-
// in every host read, never in this host cache). The effects runtime is loaded only when there is something to play.
|
|
96
|
-
const ownerEffectsIn = (values) => !!values && OWNER_EFFECT_PREF_KEYS.some((k) => own(values, k) && values[k] != null && values[k] !== "");
|
|
97
|
-
let ownerFx = initial ? ownerEffectsIn(initial.values) : false;
|
|
98
|
-
const ownerFxListeners = new Set();
|
|
99
|
-
/** Whether the person has any personal effect (fx.page/header/sidebar/box/cursor) chosen, as this page last read. */
|
|
100
|
-
export function ownerEffectsChosen() { return ownerFx; }
|
|
101
|
-
/** Called when that changes. Returns the unsubscribe. */
|
|
102
|
-
export function onOwnerEffectsChanged(fn) { ownerFxListeners.add(fn); return () => ownerFxListeners.delete(fn); }
|
|
103
94
|
|
|
104
95
|
// The host's values into the cache, now — before the sidebar, the tour or the first-run card read their keys. A key
|
|
105
96
|
// the host does not have: the default, on a device in step with the host; this device's own (to upload) otherwise.
|
|
@@ -111,9 +102,20 @@ if (host) {
|
|
|
111
102
|
}
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
/**
|
|
106
|
+
* #2855: the preferences a guest keeps as its own, on the host under its account (the same list as web-prefs.ts
|
|
107
|
+
* GUEST_OWN_PREF_KEYS); the rest of a guest's page follows the host's look and stays on this browser.
|
|
108
|
+
*/
|
|
109
|
+
export const GUEST_OWN_PREF_KEYS = Object.freeze(["theme", "lang", "nav.favorites", "nav.sort", "nav.filter"]);
|
|
110
|
+
const GUEST_OWN = new Set(GUEST_OWN_PREF_KEYS);
|
|
111
|
+
/**
|
|
112
|
+
* This page may change a preference on the host: the signed-in app, not over the public link (#1920). A guest's page
|
|
113
|
+
* (#2855): only its own keys — over the public link too, since only its own bucket is written; no key named: none.
|
|
114
|
+
*/
|
|
115
|
+
export function prefsWritable(key) {
|
|
116
|
+
if (!initial) return false;
|
|
117
|
+
if (guestPage()) return key !== undefined && GUEST_OWN.has(key);
|
|
118
|
+
return !(typeof document !== "undefined" && document.body && document.body.dataset.publicLink === "1");
|
|
117
119
|
}
|
|
118
120
|
/**
|
|
119
121
|
* The value in force for a key, in memory — never only in this browser's storage (a full storage must not bring back
|
|
@@ -140,8 +142,6 @@ function take(body) {
|
|
|
140
142
|
if (!isView(body) || (rev !== null && body.rev < rev)) return false;
|
|
141
143
|
rev = body.rev;
|
|
142
144
|
host = hostValues(body.values);
|
|
143
|
-
const fx = ownerEffectsIn(body.values);
|
|
144
|
-
if (fx !== ownerFx) { ownerFx = fx; for (const fn of [...ownerFxListeners]) { try { fn(fx); } catch { /* the rest still hear */ } } }
|
|
145
145
|
return true;
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -153,7 +153,7 @@ export function savePref(key, value) {
|
|
|
153
153
|
if (!hostKey(key)) return;
|
|
154
154
|
if (holding && HELD_KEYS.has(key)) { hold(key, value); return; }
|
|
155
155
|
cache(key, value);
|
|
156
|
-
if (!prefsWritable()) return;
|
|
156
|
+
if (!prefsWritable(key)) return;
|
|
157
157
|
pending.set(key, value);
|
|
158
158
|
mine.set(key, value);
|
|
159
159
|
clearTimeout(flushTimer);
|
|
@@ -328,9 +328,9 @@ function apply(changed) {
|
|
|
328
328
|
* refused session or an unreadable store leaves the device unmarked, so the next load tries again.
|
|
329
329
|
*/
|
|
330
330
|
export async function migratePrefs() {
|
|
331
|
-
if (!
|
|
331
|
+
if (!initial || inStep() || !HOST_KEYS.some((key) => prefsWritable(key))) return false;
|
|
332
332
|
for (const key of HOST_KEYS) {
|
|
333
|
-
if (own(host, key)) continue;
|
|
333
|
+
if (own(host, key) || !prefsWritable(key)) continue;
|
|
334
334
|
const local = SYNCED[key].fromLocal(readLocal(SYNCED[key].ls));
|
|
335
335
|
if (local === undefined) continue;
|
|
336
336
|
let r;
|
|
@@ -7,7 +7,7 @@ import { Toasts } from "./ui-toast.js";
|
|
|
7
7
|
import { ConfirmHost, confirmDialog } from "./ui-confirm.js";
|
|
8
8
|
import { ColorDialogHost } from "./identity-color.js";
|
|
9
9
|
import { t, lang, setLang, onLang } from "./app-i18n.js";
|
|
10
|
-
import { appStore, createStore, guestBadge, guestPage, useStore } from "./app-store.js";
|
|
10
|
+
import { appStore, createStore, guestBadge, guestPage, guestTrusted, useStore } from "./app-store.js";
|
|
11
11
|
import { navStore } from "./app-nav.js";
|
|
12
12
|
import { toast } from "./ui-toast.js";
|
|
13
13
|
import { chatPath, viewPath, settingsPath, routeKey, NEEDS_PATH, fleetEntryPath } from "./app-route.js";
|
|
@@ -17,6 +17,7 @@ import { applyPageTheme, pageNow, resolvedMode, themesStore } from "./app-themes
|
|
|
17
17
|
import { ErrorState, Skeleton } from "./ui-states.js";
|
|
18
18
|
import { InstanceNav, RosterNav, namesStore, setNamesPrimary } from "./instance-nav.js";
|
|
19
19
|
import { usageRailStore } from "./header-tools.js";
|
|
20
|
+
import { useWebAuthorization } from "./web-authorize.js";
|
|
20
21
|
import { RightRail, sidePanelStore } from "./side-panel.js";
|
|
21
22
|
import { OwnerCssKeepBar } from "./owner-css.js";
|
|
22
23
|
import { AgentWidget, fleetBinds, sidebarWidget, sidebarWidgetStore } from "./agent-widget.js";
|
|
@@ -78,7 +79,8 @@ export const searchShortcut = () => (isMacLike() ? "⌘K" : "Ctrl K");
|
|
|
78
79
|
export function searchOffered(admin) {
|
|
79
80
|
const body = typeof document !== "undefined" ? document.body : null;
|
|
80
81
|
const offLink = !!body && body.dataset.publicLink === "1" && body.dataset.searchLink === "0";
|
|
81
|
-
|
|
82
|
+
// #2855: a trusted guest searches too (only what it can see: the server's rule).
|
|
83
|
+
return (guestPage() ? guestTrusted() : !!admin) && !offLink;
|
|
82
84
|
}
|
|
83
85
|
let searchModule = null;
|
|
84
86
|
/** Open search; `preset.instance`: narrowed to that agent (Ctrl+F in its chat). */
|
|
@@ -107,6 +109,10 @@ export function handleSearchKey(e) {
|
|
|
107
109
|
openSearch(key === "f" ? { instance: route.instance } : {});
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
/** The sidebar's Search row (Ctrl+K / ⌘K). */
|
|
113
|
+
const searchRow = () => html`<button type="button" class="side-row side-search" onClick=${() => openSearch()} title=${t("app.searchTitle", searchShortcut())}
|
|
114
|
+
aria-keyshortcuts=${isMacLike() ? "Meta+K" : "Control+K"}><${Icon} name="search" /><span class="grow">${t("app.search")}</span><kbd class="kbd-hint" aria-hidden="true">${searchShortcut()}</kbd></button>`;
|
|
115
|
+
|
|
110
116
|
function readCollapsed() { try { return localStorage.getItem("agend_sidebar") === "hidden"; } catch { return false; } }
|
|
111
117
|
const narrow = () => typeof matchMedia === "function" && matchMedia("(max-width: 899px)").matches;
|
|
112
118
|
|
|
@@ -350,6 +356,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
350
356
|
<a class="brand" href="/ui" onClick=${closeDrawer}>${t("app.brand")}</a>
|
|
351
357
|
<button type="button" class="icon-btn side-collapse" onClick=${toggleSidebar} aria-label=${narrow() ? t("app.closeMenu") : t("app.collapse")} title=${narrow() ? t("app.closeMenu") : t("app.collapse")} aria-controls="sidebar"><${Icon} name="sidebar" /></button>
|
|
352
358
|
</div>
|
|
359
|
+
${searchOffered(false) ? html`<nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>${searchRow()}</nav>` : null}
|
|
353
360
|
<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false} exec=${app.exec} awaiting=${app.awaiting} onPick=${closeDrawer} />
|
|
354
361
|
<div class="side-foot" data-trust="navigation">
|
|
355
362
|
<${GuestChip} />
|
|
@@ -372,8 +379,7 @@ function Sidebar({ route, onNewInstance, viewOnly }) {
|
|
|
372
379
|
<nav data-trust="navigation" class="side-nav" aria-label=${t("app.menu")}>
|
|
373
380
|
${overview}
|
|
374
381
|
${navLink("needs", NEEDS_PATH, "inbox", t("app.needsNav"), needsCount)}
|
|
375
|
-
${searchOffered(canCreate) ?
|
|
376
|
-
aria-keyshortcuts=${isMacLike() ? "Meta+K" : "Control+K"}><${Icon} name="search" /><span class="grow">${t("app.search")}</span><kbd class="kbd-hint" aria-hidden="true">${searchShortcut()}</kbd></button>` : null}
|
|
382
|
+
${searchOffered(canCreate) ? searchRow() : null}
|
|
377
383
|
${canCreate && onNewInstance ? html`<button type="button" class="side-row side-new" onClick=${onNewInstance}><${Icon} name="plus" /><span class="grow">${t("app.newAgent")}</span></button>` : null}
|
|
378
384
|
</nav>
|
|
379
385
|
${shell.side ? html`<${shell.side.Component} />` : html`<${InstanceNav} route=${route} items=${app.instances} loaded=${app.ready !== false}
|
|
@@ -393,8 +399,7 @@ function GuestChip() {
|
|
|
393
399
|
const { name, platform } = guestBadge();
|
|
394
400
|
const where = PLATFORM_NAMES[platform] || platform;
|
|
395
401
|
const label = name ? (where ? t("app.guestAsOn", name, where) : t("app.guestAs", name)) : t("app.guestChip");
|
|
396
|
-
// Inside .side-foot (a marked
|
|
397
|
-
// refuses any element it does not know, which would hide a guest's static effect posters (#2101 review).
|
|
402
|
+
// Inside .side-foot (a marked element): no trust marker of its own (#2101 review).
|
|
398
403
|
return html`<p class="guest-chip" title=${t("app.guestHint")}><${Icon} name="user" size=${14} /><span class="grow">${label}</span></p>`;
|
|
399
404
|
}
|
|
400
405
|
|
|
@@ -571,13 +576,46 @@ export function PendingChanges({ inline = false } = {}) {
|
|
|
571
576
|
${p.dismiss ? html`<button type="button" class="icon-btn" aria-label=${t("app.close")} title=${t("app.close")} onClick=${p.dismiss}><${Icon} name="close" size=${14} /></button>` : null}</div>
|
|
572
577
|
${p.label ? html`<div class="pending-label">${p.label}</div>` : null}
|
|
573
578
|
${Array.isArray(p.summary) && p.summary.length ? html`<ul class="pending-summary">${p.summary.map((line, i) => html`<li key=${i}>${line}</li>`)}</ul>` : null}
|
|
574
|
-
${p.state === "pending" ? html
|
|
579
|
+
${p.state === "pending" ? html`<${PendingHow} p=${p} />` : null}
|
|
575
580
|
${(p.state === "applying" || p.state === "applied") && p.confirmation && p.confirmation.kind === "web" ? html`<p class="note">${t("app.pendingWeb")}</p>` : null}
|
|
576
581
|
${p.state !== "pending" && p.state !== "applying" && p.state !== "applied" && p.outcome && p.outcome.message ? html`<p class="note">${p.outcome.message}</p>` : null}
|
|
577
582
|
${p.withdraw ? html`<div class="pending-actions"><button type="button" class="btn btn-sm" onClick=${p.withdraw}>${t("app.withdraw")}</button></div>` : null}
|
|
578
583
|
</section>`)}</div>`;
|
|
579
584
|
}
|
|
580
585
|
|
|
586
|
+
/** #2868: where the confirm card was posted: "Discord", or "Discord (team-b)" for a connection with an id of its own. */
|
|
587
|
+
function whereText(where) {
|
|
588
|
+
const names = (Array.isArray(where) ? where : []).map((w) => `${PLATFORM_NAMES[w.platform] || w.platform}${w.connection && w.connection !== w.platform ? ` (${w.connection})` : ""}`);
|
|
589
|
+
return names.length ? [...new Set(names)].join(" / ") : "Discord / Telegram";
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* #2868: how a pending change is confirmed, in the order a person can act on it. First where: the General card (naming
|
|
593
|
+
* the platform it was posted on), or the host's command when no General could take it. Then, when this browser could
|
|
594
|
+
* confirm it itself (the server's read says so: #2170's rule), "Apply now"; when it isn't verified but could be,
|
|
595
|
+
* "Authorize this browser" (the same DM card as the composer's). The host's command otherwise sits folded under
|
|
596
|
+
* "Advanced".
|
|
597
|
+
*/
|
|
598
|
+
function PendingHow({ p }) {
|
|
599
|
+
const a = useWebAuthorization();
|
|
600
|
+
const [refused, setRefused] = useState(false);
|
|
601
|
+
const c = p.confirmation || {}, cmd = `agend settings confirm ${p.id}`;
|
|
602
|
+
const lead = c.kind === "posting" ? t("app.pendingPosting") : c.kind === "host_cli" ? t("app.pendingHostOnly", cmd) : t("app.pendingChatWhere", whereText(c.where));
|
|
603
|
+
// A browser that isn't verified but could be: authorizing it makes this one (and later ones) "Apply now".
|
|
604
|
+
const route = c.kind !== "host_cli" && !p.applyNow && a.st;
|
|
605
|
+
const canAsk = route && a.st.state === "none" && (a.st.reason === "not_authorized" || a.st.reason === "authorization_expired");
|
|
606
|
+
const asking = route && a.st.state === "pending";
|
|
607
|
+
const apply = async () => { setRefused(false); const r = await p.applyNow(); if (!r || !r.ok) setRefused(true); };
|
|
608
|
+
return html`<p class="note">${lead}</p>
|
|
609
|
+
${p.applyNow ? html`<div class="pending-self"><span class="note">${t("app.pendingSelf")}</span>
|
|
610
|
+
<button type="button" class="btn btn-sm btn-primary" onClick=${apply}>${t("app.pendingApplyNow")}</button></div>` : null}
|
|
611
|
+
${refused && !p.applyNow ? html`<p class="note" role="status">${t("app.pendingApplyRefused")}</p>` : null}
|
|
612
|
+
${canAsk ? html`<div class="pending-self"><span class="note">${t("app.pendingAuthorize")}</span>
|
|
613
|
+
<button type="button" class="btn btn-sm" onClick=${a.ask}>${t("webauth.authorize")}</button></div>` : null}
|
|
614
|
+
${asking ? html`<p class="note">${t("webauth.waiting", mmss(a.left))}</p>` : null}
|
|
615
|
+
${a.notice && (canAsk || asking) ? html`<p class="note" role="status">${a.notice}</p>` : null}
|
|
616
|
+
${c.kind !== "host_cli" ? html`<details class="pending-host"><summary>${t("app.pendingAdvanced")}</summary><code>${cmd}</code></details>` : null}`;
|
|
617
|
+
}
|
|
618
|
+
|
|
581
619
|
/** Settings' Apply, followed from another panel: one line, and the way back to it (§5: progress shows in any panel). */
|
|
582
620
|
function OperationLine({ route }) {
|
|
583
621
|
const { settingsOp: op } = useStore(appStore);
|
|
@@ -781,7 +819,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
781
819
|
document.removeEventListener("compositionstart", start, true); document.removeEventListener("compositionend", end, true);
|
|
782
820
|
};
|
|
783
821
|
}, []);
|
|
784
|
-
const guest = guestPage(); // #1920: a guest's page — no operations,
|
|
822
|
+
const guest = guestPage(); // #1920: a guest's page — no operations, pending changes or owner CSS (#2855: usage, yes)
|
|
785
823
|
const cls = ["shell", shell.collapsed ? "sb-collapsed" : "", shell.drawer ? "sb-open" : "", shell.keyboard ? "kb-open" : "", guest ? "guest" : ""].filter(Boolean).join(" ");
|
|
786
824
|
return html`<div ref=${root} class=${cls}>
|
|
787
825
|
<a data-trust="navigation" class="skip" href="#main">${t("app.skip")}</a>
|
|
@@ -795,7 +833,7 @@ export function Shell({ panels, onNewInstance, viewOnly = false }) {
|
|
|
795
833
|
${viewOnly || guest || !shell.banner ? null : html`<${shell.banner} />`}
|
|
796
834
|
<${Outlet} route=${nav.route} seq=${nav.seq} panels=${panels} />
|
|
797
835
|
</main>
|
|
798
|
-
|
|
836
|
+
<${RightRail} route=${nav.route} />
|
|
799
837
|
<${BottomTabs} route=${nav.route} viewOnly=${viewOnly} />
|
|
800
838
|
${shell.dialog ? html`<${shell.dialog.Component} ...${shell.dialog.props} onClose=${closeDialog} />` : null}
|
|
801
839
|
${viewOnly || guest || (nav.route && nav.route.panel === "settings") ? null : html`<${PendingChanges} />`}
|
|
@@ -24,7 +24,7 @@ export function createStore(initial) {
|
|
|
24
24
|
* (app-stream.js catchUp) — "catching" / "retrying" / "failed" / "ok"; `retryHydration` asks for it again.
|
|
25
25
|
* `connections`: how many chat connections the fleet has (#1519 P7: 0 is web-only — the first-run card); null until known. */
|
|
26
26
|
export const appStore = createStore({ ready: false, instances: [], uptime: 0, exec: {}, awaiting: {}, connection: "connecting", needs: [], connections: null,
|
|
27
|
-
hydration: "none", retryHydration: null
|
|
27
|
+
hydration: "none", retryHydration: null });
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* #1920 phase 3: this page is a guest's — the server says so on <body> (data-tier="guest"). A guest sees only its own
|
|
@@ -34,6 +34,10 @@ export const appStore = createStore({ ready: false, instances: [], uptime: 0, ex
|
|
|
34
34
|
export function guestPage() {
|
|
35
35
|
return typeof document !== "undefined" && !!document.body && document.body.dataset.tier === "guest";
|
|
36
36
|
}
|
|
37
|
+
/** #2855: this guest's access is Trusted — its page offers View (read-only) and search (the server's, on <body>). */
|
|
38
|
+
export function guestTrusted() {
|
|
39
|
+
return guestPage() && document.body.dataset.guestLevel === "trusted";
|
|
40
|
+
}
|
|
37
41
|
/** Who a guest's page is signed in as: its web name and its platform (the server's, on <body>). */
|
|
38
42
|
export function guestBadge() {
|
|
39
43
|
const d = typeof document !== "undefined" && document.body ? document.body.dataset : {};
|
|
@@ -49,7 +53,7 @@ export function applyStatus(d) {
|
|
|
49
53
|
exec[i.name] = (i.execution_state !== undefined ? i.execution_state : i.state) ?? null;
|
|
50
54
|
if (i.state === "awaiting_input") awaiting[i.name] = typeof i.interaction_summary === "string" ? i.interaction_summary : "";
|
|
51
55
|
}
|
|
52
|
-
appStore.set({
|
|
56
|
+
appStore.set({ ready: true, instances: d.instances, uptime: d.uptime ?? 0, exec, awaiting,
|
|
53
57
|
startup: d.startup?.phase === "starting" ? d.startup : null,
|
|
54
58
|
connections: typeof d.connections === "number" ? d.connections : null,
|
|
55
59
|
// #1665: the host preferences' revision (app-prefs.js re-reads them when it moves).
|
package/dist/ui/shared/app.css
CHANGED
|
@@ -952,6 +952,8 @@ img.amb-p { object-fit: contain; }
|
|
|
952
952
|
.s-guests .g-chips { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
|
|
953
953
|
.s-guests .g-chip { padding: 1px var(--s-2); border: 1px solid color-mix(in srgb, var(--text-3) 55%, transparent); border-radius: 0; font-size: var(--fs-sm); color: var(--text-2); white-space: nowrap; }
|
|
954
954
|
.s-guests .g-edit { margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
|
|
955
|
+
.s-guests .g-level { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
|
|
956
|
+
.s-guests .g-level > .note:last-child { flex-basis: 100%; margin: 0; }
|
|
955
957
|
.s-guests .guest-actions { display: flex; flex-wrap: wrap; gap: var(--s-1); margin: var(--s-2) 0 0 calc(28px + var(--s-2)); }
|
|
956
958
|
.s-guests .g-error { color: var(--danger); margin-left: calc(28px + var(--s-2)); }
|
|
957
959
|
.s-guests .g-search { width: 100%; max-width: 360px; margin: var(--s-1) 0 var(--s-2); }
|
|
@@ -1212,6 +1214,18 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1212
1214
|
.v-filter-box input::placeholder { color: var(--text-3); }
|
|
1213
1215
|
.v-filter-clear { width: 28px; height: 28px; }
|
|
1214
1216
|
.v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
|
|
1217
|
+
/* A short viewport (a phone in landscape: about 300–480 px tall): the head, nav, filter and footer pinned around the
|
|
1218
|
+
list left it no room, and what did not fit was drawn over the footer (user report 2026-10-30). There the sidebar
|
|
1219
|
+
scrolls as one column: nothing shrinks, the list grows to its rows, the nav rows sit two to a row, and the filter
|
|
1220
|
+
stays in view at the bottom while the list scrolls past. Portrait phones and desktops are taller than this. */
|
|
1221
|
+
@media (max-height: 500px) {
|
|
1222
|
+
.sidebar { overflow-y: auto; overscroll-behavior: contain; }
|
|
1223
|
+
.sidebar > *, .view-roster > * { flex-shrink: 0; }
|
|
1224
|
+
.view-roster { flex: none; }
|
|
1225
|
+
.view-roster > .side-section { flex: none; overflow: visible; }
|
|
1226
|
+
.side-nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
1227
|
+
.v-filter { position: sticky; bottom: 0; z-index: 2; background: var(--bg-sidebar); }
|
|
1228
|
+
}
|
|
1215
1229
|
/* alpha.2 (N1): status and CLI chips under the filter box — each a group of checkboxes under its header (#1700). */
|
|
1216
1230
|
/* #1776: the "N / M" count is on the headers' row, at its right end — not a row of its own. The headers (and an open
|
|
1217
1231
|
group's list, the reset) wrap in their own box beside it, so a filter's summary or a narrow sidebar moves a header
|
|
@@ -1265,6 +1279,7 @@ body.side-resizing, body.side-resizing * { cursor: col-resize !important; user-s
|
|
|
1265
1279
|
.v-pre .ansi[data-b] { font-weight: var(--fw-bold); }
|
|
1266
1280
|
/* #1582: faint text (SGR 2) — a CLI's suggestion or placeholder, never input — stays faint here. */
|
|
1267
1281
|
.v-pre .ansi[data-dim] { opacity: .5; }
|
|
1282
|
+
.v-watching { flex-shrink: 0; margin: 0; padding: var(--s-2) var(--s-4); font-size: var(--fs-sm); color: var(--text-2); background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
1268
1283
|
.v-term-note { position: absolute; left: var(--s-3); right: var(--s-3); top: var(--s-3); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); background: var(--warn-bg); border: 1px solid var(--warn-line); color: #f0f0f0; font-size: var(--fs-sm); }
|
|
1269
1284
|
.v-probe { position: absolute; visibility: hidden; white-space: pre; left: -9999px; top: 0; font-size: 100px; }
|
|
1270
1285
|
.help-list { list-style: disc; padding-left: var(--s-5); display: flex; flex-direction: column; gap: var(--s-2); font-size: var(--fs-sm); }
|
|
@@ -1513,6 +1528,9 @@ textarea.s-yaml { white-space: pre; overflow-x: auto; }
|
|
|
1513
1528
|
.pending-label { color: var(--text-2); }
|
|
1514
1529
|
.pending-summary { list-style: disc; padding-left: var(--s-5); color: var(--text-2); font-size: var(--fs-xs); }
|
|
1515
1530
|
.pending-actions { display: flex; justify-content: flex-end; }
|
|
1531
|
+
.pending-self { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
1532
|
+
.pending-host summary { cursor: pointer; color: var(--text-2); font-size: var(--fs-xs); }
|
|
1533
|
+
.pending-host code { display: block; margin-top: var(--s-1); font-size: var(--fs-xs); overflow-wrap: anywhere; user-select: all; }
|
|
1516
1534
|
/* Settings' pending bar sits at the bottom of the page: toasts and request cards stand above it, never on its buttons. */
|
|
1517
1535
|
.shell:has(.s-pending) .toasts { bottom: calc(var(--s-6) + 64px); }
|
|
1518
1536
|
/* In a chat the composer owns the bottom (Stop reply, Send): toasts come in under the header instead (alpha.2 sweep). */
|
|
@@ -2201,17 +2219,6 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2201
2219
|
.s-limit-warnings { margin: var(--s-2) 0; padding-left: var(--s-4); }
|
|
2202
2220
|
@media (max-width: 479px) { .grid3 { grid-template-columns: minmax(0, 1fr); } }
|
|
2203
2221
|
|
|
2204
|
-
/* #1629 B2: effect library controls. No runtime layer is created by this card. */
|
|
2205
|
-
.s-fx-actions { display: flex; flex-wrap: wrap; gap: var(--s-2); align-items: center; }
|
|
2206
|
-
.s-fx-item { margin-top: var(--s-3); padding-top: var(--s-3); border-top: 1px solid var(--border); }
|
|
2207
|
-
.s-fx-item h4 { margin: 0; }
|
|
2208
|
-
.s-fx-poster { display: block; max-width: min(240px, 100%); max-height: 160px; object-fit: contain; border-radius: var(--r-sm); }
|
|
2209
|
-
|
|
2210
|
-
.s-fx-chat, .s-fx-params label:not(.check) { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s-2); }
|
|
2211
|
-
.s-fx-chat { margin: var(--s-2) 0; }
|
|
2212
|
-
.s-fx-params { display: flex; flex-direction: column; gap: var(--s-2); margin: var(--s-2) 0; max-width: 100%; }
|
|
2213
|
-
.s-fx-target, .s-fx-params input:not([type=checkbox]), .s-fx-params select { min-height: 36px; min-width: 0; max-width: 100%; padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--bg); color: var(--text); }
|
|
2214
|
-
|
|
2215
2222
|
/* #1629: stock paint bands. Structural ancestors remain neutral; only bounded content owns level 1.
|
|
2216
2223
|
The viewport effect root is a direct body sibling: behind 0 / stock content 1 (header 2) / front 20.
|
|
2217
2224
|
Nested menus cannot cross a content stacking context: the unmounted trust guard suspends fronts for overlays. */
|
|
@@ -2222,38 +2229,14 @@ html.oo-immersive .p-fleet > .panel-body { z-index: 50; }
|
|
|
2222
2229
|
/* Header menus must stay above the later scroller stacking context. */
|
|
2223
2230
|
.panel-head { z-index: 2; }
|
|
2224
2231
|
@media (min-width: 900px) { .chat-split > .pv-divider, .chat-split > .pv-panel { position: relative; z-index: 1; } }
|
|
2225
|
-
/* #1629: owner choices share the existing control font floor. */
|
|
2226
|
-
.s-fx-owner{margin-top:var(--s-4);padding-top:var(--s-3);border-top:1px solid var(--border)}
|
|
2227
|
-
.s-fx-owner-row{display:flex;align-items:end;flex-wrap:wrap;gap:var(--s-2);margin-top:var(--s-2)}
|
|
2228
|
-
.s-fx-owner-row>label{display:flex;flex-direction:column;gap:var(--s-1);min-width:0;flex:1}
|
|
2229
|
-
|
|
2230
|
-
/* #2044: the effect frame's document declares no colour scheme (light). An iframe whose own colour scheme differs
|
|
2231
|
-
from its document's gets an opaque backdrop, so under the dark theme the frame hid the whole chat: keep them equal. */
|
|
2232
|
-
.agend-effect-frame { color-scheme: normal !important; }
|
|
2233
|
-
|
|
2234
|
-
/* #1629: host-created static poster IMG; geometry and visibility belong to the trusted CSSOM binding. */
|
|
2235
|
-
.agend-effect-poster { object-fit: cover !important; object-position: center !important; user-select: none; }
|
|
2236
|
-
|
|
2237
2232
|
/* #1629: full-app bounded stock surfaces. Panel roots stay neutral; effects never raise the whole app. */
|
|
2238
2233
|
.p-fleet > .seg, .p-settings > .seg,
|
|
2239
2234
|
.p-fleet > .panel-body, .p-needs > .panel-body, .p-settings > .panel-body,
|
|
2240
2235
|
.p-log > .panel-body, .p-details > .panel-body, .p-view > .panel-body,
|
|
2241
|
-
.p-log > .log-bar, .p-view > .v-term, .p-view > .v-id { position: relative; z-index: 1; }
|
|
2236
|
+
.p-log > .log-bar, .p-view > .v-term, .p-view > .v-id, .p-view > .v-watching { position: relative; z-index: 1; }
|
|
2242
2237
|
/* Keep the existing Apply bar's sticky behavior, in its own bounded stock band. */
|
|
2243
2238
|
.p-settings > .s-pending { z-index: 1; }
|
|
2244
2239
|
|
|
2245
|
-
/* #1629: host-owned local Trial panel. Only its canvas frame lives inside the paint-contained stage. */
|
|
2246
|
-
.effect-trial-head, .effect-trial-controls, .effect-trial-note { position: relative; z-index: 1; background: var(--bg); padding: var(--s-3); margin: 0; }
|
|
2247
|
-
.effect-trial-head { display: flex; align-items: center; gap: var(--s-2); border-bottom: 1px solid var(--border); }
|
|
2248
|
-
.effect-trial-head > span { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
2249
|
-
.effect-trial-body { flex: 1; min-height: 0; display: flex; padding: var(--s-3); }
|
|
2250
|
-
.effect-trial-stage { position: relative; z-index: 0; flex: 1; min-width: 0; min-height: 0; contain: paint; overflow: clip; padding: 0; border: 0; outline: none; box-shadow: none; }
|
|
2251
|
-
|
|
2252
|
-
/* #2115: diagnostics remain inside the stock protected panel header, never in an effect plane. */
|
|
2253
|
-
.panel-head:has(> .chat-fx-notice) { flex-wrap: wrap; }
|
|
2254
|
-
.chat-fx-notice { order: 10; flex: 1 1 100%; min-width: 0; max-width: 100%; font-size: var(--fs-sm); overflow-wrap: anywhere; }
|
|
2255
|
-
.chat-fx-notice > span { display: block; }
|
|
2256
|
-
|
|
2257
2240
|
/* ── #2293: global search — the sidebar row's shortcut, the header's button (collapsed sidebar, phone), the dialog ── */
|
|
2258
2241
|
/* #2478: a touch screen has no Ctrl/⌘ K — the shortcut's hint is not shown there (the row is). */
|
|
2259
2242
|
@media (pointer: coarse) { .side-search .kbd-hint { display: none; } }
|
package/dist/ui/shared/app.js
CHANGED
|
@@ -13,7 +13,7 @@ import { installSwipe } from "./app-swipe.js";
|
|
|
13
13
|
import { installTextUndo } from "./text-undo.js";
|
|
14
14
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
15
15
|
import { createStream } from "./app-stream.js";
|
|
16
|
-
import { appStore, applyStatus, applyActivity, guestPage } from "./app-store.js";
|
|
16
|
+
import { appStore, applyStatus, applyActivity, guestPage, guestTrusted } from "./app-store.js";
|
|
17
17
|
import { chatPath } from "./app-route.js";
|
|
18
18
|
import { startNeedsNotifier } from "./app-needs.js";
|
|
19
19
|
import { toast } from "./ui-toast.js";
|
|
@@ -101,7 +101,8 @@ if (mode === "full") {
|
|
|
101
101
|
import("/ui/js/backend-signin.js").then((m) => m.installSignIn(), () => {});
|
|
102
102
|
}
|
|
103
103
|
// #1665: this device's first upload of its own choices (once), then another device's changes at each status frame.
|
|
104
|
-
// A guest's page only
|
|
104
|
+
// A guest's page (#2855) uploads and saves only its own keys (prefsWritable(key)); another device's changes reach it at
|
|
105
|
+
// its next load (its status frames carry no prefs_rev).
|
|
105
106
|
bootPrefs();
|
|
106
107
|
// #1629: agents' themes, read when the status frame's themes_rev moves (a guest's frames never carry it).
|
|
107
108
|
if (!guest) startThemes();
|
|
@@ -127,8 +128,8 @@ if (!guest) installOwnerCss();
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
document.addEventListener("keydown", handleKey);
|
|
130
|
-
// #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app
|
|
131
|
-
if (mode === "full" && !guest) document.addEventListener("keydown", handleSearchKey);
|
|
131
|
+
// #2293: Ctrl+K / ⌘K (and Ctrl+F in a chat) — an admin's full app, and a trusted guest's (#2855: what it can see).
|
|
132
|
+
if (mode === "full" && (!guest || guestTrusted())) document.addEventListener("keydown", handleSearchKey);
|
|
132
133
|
// #2342: Ctrl/⌘+Z and Ctrl+Shift+Z / Ctrl+Y / ⌘⇧Z in every text field, one history per field (the browser's is one
|
|
133
134
|
// for the whole page, and any script write wipes it).
|
|
134
135
|
installTextUndo({ mac: isMacLike() });
|
|
@@ -504,18 +504,18 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
504
504
|
const known = new Set(byName.keys());
|
|
505
505
|
const favs = nav.favorites.filter((n) => known.has(n));
|
|
506
506
|
const favSet = new Set(favs);
|
|
507
|
-
const canStar = !viewOnly && prefsWritable();
|
|
508
|
-
// #1920: a guest's list is
|
|
509
|
-
//
|
|
507
|
+
const canStar = !viewOnly && prefsWritable("nav.favorites");
|
|
508
|
+
// #1920: a guest's list is not reordered (the order is the host's). #2855: it stars, sorts and filters as its own
|
|
509
|
+
// preferences (kept on the host under its account), with reply times that only count replies its channel showed.
|
|
510
510
|
const guest = guestPage();
|
|
511
|
-
// #2179: when each row last replied (a guest's
|
|
511
|
+
// #2179: when each row last replied (a guest's: replies its channel showed). "recent" mode: the favorites keep
|
|
512
512
|
// their order or are sorted too, and every other row is one list, the last reply first. "custom" (the default) is the
|
|
513
513
|
// person's own order — a reply never moves a row.
|
|
514
514
|
const now = Date.now();
|
|
515
515
|
// The anonymous View reader (viewOnly) has no reply times (/api/profiles carries none) and no way to change the sort:
|
|
516
516
|
// its list is always Custom, whatever this browser kept from a signed-in page (#2181 review).
|
|
517
|
-
const ageOf = (n) => (
|
|
518
|
-
const recent = !
|
|
517
|
+
const ageOf = (n) => (viewOnly ? null : replyAge(byName.get(n), now));
|
|
518
|
+
const recent = !viewOnly && nav.sort.mode === "recent";
|
|
519
519
|
const favsSorted = recent && nav.sort.favs === "recent";
|
|
520
520
|
let shownGroups, shownInsts;
|
|
521
521
|
if (recent) {
|
|
@@ -528,7 +528,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
528
528
|
shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
|
|
529
529
|
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
530
530
|
}
|
|
531
|
-
const f =
|
|
531
|
+
const f = nav.filter;
|
|
532
532
|
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
533
533
|
// #1936: while a filter is on, the instance you have open stays on top, pinned and marked "Current", whether or not
|
|
534
534
|
// it matches; a match is not repeated below. "N / M" still counts the filter's matches (the pinned row counts only
|
|
@@ -626,7 +626,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
626
626
|
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
627
627
|
<div data-trust="navigation" class="side-section" id="instanceList" ref=${list}>
|
|
628
628
|
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
629
|
-
${canStar && items.length
|
|
629
|
+
${canStar && items.length ? html`<${SortMenu} sort=${nav.sort} />`
|
|
630
630
|
: recent ? html`<span class="chip-btn nav-sort-btn on nav-sort-static"><${Icon} name="sort" size=${12} /><span>${tn("sortRecent")}</span></span>` : null}
|
|
631
631
|
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
632
632
|
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
@@ -653,7 +653,7 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
653
653
|
})}
|
|
654
654
|
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
655
655
|
</div>
|
|
656
|
-
${
|
|
656
|
+
${html`<div class="v-filter">
|
|
657
657
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
658
658
|
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
659
659
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|