@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli-instructions.md +6 -2
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +3 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +16 -4
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +17 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +126 -18
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +27 -1
- package/dist/backend/kiro.js +63 -10
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +48 -2
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +26 -1
- package/dist/channel/adapters/discord.js +103 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +35 -3
- package/dist/channel/adapters/telegram.js +122 -23
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +6 -5
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +46 -4
- package/dist/cli.js +184 -34
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +129 -1
- package/dist/daemon.js +754 -94
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +64 -8
- package/dist/delivery-outbox.js +315 -24
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -1
- package/dist/fleet-manager.d.ts +392 -19
- package/dist/fleet-manager.js +2030 -275
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +12 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +76 -27
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +3 -3
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/kiro-log-gate.d.ts +42 -0
- package/dist/kiro-log-gate.js +39 -0
- package/dist/kiro-log-gate.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +268 -42
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +99 -0
- package/dist/log-service.js +243 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +169 -0
- package/dist/log-steps.js +858 -0
- package/dist/log-steps.js.map +1 -0
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +277 -93
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +30 -15
- package/dist/outbound-schemas.js +23 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +101 -0
- package/dist/public-preview.js +236 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +20 -6
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +24 -1
- package/dist/public-web-link.js +63 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +89 -5
- package/dist/reply-buttons-controller.js +273 -8
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +33 -2
- package/dist/restart-progress.js +30 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +55 -0
- package/dist/service-installer.js +90 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +14 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +4 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tool-permissions.js +3 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +71 -4
- package/dist/topic-commands.js +167 -34
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +85 -3
- package/dist/transcript-jsonl.js +156 -13
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +11 -0
- package/dist/transcript-monitor.js +61 -13
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +24 -0
- package/dist/transcript-sources.js +211 -56
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +22 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +80 -10
- package/dist/ui/chat-store.js +70 -6
- package/dist/ui/chat-strings.js +62 -32
- package/dist/ui/chat-thread.js +199 -24
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/chat-tour.js +17 -14
- package/dist/ui/panel-chat.js +45 -21
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +213 -58
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +14 -10
- package/dist/ui/panel-settings.js +82 -17
- package/dist/ui/preview-panel.js +55 -15
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +44 -3
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-strings.js +32 -12
- package/dist/ui/settings-token.js +8 -1
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/app-i18n.js +41 -15
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +6 -1
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +26 -5
- package/dist/ui/shared/app.css +170 -30
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +83 -13
- package/dist/ui/shared/instance-nav.js +178 -51
- package/dist/ui/shared/instance-switch.js +8 -5
- package/dist/ui/shared/panel-view.js +21 -4
- package/dist/ui/shared/signin.js +15 -9
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +40 -4
- package/dist/ui/signin.html +8 -3
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +13 -1
- package/dist/update-install.js +16 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +62 -13
- package/dist/usage/providers.js +256 -68
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.js +31 -9
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +82 -4
- package/dist/web-api.js +632 -88
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +5 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +26 -1
- package/dist/web-chat-history.js +47 -8
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +41 -5
- package/dist/web-commands.js +25 -7
- package/dist/web-commands.js.map +1 -1
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +81 -0
- package/dist/web-prefs.js +297 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +11 -1
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -0
- package/dist/web-shell-routes.js +9 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +135 -7
- package/dist/web-upload.js +315 -51
- package/dist/web-upload.js.map +1 -1
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
import { html, useLayoutEffect, useRef, useState } from "./app-html.js";
|
|
11
11
|
import { t } from "./app-i18n.js";
|
|
12
12
|
import { appStore, createStore, useStore } from "./app-store.js";
|
|
13
|
-
import { chatPath, detailsPath, viewPath } from "./app-route.js";
|
|
13
|
+
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
14
14
|
import { Icon } from "./ui-icons.js";
|
|
15
15
|
import { Skeleton } from "./ui-states.js";
|
|
16
16
|
import { viewStore } from "./view-roster-store.js";
|
|
17
|
+
import { onPrefsChanged, prefInForce, prefsWritable, savePref } from "./app-prefs.js";
|
|
17
18
|
import "./view-strings.js";
|
|
18
19
|
|
|
19
20
|
/** view.* text with named values. */
|
|
@@ -23,7 +24,6 @@ function tn(key, values = {}) {
|
|
|
23
24
|
return s;
|
|
24
25
|
}
|
|
25
26
|
const stored = (k) => { try { return localStorage.getItem(k); } catch { return null; } };
|
|
26
|
-
const store = (k, v) => { try { localStorage.setItem(k, v); } catch { /* this page only */ } };
|
|
27
27
|
/** A row's instance name: `name` (status frames) or `instance_name` (View's roster). */
|
|
28
28
|
const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
|
|
29
29
|
|
|
@@ -101,14 +101,24 @@ export function instanceTooltip(it) {
|
|
|
101
101
|
return lines.join("\n");
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// ──
|
|
104
|
+
// ── The order (drag), folded groups and filter: host preferences since #1665 (nav.*), cached in this browser ──
|
|
105
105
|
const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999: everyone's arrangement carries over
|
|
106
106
|
const COLLAPSED_KEY = "agend_instance_collapsed";
|
|
107
107
|
const FILTER_KEY = "agend_instance_filter";
|
|
108
|
+
const FAVORITES_KEY = "agend_favorites"; // #1664
|
|
109
|
+
/**
|
|
110
|
+
* A synced sidebar preference, as the JSON its reader parses: the value in force in memory (app-prefs.js — this
|
|
111
|
+
* page's own change, else the host's; a full or blocked storage never brings an old one back), else this browser's copy.
|
|
112
|
+
*/
|
|
113
|
+
function inForce(prefKey, lsKey) {
|
|
114
|
+
const p = prefInForce(prefKey);
|
|
115
|
+
if (!p) return stored(lsKey);
|
|
116
|
+
return p.value === undefined ? null : JSON.stringify(p.value);
|
|
117
|
+
}
|
|
108
118
|
function readOrder() {
|
|
109
119
|
const groups = new Map(), insts = new Map();
|
|
110
120
|
try {
|
|
111
|
-
const rows = JSON.parse(
|
|
121
|
+
const rows = JSON.parse(inForce("nav.order", SIDEBAR_ORDER_KEY) || "[]");
|
|
112
122
|
if (Array.isArray(rows)) for (const row of rows) {
|
|
113
123
|
if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
|
|
114
124
|
else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
|
|
@@ -117,15 +127,18 @@ function readOrder() {
|
|
|
117
127
|
return { groups, insts };
|
|
118
128
|
}
|
|
119
129
|
function readCollapsed() {
|
|
120
|
-
try { const a = JSON.parse(
|
|
130
|
+
try { const a = JSON.parse(inForce("nav.collapsed", COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
|
|
121
131
|
}
|
|
122
132
|
const strings = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === "string") : []);
|
|
123
133
|
function readFilter() {
|
|
124
134
|
try {
|
|
125
|
-
const f = JSON.parse(
|
|
135
|
+
const f = JSON.parse(inForce("nav.filter", FILTER_KEY) || "{}");
|
|
126
136
|
return { q: f && typeof f.q === "string" ? f.q : "", status: strings(f && f.status).filter((s) => STATUS_FACETS.includes(s)), cli: strings(f && f.cli) };
|
|
127
137
|
} catch { return { q: "", status: [], cli: [] }; }
|
|
128
138
|
}
|
|
139
|
+
function readFavorites() {
|
|
140
|
+
try { const a = JSON.parse(inForce("nav.favorites", FAVORITES_KEY) || "[]"); return Array.isArray(a) ? [...new Set(a.filter((x) => typeof x === "string"))] : []; } catch { return []; }
|
|
141
|
+
}
|
|
129
142
|
/** Display order: this browser's saved order first, then new groups/instances by tag and name. */
|
|
130
143
|
export function computeOrder(roster, order) {
|
|
131
144
|
const groups = new Map();
|
|
@@ -159,21 +172,61 @@ function moveIn(arr, moving, target, before) {
|
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
/** The list's state for the page's life: this browser's order, folded groups and filter (each also kept in storage). */
|
|
162
|
-
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() });
|
|
175
|
+
export const navStore = createStore({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
|
|
163
176
|
/** Read them again from this browser's storage — what a page load does (the store above is made that way). */
|
|
164
|
-
export function loadNavPrefs() {
|
|
177
|
+
export function loadNavPrefs() {
|
|
178
|
+
navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter(), favorites: readFavorites() });
|
|
179
|
+
namesStore.set({ primary: readNamesPrimary() });
|
|
180
|
+
}
|
|
165
181
|
/** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
|
|
166
182
|
export function setNavFilter(patch) {
|
|
167
183
|
const filter = { ...navStore.get().filter, ...patch };
|
|
168
184
|
navStore.set({ filter });
|
|
169
|
-
|
|
185
|
+
savePref("nav.filter", { q: filter.q, status: filter.status, cli: filter.cli });
|
|
186
|
+
}
|
|
187
|
+
function setCollapsed(collapsed) { navStore.set({ collapsed }); savePref("nav.collapsed", [...collapsed]); }
|
|
188
|
+
// Another device rearranged, folded or filtered: this page's list follows (its cache is already updated).
|
|
189
|
+
onPrefsChanged((keys) => { if (keys.has("nav.order") || keys.has("nav.collapsed") || keys.has("nav.filter") || keys.has("nav.favorites")) loadNavPrefs(); });
|
|
190
|
+
|
|
191
|
+
// ── #1664: favorites — starred instances, pinned at the top in the order they were starred (or moved) ──
|
|
192
|
+
export const MAX_FAVORITES = 50;
|
|
193
|
+
/** The favorites that are instances now (`known`: the names listed): what is shown, and the base of every change — so
|
|
194
|
+
* a deleted instance is dropped, never sent back to the host (which would refuse it). */
|
|
195
|
+
function liveFavorites(known) { return navStore.get().favorites.filter((n) => known.has(n)); }
|
|
196
|
+
function setFavorites(next) { navStore.set({ favorites: next }); savePref("nav.favorites", next); }
|
|
197
|
+
/** Star or unstar `name`. False: not done (the list is full). */
|
|
198
|
+
export function toggleFavorite(name, known) {
|
|
199
|
+
const cur = liveFavorites(known);
|
|
200
|
+
if (cur.includes(name)) { setFavorites(cur.filter((n) => n !== name)); return true; }
|
|
201
|
+
if (cur.length >= MAX_FAVORITES || !known.has(name)) return false;
|
|
202
|
+
setFavorites([...cur, name]);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
/** Move a favorite one place up (-1) or down (+1). */
|
|
206
|
+
export function moveFavorite(name, delta, known) {
|
|
207
|
+
const cur = liveFavorites(known);
|
|
208
|
+
const from = cur.indexOf(name), to = from + delta;
|
|
209
|
+
if (from < 0 || to < 0 || to >= cur.length) return;
|
|
210
|
+
cur.splice(to, 0, cur.splice(from, 1)[0]);
|
|
211
|
+
setFavorites(cur);
|
|
212
|
+
}
|
|
213
|
+
// ── #1700: the filter's facet groups, open or folded — per device (localStorage; blocked or full storage: this page only) ──
|
|
214
|
+
const FACETS_KEY = "agend_nav_facets";
|
|
215
|
+
function readFacetsOpen() {
|
|
216
|
+
try {
|
|
217
|
+
const v = JSON.parse(stored(FACETS_KEY) || "{}");
|
|
218
|
+
return { status: !!(v && v.status === true), cli: !!(v && v.cli === true) };
|
|
219
|
+
} catch { return { status: false, cli: false }; }
|
|
170
220
|
}
|
|
171
|
-
function
|
|
221
|
+
function writeFacetsOpen(open) { try { localStorage.setItem(FACETS_KEY, JSON.stringify({ status: !!open.status, cli: !!open.cli })); } catch { /* this page only */ } }
|
|
222
|
+
|
|
223
|
+
/** The Favorites group's key among the tag groups: no tag can be it (a group name is never a control character). */
|
|
224
|
+
const FAV_GROUP = "\u0000favorites";
|
|
172
225
|
function persistOrder(groupNames, instByGroup) {
|
|
173
226
|
const rows = [];
|
|
174
227
|
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
175
228
|
for (const g of groupNames) (instByGroup.get(g) || []).forEach((n, j) => rows.push({ item_type: "instance", item_name: n, sort_index: j, group_name: g }));
|
|
176
|
-
|
|
229
|
+
savePref("nav.order", rows);
|
|
177
230
|
navStore.set({ order: { groups: new Map(groupNames.map((g, i) => [g, i])), insts: new Map(rows.filter((r) => r.item_type === "instance").map((r) => [r.item_name, r.sort_index])) } });
|
|
178
231
|
}
|
|
179
232
|
|
|
@@ -195,6 +248,35 @@ export function labelOf(it) {
|
|
|
195
248
|
const d = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
|
|
196
249
|
return d || nameOf(it);
|
|
197
250
|
}
|
|
251
|
+
|
|
252
|
+
// ── #1722: both names — which one leads is a synced preference (#1665 "names.primary", default the nickname) ──
|
|
253
|
+
const NAMES_KEY = "agend_names_primary";
|
|
254
|
+
function readNamesPrimary() {
|
|
255
|
+
const p = prefInForce("names.primary");
|
|
256
|
+
const v = p ? p.value : stored(NAMES_KEY);
|
|
257
|
+
return v === "instance" ? "instance" : "nickname";
|
|
258
|
+
}
|
|
259
|
+
/** { primary: "nickname" | "instance" } — every surface that names an instance (sidebar, chat header, Needs you) reads it. */
|
|
260
|
+
export const namesStore = createStore({ primary: readNamesPrimary() });
|
|
261
|
+
export function setNamesPrimary(value) {
|
|
262
|
+
const primary = value === "instance" ? "instance" : "nickname";
|
|
263
|
+
namesStore.set({ primary });
|
|
264
|
+
savePref("names.primary", primary);
|
|
265
|
+
}
|
|
266
|
+
onPrefsChanged((keys) => { if (keys.has("names.primary")) namesStore.set({ primary: readNamesPrimary() }); });
|
|
267
|
+
/**
|
|
268
|
+
* An instance's two names as the web shows them: the one leading (`primary`) and the other (`secondary`), or "" when
|
|
269
|
+
* there is no other — no nickname, or a nickname that is the instance name itself (one line).
|
|
270
|
+
*/
|
|
271
|
+
export function namesOf(it, primary = namesStore.get().primary) {
|
|
272
|
+
const real = nameOf(it);
|
|
273
|
+
const nick = it && typeof it.display_name === "string" ? it.display_name.trim() : "";
|
|
274
|
+
if (!nick || nick === real) return { primary: real, secondary: "" };
|
|
275
|
+
return primary === "instance" ? { primary: real, secondary: nick } : { primary: nick, secondary: real };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** #1722: the CLI on the avatar's corner — a letter, so it reads without colour (its name is in the row's text too). */
|
|
279
|
+
const CLI_MONOGRAM = { "claude-code": "C", codex: "X", "kiro-cli": "K", grok: "G", antigravity: "A", opencode: "O", muse: "M" };
|
|
198
280
|
// One path segment under /ui/img/<kind>/ — no further slash or backslash, and not a dot segment ("." / ".." or their
|
|
199
281
|
// %2e spellings, which a browser resolves too): the picture can only ever be one of those routes (#1642 review).
|
|
200
282
|
const sameOrigin = (src) => typeof src === "string"
|
|
@@ -205,7 +287,8 @@ export function avatarOf(it) {
|
|
|
205
287
|
if (a && a.kind === "image" && sameOrigin(a.src)) return { kind: "image", src: a.src };
|
|
206
288
|
if (a && a.kind === "emoji" && sameOrigin(a.src)) return { kind: "emoji", src: a.src, alt: typeof a.alt === "string" ? a.alt : "" };
|
|
207
289
|
if (a && a.kind === "emoji" && typeof a.text === "string" && a.text.trim()) return { kind: "emoji", text: a.text.trim() };
|
|
208
|
-
|
|
290
|
+
// #1722: the initial is the leading name's ("Names"), not the server's (which is always the nickname's).
|
|
291
|
+
return { kind: "initial", text: firstCharacter(namesOf(it).primary) || (a && a.kind === "initial" && typeof a.text === "string" ? a.text : "") };
|
|
209
292
|
}
|
|
210
293
|
const ACCENT = /^#[0-9a-f]{6}$/i;
|
|
211
294
|
/** The status dot on the avatar: needs you, crashed, paused, stopped, looks stuck, working, idle — colour AND a label. */
|
|
@@ -224,7 +307,7 @@ const DOT = {
|
|
|
224
307
|
export function dotLabel(state) { return (DOT[state] || DOT.stopped)[1](); }
|
|
225
308
|
|
|
226
309
|
/** An instance's avatar with its status dot. The ring (its bot's colour) is a CSS custom property set through the CSSOM. */
|
|
227
|
-
export function InstanceAvatar({ it, state }) {
|
|
310
|
+
export function InstanceAvatar({ it, state, cli = false }) {
|
|
228
311
|
const a = avatarOf(it);
|
|
229
312
|
const [broken, setBroken] = useState("");
|
|
230
313
|
const el = useRef(null);
|
|
@@ -235,12 +318,13 @@ export function InstanceAvatar({ it, state }) {
|
|
|
235
318
|
if (accent) node.style.setProperty("--ring", accent); else node.style.removeProperty("--ring");
|
|
236
319
|
}, [accent]);
|
|
237
320
|
const src = a.kind !== "initial" && a.src && broken !== a.src ? a.src : "";
|
|
238
|
-
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(
|
|
321
|
+
const text = a.kind === "emoji" && a.text ? a.text : a.kind === "initial" ? a.text : firstCharacter(namesOf(it).primary);
|
|
239
322
|
const [cls] = DOT[state] || DOT.stopped;
|
|
240
323
|
const label = dotLabel(state);
|
|
241
324
|
return html`<span ref=${el} class=${`av${accent ? " ring" : ""}${a.kind === "emoji" ? " av-emoji" : ""}`}>
|
|
242
325
|
${src ? html`<img src=${src} alt="" decoding="async" onError=${() => setBroken(src)} />` : html`<span class="av-text" aria-hidden="true">${text}</span>`}
|
|
243
|
-
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span
|
|
326
|
+
<span class=${`av-dot dot ${cls}`} role="img" aria-label=${label} title=${label}></span>${cli
|
|
327
|
+
? html`<span class="av-cli" aria-hidden="true" title=${backendLabel(it.backend)}>${CLI_MONOGRAM[it.backend] || "·"}</span>` : null}</span>`;
|
|
244
328
|
}
|
|
245
329
|
|
|
246
330
|
// ── The list ──
|
|
@@ -256,32 +340,47 @@ const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet ===
|
|
|
256
340
|
*/
|
|
257
341
|
export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
|
|
258
342
|
const nav = useStore(navStore);
|
|
343
|
+
const names = useStore(namesStore); // #1722: which name leads
|
|
259
344
|
const filterRef = useRef(null);
|
|
260
345
|
const list = useRef(null);
|
|
261
346
|
const drag = useRef(null);
|
|
347
|
+
// #1700: which facet groups are open — this device's (screen layout, not a synced preference), read when the list mounts.
|
|
348
|
+
const [facetOpen, setFacetOpen] = useState(readFacetsOpen);
|
|
349
|
+
const toggleFacet = (key) => setFacetOpen((cur) => { const next = { ...cur, [key]: !cur[key] }; writeFacetsOpen(next); return next; });
|
|
350
|
+
// #1664: "☆ Edit" — the stars on every row (on a phone the only way to them) and ↑ ↓ on the favorites — only on the
|
|
351
|
+
// full list, as drag: in a filtered one the row above may not be the one above in the list (#1677 review).
|
|
352
|
+
const [editing, setEditing] = useState(false);
|
|
262
353
|
// Where a row goes: the view the page is in (#1523 §3.1). View → View; Details, or a Fleet tab (Q4 = A), → Details;
|
|
263
354
|
// everywhere else → Chat. The anonymous reader only ever has View.
|
|
264
355
|
const rowPath = viewOnly || (route && route.panel === "view") ? viewPath
|
|
356
|
+
: route && route.panel === "log" ? logPath // #1218: Log → Log
|
|
265
357
|
: route && (route.panel === "details" || route.panel === "fleet") ? detailsPath : chatPath;
|
|
266
|
-
const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "details") ? route.instance : null;
|
|
358
|
+
const active = route && (route.panel === "chat" || route.panel === "view" || route.panel === "log" || route.panel === "details") ? route.instance : null;
|
|
267
359
|
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
|
|
268
360
|
const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
|
|
269
361
|
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
270
362
|
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
271
363
|
const { groupNames, instByGroup } = computeOrder(items, nav.order);
|
|
364
|
+
// #1664: the favorites that are instances now, as a group of their own on top; the other groups keep the rest, in
|
|
365
|
+
// their order. Changing them: a signed-in page that may write preferences (not the public link, not the anonymous
|
|
366
|
+
// reader — they still see the list).
|
|
367
|
+
const known = new Set(byName.keys());
|
|
368
|
+
const favs = nav.favorites.filter((n) => known.has(n));
|
|
369
|
+
const favSet = new Set(favs);
|
|
370
|
+
const canStar = !viewOnly && prefsWritable();
|
|
371
|
+
const shownGroups = favs.length ? [FAV_GROUP, ...groupNames] : groupNames;
|
|
372
|
+
const shownInsts = new Map([...instByGroup].map(([g, names]) => [g, names.filter((n) => !favSet.has(n))]));
|
|
373
|
+
if (favs.length) shownInsts.set(FAV_GROUP, favs);
|
|
272
374
|
const f = nav.filter;
|
|
273
|
-
const shown = filterSidebar(
|
|
375
|
+
const shown = filterSidebar(shownGroups, shownInsts, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
274
376
|
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
275
|
-
// #1630: a display name two rows share would tell them apart from nothing — only then does the row keep its name too.
|
|
276
|
-
const labelCount = new Map();
|
|
277
|
-
for (const it of items) { const l = labelOf(it); labelCount.set(l, (labelCount.get(l) || 0) + 1); }
|
|
278
377
|
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
279
378
|
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
280
379
|
const flip = (key, value) => { const cur = new Set(navStore.get().filter[key]); if (cur.has(value)) cur.delete(value); else cur.add(value); setNavFilter({ [key]: [...cur] }); };
|
|
281
380
|
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
282
381
|
// their group.
|
|
283
382
|
const dragProps = (item) => shown.active ? {} : {
|
|
284
|
-
draggable:
|
|
383
|
+
draggable: true,
|
|
285
384
|
onDragStart: (e) => { drag.current = item; e.currentTarget.classList.add("dragging"); try { e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", item.name); } catch { /* Firefox needs data */ } },
|
|
286
385
|
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
287
386
|
onDragOver: (e) => {
|
|
@@ -307,58 +406,86 @@ export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting =
|
|
|
307
406
|
persistOrder(names, insts);
|
|
308
407
|
},
|
|
309
408
|
};
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
409
|
+
// #1700: a facet group (Status, CLI) folds under a real header button. Folded with a filter set, the header says so:
|
|
410
|
+
// one or two choices by name, more as "N selected" — a filter is never out of sight.
|
|
411
|
+
const chips = (key, values, label, text) => {
|
|
412
|
+
const open = !!facetOpen[key];
|
|
413
|
+
const sel = f[key];
|
|
414
|
+
const summary = !open && sel.length ? (sel.length <= 2 ? sel.map(text).join(", ") : tn("facetSelected", { n: sel.length })) : "";
|
|
415
|
+
const hint = tn(open ? "facetHide" : "facetShow", { group: label });
|
|
416
|
+
return html`<div class=${`nav-facet${open ? " open" : ""}`}>
|
|
417
|
+
<button type="button" class="nav-facet-head" aria-expanded=${open ? "true" : "false"} aria-controls=${`nav-facet-${key}`}
|
|
418
|
+
title=${summary ? `${hint} (${label} · ${summary})` : hint} onClick=${() => toggleFacet(key)}>
|
|
419
|
+
<${Icon} name="chevron" size=${12} cls=${open ? "caret" : "caret folded"} /><span class="nav-facet-label">${label}</span>
|
|
420
|
+
${summary ? html`<span class="nav-facet-sum">· ${summary}</span>` : sel.length ? html`<span class="nav-facet-n">${sel.length}</span>` : null}
|
|
421
|
+
</button>
|
|
422
|
+
${open ? html`<div id=${`nav-facet-${key}`} class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
|
|
423
|
+
<input type="checkbox" checked=${sel.includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>` : null}
|
|
424
|
+
</div>`;
|
|
425
|
+
};
|
|
426
|
+
const row = (name, g, i, count) => {
|
|
427
|
+
const it = byName.get(name);
|
|
428
|
+
// #1722: both names, the preferred one first; one line when there is no other.
|
|
429
|
+
const { primary: label, secondary } = namesOf(it, names.primary);
|
|
430
|
+
const facet = facetOf(name);
|
|
431
|
+
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
432
|
+
const on = name === active;
|
|
433
|
+
const fav = g === FAV_GROUP, starred = favSet.has(name);
|
|
434
|
+
const full = !starred && favs.length >= MAX_FAVORITES;
|
|
435
|
+
return html`<li key=${name} class="v-li"><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
436
|
+
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${fav ? {} : dragProps({ type: "instance", name, group: g })}>
|
|
437
|
+
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} cli=${true} />
|
|
438
|
+
<span class="inst-text"><span class="inst-name" title=${label}>${label}</span>
|
|
439
|
+
${secondary ? html`<span class="inst-alt" title=${secondary}>${secondary}</span>` : null}
|
|
440
|
+
${facet === "needs" ? html`<span class="inst-sub"><span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span></span>` : null}
|
|
441
|
+
<span class="sr-only">${tn("tooltipBackend", { value: backendLabel(it.backend) })}</span></span>
|
|
442
|
+
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
443
|
+
</a>${fav && editing && canStar ? html`<button type="button" class="icon-btn inst-move" aria-label=${tn("favUp", { name: label })}
|
|
444
|
+
title=${shown.active ? tn("favFiltered") : tn("favUp", { name: label })} disabled=${shown.active || i === 0} onClick=${() => { if (!shown.active) moveFavorite(name, -1, known); }}><${Icon} name="up" size=${14} /></button><button type="button" class="icon-btn inst-move"
|
|
445
|
+
aria-label=${tn("favDown", { name: label })} title=${shown.active ? tn("favFiltered") : tn("favDown", { name: label })} disabled=${shown.active || i === count - 1}
|
|
446
|
+
onClick=${() => { if (!shown.active) moveFavorite(name, 1, known); }}><${Icon} name="down" size=${14} /></button>` : null}${canStar
|
|
447
|
+
? html`<button type="button" class=${`icon-btn inst-star${starred ? " on" : ""}`} aria-pressed=${starred ? "true" : "false"} disabled=${full}
|
|
448
|
+
aria-label=${tn(starred ? "favRemove" : "favAdd", { name: label })} title=${full ? tn("favFull", { max: MAX_FAVORITES }) : tn(starred ? "favRemove" : "favAdd", { name: label })}
|
|
449
|
+
onClick=${() => toggleFavorite(name, known)}><${Icon} name="star" size=${14} /></button>` : null}</li>`;
|
|
450
|
+
};
|
|
315
451
|
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
316
|
-
return html`<div class
|
|
452
|
+
return html`<div class=${`view-roster inst-nav${editing && canStar ? " fav-editing" : ""}`}>
|
|
317
453
|
<div class="side-section" id="instanceList" ref=${list}>
|
|
318
|
-
<h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
454
|
+
<div class="inst-head"><h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
455
|
+
${canStar && items.length ? html`<button type="button" class="chip-btn fav-edit" aria-pressed=${editing ? "true" : "false"} onClick=${() => setEditing(!editing)}>
|
|
456
|
+
${editing ? tn("favDone") : html`<${Icon} name="star" size=${12} />${tn("favEdit")}`}</button>` : null}</div>
|
|
319
457
|
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
320
458
|
${shown.groups.map(({ group: g, names }) => {
|
|
459
|
+
// #1664: Favorites — always open, not dragged (↑ ↓ in Edit), named in the page's language.
|
|
460
|
+
if (g === FAV_GROUP) return html`<div key=${g} class="v-group v-fav" role="group" aria-label=${tn("favorites")}>
|
|
461
|
+
<div class="v-group-head fav-head"><${Icon} name="star" size=${14} cls="fav-mark" /><span class="grow">${tn("favorites")}</span><span class="count">${names.length}</span></div>
|
|
462
|
+
<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul></div>`;
|
|
321
463
|
const folded = nav.collapsed.has(g);
|
|
322
464
|
return html`<div key=${g} class="v-group">
|
|
323
465
|
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
324
466
|
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
325
467
|
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
326
|
-
${folded ? null : html`<ul class="inst-list">${names.map((name) =>
|
|
327
|
-
const it = byName.get(name);
|
|
328
|
-
const label = labelOf(it);
|
|
329
|
-
// The instance name under a display name only when another row has the same display name.
|
|
330
|
-
const twin = label !== name && (labelCount.get(label) || 0) > 1 ? name : "";
|
|
331
|
-
const facet = facetOf(name);
|
|
332
|
-
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
333
|
-
const on = name === active;
|
|
334
|
-
return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${rowPath(name)} title=${instanceTooltip(it)}
|
|
335
|
-
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
|
|
336
|
-
<${InstanceAvatar} it=${it} state=${dotState(it, own(exec, name) ? exec[name] : null, waiting)} />
|
|
337
|
-
<span class="inst-text"><span class="inst-name">${label}</span>
|
|
338
|
-
${twin || facet === "needs" ? html`<span class="inst-sub">${twin ? html`<span class="inst-alias inst-id">${twin}</span>` : null}
|
|
339
|
-
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}</span>
|
|
340
|
-
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
341
|
-
<span class=${`cli-icon cli-${KNOWN_BACKENDS.has(it.backend) ? it.backend : "other"}`} title=${backendLabel(it.backend)} aria-label=${tn("tooltipBackend", { value: backendLabel(it.backend) })}></span>
|
|
342
|
-
</a></li>`;
|
|
343
|
-
})}</ul>`}
|
|
468
|
+
${folded ? null : html`<ul class="inst-list">${names.map((name, i) => row(name, g, i, names.length))}</ul>`}
|
|
344
469
|
</div>`;
|
|
345
470
|
})}
|
|
346
471
|
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
347
472
|
</div>
|
|
348
473
|
<div class="v-filter">
|
|
349
474
|
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
350
|
-
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck
|
|
475
|
+
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck=${false} placeholder=${tn("filterPh")} value=${f.q}
|
|
351
476
|
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
352
477
|
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
|
|
353
478
|
${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
354
479
|
onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
355
480
|
</label>
|
|
356
481
|
<div class="nav-facets">
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
482
|
+
<div class="nav-facet-groups">
|
|
483
|
+
${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
|
|
484
|
+
${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
|
|
485
|
+
${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
|
|
486
|
+
</div>
|
|
487
|
+
<p class="note nav-shown" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
360
488
|
</div>
|
|
361
|
-
<p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
362
489
|
</div>
|
|
363
490
|
</div>`;
|
|
364
491
|
}
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
// #1523 N2 (§3.2): the instance view switch — Chat | View | Details of ONE instance. Each segment is a link to that
|
|
2
2
|
// view's URL for the same instance, so switching keeps the instance (and the sidebar, which is never remounted).
|
|
3
|
-
// Desktop: links in the header; a phone: the same
|
|
3
|
+
// Desktop: links in the header; a phone: the same four as top tabs under it (app.css). The anonymous View reader
|
|
4
4
|
// gets none (Chat and Details need a session): the caller does not render it there.
|
|
5
5
|
import { html } from "./app-html.js";
|
|
6
6
|
import { t } from "./app-i18n.js";
|
|
7
|
-
import { chatPath, detailsPath, viewPath } from "./app-route.js";
|
|
7
|
+
import { chatPath, detailsPath, logPath, viewPath } from "./app-route.js";
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* current: "chat" | "view" | "log" | "details". #1218: Log sits between View and Details. `data-label`: each tab is as
|
|
11
|
+
* wide as its label in the current tab's weight (app.css), so the bar keeps its size whichever tab is current.
|
|
12
|
+
*/
|
|
10
13
|
export function InstanceSwitch({ name, current }) {
|
|
11
14
|
const seg = (key, href, label) => html`<a key=${key} class=${`seg-item${current === key ? " active" : ""}`} href=${href}
|
|
12
|
-
aria-current=${current === key ? "page" : undefined}>${label}</a>`;
|
|
15
|
+
aria-current=${current === key ? "page" : undefined} data-label=${label}>${label}</a>`;
|
|
13
16
|
return html`<nav class="inst-switch seg" aria-label=${t("app.instanceViews", name)}>
|
|
14
|
-
${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("details", detailsPath(name), t("app.details"))}
|
|
17
|
+
${seg("chat", chatPath(name), t("app.chat"))}${seg("view", viewPath(name), t("app.view"))}${seg("log", logPath(name), t("app.log"))}${seg("details", detailsPath(name), t("app.details"))}
|
|
15
18
|
</nav>`;
|
|
16
19
|
}
|
|
@@ -114,7 +114,9 @@ export { viewStore };
|
|
|
114
114
|
// ── The panel ──
|
|
115
115
|
|
|
116
116
|
// readStream: one stream of reads under a lease (read-stream.js; View's pane and roster use it).
|
|
117
|
-
|
|
117
|
+
// S / M / L: the fitted size kept within 12–22 px, then scaled. Fit (#1869): never larger than what fits — only a 6 px
|
|
118
|
+
// floor for legibility (a grid too wide for the box even at 6 px, e.g. 200 columns on a phone, still scrolls).
|
|
119
|
+
const MIN_PX = 12, MAX_PX = 22, FIT_FLOOR_PX = 6;
|
|
118
120
|
|
|
119
121
|
export function ViewPanel({ route, navKey }) {
|
|
120
122
|
const lease = useLease(navKey);
|
|
@@ -201,7 +203,13 @@ function Terminal({ name, lease, density }) {
|
|
|
201
203
|
const term = useRef(null), pre = useRef(null);
|
|
202
204
|
const grid = useRef({ cols: 0, rows: 0, applied: 0, key: "" });
|
|
203
205
|
const [problem, setProblem] = useState(null);
|
|
206
|
+
// #1674: the text size as of this render. The pane refresh and the resize handlers below are set up once per mount
|
|
207
|
+
// and keep the fit() of that render; reading the size here, not from that render's props, keeps them on the current
|
|
208
|
+
// choice (they used to put the mount-time size back within one 800 ms refresh).
|
|
209
|
+
const dens = useRef(density);
|
|
210
|
+
dens.current = density;
|
|
204
211
|
const fit = () => {
|
|
212
|
+
const density = dens.current;
|
|
205
213
|
const g = grid.current, box = term.current, p = pre.current;
|
|
206
214
|
if (!box || !p || g.cols < 1 || g.rows < 1) return;
|
|
207
215
|
const cs = getComputedStyle(box);
|
|
@@ -213,10 +221,11 @@ function Terminal({ name, lease, density }) {
|
|
|
213
221
|
const lh = parseFloat(pcs.lineHeight) / parseFloat(pcs.fontSize);
|
|
214
222
|
const lineH = Number.isFinite(lh) && lh > 0 ? lh : 1.2;
|
|
215
223
|
let size = Math.min(innerW / (g.cols * glyphRatio(p)), innerH / (g.rows * lineH)) * 0.995;
|
|
216
|
-
size = Math.max(
|
|
217
|
-
size = Math.max(8, Math.min(size, 32));
|
|
224
|
+
if (density === "fit") size = Math.max(FIT_FLOOR_PX, Math.min(size, MAX_PX));
|
|
225
|
+
else size = Math.max(8, Math.min(Math.max(MIN_PX, Math.min(size, MAX_PX)) * (VIEW_SCALE[density] || 1), 32));
|
|
218
226
|
g.key = key;
|
|
219
|
-
|
|
227
|
+
// A small change is skipped only when growing (the size applied still fits); a smaller box always shrinks it.
|
|
228
|
+
if (size >= g.applied && size - g.applied < 0.75) return;
|
|
220
229
|
g.applied = size;
|
|
221
230
|
p.style.fontSize = `${size.toFixed(2)}px`; // CSSOM, never a style attribute (#1300)
|
|
222
231
|
};
|
|
@@ -308,6 +317,8 @@ function EditProfile({ it, onClose }) {
|
|
|
308
317
|
const [f, setF] = useState({ display: it.display_name || "", role: it.role || "", desc: it.description || "" });
|
|
309
318
|
const [busy, setBusy] = useState(false);
|
|
310
319
|
const [msg, setMsg] = useState(null);
|
|
320
|
+
// #1630 follow-up: the uploaded picture is removed on Save (Cancel keeps it); a newly picked file wins over removing.
|
|
321
|
+
const [removePic, setRemovePic] = useState(false);
|
|
311
322
|
const file = useRef(null);
|
|
312
323
|
async function save() {
|
|
313
324
|
if (busy) return;
|
|
@@ -317,6 +328,7 @@ function EditProfile({ it, onClose }) {
|
|
|
317
328
|
const name = encodeURIComponent(it.instance_name);
|
|
318
329
|
const draft = { display_name: f.display, role: f.role, description: f.desc };
|
|
319
330
|
const picked = file.current && file.current.files && file.current.files[0];
|
|
331
|
+
const removing = removePic;
|
|
320
332
|
try {
|
|
321
333
|
const r = await fetch(`/api/profile/${name}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(draft) });
|
|
322
334
|
if (!lease.current()) return;
|
|
@@ -325,6 +337,9 @@ function EditProfile({ it, onClose }) {
|
|
|
325
337
|
if (picked) {
|
|
326
338
|
const ar = await fetch(`/api/avatar/${name}`, { method: "POST", headers: { "Content-Type": picked.type }, body: picked });
|
|
327
339
|
if (!ar.ok) { const e = await ar.json().catch(() => ({})); throw new Error(tn("avatarFailed", { error: e.error || ar.status })); }
|
|
340
|
+
} else if (removing) { // a newly picked file replaces it instead
|
|
341
|
+
const dr = await fetch(`/api/avatar/${name}`, { method: "DELETE" });
|
|
342
|
+
if (!dr.ok) { const e = await dr.json().catch(() => ({})); throw new Error(tn("avatarRemoveFailed", { error: e.error || dr.status })); }
|
|
328
343
|
}
|
|
329
344
|
if (!lease.current()) return;
|
|
330
345
|
setBusy(false);
|
|
@@ -339,6 +354,8 @@ function EditProfile({ it, onClose }) {
|
|
|
339
354
|
<label class="field"><span>${tn("role")}</span><input value=${f.role} onInput=${(e) => setF({ ...f, role: e.target.value })} /></label>
|
|
340
355
|
<label class="field"><span>${tn("description")}</span><textarea rows="3" value=${f.desc} onInput=${(e) => setF({ ...f, desc: e.target.value })}></textarea></label>
|
|
341
356
|
<label class="field"><span>${tn("avatar")}</span><input ref=${file} type="file" accept="image/png,image/jpeg,image/gif,image/webp" /></label>
|
|
357
|
+
${it.has_avatar ? html`<div class="field-row"><button type="button" class="btn btn-sm" aria-pressed=${removePic ? "true" : "false"}
|
|
358
|
+
onClick=${() => setRemovePic(!removePic)}>${tn(removePic ? "keepPicture" : "removePicture")}</button>${removePic ? html`<span class="muted">${tn("removePictureNote")}</span>` : null}</div>` : null}
|
|
342
359
|
</fieldset></form>
|
|
343
360
|
${msg ? html`<p class="err" role="alert">${msg}${msg === tn("signInToSave") ? html` <a class="link" href=${signInHref()}>${t("app.signIn")}</a>` : null}</p>` : null}
|
|
344
361
|
</${Dialog}>`;
|
package/dist/ui/shared/signin.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
const $ = (id) => document.getElementById(id);
|
|
12
12
|
const T = {
|
|
13
13
|
en: {
|
|
14
|
-
checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter
|
|
14
|
+
checking: "Checking your session…", title: "Sign in to AgEnD", lead: "Enter your sign-in code.",
|
|
15
15
|
label: "One-time code", submit: "Sign in", working: "Signing in…",
|
|
16
|
-
hint: "Send /web
|
|
17
|
-
|
|
16
|
+
hint: "No code? Send /web in General, then press a sign-in button to get one.",
|
|
17
|
+
nobot: "No chat bot?", nobot_body: "Ask the operator to run agend web --code on the AgEnD computer.",
|
|
18
|
+
refused: "The code is invalid or no longer works. Send /web in General, then press a sign-in button for a new code.",
|
|
19
|
+
unconfirmed: "Sign-in could not be confirmed. Get a new code and try again; if it keeps failing, contact the operator.",
|
|
18
20
|
paused: "Too many wrong codes — sign-in is paused for a few minutes.",
|
|
19
21
|
network: "Could not reach AgEnD. Check the connection and try again.",
|
|
20
22
|
resend: "Send me a new code", resent: "A new code was sent to you privately in chat. Enter it above.",
|
|
@@ -22,10 +24,12 @@
|
|
|
22
24
|
resend_refused: "A new code could not be sent. Ask for one with /web.",
|
|
23
25
|
},
|
|
24
26
|
"zh-TW": {
|
|
25
|
-
checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "
|
|
27
|
+
checking: "正在確認登入狀態…", title: "登入 AgEnD", lead: "輸入登入碼。",
|
|
26
28
|
label: "一次性登入碼", submit: "登入", working: "登入中…",
|
|
27
|
-
hint: "
|
|
28
|
-
|
|
29
|
+
hint: "沒收到?在 General 傳 /web,再按登入按鈕取碼。",
|
|
30
|
+
nobot: "沒有聊天 bot?", nobot_body: "請管理員在 AgEnD 電腦執行 agend web --code。",
|
|
31
|
+
refused: "登入碼無效或已失效。請在 General 傳 /web,再按登入按鈕取新碼。",
|
|
32
|
+
unconfirmed: "登入暫時無法確認。請重新取碼再試;若仍失敗,請聯絡管理員。",
|
|
29
33
|
paused: "錯誤次數過多,登入暫停幾分鐘。",
|
|
30
34
|
network: "連不上 AgEnD,請檢查連線後再試。",
|
|
31
35
|
resend: "傳一組新的登入碼給我", resent: "新的登入碼已私訊給你,請在上方輸入。",
|
|
@@ -47,8 +51,8 @@
|
|
|
47
51
|
history.replaceState(null, "", here.pathname + here.search + here.hash);
|
|
48
52
|
}
|
|
49
53
|
} catch { /* nothing to clean */ }
|
|
50
|
-
// The command in the
|
|
51
|
-
// and a word, alpha.2 sweep); the text around it is text.
|
|
54
|
+
// The command in the "No chat bot?" help is a <code> that never breaks across lines (a break inside "--code" reads as
|
|
55
|
+
// two dashes and a word, alpha.2 sweep); the text around it is text.
|
|
52
56
|
const CMD = "agend web --code";
|
|
53
57
|
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
54
58
|
const text = t(el.getAttribute("data-i18n"));
|
|
@@ -163,7 +167,9 @@
|
|
|
163
167
|
body: JSON.stringify({ code: $("code").value }),
|
|
164
168
|
});
|
|
165
169
|
if (r.ok) { clearBounce(); location.replace(nextTarget()); return; }
|
|
166
|
-
|
|
170
|
+
// #1854: a 5xx is the server failing to confirm the sign-in (503: a public link's notice did not go out), not a
|
|
171
|
+
// wrong code — say so. The code is spent either way: the way on is a new one.
|
|
172
|
+
msg.textContent = r.status === 429 ? t("paused") : r.status >= 500 ? t("unconfirmed") : t("refused");
|
|
167
173
|
} catch {
|
|
168
174
|
msg.textContent = t("network");
|
|
169
175
|
}
|
package/dist/ui/shared/theme.js
CHANGED
|
@@ -1,29 +1,61 @@
|
|
|
1
1
|
// Light / dark for a panel (#1307). Loaded from /assets/theme.js in <head>, before the page paints, so a chosen theme
|
|
2
2
|
// never flashes the other one first. "system" (the default) follows the device's prefers-color-scheme through CSS;
|
|
3
|
-
// "light" or "dark"
|
|
3
|
+
// "light" or "dark" lives in localStorage, like agend_lang.
|
|
4
|
+
//
|
|
5
|
+
// #1665: the choice is kept on the host and follows the person between devices. The signed-in app's page carries the
|
|
6
|
+
// host's values in <html data-prefs>: the host's theme is what this page paints and holds, whatever this browser's
|
|
7
|
+
// storage does (the copy kept there is best effort — a full or blocked storage changes nothing on screen). Nothing
|
|
8
|
+
// chosen on the host: a device already in step with it (app-prefs.js's migration done) takes the default; one not yet
|
|
9
|
+
// in step keeps its own until it has uploaded it. Without data-prefs (the sign-in page, the anonymous View reader) this
|
|
10
|
+
// browser's copy decides, as before.
|
|
4
11
|
//
|
|
5
12
|
// A page's tokens follow the pattern: dark under :root, light under @media (prefers-color-scheme: light) for
|
|
6
13
|
// :root:not([data-theme="dark"]), and again under :root[data-theme="light"].
|
|
7
14
|
(function () {
|
|
8
15
|
"use strict";
|
|
9
16
|
var KEY = "agend_theme";
|
|
17
|
+
var SYNCED_FLAG = "agend_prefs_synced";
|
|
10
18
|
function valid(v) { return v === "light" || v === "dark" ? v : "system"; }
|
|
11
19
|
function read() {
|
|
12
20
|
try { return valid(localStorage.getItem(KEY)); } catch (e) { return "system"; } // storage blocked: follow the device
|
|
13
21
|
}
|
|
22
|
+
function cache(v) {
|
|
23
|
+
try { if (v === "system") localStorage.removeItem(KEY); else localStorage.setItem(KEY, v); } catch (e) { /* this page only */ }
|
|
24
|
+
}
|
|
14
25
|
function apply(v) {
|
|
15
26
|
var root = document.documentElement;
|
|
16
27
|
if (v === "system") root.removeAttribute("data-theme");
|
|
17
28
|
else root.setAttribute("data-theme", v);
|
|
18
29
|
}
|
|
19
|
-
|
|
30
|
+
/** The host's choice for this page: a theme, or null — none known here (no data-prefs, or not in step yet). */
|
|
31
|
+
function hostChoice() {
|
|
32
|
+
var d = null;
|
|
33
|
+
try { d = JSON.parse(document.documentElement.getAttribute("data-prefs") || "null"); } catch (e) { d = null; }
|
|
34
|
+
if (!d || typeof d !== "object" || !d.values || typeof d.values !== "object") return null;
|
|
35
|
+
var v = d.values.theme;
|
|
36
|
+
if (v === "light" || v === "dark" || v === "system") return v;
|
|
37
|
+
var inStep = false;
|
|
38
|
+
try { inStep = localStorage.getItem(SYNCED_FLAG) === "1"; } catch (e) { inStep = false; }
|
|
39
|
+
return inStep ? "system" : null;
|
|
40
|
+
}
|
|
41
|
+
var current = hostChoice();
|
|
42
|
+
if (current !== null) cache(current); else current = read();
|
|
43
|
+
apply(current);
|
|
44
|
+
var listeners = [];
|
|
20
45
|
window.AgendTheme = {
|
|
21
|
-
|
|
22
|
-
|
|
46
|
+
/** The theme this page shows. */
|
|
47
|
+
get: function () { return current; },
|
|
48
|
+
/** Choose "system", "light" or "dark" (this browser's copy; app-prefs.js keeps the host's); applied now even when it cannot be stored. */
|
|
23
49
|
set: function (v) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
apply(
|
|
50
|
+
current = valid(v);
|
|
51
|
+
cache(current);
|
|
52
|
+
apply(current);
|
|
53
|
+
for (var i = 0; i < listeners.length; i++) { try { listeners[i](current); } catch (e) { /* one listener's error stops no other */ } }
|
|
54
|
+
},
|
|
55
|
+
/** Call `fn` with the theme whenever it changes on this page (an open Preferences shows it); returns the unsubscribe. */
|
|
56
|
+
onChange: function (fn) {
|
|
57
|
+
listeners.push(fn);
|
|
58
|
+
return function () { var i = listeners.indexOf(fn); if (i >= 0) listeners.splice(i, 1); };
|
|
27
59
|
},
|
|
28
60
|
};
|
|
29
61
|
})();
|