@songsid/agend 2.2.0 → 2.3.0-beta.1
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 +248 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +2 -0
- package/dist/agent-endpoint.js +25 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.d.ts +30 -0
- package/dist/agent-file-read.js +129 -0
- package/dist/agent-file-read.js.map +1 -0
- package/dist/agent-whats-new.js +13 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/atomic-write.js +9 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.js +17 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +3 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/launch-effort.js +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/muse.js +10 -2
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +2 -0
- package/dist/channel/adapters/discord.js +17 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +8 -1
- package/dist/channel/adapters/telegram.js +74 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +34 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +15 -0
- package/dist/config-validator.js +17 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.js +244 -20
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-manager.d.ts +89 -0
- package/dist/fleet-manager.js +401 -25
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +258 -0
- package/dist/instance-avatar.js +3 -0
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +3 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +9 -4
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/needs-you-hub.d.ts +4 -0
- package/dist/needs-you-hub.js +31 -6
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +1 -13
- package/dist/needs-you.js +3 -37
- package/dist/needs-you.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -0
- package/dist/outbound-handlers.js +30 -9
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +64 -0
- package/dist/outbound-schemas.js +50 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +14 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/repo-worktree-guard.d.ts +36 -0
- package/dist/repo-worktree-guard.js +143 -0
- package/dist/repo-worktree-guard.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +58 -1
- package/dist/scheduler/scheduler.js +176 -10
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.js +74 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +8 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +16 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +5 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/tool-permissions.js +50 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +10 -6
- package/dist/topic-commands.js +21 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.d.ts +128 -0
- package/dist/topic-link.js +269 -0
- package/dist/topic-link.js.map +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/ui/app-effects.js +187 -0
- package/dist/ui/chat-effect-posters.js +42 -0
- package/dist/ui/chat-effects.js +72 -0
- package/dist/ui/chat-live-replies.js +58 -0
- package/dist/ui/chat-render.js +4 -4
- package/dist/ui/chat-store.js +9 -3
- package/dist/ui/chat-strings.js +15 -0
- package/dist/ui/chat-thread.js +47 -21
- package/dist/ui/chat-tour.js +1 -1
- package/dist/ui/effect-trial-controller.js +119 -0
- package/dist/ui/effect-trial-offer.js +31 -0
- package/dist/ui/effect-trial-panel.js +37 -0
- package/dist/ui/image-lightbox.js +1 -1
- package/dist/ui/panel-chat.js +85 -17
- package/dist/ui/panel-details.js +18 -5
- package/dist/ui/panel-fleet.js +3 -3
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +3 -3
- package/dist/ui/panel-settings.js +8 -5
- package/dist/ui/preview-panel.js +25 -9
- package/dist/ui/settings-appearance.js +325 -0
- package/dist/ui/settings-effects.js +201 -0
- package/dist/ui/settings-mods.js +257 -0
- package/dist/ui/settings-owner-effects.js +80 -0
- package/dist/ui/settings-strings.js +245 -0
- package/dist/ui/shared/agend-auth.js +27 -3
- package/dist/ui/shared/agent-widget.js +248 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-prefs.js +30 -10
- package/dist/ui/shared/app-route.js +23 -1
- package/dist/ui/shared/app-session.js +1 -1
- package/dist/ui/shared/app-shell.js +29 -16
- package/dist/ui/shared/app-store.js +2 -0
- package/dist/ui/shared/app-stream.js +12 -7
- package/dist/ui/shared/app-themes.js +188 -0
- package/dist/ui/shared/app.css +214 -1
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/card-strings.js +28 -0
- package/dist/ui/shared/effect-app-runtime.js +248 -0
- package/dist/ui/shared/effect-baseline.js +64 -0
- package/dist/ui/shared/effect-chat-runtime.js +154 -0
- package/dist/ui/shared/effect-cursor.js +125 -0
- package/dist/ui/shared/effect-frame.js +127 -0
- package/dist/ui/shared/effect-geometry.js +122 -0
- package/dist/ui/shared/effect-host-style.js +10 -0
- package/dist/ui/shared/effect-owner-regions.js +74 -0
- package/dist/ui/shared/effect-owner-source.js +115 -0
- package/dist/ui/shared/effect-policy.js +137 -0
- package/dist/ui/shared/effect-poster-reader.js +98 -0
- package/dist/ui/shared/effect-posters.js +117 -0
- package/dist/ui/shared/effect-profiler.js +162 -0
- package/dist/ui/shared/effect-profiles.js +65 -0
- package/dist/ui/shared/effect-registry.js +496 -0
- package/dist/ui/shared/effect-runtime-loader.js +234 -0
- package/dist/ui/shared/effect-safety.js +153 -0
- package/dist/ui/shared/effect-stock-snapshot.js +34 -0
- package/dist/ui/shared/effect-stock-trust.js +165 -0
- package/dist/ui/shared/effect-trial-source.js +103 -0
- package/dist/ui/shared/effect-trial-stage.js +147 -0
- package/dist/ui/shared/header-tools.js +1 -1
- package/dist/ui/shared/identity-card.js +202 -0
- package/dist/ui/shared/identity-color.js +261 -0
- package/dist/ui/shared/instance-nav.js +22 -14
- package/dist/ui/shared/owner-css.js +221 -0
- package/dist/ui/shared/owner-effect-prefs.js +117 -0
- package/dist/ui/shared/panel-view.js +81 -8
- package/dist/ui/shared/signin.js +2 -0
- package/dist/ui/shared/ui-dialog.js +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/ui-menu.js +1 -1
- package/dist/ui/shared/ui-toast.js +1 -1
- package/dist/ui/shared/view-strings.js +30 -0
- package/dist/ui/signin.html +1 -0
- package/dist/view-api.d.ts +29 -0
- package/dist/view-api.js +188 -8
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +42 -0
- package/dist/web-api.js +896 -36
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-limits.d.ts +82 -0
- package/dist/web-appearance-limits.js +83 -0
- package/dist/web-appearance-limits.js.map +1 -0
- package/dist/web-appearance-policy.d.ts +66 -0
- package/dist/web-appearance-policy.js +94 -0
- package/dist/web-appearance-policy.js.map +1 -0
- package/dist/web-appearance-tools.d.ts +50 -0
- package/dist/web-appearance-tools.js +105 -0
- package/dist/web-appearance-tools.js.map +1 -0
- package/dist/web-appearance-view.d.ts +99 -0
- package/dist/web-appearance-view.js +163 -0
- package/dist/web-appearance-view.js.map +1 -0
- package/dist/web-appearance.d.ts +79 -0
- package/dist/web-appearance.js +298 -0
- package/dist/web-appearance.js.map +1 -0
- package/dist/web-asset-parts.d.ts +98 -0
- package/dist/web-asset-parts.js +133 -0
- package/dist/web-asset-parts.js.map +1 -0
- package/dist/web-auth.js +17 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-card.d.ts +96 -0
- package/dist/web-card.js +232 -0
- package/dist/web-card.js.map +1 -0
- package/dist/web-chat-history.d.ts +13 -0
- package/dist/web-chat-history.js +23 -2
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +28 -0
- package/dist/web-effect-bundle.js +126 -0
- package/dist/web-effect-bundle.js.map +1 -0
- package/dist/web-effect-canonical.d.ts +8 -0
- package/dist/web-effect-canonical.js +27 -0
- package/dist/web-effect-canonical.js.map +1 -0
- package/dist/web-effect-library.d.ts +114 -0
- package/dist/web-effect-library.js +530 -0
- package/dist/web-effect-library.js.map +1 -0
- package/dist/web-effect-poster.d.ts +8 -0
- package/dist/web-effect-poster.js +52 -0
- package/dist/web-effect-poster.js.map +1 -0
- package/dist/web-effect-profiles.d.ts +118 -0
- package/dist/web-effect-profiles.js +180 -0
- package/dist/web-effect-profiles.js.map +1 -0
- package/dist/web-effect-service.d.ts +86 -0
- package/dist/web-effect-service.js +312 -0
- package/dist/web-effect-service.js.map +1 -0
- package/dist/web-effect-tools.d.ts +47 -0
- package/dist/web-effect-tools.js +126 -0
- package/dist/web-effect-tools.js.map +1 -0
- package/dist/web-effect-trials.d.ts +86 -0
- package/dist/web-effect-trials.js +204 -0
- package/dist/web-effect-trials.js.map +1 -0
- package/dist/web-effect-upload.d.ts +29 -0
- package/dist/web-effect-upload.js +190 -0
- package/dist/web-effect-upload.js.map +1 -0
- package/dist/web-effect-worker.d.ts +8 -0
- package/dist/web-effect-worker.js +170 -0
- package/dist/web-effect-worker.js.map +1 -0
- package/dist/web-effect.d.ts +89 -0
- package/dist/web-effect.js +132 -0
- package/dist/web-effect.js.map +1 -0
- package/dist/web-identity-color.d.ts +87 -0
- package/dist/web-identity-color.js +209 -0
- package/dist/web-identity-color.js.map +1 -0
- package/dist/web-identity-palette.d.ts +113 -0
- package/dist/web-identity-palette.js +84 -0
- package/dist/web-identity-palette.js.map +1 -0
- package/dist/web-image-meta.d.ts +25 -0
- package/dist/web-image-meta.js +200 -0
- package/dist/web-image-meta.js.map +1 -0
- package/dist/web-mod-agent.d.ts +50 -0
- package/dist/web-mod-agent.js +203 -0
- package/dist/web-mod-agent.js.map +1 -0
- package/dist/web-mod-bundle.d.ts +144 -0
- package/dist/web-mod-bundle.js +464 -0
- package/dist/web-mod-bundle.js.map +1 -0
- package/dist/web-mod-export.d.ts +59 -0
- package/dist/web-mod-export.js +79 -0
- package/dist/web-mod-export.js.map +1 -0
- package/dist/web-mod-store.d.ts +283 -0
- package/dist/web-mod-store.js +624 -0
- package/dist/web-mod-store.js.map +1 -0
- package/dist/web-org.d.ts +3 -3
- package/dist/web-org.js +11 -17
- package/dist/web-org.js.map +1 -1
- package/dist/web-owner-css-store.d.ts +180 -0
- package/dist/web-owner-css-store.js +384 -0
- package/dist/web-owner-css-store.js.map +1 -0
- package/dist/web-owner-css.d.ts +58 -0
- package/dist/web-owner-css.js +552 -0
- package/dist/web-owner-css.js.map +1 -0
- package/dist/web-prefs.d.ts +48 -0
- package/dist/web-prefs.js +209 -33
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +9 -1
- package/dist/web-preview.js +24 -7
- package/dist/web-preview.js.map +1 -1
- package/dist/web-settings-patch.d.ts +18 -0
- package/dist/web-settings-patch.js +51 -0
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-theme-store.d.ts +218 -0
- package/dist/web-theme-store.js +516 -0
- package/dist/web-theme-store.js.map +1 -0
- package/dist/web-theme.d.ts +121 -0
- package/dist/web-theme.js +407 -0
- package/dist/web-theme.js.map +1 -0
- package/dist/web-widget.d.ts +72 -0
- package/dist/web-widget.js +245 -0
- package/dist/web-widget.js.map +1 -0
- package/package.json +1 -1
|
@@ -288,6 +288,129 @@ register("settings", {
|
|
|
288
288
|
accessTitle: "Access",
|
|
289
289
|
webChat: "Web chat",
|
|
290
290
|
sec_web: "Web",
|
|
291
|
+
// #1629 r4 §3: Settings → Appearance (owner CSS).
|
|
292
|
+
sec_appearance: "Web appearance",
|
|
293
|
+
colorTitle: "Identity colours", colorLead: "Each agent's colour in the sidebar: a person's pick, the agent's own, its bot's Discord role colour, or an automatic soft colour. Change one agent's from its ⋯ menu → Colour….",
|
|
294
|
+
colorPlaces: "Where colours show, unless an agent's own setting says otherwise", colorOff: "Turn off colours",
|
|
295
|
+
colorOffLead: "Hides every identity colour on this host. The colours people picked are kept, and come back when you turn colours on again.",
|
|
296
|
+
restoreTitle: "Restore defaults", restoreLead: "Puts this host's appearance settings back to their defaults. It asks first and says what it resets.",
|
|
297
|
+
restoreConfirm: "This resets, for this host: showing agents' themes and effects, the card size and each card's own size, your sidebar widget, where colours show, and whether colours are off. It also turns your custom CSS off (its versions are kept) and its switch on this device. The colours people picked for agents, installed mods, uploaded effects and the card limits stay as they are.",
|
|
298
|
+
restoreGo: "Restore defaults", restoreDone: "Appearance settings are back to their defaults.", restoreCssFailed: "The settings are back to their defaults, but your custom CSS could not be turned off: {0}",
|
|
299
|
+
cardSizeTitle: "Identity cards",
|
|
300
|
+
cardSizeLead: "How large identity cards are drawn on all your devices. A card's own S / M / L sets that card apart.",
|
|
301
|
+
cardLimitsTitle: "Identity card upload limits",
|
|
302
|
+
cardLimitsLead: "The largest portrait and banner anyone may upload, and how many pictures each agent may keep. Empty = the default (shown greyed). A lower limit applies at once; a higher one waits for a fleet admin's confirmation. Pictures already stored stay.",
|
|
303
|
+
cardLimitsUnavailable: "Only a signed-in admin can see and change these here (or edit web.appearance in fleet.yaml).",
|
|
304
|
+
cardLimit_portrait_max_width: "Portrait width (px)", cardLimit_portrait_max_height: "Portrait height (px)", cardLimit_portrait_max_kib: "Portrait size (KiB)",
|
|
305
|
+
cardLimit_banner_max_width: "Banner width (px)", cardLimit_banner_max_height: "Banner height (px)", cardLimit_banner_max_kib: "Banner size (KiB)",
|
|
306
|
+
cardLimit_instance_quota_files: "Pictures per agent", cardLimit_instance_quota_mib: "Picture space per agent (MiB)",
|
|
307
|
+
cardLimitInForce: "In force: {0} (allowed {1}–{2})",
|
|
308
|
+
cardLimitsWhole: "Each limit is a whole number, or empty for the default.",
|
|
309
|
+
cardLimitsNoChange: "Nothing changed.",
|
|
310
|
+
cardLimitsSave: "Save limits",
|
|
311
|
+
cardLimitsSaved: "Saved.",
|
|
312
|
+
cardLimitsFailed: "Not saved: {0}",
|
|
313
|
+
themesTitle: "Agents' themes",
|
|
314
|
+
themesShow: "Show agents' themes, effects and widgets",
|
|
315
|
+
themesShowLead: "On all your devices. When it's off, every chat looks the same, and agents' pictures and ring colours are their bots' again.",
|
|
316
|
+
themesSafeMode: "This page was opened with ?safe=1: agents' themes are not shown on it.",
|
|
317
|
+
widgetTitle: "Your sidebar widget",
|
|
318
|
+
widgetLead: "A small panel under the instance list, on all your devices: the dashboard draws it from a spec and values you write here as JSON — no HTML, script or CSS. Its live values are your fleet's counts (fleet.working, fleet.needs_you, fleet.idle, fleet.stopped). Agents can't change it.",
|
|
319
|
+
widgetNotJson: "Not JSON.",
|
|
320
|
+
widgetSave: "Save widget",
|
|
321
|
+
widgetSaved: "Sidebar widget saved.",
|
|
322
|
+
widgetExample: "Start from an example",
|
|
323
|
+
widgetRemove: "Remove widget",
|
|
324
|
+
widgetRemoved: "Sidebar widget removed.",
|
|
325
|
+
modsTitle: "Mods",
|
|
326
|
+
modsLead: "A mod is one file (.agendmod.json) that carries a look: an agent's theme and pictures, its card text, widgets, effects, and custom CSS. Importing shows everything in it first; only what you tick is applied, all at once. Uninstalling removes the mod from this list; what it applied stays until you reset it.",
|
|
327
|
+
modsLoadFailed: "Couldn't load the installed mods. Reload the page to try again.",
|
|
328
|
+
modsNone: "No mods installed.",
|
|
329
|
+
modChoose: "Import a mod file…",
|
|
330
|
+
modPlan: "What this mod would change",
|
|
331
|
+
modAuthor: "By {0} (unverified)",
|
|
332
|
+
modSizeId: "{0} · id {1}",
|
|
333
|
+
modAlready: "already installed",
|
|
334
|
+
modReplace: "Replace the installed version {0}",
|
|
335
|
+
modPartTheme: "Theme (colours, font, pictures)",
|
|
336
|
+
modPartIdentity: "Card text (tagline and bio)",
|
|
337
|
+
modPartWidget_chat_header: "Chat header widget",
|
|
338
|
+
modPartWidget_details: "Details page widget",
|
|
339
|
+
modPartWidget_sidebar: "Your sidebar widget",
|
|
340
|
+
modPartEffect: "Effect: {0}",
|
|
341
|
+
modPartCss: "Custom CSS (on your screens only)",
|
|
342
|
+
modCssWarn: "Custom CSS from this file — read it before turning it on.",
|
|
343
|
+
modCssAssets: "{0} files for it ({1})",
|
|
344
|
+
modAccent: "Accent", modFont: "Font", modPictures: "{0} pictures ({1})", modPlaces: "places {0} effects",
|
|
345
|
+
modColorsLight: "Light colours", modColorsDark: "Dark colours", modBubble: "Bubbles", modWorkingLabel: "Working label",
|
|
346
|
+
modBackground: "Background picture", modBackgroundFit: "Background fit and opacity", modAvatar: "Picture (face)",
|
|
347
|
+
modPlacesEffect: "Places effect", modAllFields: "Every field, as it will be applied",
|
|
348
|
+
modStageGone: "That import was replaced or ran out of time. Showing what is staged now.",
|
|
349
|
+
modRegions: "draws on: {0}", modWants: "hears: {0}",
|
|
350
|
+
modTarget: "Apply to",
|
|
351
|
+
modTargetPick: "Choose an agent…",
|
|
352
|
+
modTargetNeeded: "Choose the agent this look goes to.",
|
|
353
|
+
modAppliedTo: "applied to {0}",
|
|
354
|
+
modInstall: "Install",
|
|
355
|
+
modCancel: "Cancel",
|
|
356
|
+
modInstalled: "Installed {0}.",
|
|
357
|
+
modFailed: "Not done: {0}",
|
|
358
|
+
modDownload: "Download",
|
|
359
|
+
modUninstall: "Uninstall",
|
|
360
|
+
modUninstallConfirm: "Uninstall {0}? Its effects leave your library unless something still uses them. What it applied stays.",
|
|
361
|
+
modUninstalled: "Uninstalled {0}.",
|
|
362
|
+
modExport: "Export a mod",
|
|
363
|
+
modExportLead: "Make a mod file from what is in force now: an agent's theme, card text and widgets, your sidebar widget, your custom CSS.",
|
|
364
|
+
modExportInstance: "Agent",
|
|
365
|
+
modExportName: "Name (a-z, 0-9, -)",
|
|
366
|
+
modExportVersion: "Version",
|
|
367
|
+
modExportTitle: "Title",
|
|
368
|
+
modExportGo: "Download the file",
|
|
369
|
+
modExportOmitted: "{0} effect(s) placed there are someone else's and were left out: only your own and shared effects travel.",
|
|
370
|
+
modProposals: "Proposed by agents",
|
|
371
|
+
modProposedBy: "{0} proposes",
|
|
372
|
+
modReview: "Review",
|
|
373
|
+
modDismiss: "Dismiss",
|
|
374
|
+
cssTitle: "Custom CSS (yours only)",
|
|
375
|
+
cssLead: "Your own stylesheet, applied to your screens only: every device where you are signed in, never anyone else's, and never on Settings. It can't load anything from the internet. Put pictures and fonts under Assets below and name them url(\"asset:<id>\").",
|
|
376
|
+
cssLoadFailed: "Couldn't load your custom CSS. Reload the page to try again.",
|
|
377
|
+
cssPublicLink: "Custom CSS isn't available on a temporary public link.",
|
|
378
|
+
cssInForce: "In force: {0}",
|
|
379
|
+
cssOnTrial: "On trial: {0}. Choose Keep, or it reverts.",
|
|
380
|
+
cssNone: "No custom CSS in force.",
|
|
381
|
+
cssSave: "Save and try",
|
|
382
|
+
cssTrying: "Custom CSS saved. Look around, then choose Keep within 15 seconds.",
|
|
383
|
+
cssOff: "Turn off (all devices)",
|
|
384
|
+
cssVersions: "Earlier versions ({0})",
|
|
385
|
+
cssCurrent: "in force",
|
|
386
|
+
cssRestore: "Use this",
|
|
387
|
+
cssAssets: "Assets",
|
|
388
|
+
cssAssetsLead: "PNG, WebP, JPEG, GIF or WOFF2. {0} of {1} files, {2} in total (at most 1 MiB). Metadata such as GPS is removed.",
|
|
389
|
+
cssAssetAdd: "Add a picture or font",
|
|
390
|
+
cssAssetCopy: "Copy reference",
|
|
391
|
+
cssCopied: "Copied {0}",
|
|
392
|
+
cssAssetRemove: "Remove",
|
|
393
|
+
cssDeviceTitle: "On this device",
|
|
394
|
+
cssDeviceOff: "Turn off custom CSS on this device",
|
|
395
|
+
cssDeviceOffLead: "Only this browser. Your other devices keep it.",
|
|
396
|
+
cssSafe: "Is something hidden or broken? Add ?safe=1 to the end of any page's address and it loads without custom CSS.",
|
|
397
|
+
cssError_at_rule: "Not saved: {0} isn't allowed.",
|
|
398
|
+
cssError_url: "Not saved: the only url() allowed is url(\"asset:<id>\") for a file in Assets.",
|
|
399
|
+
cssError_unknown_asset: "Not saved: there's no asset {0}. Add it under Assets first.",
|
|
400
|
+
cssError_function: "Not saved: {0} isn't allowed.",
|
|
401
|
+
cssError_property: "Not saved: {0} isn't allowed.",
|
|
402
|
+
cssError_script_scheme: "Not saved: javascript: and vbscript: aren't allowed.",
|
|
403
|
+
cssError_too_large: "Not saved: custom CSS can be at most 64 KiB.",
|
|
404
|
+
cssError_malformed: "Not saved: a string or url() isn't closed.",
|
|
405
|
+
cssError_nul: "Not saved: the text contains a NUL character.",
|
|
406
|
+
cssError_unserialisable: "Not saved: the CSS couldn't be read back safely.",
|
|
407
|
+
cssError_asset_type: "Not added: only PNG, WebP, JPEG, GIF or WOFF2 files are allowed.",
|
|
408
|
+
cssError_asset_unreadable: "Not added: the file couldn't be read.",
|
|
409
|
+
cssError_asset_quota: "Not added: Assets hold at most 16 files and 1 MiB in total.",
|
|
410
|
+
cssError_asset_in_use: "Not removed: a saved version still uses it.",
|
|
411
|
+
cssError_owner_css_read_only: "This sign-in can view but not change custom CSS.",
|
|
412
|
+
cssError_owner_css_needs_a_signed_in_person: "Sign in in the browser to change custom CSS.",
|
|
413
|
+
cssError_other: "Not saved ({0}).",
|
|
291
414
|
webLead: "How the web dashboard behaves. A change that loosens access (longer sessions, the browser terminal back on or for longer, the public link) waits for a fleet admin to confirm it in chat (General topic) or on the host; one that tightens it applies at once.",
|
|
292
415
|
webPublicRefused: "Web settings can't be changed over a temporary public link. Open the dashboard at its own address to change them.",
|
|
293
416
|
webSessionTitle: "Sign-in sessions",
|
|
@@ -661,6 +784,128 @@ register("settings", {
|
|
|
661
784
|
accessTitle: "存取",
|
|
662
785
|
webChat: "網頁聊天",
|
|
663
786
|
sec_web: "網頁",
|
|
787
|
+
sec_appearance: "網頁外觀",
|
|
788
|
+
colorTitle: "識別色", colorLead: "每個 agent 在側欄的顏色:有人挑選的、agent 自己的、它的 bot 在 Discord 身分組的顏色,或自動配的柔和色。要改某個 agent 的顏色,從它的 ⋯ 選單 → 顏色…。",
|
|
789
|
+
colorPlaces: "顏色顯示的位置(agent 自己另有設定時除外)", colorOff: "關閉顏色",
|
|
790
|
+
colorOffLead: "在這台主機隱藏所有識別色。有人挑過的顏色會保留,重新開啟顏色後就會回來。",
|
|
791
|
+
restoreTitle: "還原預設值", restoreLead: "把這台主機的外觀設定改回預設值。執行前會先詢問,並說明會重設哪些項目。",
|
|
792
|
+
restoreConfirm: "會重設這台主機的:是否顯示 agent 的主題與效果、卡片大小與各張卡片自己的大小、你的側欄小工具、顏色顯示的位置,以及是否關閉顏色;也會關閉你的自訂 CSS(版本會保留)以及這台裝置上的 CSS 開關。有人替 agent 挑的顏色、已安裝的 mod、上傳的效果與卡片上限都不會變。",
|
|
793
|
+
restoreGo: "還原預設值", restoreDone: "外觀設定已改回預設值。", restoreCssFailed: "設定已改回預設值,但無法關閉你的自訂 CSS:{0}",
|
|
794
|
+
cardSizeTitle: "身分卡",
|
|
795
|
+
cardSizeLead: "身分卡在你所有裝置上的顯示大小。卡片上自己的 小/中/大 可以單獨設定那張卡片。",
|
|
796
|
+
cardLimitsTitle: "身分卡上傳限制",
|
|
797
|
+
cardLimitsLead: "任何人可上傳的肖像與橫幅上限,以及每個 agent 可保留的圖片數量。留空=預設值(以灰字顯示)。調低立即生效;調高需 fleet admin 確認。已存的圖片不受影響。",
|
|
798
|
+
cardLimitsUnavailable: "只有已登入的管理者能在這裡查看與修改(或在 fleet.yaml 編輯 web.appearance)。",
|
|
799
|
+
cardLimit_portrait_max_width: "肖像寬度(px)", cardLimit_portrait_max_height: "肖像高度(px)", cardLimit_portrait_max_kib: "肖像大小(KiB)",
|
|
800
|
+
cardLimit_banner_max_width: "橫幅寬度(px)", cardLimit_banner_max_height: "橫幅高度(px)", cardLimit_banner_max_kib: "橫幅大小(KiB)",
|
|
801
|
+
cardLimit_instance_quota_files: "每個 agent 的圖片數", cardLimit_instance_quota_mib: "每個 agent 的圖片空間(MiB)",
|
|
802
|
+
cardLimitInForce: "實際生效:{0}(允許 {1}–{2})",
|
|
803
|
+
cardLimitsWhole: "每個限制須為整數,或留空使用預設值。",
|
|
804
|
+
cardLimitsNoChange: "沒有變更。",
|
|
805
|
+
cardLimitsSave: "儲存限制",
|
|
806
|
+
cardLimitsSaved: "已儲存。",
|
|
807
|
+
cardLimitsFailed: "未儲存:{0}",
|
|
808
|
+
themesTitle: "Agent 的主題",
|
|
809
|
+
themesShow: "顯示 agent 的主題、效果與小工具",
|
|
810
|
+
themesShowLead: "套用到你所有的裝置。關閉時每個聊天看起來都一樣,agent 的圖片與外框顏色也會換回各自 bot 的。",
|
|
811
|
+
themesSafeMode: "這個頁面是用 ?safe=1 開啟的:不顯示 agent 的主題。",
|
|
812
|
+
widgetTitle: "你的側邊欄小工具",
|
|
813
|
+
widgetLead: "實例清單下方的一個小面板,套用到你所有的裝置:儀表板依你在這裡用 JSON 寫的規格與數值繪製——沒有 HTML、腳本或 CSS。即時數值是你 fleet 的計數(fleet.working、fleet.needs_you、fleet.idle、fleet.stopped)。agent 無法變更它。",
|
|
814
|
+
widgetNotJson: "不是 JSON。",
|
|
815
|
+
widgetSave: "儲存小工具",
|
|
816
|
+
widgetSaved: "已儲存側邊欄小工具。",
|
|
817
|
+
widgetExample: "從範例開始",
|
|
818
|
+
widgetRemove: "移除小工具",
|
|
819
|
+
widgetRemoved: "已移除側邊欄小工具。",
|
|
820
|
+
modsTitle: "Mod",
|
|
821
|
+
modsLead: "Mod 是一個檔案(.agendmod.json),帶著一套外觀:agent 的主題與圖片、身分卡文字、小工具、效果,以及自訂 CSS。匯入時會先完整列出內容,只有你勾選的部分會一次套用。解除安裝會把 mod 從這個清單移除;它已套用的外觀會保留,直到你重設。",
|
|
822
|
+
modsLoadFailed: "無法載入已安裝的 mod。請重新載入頁面再試一次。",
|
|
823
|
+
modsNone: "尚未安裝任何 mod。",
|
|
824
|
+
modChoose: "匯入 mod 檔案…",
|
|
825
|
+
modPlan: "這個 mod 會變更的內容",
|
|
826
|
+
modAuthor: "作者:{0}(未經驗證)",
|
|
827
|
+
modSizeId: "{0} · id {1}",
|
|
828
|
+
modAlready: "已安裝",
|
|
829
|
+
modReplace: "取代已安裝的 {0} 版",
|
|
830
|
+
modPartTheme: "主題(顏色、字型、圖片)",
|
|
831
|
+
modPartIdentity: "身分卡文字(標語與簡介)",
|
|
832
|
+
modPartWidget_chat_header: "聊天標頭小工具",
|
|
833
|
+
modPartWidget_details: "詳細資料頁小工具",
|
|
834
|
+
modPartWidget_sidebar: "你的側邊欄小工具",
|
|
835
|
+
modPartEffect: "效果:{0}",
|
|
836
|
+
modPartCss: "自訂 CSS(只套用在你的畫面)",
|
|
837
|
+
modCssWarn: "這是檔案裡的自訂 CSS——開啟前請先讀過。",
|
|
838
|
+
modCssAssets: "附帶 {0} 個檔案({1})",
|
|
839
|
+
modAccent: "強調色", modFont: "字型", modPictures: "{0} 張圖片({1})", modPlaces: "放置 {0} 個效果",
|
|
840
|
+
modColorsLight: "淺色模式顏色", modColorsDark: "深色模式顏色", modBubble: "對話泡泡", modWorkingLabel: "工作中文字",
|
|
841
|
+
modBackground: "背景圖片", modBackgroundFit: "背景填滿方式與不透明度", modAvatar: "圖片(頭像)",
|
|
842
|
+
modPlacesEffect: "放置效果", modAllFields: "所有欄位(套用時的樣子)",
|
|
843
|
+
modStageGone: "那次匯入已被取代或已逾時。以下顯示目前暫存的內容。",
|
|
844
|
+
modRegions: "繪製於:{0}", modWants: "接收:{0}",
|
|
845
|
+
modTarget: "套用到",
|
|
846
|
+
modTargetPick: "選擇一個 agent…",
|
|
847
|
+
modTargetNeeded: "請選擇這套外觀要套用到哪個 agent。",
|
|
848
|
+
modAppliedTo: "套用於 {0}",
|
|
849
|
+
modInstall: "安裝",
|
|
850
|
+
modCancel: "取消",
|
|
851
|
+
modInstalled: "已安裝 {0}。",
|
|
852
|
+
modFailed: "未完成:{0}",
|
|
853
|
+
modDownload: "下載",
|
|
854
|
+
modUninstall: "解除安裝",
|
|
855
|
+
modUninstallConfirm: "要解除安裝 {0} 嗎?它的效果會離開你的效果庫,除非還有東西在使用。它已套用的外觀會保留。",
|
|
856
|
+
modUninstalled: "已解除安裝 {0}。",
|
|
857
|
+
modExport: "匯出 mod",
|
|
858
|
+
modExportLead: "用目前生效的外觀做成 mod 檔案:某個 agent 的主題、身分卡文字與小工具、你的側邊欄小工具、你的自訂 CSS。",
|
|
859
|
+
modExportInstance: "Agent",
|
|
860
|
+
modExportName: "名稱(a-z、0-9、-)",
|
|
861
|
+
modExportVersion: "版本",
|
|
862
|
+
modExportTitle: "標題",
|
|
863
|
+
modExportGo: "下載檔案",
|
|
864
|
+
modExportOmitted: "有 {0} 個放在那裡的效果屬於別人,已略過:只有你自己的和共用的效果會被帶走。",
|
|
865
|
+
modProposals: "Agent 提議的 mod",
|
|
866
|
+
modProposedBy: "{0} 提議",
|
|
867
|
+
modReview: "檢視",
|
|
868
|
+
modDismiss: "略過",
|
|
869
|
+
cssTitle: "自訂 CSS(只套用在你自己)",
|
|
870
|
+
cssLead: "你自己的樣式表,只套用在你自己的畫面:你登入的每一台裝置都會套用,不會影響別人,也不會套用在設定頁。它無法從網路載入任何東西;圖片和字型請放在下方的「資源」,再用 url(\"asset:<id>\") 引用。",
|
|
871
|
+
cssLoadFailed: "無法載入你的自訂 CSS。請重新整理頁面再試一次。",
|
|
872
|
+
cssPublicLink: "臨時公開連結無法使用自訂 CSS。",
|
|
873
|
+
cssInForce: "使用中:{0}",
|
|
874
|
+
cssOnTrial: "試用中:{0}。請按「保留」,否則會還原。",
|
|
875
|
+
cssNone: "目前沒有使用自訂 CSS。",
|
|
876
|
+
cssSave: "儲存並試用",
|
|
877
|
+
cssTrying: "已儲存自訂 CSS。請到各頁看看,15 秒內按「保留」。",
|
|
878
|
+
cssOff: "關閉(所有裝置)",
|
|
879
|
+
cssVersions: "先前的版本({0})",
|
|
880
|
+
cssCurrent: "使用中",
|
|
881
|
+
cssRestore: "改用這版",
|
|
882
|
+
cssAssets: "資源",
|
|
883
|
+
cssAssetsLead: "PNG、WebP、JPEG、GIF 或 WOFF2。目前 {0} / {1} 個檔案,共 {2}(上限 1 MiB)。會移除 GPS 等中繼資料。",
|
|
884
|
+
cssAssetAdd: "加入圖片或字型",
|
|
885
|
+
cssAssetCopy: "複製引用",
|
|
886
|
+
cssCopied: "已複製 {0}",
|
|
887
|
+
cssAssetRemove: "移除",
|
|
888
|
+
cssDeviceTitle: "這台裝置",
|
|
889
|
+
cssDeviceOff: "在這台裝置關閉自訂 CSS",
|
|
890
|
+
cssDeviceOffLead: "只影響這個瀏覽器,你的其他裝置照常套用。",
|
|
891
|
+
cssSafe: "畫面被藏起來或壞掉了嗎?在任何頁面網址的最後加上 ?safe=1,就會不套用自訂 CSS 地載入。",
|
|
892
|
+
cssError_at_rule: "未儲存:不允許使用 {0}。",
|
|
893
|
+
cssError_url: "未儲存:url() 只能用 url(\"asset:<id>\") 引用「資源」裡的檔案。",
|
|
894
|
+
cssError_unknown_asset: "未儲存:找不到資源 {0}。請先在「資源」加入它。",
|
|
895
|
+
cssError_function: "未儲存:不允許使用 {0}。",
|
|
896
|
+
cssError_property: "未儲存:不允許使用 {0}。",
|
|
897
|
+
cssError_script_scheme: "未儲存:不允許使用 javascript: 和 vbscript:。",
|
|
898
|
+
cssError_too_large: "未儲存:自訂 CSS 最多 64 KiB。",
|
|
899
|
+
cssError_malformed: "未儲存:有字串或 url() 沒有結束。",
|
|
900
|
+
cssError_nul: "未儲存:內容含有 NUL 字元。",
|
|
901
|
+
cssError_unserialisable: "未儲存:這份 CSS 無法安全地重新輸出。",
|
|
902
|
+
cssError_asset_type: "未加入:只接受 PNG、WebP、JPEG、GIF 或 WOFF2 檔案。",
|
|
903
|
+
cssError_asset_unreadable: "未加入:無法讀取這個檔案。",
|
|
904
|
+
cssError_asset_quota: "未加入:資源最多 16 個檔案、合計 1 MiB。",
|
|
905
|
+
cssError_asset_in_use: "未移除:仍有已儲存的版本在使用它。",
|
|
906
|
+
cssError_owner_css_read_only: "這個登入只能檢視、不能變更自訂 CSS。",
|
|
907
|
+
cssError_owner_css_needs_a_signed_in_person: "請在瀏覽器登入後再變更自訂 CSS。",
|
|
908
|
+
cssError_other: "未儲存({0})。",
|
|
664
909
|
webLead: "網頁儀表板的行為。放寬存取的變更(延長 session、重新開啟或延長瀏覽器終端機、公開連結)需要 fleet admin 在聊天(General topic)或主機上確認;收緊的變更會立即套用。",
|
|
665
910
|
webPublicRefused: "無法透過臨時公開連結變更網頁設定。請從儀表板本身的網址開啟後再變更。",
|
|
666
911
|
webSessionTitle: "登入 session",
|
|
@@ -27,6 +27,20 @@
|
|
|
27
27
|
|
|
28
28
|
const nativeFetch = window.fetch.bind(window);
|
|
29
29
|
let sessionPending = null;
|
|
30
|
+
// Observation only: no server authority, owner/CSRF data or way to restore this page's episode.
|
|
31
|
+
let ended = null;
|
|
32
|
+
const endListeners = new Set();
|
|
33
|
+
function endSession(reason) {
|
|
34
|
+
if (ended) return;
|
|
35
|
+
ended = Object.freeze({ reason });
|
|
36
|
+
const listeners = [...endListeners]; endListeners.clear();
|
|
37
|
+
for (const fn of listeners) { try { fn(ended); } catch { /* one observer cannot delay another's cleanup */ } }
|
|
38
|
+
}
|
|
39
|
+
function onSessionEnd(fn) {
|
|
40
|
+
if (typeof fn !== "function") return () => {};
|
|
41
|
+
if (ended) { try { fn(ended); } catch { /* late observers fail closed */ } return () => {}; }
|
|
42
|
+
endListeners.add(fn); return () => endListeners.delete(fn);
|
|
43
|
+
}
|
|
30
44
|
|
|
31
45
|
/**
|
|
32
46
|
* This page's sign-in, read once from GET /auth/session: { csrf, handle } or null (no session, or the read failed —
|
|
@@ -36,8 +50,8 @@
|
|
|
36
50
|
function session() {
|
|
37
51
|
if (!sessionPending) {
|
|
38
52
|
sessionPending = nativeFetch("/auth/session", { credentials: "same-origin", cache: "no-store" })
|
|
39
|
-
.then((r) => (r.ok ? r.json() : null)
|
|
40
|
-
.then((j) => (j && typeof j.csrf === "string" ? { csrf: j.csrf, handle: typeof j.handle === "string" ? j.handle : null } : null))
|
|
53
|
+
.then((r) => { if (r.status === 401) endSession("unauthorized"); return r.ok ? r.json() : null; })
|
|
54
|
+
.then((j) => (j && typeof j.csrf === "string" ? { csrf: j.csrf, handle: typeof j.handle === "string" ? j.handle : null, canWrite: j.tier === "admin", localAdmin: j.tier === "admin" && j.surface === "local" } : null))
|
|
41
55
|
.catch(() => null)
|
|
42
56
|
.then((value) => { if (!value) sessionPending = null; return value; });
|
|
43
57
|
}
|
|
@@ -51,6 +65,8 @@
|
|
|
51
65
|
bannerShown = true;
|
|
52
66
|
const bar = document.createElement("div");
|
|
53
67
|
bar.setAttribute("role", "alert");
|
|
68
|
+
bar.setAttribute("data-trust", "session-ended");
|
|
69
|
+
bar.className = "agend-session-ended";
|
|
54
70
|
bar.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:10px 16px;background:#f85149;color:#fff;font:14px system-ui,sans-serif;text-align:center";
|
|
55
71
|
const zh = /^zh/i.test(navigator.language || "");
|
|
56
72
|
bar.append(document.createTextNode(zh ? "登入已結束。" : "Your session has ended. "));
|
|
@@ -77,15 +93,23 @@
|
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
const response = await nativeFetch(input, init);
|
|
80
|
-
if (sameOrigin && response.status === 401 && !target.pathname.startsWith("/auth/")) sessionEndedBanner();
|
|
96
|
+
if (sameOrigin && response.status === 401 && !target.pathname.startsWith("/auth/")) { endSession("unauthorized"); sessionEndedBanner(); }
|
|
81
97
|
return response;
|
|
82
98
|
};
|
|
83
99
|
|
|
84
100
|
window.AgendAuth = {
|
|
85
101
|
csrf,
|
|
102
|
+
/** Host runtimes subscribe before admission. Late subscribers observe the same closed episode immediately. */
|
|
103
|
+
onSessionEnd,
|
|
104
|
+
/** Display hint from the existing session read. Every write still requires server authorization. */
|
|
105
|
+
canWrite() { return session().then(s => !ended && s?.canWrite === true); },
|
|
106
|
+
/** Display hint from the same server session response; Start still reauthorizes. No CSRF is exposed here. */
|
|
107
|
+
trialSession() { return session().then(s => !ended && s?.handle && /^[a-f0-9]{16}$/.test(s.handle) ? Object.freeze({ epoch: s.handle, localAdmin: s.localAdmin === true }) : null); },
|
|
108
|
+
sessionEnded() { return ended !== null; },
|
|
86
109
|
/** #1568: the session handle of this page's sign-in, or null when there is none. */
|
|
87
110
|
sessionHandle() { return session().then((s) => (s && s.handle ? s.handle : null)); },
|
|
88
111
|
async signOut() {
|
|
112
|
+
endSession("sign_out");
|
|
89
113
|
const value = await csrf();
|
|
90
114
|
const r = await nativeFetch("/auth/logout", { method: "POST", credentials: "same-origin", headers: value ? { "X-Agend-CSRF": value } : {} });
|
|
91
115
|
// A sign-out the server could not make durable is not quietly reported as done: say so before leaving.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// #1629 r4-4 (option A): widgets — an agent's in its own chat header and Details page, a person's in their sidebar —
|
|
2
|
+
// drawn by the page's own components from a closed declarative spec and its values (src/web-widget.ts). Nothing from a
|
|
3
|
+
// mod becomes markup, script, a handler, a URL or a style: text is text nodes, a number fills a <progress> or an SVG
|
|
4
|
+
// polyline's points, and tones are fixed class tokens. Colours are the theme's tokens (and a person's own CSS). The box
|
|
5
|
+
// always carries the page's own ✦ chip, so a widget never reads as AgEnD's chrome, and nothing in it can be clicked.
|
|
6
|
+
//
|
|
7
|
+
// What the host sends is checked again here with the same rules; a widget that fails is not drawn, and a value that
|
|
8
|
+
// fails shows "—". A `bind` is a value the page already has (the instance's own state, context and cost; the fleet's
|
|
9
|
+
// counts in the person's sidebar only) — nothing new is fetched for it.
|
|
10
|
+
import { html } from "./app-html.js";
|
|
11
|
+
import { register, t } from "./app-i18n.js";
|
|
12
|
+
import { statusFacet } from "./instance-nav.js";
|
|
13
|
+
import { themesShown, themesStore } from "./app-themes.js";
|
|
14
|
+
import { onPrefsChanged, prefInForce, savePref } from "./app-prefs.js";
|
|
15
|
+
import { createStore } from "./app-store.js";
|
|
16
|
+
import { safeMode } from "./owner-css.js";
|
|
17
|
+
|
|
18
|
+
register("widget", {
|
|
19
|
+
en: {
|
|
20
|
+
label: "Widget: {0}",
|
|
21
|
+
chip: "A widget — drawn by AgEnD from its owner's data",
|
|
22
|
+
byHuman: "placed by a person",
|
|
23
|
+
none: "—",
|
|
24
|
+
},
|
|
25
|
+
"zh-TW": {
|
|
26
|
+
label: "小工具:{0}",
|
|
27
|
+
chip: "小工具——由 AgEnD 依擁有者的資料繪製",
|
|
28
|
+
byHuman: "由人放置",
|
|
29
|
+
none: "—",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const WIDGET_ITEM_TYPES = ["stat", "progress", "text", "list", "badge", "spark"];
|
|
34
|
+
export const WIDGET_TONES = ["ok", "warn", "bad", "info", "muted"];
|
|
35
|
+
const INSTANCE_BINDS = ["state", "context_pct", "cost_usd"];
|
|
36
|
+
const FLEET_BINDS = ["fleet.idle", "fleet.working", "fleet.needs_you", "fleet.stopped"];
|
|
37
|
+
export const SLOT_RULES = {
|
|
38
|
+
chat_header: { maxItems: 4, types: ["stat", "progress", "text", "badge"], binds: INSTANCE_BINDS },
|
|
39
|
+
details: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: INSTANCE_BINDS },
|
|
40
|
+
sidebar: { maxItems: 8, types: WIDGET_ITEM_TYPES, binds: FLEET_BINDS },
|
|
41
|
+
};
|
|
42
|
+
const BINDS_FOR = {
|
|
43
|
+
stat: ["context_pct", "cost_usd", ...FLEET_BINDS], progress: ["context_pct"], text: ["state"], badge: ["state"], list: [], spark: [],
|
|
44
|
+
};
|
|
45
|
+
const ITEM_KEYS = {
|
|
46
|
+
stat: ["type", "label", "key", "bind", "unit"], progress: ["type", "label", "key", "bind", "max"], text: ["type", "label", "key", "bind"],
|
|
47
|
+
list: ["type", "label", "key"], badge: ["type", "label", "key", "bind", "tone", "tones"], spark: ["type", "label", "key"],
|
|
48
|
+
};
|
|
49
|
+
const LABEL_REQUIRED = ["stat", "progress", "spark"];
|
|
50
|
+
const KEY = /^[a-z][a-z0-9_]{0,31}$/;
|
|
51
|
+
const NUMBER_MAX = 1e15;
|
|
52
|
+
const own = (o, k) => !!o && Object.prototype.hasOwnProperty.call(o, k);
|
|
53
|
+
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
54
|
+
|
|
55
|
+
// One line of text, as the host's canonicalText: NFC, no control, format or separator character but the zero-width
|
|
56
|
+
// joiner, at least one character that draws something, 1..max graphemes.
|
|
57
|
+
const FORBIDDEN = /(?!)[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
58
|
+
const VISIBLE = /[^\p{M}\p{C}\p{Z}ᅟᅠㅤᅠ⠀]/u;
|
|
59
|
+
let graphemes = null;
|
|
60
|
+
export function lineOk(s, max) {
|
|
61
|
+
if (typeof s !== "string" || s.normalize("NFC") !== s || FORBIDDEN.test(s) || s.trim() !== s || !VISIBLE.test(s)) return false;
|
|
62
|
+
try { graphemes ??= new Intl.Segmenter(undefined, { granularity: "grapheme" }); } catch { return false; }
|
|
63
|
+
const n = [...graphemes.segment(s)].length;
|
|
64
|
+
return n >= 1 && n <= max;
|
|
65
|
+
}
|
|
66
|
+
const isNumber = (v) => typeof v === "number" && Number.isFinite(v) && Math.abs(v) <= NUMBER_MAX;
|
|
67
|
+
|
|
68
|
+
/** Whether `v` is a value an item of `type` can show (web-widget.ts valueFits). */
|
|
69
|
+
export function valueFits(type, v) {
|
|
70
|
+
switch (type) {
|
|
71
|
+
case "stat": return isNumber(v) || lineOk(v, 24);
|
|
72
|
+
case "progress": return isNumber(v);
|
|
73
|
+
case "text": return lineOk(v, 120);
|
|
74
|
+
case "badge": return lineOk(v, 24);
|
|
75
|
+
case "list": return Array.isArray(v) && v.length >= 1 && v.length <= 6 && v.every((s) => lineOk(s, 60));
|
|
76
|
+
case "spark": return Array.isArray(v) && v.length >= 2 && v.length <= 48 && v.every(isNumber);
|
|
77
|
+
default: return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A widget `{ spec, data, by? }` for `slot`, checked again: `{ widget }`, or `{ problem }` (where, and what) when its
|
|
83
|
+
* spec fails — then the whole widget is not drawn. A data value that fails is left out (shown as "—").
|
|
84
|
+
*/
|
|
85
|
+
export function explainWidget(slot, w) {
|
|
86
|
+
const no = (problem) => ({ problem });
|
|
87
|
+
const rules = own(SLOT_RULES, slot) ? SLOT_RULES[slot] : null;
|
|
88
|
+
if (!rules) return no("slot");
|
|
89
|
+
if (!isObj(w) || !isObj(w.spec)) return no("spec: an object { v: 1, title, items }");
|
|
90
|
+
const s = w.spec;
|
|
91
|
+
const extra = Object.keys(s).find((k) => !["v", "title", "items"].includes(k));
|
|
92
|
+
if (extra) return no(`spec.${extra}: unknown key`);
|
|
93
|
+
if (s.v !== 1) return no("spec.v: must be 1");
|
|
94
|
+
if (!lineOk(s.title, 24)) return no("spec.title: 1..24 characters on one line");
|
|
95
|
+
if (!Array.isArray(s.items) || s.items.length < 1 || s.items.length > rules.maxItems) return no(`spec.items: 1..${rules.maxItems} items`);
|
|
96
|
+
const items = [];
|
|
97
|
+
for (const [i, it] of s.items.entries()) {
|
|
98
|
+
const at = `spec.items.${i}`;
|
|
99
|
+
if (!isObj(it) || !rules.types.includes(it.type)) return no(`${at}.type: ${rules.types.join(", ")}`);
|
|
100
|
+
const unknown = Object.keys(it).find((k) => !ITEM_KEYS[it.type].includes(k));
|
|
101
|
+
if (unknown) return no(`${at}.${unknown}: unknown key`);
|
|
102
|
+
if ((it.label !== undefined || LABEL_REQUIRED.includes(it.type)) && !lineOk(it.label, 24)) return no(`${at}.label: 1..24 characters`);
|
|
103
|
+
if ((it.key === undefined) === (it.bind === undefined)) return no(`${at}: exactly one of key or bind`);
|
|
104
|
+
if (it.key !== undefined && !(typeof it.key === "string" && KEY.test(it.key))) return no(`${at}.key: a-z, 0-9 and _`);
|
|
105
|
+
if (it.bind !== undefined && !(BINDS_FOR[it.type].includes(it.bind) && rules.binds.includes(it.bind))) return no(`${at}.bind: ${BINDS_FOR[it.type].filter((b) => rules.binds.includes(b)).join(", ") || "none here"}`);
|
|
106
|
+
if (it.unit !== undefined && !lineOk(it.unit, 8)) return no(`${at}.unit: 1..8 characters`);
|
|
107
|
+
if (it.max !== undefined && !(isNumber(it.max) && it.max > 0)) return no(`${at}.max: a number > 0`);
|
|
108
|
+
if (it.tone !== undefined && !WIDGET_TONES.includes(it.tone)) return no(`${at}.tone: ${WIDGET_TONES.join(", ")}`);
|
|
109
|
+
let tones;
|
|
110
|
+
if (it.tones !== undefined) {
|
|
111
|
+
if (!isObj(it.tones)) return no(`${at}.tones: { value: tone }`);
|
|
112
|
+
tones = new Map();
|
|
113
|
+
for (const [k, v] of Object.entries(it.tones)) { if (!lineOk(k, 24) || !WIDGET_TONES.includes(v)) return no(`${at}.tones: { value: ${WIDGET_TONES.join(" | ")} }`); tones.set(k, v); }
|
|
114
|
+
if (tones.size < 1 || tones.size > 8) return no(`${at}.tones: 1..8 entries`);
|
|
115
|
+
}
|
|
116
|
+
items.push({ type: it.type, label: it.label, key: it.key, bind: it.bind, unit: it.unit, max: it.max, tone: it.tone, tones });
|
|
117
|
+
}
|
|
118
|
+
const data = new Map();
|
|
119
|
+
if (isObj(w.data)) items.forEach((it, i) => { if (it.key && own(w.data, it.key) && valueFits(it.type, w.data[it.key])) data.set(`${i}`, w.data[it.key]); });
|
|
120
|
+
return { widget: { title: s.title, items, data, by: w.by === "human" ? "human" : "agent" } };
|
|
121
|
+
}
|
|
122
|
+
/** explainWidget's widget, or null. */
|
|
123
|
+
export function checkWidget(slot, w) { return explainWidget(slot, w).widget || null; }
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* What the host would refuse in a person's `{ spec, data }` for `slot` (its whole check, web-widget.ts and web-prefs.ts),
|
|
127
|
+
* or null — so the editor can say why before anything is saved.
|
|
128
|
+
*/
|
|
129
|
+
export function widgetProblem(slot, v) {
|
|
130
|
+
if (!isObj(v) || Object.keys(v).length !== 2 || !own(v, "spec") || !own(v, "data")) return "{ \"spec\": …, \"data\": … }";
|
|
131
|
+
const r = explainWidget(slot, v);
|
|
132
|
+
if (r.problem) return r.problem;
|
|
133
|
+
if (!isObj(v.data)) return "data: an object { key: value }";
|
|
134
|
+
for (const [k, val] of Object.entries(v.data)) {
|
|
135
|
+
const users = r.widget.items.filter((it) => it.key === k);
|
|
136
|
+
if (!users.length) return `data.${k}: no item shows this key`;
|
|
137
|
+
const bad = users.find((it) => !valueFits(it.type, val));
|
|
138
|
+
if (bad) return `data.${k}: not a value a ${bad.type} shows`;
|
|
139
|
+
}
|
|
140
|
+
const bytes = (x) => new TextEncoder().encode(JSON.stringify(x)).length;
|
|
141
|
+
if (bytes(v.spec) > 2048) return "spec: at most 2048 bytes";
|
|
142
|
+
if (bytes(v.data) > 4096) return "data: at most 4096 bytes";
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The values the page has for one instance: its state word (a sidebar facet), context % and cost. */
|
|
147
|
+
export function instanceBinds(inst, exec, awaiting) {
|
|
148
|
+
if (!inst) return {};
|
|
149
|
+
return {
|
|
150
|
+
state: statusFacet(inst, exec, awaiting),
|
|
151
|
+
context_pct: isNumber(inst.context_pct) ? Math.round(Math.max(0, Math.min(100, inst.context_pct))) : undefined,
|
|
152
|
+
cost_usd: isNumber(inst.cost) ? inst.cost : undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** The fleet's counts, for the person's sidebar widget only: how many instances are idle, working, need you, stopped. */
|
|
156
|
+
export function fleetBinds(instances, exec, awaiting) {
|
|
157
|
+
const n = { "fleet.idle": 0, "fleet.working": 0, "fleet.needs_you": 0, "fleet.stopped": 0 };
|
|
158
|
+
for (const it of Array.isArray(instances) ? instances : []) {
|
|
159
|
+
const f = statusFacet(it, exec && exec[it.name], awaiting && own(awaiting, it.name) ? awaiting[it.name] : null);
|
|
160
|
+
n[f === "needs" ? "fleet.needs_you" : f === "working" ? "fleet.working" : f === "idle" ? "fleet.idle" : "fleet.stopped"] += 1;
|
|
161
|
+
}
|
|
162
|
+
return n;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let numbers = null;
|
|
166
|
+
function fmt(v) {
|
|
167
|
+
try { numbers ??= new Intl.NumberFormat(undefined, { maximumFractionDigits: 2 }); return numbers.format(v); } catch { return String(v); }
|
|
168
|
+
}
|
|
169
|
+
/** An item's value: its data, or its bind; undefined when there is none (shown as "—"). */
|
|
170
|
+
function valueOf(item, i, w, binds) {
|
|
171
|
+
if (item.key) return w.data.get(`${i}`);
|
|
172
|
+
const v = binds && own(binds, item.bind) ? binds[item.bind] : undefined;
|
|
173
|
+
return item.bind === "state" ? (lineOk(v, 24) ? v : undefined) : isNumber(v) ? v : undefined;
|
|
174
|
+
}
|
|
175
|
+
function shown(item, v) {
|
|
176
|
+
if (v === undefined) return t("widget.none");
|
|
177
|
+
if (item.bind === "cost_usd") return `$${v.toFixed(2)}`;
|
|
178
|
+
if (item.bind === "context_pct") return `${v}%`;
|
|
179
|
+
return typeof v === "number" ? fmt(v) : v;
|
|
180
|
+
}
|
|
181
|
+
/** The sparkline's points in a 100×24 box (numbers only; nothing else reaches the SVG). */
|
|
182
|
+
export function sparkPoints(values) {
|
|
183
|
+
const lo = Math.min(...values), hi = Math.max(...values), span = hi - lo || 1;
|
|
184
|
+
return values.map((v, i) => `${((i / (values.length - 1)) * 100).toFixed(2)},${(22 - ((v - lo) / span) * 20).toFixed(2)}`).join(" ");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function Item({ item, v }) {
|
|
188
|
+
const label = item.label ? html`<span class="aw-label">${item.label}</span>` : null;
|
|
189
|
+
switch (item.type) {
|
|
190
|
+
case "stat":
|
|
191
|
+
return html`<div class="aw-item aw-stat">${label}<span class="aw-value">${shown(item, v)}${v !== undefined && item.unit ? html`<span class="aw-unit">${item.unit}</span>` : null}</span></div>`;
|
|
192
|
+
case "progress": {
|
|
193
|
+
const max = item.max ?? 100;
|
|
194
|
+
const pct = v === undefined ? null : Math.round((Math.max(0, Math.min(max, v)) / max) * 100);
|
|
195
|
+
// No value: no bar at all (a <progress> without one is the browser's animated "busy" bar), only "—".
|
|
196
|
+
return html`<div class="aw-item aw-progress">${label}${pct === null ? null : html`<progress class="aw-bar" max=${max} value=${Math.max(0, Math.min(max, v))} aria-label=${item.label}></progress>`}<span class="aw-value">${pct === null ? t("widget.none") : `${pct}%`}</span></div>`;
|
|
197
|
+
}
|
|
198
|
+
case "text":
|
|
199
|
+
return html`<div class="aw-item aw-text">${label}<span class="aw-value">${shown(item, v)}</span></div>`;
|
|
200
|
+
case "badge": {
|
|
201
|
+
const tone = v !== undefined && item.tones && item.tones.has(v) ? item.tones.get(v) : item.tone || "muted";
|
|
202
|
+
return html`<div class="aw-item aw-badge-item">${label}<span class=${`aw-badge aw-tone-${WIDGET_TONES.includes(tone) ? tone : "muted"}`}>${shown(item, v)}</span></div>`;
|
|
203
|
+
}
|
|
204
|
+
case "list":
|
|
205
|
+
return html`<div class="aw-item aw-list">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>` : html`<ul>${v.map((line, i) => html`<li key=${i}>${line}</li>`)}</ul>`}</div>`;
|
|
206
|
+
case "spark":
|
|
207
|
+
return html`<div class="aw-item aw-spark-item">${label}${v === undefined ? html`<span class="aw-value">${t("widget.none")}</span>`
|
|
208
|
+
: html`<svg class="aw-spark" viewBox="0 0 100 24" preserveAspectRatio="none" aria-hidden="true"><polyline points=${sparkPoints(v)} /></svg><span class="aw-value">${fmt(v[v.length - 1])}</span>`}</div>`;
|
|
209
|
+
default: return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** One widget, checked (checkWidget's result), in `slot`. `binds`: instanceBinds or fleetBinds. */
|
|
214
|
+
export function AgentWidget({ slot, widget, binds, tools = null }) {
|
|
215
|
+
if (!widget) return null;
|
|
216
|
+
return html`<section data-trust="widget" class=${`aw aw-${slot}`} aria-label=${t("widget.label", widget.title)} data-widget-slot=${slot}>
|
|
217
|
+
<div class="aw-head"><span class="aw-chip" title=${t("widget.chip")}>✦ ${widget.title}</span>
|
|
218
|
+
${widget.by === "human" ? html`<span class="aw-by">${t("widget.byHuman")}</span>` : null}${tools}</div>
|
|
219
|
+
<div class="aw-items">${widget.items.map((item, i) => html`<${Item} key=${i} item=${item} v=${valueOf(item, i, widget, binds)} />`)}</div>
|
|
220
|
+
</section>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** An agent's widget in `slot` ("chat_header" | "details"), checked; null when none, or agents' looks are not shown here. */
|
|
224
|
+
export function widgetFor(name, slot) {
|
|
225
|
+
const s = themesStore.get();
|
|
226
|
+
if (!s.shown || !themesShown() || !own(s.widgets, name)) return null;
|
|
227
|
+
const slots = s.widgets[name];
|
|
228
|
+
return isObj(slots) && own(slots, slot) ? checkWidget(slot, slots[slot]) : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const SIDEBAR_KEY = "widget.sidebar";
|
|
232
|
+
const publicLink = () => typeof document !== "undefined" && !!document.body && document.body.dataset.publicLink === "1";
|
|
233
|
+
/** The person's own sidebar widget (a synced preference), raw; null when none. Never on the public link or in safe mode. */
|
|
234
|
+
export function sidebarWidgetRaw() {
|
|
235
|
+
if (publicLink() || safeMode()) return null;
|
|
236
|
+
const f = prefInForce(SIDEBAR_KEY);
|
|
237
|
+
let v = f ? f.value : undefined;
|
|
238
|
+
if (!f) { try { v = JSON.parse(localStorage.getItem("agend_widget_sidebar") || "null"); } catch { v = null; } }
|
|
239
|
+
return isObj(v) ? v : null;
|
|
240
|
+
}
|
|
241
|
+
/** The person's sidebar widget, checked; null when none. */
|
|
242
|
+
export function sidebarWidget() { const v = sidebarWidgetRaw(); return v ? checkWidget("sidebar", v) : null; }
|
|
243
|
+
/** Bumped when the person's sidebar widget changes (here, or on another device), so the sidebar redraws. */
|
|
244
|
+
export const sidebarWidgetStore = createStore({ n: 0 });
|
|
245
|
+
const bump = () => sidebarWidgetStore.set((s) => ({ n: s.n + 1 }));
|
|
246
|
+
onPrefsChanged((changed) => { if (changed.has(SIDEBAR_KEY)) bump(); });
|
|
247
|
+
/** Set (or with null, remove) the person's sidebar widget on all their devices. */
|
|
248
|
+
export function setSidebarWidget(value) { savePref(SIDEBAR_KEY, value); bump(); }
|
|
@@ -68,6 +68,7 @@ register("app", {
|
|
|
68
68
|
previews: "Allow HTML previews", tour: "Tour", tourTitle: "Show the web chat tour again",
|
|
69
69
|
needsYou: "needs you", approxNote: "Read from the terminal, so approximate", chat: "Chat", details: "Details", log: "Log", instanceViews: "Views of {0}",
|
|
70
70
|
textSize: "Text size: {0}", textSize_s: "S", textSize_m: "M", textSize_l: "L", textSize_fit: "Fit", usage: "Usage", usageHide: "Hide usage",
|
|
71
|
+
ownerCssKeep: "Keep this custom CSS?", ownerCssKeepLeft: "Reverts in {0} s", ownerCssKeepYes: "Keep", ownerCssKeepNo: "Revert",
|
|
71
72
|
statusRunning: "running", statusStopped: "stopped", statusPaused: "paused", statusCrashed: "crashed", statusWorking: "working", statusStuck: "looks stuck", statusIdle: "idle",
|
|
72
73
|
connReconnecting: "Can't reach AgEnD — it may be restarting. Reconnecting…", connReconnectingIn: "Can't reach AgEnD — it may be restarting. Trying again in {0} s.",
|
|
73
74
|
hydrating: "Loading what is open now…", hydrateFailed: "Could not load what is open now (prompts, ticks).",
|
|
@@ -99,6 +100,7 @@ register("app", {
|
|
|
99
100
|
previews: "允許預覽 HTML", tour: "導覽", tourTitle: "再看一次網頁聊天導覽",
|
|
100
101
|
needsYou: "等你回應", approxNote: "從終端畫面判讀,僅供參考", chat: "聊天", details: "詳細資訊", log: "紀錄", instanceViews: "{0} 的檢視",
|
|
101
102
|
textSize: "字級:{0}", textSize_s: "小", textSize_m: "中", textSize_l: "大", textSize_fit: "填滿", usage: "用量", usageHide: "隱藏用量",
|
|
103
|
+
ownerCssKeep: "保留這份自訂 CSS?", ownerCssKeepLeft: "{0} 秒後還原", ownerCssKeepYes: "保留", ownerCssKeepNo: "還原",
|
|
102
104
|
statusRunning: "執行中", statusStopped: "已停止", statusPaused: "已暫停", statusCrashed: "已當掉", statusWorking: "工作中", statusStuck: "似乎卡住", statusIdle: "閒置",
|
|
103
105
|
connReconnecting: "連不上 AgEnD——可能正在重新啟動。正在重新連線…", connReconnectingIn: "連不上 AgEnD——可能正在重新啟動。{0} 秒後再試。",
|
|
104
106
|
hydrating: "正在載入目前的狀態…", hydrateFailed: "無法載入目前的狀態(提示、送達標記)。",
|