@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3
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 +6 -4
- package/README.zh-TW.md +6 -4
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +82 -14
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +82 -33
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +8 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +131 -0
- package/dist/cache-service.js +452 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +376 -54
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +18 -0
- package/dist/daemon.js +256 -63
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +53 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +122 -7
- package/dist/fleet-manager.js +871 -152
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +96 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +7 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +110 -33
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +53 -0
- package/dist/restart-guard.js +114 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.js +6 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-activation.d.ts +62 -7
- package/dist/service-activation.js +237 -55
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +152 -12
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/settings-api.js +92 -10
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +37 -8
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +15 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +27 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +44 -0
- package/dist/tmux-control.js +170 -21
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +56 -85
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +34 -8
- package/dist/ui/chat-store.js +51 -2
- package/dist/ui/chat-strings.js +30 -2
- package/dist/ui/chat-thread.js +141 -47
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +117 -23
- package/dist/ui/panel-fleet.js +31 -17
- package/dist/ui/panel-settings.js +29 -15
- package/dist/ui/preview-panel.js +237 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +50 -8
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +25 -37
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +181 -5
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/ui/signin.html +2 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +78 -8
- package/dist/update-install.js +158 -30
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.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 +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +22 -0
- package/dist/web-api.js +115 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.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/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +118 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +12 -4
- package/scripts/preinstall-guard.cjs +40 -21
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +4 -4
package/dist/ui/shared/app.css
CHANGED
|
@@ -54,7 +54,11 @@ a { color: inherit; text-decoration: none; }
|
|
|
54
54
|
.link:hover { text-decoration: underline; }
|
|
55
55
|
|
|
56
56
|
/* ── The frame ── */
|
|
57
|
-
|
|
57
|
+
/* The page itself never scrolls: only its panes do. The shell is the containing block of anything absolutely positioned
|
|
58
|
+
inside it (an .sr-only label), so its overflow: hidden clips it — an absolute element whose containing block is the
|
|
59
|
+
viewport escapes every scroll container and made the whole page scroll (alpha.2: two scrollbars on /ui/chat). */
|
|
60
|
+
.shell { display: flex; height: 100vh; height: 100dvh; overflow: hidden; position: relative; }
|
|
61
|
+
html:has(.shell), html:has(.shell) body { overflow: hidden; }
|
|
58
62
|
.skip { position: absolute; left: var(--s-2); top: -60px; z-index: 100; background: var(--accent-fill); color: var(--on-accent); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); }
|
|
59
63
|
.skip:focus { top: var(--s-2); }
|
|
60
64
|
|
|
@@ -76,7 +80,7 @@ a { color: inherit; text-decoration: none; }
|
|
|
76
80
|
.side-row .meta { color: var(--text-3); font-size: var(--fs-xs); max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
77
81
|
.side-row.toggle { cursor: pointer; color: var(--text-2); }
|
|
78
82
|
.side-row.toggle input { margin: 0; accent-color: var(--accent); }
|
|
79
|
-
.side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); }
|
|
83
|
+
.side-section { flex: 1; min-height: 0; overflow-y: auto; padding: var(--s-2) var(--s-2); position: relative; } /* its .sr-only labels scroll (and clip) with it */
|
|
80
84
|
/* The sidebar's list scroller: thin and in the theme's colours, not the UA's light bar with arrows. WebKit/Blink take
|
|
81
85
|
the pseudo-elements; Chromium ignores them once scrollbar-color is set, so the standard properties are Firefox-only. */
|
|
82
86
|
.side-section::-webkit-scrollbar { width: 10px; }
|
|
@@ -127,7 +131,7 @@ a { color: inherit; text-decoration: none; }
|
|
|
127
131
|
.dot.warn { background: var(--warn); }
|
|
128
132
|
.dot.bad { background: var(--danger); }
|
|
129
133
|
.dot.off { background: var(--text-3); }
|
|
130
|
-
@keyframes pulse { 0%, 100% { opacity: 0.
|
|
134
|
+
@keyframes pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } } /* never near-invisible on a dark surface */
|
|
131
135
|
|
|
132
136
|
.scrim { display: none; }
|
|
133
137
|
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
@@ -159,6 +163,10 @@ a { color: inherit; text-decoration: none; }
|
|
|
159
163
|
.empty-title { color: var(--text); font-size: var(--fs-lg); font-weight: var(--fw-semibold); }
|
|
160
164
|
.empty-hint { font-size: var(--fs-sm); }
|
|
161
165
|
.empty .btn { margin-top: var(--s-3); }
|
|
166
|
+
/* #1519 P7: the first-run card (a fleet with no chat connection), above an empty chat or a Settings list. */
|
|
167
|
+
.first-run-stack { width: 100%; max-width: var(--col); }
|
|
168
|
+
.first-run h3 { display: flex; align-items: center; gap: var(--s-2); }
|
|
169
|
+
.first-run p { color: var(--text-2); font-size: var(--fs-sm); }
|
|
162
170
|
.error-state { display: flex; flex-direction: column; align-items: center; gap: var(--s-3); padding: var(--s-8) var(--s-4); color: var(--danger); text-align: center; }
|
|
163
171
|
.error-state p { color: var(--text); }
|
|
164
172
|
.skeleton { display: flex; flex-direction: column; gap: var(--s-3); padding: var(--s-6) var(--s-5); max-width: var(--col); margin: 0 auto; width: 100%; opacity: 0; animation: appear 0s linear 300ms forwards; }
|
|
@@ -295,6 +303,55 @@ a { color: inherit; text-decoration: none; }
|
|
|
295
303
|
.pv-holder:empty { display: none; }
|
|
296
304
|
.pv-holder { margin-top: var(--s-2); }
|
|
297
305
|
.preview-frame { display: block; width: 100%; border: 2px solid var(--warn-line); border-radius: var(--r-sm); background: #ffffff; }
|
|
306
|
+
/* #1481: the preview beside the conversation. A split on a wide screen (width: --pv-w, set by the divider through
|
|
307
|
+
CSSOM), a full-screen sheet on a narrow one. The frame is the card's (preview.js), sized by the panel here. */
|
|
308
|
+
.chat-split { flex: 1; min-height: 0; display: flex; }
|
|
309
|
+
.chat-main { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
|
310
|
+
.pv-divider { flex: 0 0 8px; cursor: col-resize; background: var(--bg); border-left: 1px solid var(--border); touch-action: none; position: relative; }
|
|
311
|
+
.pv-divider::after { content: ""; position: absolute; top: 50%; left: 2px; width: 3px; height: 32px; margin-top: -16px; border-radius: 2px; background: var(--border-strong); }
|
|
312
|
+
.pv-divider:hover::after, .pv-divider:focus-visible::after, .chat-split.resizing .pv-divider::after { background: var(--accent); }
|
|
313
|
+
.pv-divider:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
314
|
+
.chat-split.resizing { cursor: col-resize; user-select: none; }
|
|
315
|
+
.chat-split.resizing .preview-frame { pointer-events: none; } /* a frame under the pointer would end the drag */
|
|
316
|
+
.pv-panel { flex: 0 0 var(--pv-w, 45%); width: var(--pv-w, 45%); min-width: 320px; max-width: calc(100% - 368px); min-height: 0;
|
|
317
|
+
display: flex; flex-direction: column; background: var(--bg); container-type: inline-size; }
|
|
318
|
+
.pv-panel-head { display: flex; align-items: center; gap: var(--s-2); padding: var(--s-2) var(--s-3); border-bottom: 1px solid var(--border); }
|
|
319
|
+
.pv-panel-title { flex: 1; min-width: 0; display: flex; flex-direction: column; font-size: var(--fs-sm); }
|
|
320
|
+
.pv-source { all: unset; cursor: pointer; font-size: var(--fs-xs); color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
321
|
+
.pv-source:hover { color: var(--text); text-decoration: underline; }
|
|
322
|
+
.pv-source:focus-visible { outline: 2px solid var(--accent); border-radius: var(--r-sm); }
|
|
323
|
+
.pv-panel-bar { display: flex; align-items: center; gap: var(--s-2); padding: var(--s-2) var(--s-3); flex-wrap: wrap; }
|
|
324
|
+
.pv-panel-bar .grow { flex: 1; }
|
|
325
|
+
.pv-panel > .pv-banner, .pv-panel > .pv-note, .pv-newer { margin: 0 var(--s-3) var(--s-2); }
|
|
326
|
+
.pv-panel > .pv-note { margin-top: 0; }
|
|
327
|
+
.pv-newer { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; padding: var(--s-2) var(--s-3); border-radius: var(--r-sm);
|
|
328
|
+
background: var(--surface); font-size: var(--fs-xs); }
|
|
329
|
+
.pv-newer > span { flex: 1; min-width: 0; }
|
|
330
|
+
.pv-newer > .pv-newer-note { flex-basis: 100%; color: var(--text-3); }
|
|
331
|
+
.pv-panel-body { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 0 var(--s-3) var(--s-3); }
|
|
332
|
+
.pv-panel-body .pv-holder { flex: 1; min-height: 0; margin: 0; display: flex; flex-direction: column; }
|
|
333
|
+
.pv-panel-body.show-code .pv-holder { display: none; }
|
|
334
|
+
.preview-frame.fill { flex: 1; height: 100%; min-height: 0; }
|
|
335
|
+
.pv-code { flex: 1; min-height: 0; margin: 0; overflow: auto; font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.5;
|
|
336
|
+
background: var(--surface); border-radius: var(--r-sm); padding: var(--s-3); white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text); }
|
|
337
|
+
.msg.flash { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: var(--r-sm); transition: outline-color var(--t-med); }
|
|
338
|
+
@media (max-width: 899px) {
|
|
339
|
+
.only-wide { display: none; }
|
|
340
|
+
.pv-divider { display: none; }
|
|
341
|
+
.pv-panel { position: fixed; inset: 0; z-index: 37; width: auto; min-width: 0; max-width: none;
|
|
342
|
+
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); }
|
|
343
|
+
}
|
|
344
|
+
/* A narrow panel (a phone, or the divider dragged right): the run controls keep their icons, titles and screen-reader
|
|
345
|
+
names — except Preview, which the panel's note names. */
|
|
346
|
+
@container (max-width: 560px) {
|
|
347
|
+
.pv-panel-bar :not(.pv-panel-run) > .lbl { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
348
|
+
}
|
|
349
|
+
/* #1266: an agent reply's buttons — choices under the text; one answers for everyone. */
|
|
350
|
+
.rb-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-top: var(--s-2); }
|
|
351
|
+
.rb-btn { max-width: 100%; white-space: normal; text-align: left; }
|
|
352
|
+
.rb-btn.chosen, .rb-btn.chosen:disabled { border-color: var(--ok); color: var(--text); background: color-mix(in srgb, var(--ok) 14%, transparent); font-weight: var(--fw-semibold); opacity: 1; }
|
|
353
|
+
.rb-btn:disabled:not(.chosen) { opacity: 0.55; }
|
|
354
|
+
.rb-note { font-size: var(--fs-xs); color: var(--text-3); }
|
|
298
355
|
.pv-menu { position: relative; }
|
|
299
356
|
.pv-menu > summary { list-style: none; cursor: pointer; padding: 2px var(--s-2); border-radius: var(--r-sm); color: var(--text-3); }
|
|
300
357
|
.pv-menu > summary::-webkit-details-marker { display: none; }
|
|
@@ -334,6 +391,8 @@ a { color: inherit; text-decoration: none; }
|
|
|
334
391
|
.composer textarea::placeholder { color: var(--text-3); }
|
|
335
392
|
.btn-send { width: 36px; padding: 0; }
|
|
336
393
|
.btn-send .send-label { display: none; }
|
|
394
|
+
/* A narrow phone: Stop reply beside Send leaves the message box no room — the label is for screen readers there. */
|
|
395
|
+
@media (max-width: 479px) { .btn-stop .stop-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } }
|
|
337
396
|
.btn-stop { background: var(--bg); }
|
|
338
397
|
.hint { font-size: var(--fs-xs); color: var(--text-3); text-align: center; }
|
|
339
398
|
.pending-files { display: flex; flex-wrap: wrap; gap: var(--s-2); padding: var(--s-2) var(--s-2) 0; }
|
|
@@ -401,7 +460,8 @@ a { color: inherit; text-decoration: none; }
|
|
|
401
460
|
.dlg-box { max-height: 90vh; }
|
|
402
461
|
.dlg-actions { padding-bottom: max(var(--s-5), env(safe-area-inset-bottom)); }
|
|
403
462
|
.dlg-actions .btn { flex: 1; min-height: var(--tap); }
|
|
404
|
-
.row-item .btn { min-height: var(--tap); }
|
|
463
|
+
.row-item .btn, .s-row .btn, .n-actions .btn { min-height: var(--tap); }
|
|
464
|
+
.s-row .icon-btn { min-width: var(--tap); min-height: var(--tap); }
|
|
405
465
|
.kv { flex-direction: column; align-items: flex-start; gap: 2px; }
|
|
406
466
|
}
|
|
407
467
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -439,6 +499,19 @@ a { color: inherit; text-decoration: none; }
|
|
|
439
499
|
.v-filter-box input::placeholder { color: var(--text-3); }
|
|
440
500
|
.v-filter-clear { width: 28px; height: 28px; }
|
|
441
501
|
.v-filter .note { padding: var(--s-1) var(--s-1) 0; font-variant-numeric: tabular-nums; }
|
|
502
|
+
/* alpha.2 (N1): status and CLI chips under the filter box — each a <details> with checkboxes. */
|
|
503
|
+
.nav-facets { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--s-1) var(--s-2); padding: var(--s-2) var(--s-1) 0; }
|
|
504
|
+
.nav-facet { font-size: var(--fs-xs); color: var(--text-2); }
|
|
505
|
+
.nav-facet > summary { cursor: pointer; list-style: none; padding: 2px var(--s-2); border-radius: var(--r-sm); }
|
|
506
|
+
.nav-facet > summary::-webkit-details-marker { display: none; }
|
|
507
|
+
.nav-facet > summary::after { content: " ▾"; color: var(--text-3); }
|
|
508
|
+
.nav-facet[open] > summary::after { content: " ▴"; }
|
|
509
|
+
.nav-facet > summary:hover { background: var(--hover); }
|
|
510
|
+
.nav-facet > summary:focus-visible { outline: 2px solid var(--accent); }
|
|
511
|
+
.nav-facet-n { display: inline-block; min-width: 16px; padding: 0 4px; border-radius: 8px; background: var(--accent); color: var(--on-accent, #fff); text-align: center; font-variant-numeric: tabular-nums; }
|
|
512
|
+
.nav-facet-list { display: flex; flex-direction: column; gap: 2px; padding: var(--s-1) var(--s-2); }
|
|
513
|
+
.nav-chip { display: flex; align-items: center; gap: var(--s-2); cursor: pointer; color: var(--text); }
|
|
514
|
+
.nav-facet-reset { font-size: var(--fs-xs); }
|
|
442
515
|
|
|
443
516
|
.p-view .panel-actions .btn { gap: var(--s-1); }
|
|
444
517
|
.v-term { flex: 1; min-height: 0; overflow: auto; scrollbar-gutter: stable; padding: 5px 6px; background: #000000; position: relative; }
|
|
@@ -511,6 +584,11 @@ a { color: inherit; text-decoration: none; }
|
|
|
511
584
|
.s-row.s-conn { grid-template-columns: minmax(0, 1fr) auto; }
|
|
512
585
|
.s-conn .s-facts { grid-column: 1 / -1; grid-row: 2; }
|
|
513
586
|
}
|
|
587
|
+
/* A narrow phone: the cluster (token, state, Settings) goes under the facts, so the name keeps its line (alpha.2 sweep). */
|
|
588
|
+
@media (max-width: 479px) {
|
|
589
|
+
.s-row.s-conn { grid-template-columns: minmax(0, 1fr); }
|
|
590
|
+
.s-conn .s-actions { grid-column: 1; grid-row: 3; flex-wrap: wrap; }
|
|
591
|
+
}
|
|
514
592
|
.s-state { font-size: var(--fs-xs); }
|
|
515
593
|
.s-state.ok { color: var(--ok); }
|
|
516
594
|
.s-state.bad { color: var(--danger); }
|
|
@@ -537,6 +615,10 @@ a { color: inherit; text-decoration: none; }
|
|
|
537
615
|
.chip-x:hover { color: var(--text); background: var(--surface); }
|
|
538
616
|
.dlg-impact { margin-right: auto; align-self: center; }
|
|
539
617
|
.dlg-inline-actions { display: flex; align-items: center; gap: var(--s-2); flex-wrap: wrap; }
|
|
618
|
+
/* #1519 P1: the bot-token field — the input and its show/hide button on one row. */
|
|
619
|
+
.token-row { display: flex; align-items: center; gap: var(--s-1); }
|
|
620
|
+
.token-row input { flex: 1; min-width: 0; }
|
|
621
|
+
.feedback.ok { color: var(--ok, var(--text)); }
|
|
540
622
|
.seg-inline { display: inline-flex; gap: var(--s-1); }
|
|
541
623
|
.btn.on { background: var(--surface-2); color: var(--text); font-weight: var(--fw-semibold); }
|
|
542
624
|
.card h4 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); color: var(--text-2); }
|
|
@@ -549,6 +631,30 @@ a { color: inherit; text-decoration: none; }
|
|
|
549
631
|
.s-yaml { font-family: var(--font-mono); font-size: var(--fs-xs); line-height: 1.5; background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-sm); padding: var(--s-3);
|
|
550
632
|
overflow: auto; max-height: 60vh; white-space: pre; margin: 0; color: var(--text); }
|
|
551
633
|
textarea.s-yaml { width: 100%; min-height: 50vh; resize: vertical; }
|
|
634
|
+
/* Lines too long for the box scroll sideways; a shadow at an edge says there is more that way (alpha.2 sweep). */
|
|
635
|
+
.s-yaml {
|
|
636
|
+
background:
|
|
637
|
+
linear-gradient(to right, var(--bg) 30%, transparent) left / 24px 100% no-repeat local,
|
|
638
|
+
linear-gradient(to left, var(--bg) 30%, transparent) right / 24px 100% no-repeat local,
|
|
639
|
+
radial-gradient(farthest-side at 0 50%, color-mix(in srgb, var(--text) 22%, transparent), transparent) left / 10px 100% no-repeat scroll,
|
|
640
|
+
radial-gradient(farthest-side at 100% 50%, color-mix(in srgb, var(--text) 22%, transparent), transparent) right / 10px 100% no-repeat scroll,
|
|
641
|
+
var(--bg);
|
|
642
|
+
}
|
|
643
|
+
textarea.s-yaml { white-space: pre; overflow-x: auto; }
|
|
644
|
+
.s-br { display: none; }
|
|
645
|
+
@media (max-width: 479px) {
|
|
646
|
+
/* Settings → Developer: one toolbar row, icons only (names stay as titles and for screen readers). */
|
|
647
|
+
.s-dev-bar { flex-wrap: nowrap; }
|
|
648
|
+
.s-dev-bar .lbl { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
649
|
+
/* An agent's row: name and model on the first line, its tags and buttons from the second, on every row alike. */
|
|
650
|
+
.s-br { display: block; flex-basis: 100%; height: 0; }
|
|
651
|
+
/* View: the profile line wraps to two lines instead of hiding the model and context. */
|
|
652
|
+
.v-card-meta { white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
|
653
|
+
/* A text button in Needs you lines up with the text above it, first in its row or wrapped onto a line of its own;
|
|
654
|
+
a row's trailing Delete sits at the right. */
|
|
655
|
+
.n-actions > .btn-ghost { padding-left: 0; padding-right: 0; margin-left: -1px; }
|
|
656
|
+
.row-item > .btn-ghost:last-child { margin-left: auto; margin-right: calc(-1 * var(--s-3) - 1px); }
|
|
657
|
+
}
|
|
552
658
|
.s-pending { position: sticky; bottom: 0; display: flex; align-items: center; gap: var(--s-3); flex-wrap: wrap; padding: var(--s-3) var(--s-5);
|
|
553
659
|
background: var(--bg); border-top: 1px solid var(--border); box-shadow: var(--shadow-2); }
|
|
554
660
|
.s-pending .grow { flex: 1; }
|
|
@@ -575,6 +681,8 @@ textarea.s-yaml { width: 100%; min-height: 50vh; resize: vertical; }
|
|
|
575
681
|
.se-default { opacity: 0.7; }
|
|
576
682
|
/* #1423: requests waiting for a fleet admin, in any panel */
|
|
577
683
|
.pending-stack { position: fixed; right: var(--s-4); top: calc(var(--header-h) + var(--s-2)); z-index: 40; display: flex; flex-direction: column; gap: var(--s-2); width: min(360px, calc(100vw - 32px)); }
|
|
684
|
+
/* In Settings the cards sit in the column, right under the operation — every width (alpha.2 sweep). */
|
|
685
|
+
.pending-stack.inline, .shell:has(.s-pending) .pending-stack.inline { position: static; width: auto; margin-bottom: var(--s-4); }
|
|
578
686
|
.pending-card { background: var(--bg); border: 1px solid var(--border-strong); border-radius: var(--r-md); box-shadow: var(--shadow-2); padding: var(--s-3) var(--s-4); display: flex; flex-direction: column; gap: var(--s-1); font-size: var(--fs-sm); }
|
|
579
687
|
.pending-card.applied { border-color: var(--ok); }
|
|
580
688
|
.pending-card.rejected, .pending-card.expired, .pending-card.stale, .pending-card.failed { border-color: var(--danger-line); }
|
|
@@ -586,6 +694,8 @@ textarea.s-yaml { width: 100%; min-height: 50vh; resize: vertical; }
|
|
|
586
694
|
.pending-actions { display: flex; justify-content: flex-end; }
|
|
587
695
|
/* Settings' pending bar sits at the bottom of the page: toasts and request cards stand above it, never on its buttons. */
|
|
588
696
|
.shell:has(.s-pending) .toasts { bottom: calc(var(--s-6) + 64px); }
|
|
697
|
+
/* In a chat the composer owns the bottom (Stop reply, Send): toasts come in under the header instead (alpha.2 sweep). */
|
|
698
|
+
@media (min-width: 900px) { .shell:has(.p-chat) .toasts { top: calc(var(--header-h) + var(--s-2)); bottom: auto; } }
|
|
589
699
|
.op-line { display: flex; align-items: center; justify-content: center; gap: var(--s-2); }
|
|
590
700
|
.op-line.ok { color: var(--ok); }
|
|
591
701
|
.op-line.bad { color: var(--danger); }
|
|
@@ -672,5 +782,71 @@ strong.org-name { font-size: var(--fs-sm); color: var(--text); }
|
|
|
672
782
|
}
|
|
673
783
|
/* A phone: Fleet's five tabs fit when only the open one shows its label (the others keep theirs for screen readers). */
|
|
674
784
|
@media (max-width: 479px) {
|
|
675
|
-
.p-fleet .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
785
|
+
.p-fleet .seg-item:not(.active) > span, .p-settings .seg-item:not(.active) > span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* ── Cache analysis (#1468): Fleet → Cache. Bars are SVG attributes; nothing here is an inline style. ── */
|
|
789
|
+
.ch { display: flex; flex-direction: column; gap: var(--s-4); }
|
|
790
|
+
.ch-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--s-2) var(--s-4); margin-top: var(--s-2); }
|
|
791
|
+
.ch-scan { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
|
|
792
|
+
.ch-fleet { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s-3); }
|
|
793
|
+
.ch-notes { display: flex; flex-direction: column; gap: var(--s-1); font-size: var(--fs-xs); }
|
|
794
|
+
.ch-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr)); gap: var(--s-4); align-items: start; }
|
|
795
|
+
.ch-card { display: flex; flex-direction: column; gap: var(--s-3); min-width: 0; padding: var(--s-4); border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--bg); }
|
|
796
|
+
.ch-card.on { border-color: var(--ok); }
|
|
797
|
+
.ch-head { display: flex; align-items: flex-start; gap: var(--s-3); }
|
|
798
|
+
.ch-id { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
799
|
+
.ch-id h3 { font-size: var(--fs-sm); font-weight: var(--fw-semibold); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
800
|
+
.ch-models { font-size: var(--fs-xs); color: var(--text-3); overflow-wrap: anywhere; }
|
|
801
|
+
.ch-rec { flex-shrink: 0; font-size: 11px; line-height: 20px; padding: 0 var(--s-2); border-radius: var(--r-full); border: 1px solid var(--border); color: var(--text-2); white-space: nowrap; }
|
|
802
|
+
.ch-rec.on { color: var(--ok); border-color: var(--ok); font-weight: var(--fw-semibold); }
|
|
803
|
+
.ch-reason { font-size: var(--fs-sm); color: var(--text-2); overflow-wrap: anywhere; }
|
|
804
|
+
.ch-tag { display: inline-block; font-size: 11px; line-height: 18px; padding: 0 var(--s-2); border-radius: var(--r-full); background: var(--surface-2); color: var(--text-2); }
|
|
805
|
+
.ch-metrics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--s-2) var(--s-3); }
|
|
806
|
+
.ch-metric { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
|
807
|
+
.ch-metric dt { font-size: 11px; color: var(--text-3); line-height: var(--lh-xs); }
|
|
808
|
+
.ch-metric dd { margin: 0; font-size: var(--fs-md); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; display: flex; flex-direction: column; }
|
|
809
|
+
.ch-sub { font-size: 11px; font-weight: var(--fw-regular); color: var(--text-3); }
|
|
810
|
+
.ch-fleet .ch-metric dd { font-size: var(--fs-lg); }
|
|
811
|
+
.ch-hist { display: block; width: 100%; height: 96px; color: var(--text-2); }
|
|
812
|
+
.ch-col rect { fill: var(--accent); opacity: 0.55; }
|
|
813
|
+
.ch-col.past rect { fill: var(--warn); opacity: 0.85; }
|
|
814
|
+
.ch-n, .ch-l { font-size: 9px; fill: currentColor; }
|
|
815
|
+
.ch-ttl line { stroke: var(--danger); stroke-width: 1.5; stroke-dasharray: 3 3; }
|
|
816
|
+
.ch-ttl text { font-size: 9px; fill: var(--danger); }
|
|
817
|
+
.ch-rest .row-item { font-size: var(--fs-xs); }
|
|
818
|
+
@media (max-width: 899px) {
|
|
819
|
+
.ch-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
820
|
+
.ch-fleet { grid-template-columns: minmax(0, 1fr); }
|
|
821
|
+
.ch-bar .btn { min-height: var(--tap); }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* ── Chat commands and quick actions (#1269) ── */
|
|
825
|
+
.hd-chip { margin-left: var(--s-2); padding: 0 var(--s-2); border: 1px solid var(--border); border-radius: var(--r-full); background: transparent; color: var(--text-2);
|
|
826
|
+
font-size: var(--fs-xs); line-height: 20px; cursor: pointer; max-width: 16em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
|
|
827
|
+
.hd-chip:hover { background: var(--surface); color: var(--text); }
|
|
828
|
+
.composer-box { position: relative; }
|
|
829
|
+
.cmd-palette { position: absolute; left: 0; right: 0; bottom: calc(100% + var(--s-1)); z-index: 20; list-style: none; max-height: 280px; overflow-y: auto;
|
|
830
|
+
padding: var(--s-1); border: 1px solid var(--border); border-radius: var(--r-md); background: var(--bg); box-shadow: var(--shadow-2); }
|
|
831
|
+
.cmd-palette li { display: flex; align-items: baseline; gap: var(--s-3); padding: var(--s-2) var(--s-3); border-radius: var(--r-sm); cursor: pointer; font-size: var(--fs-sm); }
|
|
832
|
+
.cmd-palette li.on, .cmd-palette li:hover { background: var(--surface-2); }
|
|
833
|
+
.cmd-palette .cmd-name { flex-shrink: 0; white-space: nowrap; }
|
|
834
|
+
.cmd-palette .cmd-desc { color: var(--text-3); font-size: var(--fs-xs); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
835
|
+
.cmd-palette .cmd-arg { color: var(--text-3); }
|
|
836
|
+
.cmd-palette .cmd-none { color: var(--text-3); cursor: default; }
|
|
837
|
+
.cmd-card { display: flex; flex-direction: column; gap: var(--s-2); margin-bottom: var(--s-2); padding: var(--s-2) var(--s-3); border-radius: var(--r-md); background: var(--surface); font-size: var(--fs-sm); }
|
|
838
|
+
.cmd-card.bad { border-left: 3px solid var(--danger); }
|
|
839
|
+
.cmd-head { display: flex; align-items: center; justify-content: space-between; gap: var(--s-2); }
|
|
840
|
+
.cmd-name { font-weight: var(--fw-semibold); }
|
|
841
|
+
.cmd-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; font-family: var(--font-ui); color: var(--text-2); }
|
|
842
|
+
.cmd-choices { display: flex; flex-wrap: wrap; gap: var(--s-1); max-height: 180px; overflow-y: auto; }
|
|
843
|
+
.cmd-choices .btn.current { border-color: var(--accent); color: var(--accent); }
|
|
844
|
+
.quick-actions { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s-2); margin-bottom: var(--s-2); font-size: var(--fs-xs); color: var(--text-2); }
|
|
845
|
+
/* A phone: the instance's name keeps the header; /model and /effort are a "/" away. */
|
|
846
|
+
@media (max-width: 479px) { .hd-chip { display: none; } }
|
|
847
|
+
@media (max-width: 899px) {
|
|
848
|
+
.hd-chip { max-width: 9em; }
|
|
849
|
+
.cmd-palette li { min-height: var(--tap); align-items: center; flex-wrap: wrap; row-gap: 0; }
|
|
850
|
+
.cmd-palette .cmd-desc { white-space: normal; flex-basis: 100%; }
|
|
851
|
+
.cmd-choices .btn, .quick-actions .btn { min-height: var(--tap); }
|
|
676
852
|
}
|
package/dist/ui/shared/app.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// reached only by the dynamic imports below, in "full" mode. A test walks the static imports over real HTTP and asserts
|
|
7
7
|
// none of them names /ui/.
|
|
8
8
|
import { html, render } from "./app-html.js";
|
|
9
|
-
import { Shell, handleKey, showDialog, closeDrawer, retryable } from "./app-shell.js";
|
|
9
|
+
import { Shell, handleKey, showDialog, closeDrawer, retryable, setNewInstanceOpener } from "./app-shell.js";
|
|
10
10
|
import { startRouter, navStore, navigate } from "./app-nav.js";
|
|
11
11
|
import { createStream } from "./app-stream.js";
|
|
12
12
|
import { appStore, applyStatus, applyActivity } from "./app-store.js";
|
|
@@ -72,6 +72,7 @@ if (mode === "full") {
|
|
|
72
72
|
try { m = await loadFleet(); } catch { toast(t("app.loadFailed"), false); return; }
|
|
73
73
|
showDialog(m.CreateInstanceDialog);
|
|
74
74
|
};
|
|
75
|
+
setNewInstanceOpener(onNewInstance);
|
|
75
76
|
} else appStore.set({ connection: "none" });
|
|
76
77
|
|
|
77
78
|
startRouter(window);
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// alpha.2 (docs/design/web-unified-instance-nav.md, N1): the one instance list of the sidebar, on every page — Chat,
|
|
2
|
+
// View, Fleet, Settings, Needs — and for View's anonymous reader. It was two lists: the shell's (live dots, server
|
|
3
|
+
// order, no filter, every row to Chat) and View's roster (tag groups, drag order, a filter, rows to View). Now one:
|
|
4
|
+
// - live state (the status frames the app already receives; no new poll): dots, "needs you", context %, backend;
|
|
5
|
+
// - View's tag groups, folding and drag order, kept by this browser;
|
|
6
|
+
// - the filter — text plus status and CLI chips — kept by this browser and the same on every page;
|
|
7
|
+
// - a row opens the view you are in (View → View, everywhere else → Chat);
|
|
8
|
+
// - the list is never remounted on navigation, and only its own scrollTop follows the active row (#1515).
|
|
9
|
+
// Public (/assets/): an anonymous View reader gets the same list, fed by View's roster read instead of status frames.
|
|
10
|
+
import { html, useLayoutEffect, useRef } from "./app-html.js";
|
|
11
|
+
import { t } from "./app-i18n.js";
|
|
12
|
+
import { appStore, createStore, useStore } from "./app-store.js";
|
|
13
|
+
import { chatPath, viewPath } from "./app-route.js";
|
|
14
|
+
import { Icon } from "./ui-icons.js";
|
|
15
|
+
import { Skeleton } from "./ui-states.js";
|
|
16
|
+
import { viewStore } from "./view-roster-store.js";
|
|
17
|
+
import "./view-strings.js";
|
|
18
|
+
|
|
19
|
+
/** view.* text with named values. */
|
|
20
|
+
function tn(key, values = {}) {
|
|
21
|
+
let s = t(`view.${key}`);
|
|
22
|
+
for (const [k, v] of Object.entries(values)) s = s.split(`{${k}}`).join(String(v));
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
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
|
+
/** A row's instance name: `name` (status frames) or `instance_name` (View's roster). */
|
|
28
|
+
const nameOf = (it) => (it && (it.name != null ? it.name : it.instance_name)) || "";
|
|
29
|
+
|
|
30
|
+
// ── Groups, labels, the filter ──
|
|
31
|
+
export function groupOf(it) {
|
|
32
|
+
const tags = Array.isArray(it.tags) ? it.tags : [];
|
|
33
|
+
if (tags.includes("classic")) return "Classic";
|
|
34
|
+
if (tags.length) return tags[0];
|
|
35
|
+
return "Other";
|
|
36
|
+
}
|
|
37
|
+
const BACKEND_LABELS = { "claude-code": "Claude Code", "kiro-cli": "Kiro CLI", codex: "Codex", grok: "Grok Build", antigravity: "Antigravity", opencode: "OpenCode" };
|
|
38
|
+
export function backendLabel(backend) { return BACKEND_LABELS[backend] || backend || "Unknown"; }
|
|
39
|
+
export const KNOWN_BACKENDS = new Set(Object.keys(BACKEND_LABELS));
|
|
40
|
+
export const STATUS_KEYS = { running: "statusRunning", paused: "statusPaused", stopped: "statusStopped", crashed: "statusCrashed" };
|
|
41
|
+
|
|
42
|
+
/** The status chips, in order (design Q5): what the row's dot says, in five words. */
|
|
43
|
+
export const STATUS_FACETS = ["working", "needs", "idle", "stopped", "crashed"];
|
|
44
|
+
/** Which status chip a row falls under: needs you first, then crashed, not running, working (or stuck), idle. */
|
|
45
|
+
export function statusFacet(it, exec, awaiting) {
|
|
46
|
+
if (awaiting != null) return "needs";
|
|
47
|
+
if (!it || it.status === "crashed") return "crashed";
|
|
48
|
+
if (it.status !== "running") return "stopped";
|
|
49
|
+
return exec === "working" || exec === "stuck" ? "working" : "idle";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function normalizeFilter(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
|
|
53
|
+
/** Case-insensitive substring on name (incl. display name), model and backend. */
|
|
54
|
+
export function instanceMatchesFilter(it, needle, labelOf) {
|
|
55
|
+
if (!needle) return true;
|
|
56
|
+
const fields = [nameOf(it), it.display_name, it.model, it.backend, labelOf ? labelOf(it.backend) : null];
|
|
57
|
+
return fields.some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Visible groups in display order, and the "N / M" counts. `facets` (optional): { status: [...], cli: [...],
|
|
61
|
+
* statusOf(name) } — an empty list does not filter; a non-empty one keeps the rows whose status / backend is in it.
|
|
62
|
+
*/
|
|
63
|
+
export function filterSidebar(groupNames, instByGroup, rosterByName, query, labelOf, facets) {
|
|
64
|
+
const needle = normalizeFilter(query);
|
|
65
|
+
const status = new Set(facets && Array.isArray(facets.status) ? facets.status : []);
|
|
66
|
+
const cli = new Set(facets && Array.isArray(facets.cli) ? facets.cli : []);
|
|
67
|
+
const keep = (name) => {
|
|
68
|
+
const it = rosterByName.get(name);
|
|
69
|
+
if (!instanceMatchesFilter(it, needle, labelOf)) return false;
|
|
70
|
+
if (status.size && !status.has(facets.statusOf ? facets.statusOf(name) : statusFacet(it))) return false;
|
|
71
|
+
if (cli.size && !cli.has(it.backend || "")) return false;
|
|
72
|
+
return true;
|
|
73
|
+
};
|
|
74
|
+
const groups = [];
|
|
75
|
+
let shown = 0, total = 0;
|
|
76
|
+
for (const g of groupNames) {
|
|
77
|
+
const names = (instByGroup.get(g) || []).filter((name) => rosterByName.has(name));
|
|
78
|
+
total += names.length;
|
|
79
|
+
const visible = names.filter(keep);
|
|
80
|
+
shown += visible.length;
|
|
81
|
+
if (visible.length) groups.push({ group: g, names: visible });
|
|
82
|
+
}
|
|
83
|
+
return { groups, shown, total, active: needle.length > 0 || status.size > 0 || cli.size > 0 };
|
|
84
|
+
}
|
|
85
|
+
export function instanceTooltip(it) {
|
|
86
|
+
const context = it.context_pct != null && Number.isFinite(it.context_pct) ? `${Math.round(Math.min(100, Math.max(0, it.context_pct)))}%` : tn("tooltipUnavailable");
|
|
87
|
+
const status = STATUS_KEYS[it.status] ? tn(STATUS_KEYS[it.status]) : it.status || tn("statusUnknown");
|
|
88
|
+
const line = (key, value) => tn(key, { value });
|
|
89
|
+
const lines = [];
|
|
90
|
+
const name = nameOf(it);
|
|
91
|
+
if (it.display_name && it.display_name !== name) lines.push(it.display_name, `(${name})`);
|
|
92
|
+
else lines.push(name);
|
|
93
|
+
lines.push(line("tooltipBackend", backendLabel(it.backend)));
|
|
94
|
+
const src = (s) => (s === "instance" || s === "classic" ? " (configured)" : s === "fleet-default" ? " (fleet default)" : "");
|
|
95
|
+
lines.push(line("tooltipModel", (it.model || tn("tooltipUnavailable")) + src(it.model_source)));
|
|
96
|
+
lines.push(line("tooltipStatus", status));
|
|
97
|
+
lines.push(line("tooltipContext", context));
|
|
98
|
+
if (it.effort) lines.push(line("tooltipEffort", it.effort + src(it.effort_source)));
|
|
99
|
+
if (typeof it.cost === "number" && it.cost > 0) lines.push(line("tooltipCost", `$${it.cost.toFixed(2)}`));
|
|
100
|
+
if (typeof it.description === "string" && it.description.trim()) lines.push("", it.description.trim());
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── This browser's order (drag), folded groups and filter ──
|
|
105
|
+
const SIDEBAR_ORDER_KEY = "agend_view_sidebar_order"; // View's key since #999: everyone's arrangement carries over
|
|
106
|
+
const COLLAPSED_KEY = "agend_instance_collapsed";
|
|
107
|
+
const FILTER_KEY = "agend_instance_filter";
|
|
108
|
+
function readOrder() {
|
|
109
|
+
const groups = new Map(), insts = new Map();
|
|
110
|
+
try {
|
|
111
|
+
const rows = JSON.parse(stored(SIDEBAR_ORDER_KEY) || "[]");
|
|
112
|
+
if (Array.isArray(rows)) for (const row of rows) {
|
|
113
|
+
if (row && row.item_type === "group" && typeof row.item_name === "string") groups.set(row.item_name, Number(row.sort_index) || 0);
|
|
114
|
+
else if (row && row.item_type === "instance" && typeof row.item_name === "string") insts.set(row.item_name, Number(row.sort_index) || 0);
|
|
115
|
+
}
|
|
116
|
+
} catch { /* ignore invalid browser-local state */ }
|
|
117
|
+
return { groups, insts };
|
|
118
|
+
}
|
|
119
|
+
function readCollapsed() {
|
|
120
|
+
try { const a = JSON.parse(stored(COLLAPSED_KEY) || "[]"); return new Set(Array.isArray(a) ? a.filter((x) => typeof x === "string") : []); } catch { return new Set(); }
|
|
121
|
+
}
|
|
122
|
+
const strings = (a) => (Array.isArray(a) ? a.filter((x) => typeof x === "string") : []);
|
|
123
|
+
function readFilter() {
|
|
124
|
+
try {
|
|
125
|
+
const f = JSON.parse(stored(FILTER_KEY) || "{}");
|
|
126
|
+
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
|
+
} catch { return { q: "", status: [], cli: [] }; }
|
|
128
|
+
}
|
|
129
|
+
/** Display order: this browser's saved order first, then new groups/instances by tag and name. */
|
|
130
|
+
export function computeOrder(roster, order) {
|
|
131
|
+
const groups = new Map();
|
|
132
|
+
for (const it of roster) { const g = groupOf(it); if (!groups.has(g)) groups.set(g, []); groups.get(g).push(it); }
|
|
133
|
+
const gidx = (g) => (order.groups.has(g) ? order.groups.get(g) : Infinity);
|
|
134
|
+
const groupNames = [...groups.keys()].sort((a, b) => {
|
|
135
|
+
const ia = gidx(a), ib = gidx(b);
|
|
136
|
+
if (ia !== ib) return ia - ib;
|
|
137
|
+
if (a === "Other") return 1;
|
|
138
|
+
if (b === "Other") return -1;
|
|
139
|
+
return a.localeCompare(b);
|
|
140
|
+
});
|
|
141
|
+
const instByGroup = new Map();
|
|
142
|
+
for (const [g, items] of groups) {
|
|
143
|
+
instByGroup.set(g, items.map(nameOf).sort((a, b) => {
|
|
144
|
+
const ia = order.insts.has(a) ? order.insts.get(a) : Infinity, ib = order.insts.has(b) ? order.insts.get(b) : Infinity;
|
|
145
|
+
return ia - ib;
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
return { groupNames, instByGroup };
|
|
149
|
+
}
|
|
150
|
+
function moveIn(arr, moving, target, before) {
|
|
151
|
+
if (!arr) return;
|
|
152
|
+
const from = arr.indexOf(moving);
|
|
153
|
+
if (from === -1) return;
|
|
154
|
+
arr.splice(from, 1);
|
|
155
|
+
let to = arr.indexOf(target);
|
|
156
|
+
if (to === -1) { arr.push(moving); return; }
|
|
157
|
+
if (!before) to += 1;
|
|
158
|
+
arr.splice(to, 0, moving);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 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() });
|
|
163
|
+
/** Read them again from this browser's storage — what a page load does (the store above is made that way). */
|
|
164
|
+
export function loadNavPrefs() { navStore.set({ order: readOrder(), collapsed: readCollapsed(), filter: readFilter() }); }
|
|
165
|
+
/** Change the filter ({ q?, status?, cli? }): the same on every page, and kept by this browser. */
|
|
166
|
+
export function setNavFilter(patch) {
|
|
167
|
+
const filter = { ...navStore.get().filter, ...patch };
|
|
168
|
+
navStore.set({ filter });
|
|
169
|
+
store(FILTER_KEY, JSON.stringify(filter));
|
|
170
|
+
}
|
|
171
|
+
function setCollapsed(collapsed) { navStore.set({ collapsed }); store(COLLAPSED_KEY, JSON.stringify([...collapsed])); }
|
|
172
|
+
function persistOrder(groupNames, instByGroup) {
|
|
173
|
+
const rows = [];
|
|
174
|
+
groupNames.forEach((g, i) => rows.push({ item_type: "group", item_name: g, sort_index: i, group_name: null }));
|
|
175
|
+
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
|
+
store(SIDEBAR_ORDER_KEY, JSON.stringify(rows));
|
|
177
|
+
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
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── The list ──
|
|
181
|
+
|
|
182
|
+
const statusText = (facet) => (facet === "needs" ? t("app.needsYou") : facet === "working" ? t("app.statusWorking") : facet === "idle" ? t("app.statusIdle")
|
|
183
|
+
: facet === "crashed" ? t("app.statusCrashed") : t("app.statusStopped"));
|
|
184
|
+
const dotClass = (facet, exec) => (facet === "needs" ? "warn" : facet === "crashed" ? "bad" : facet === "stopped" ? "off" : exec === "stuck" ? "warn" : facet === "working" ? "busy" : "ok");
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The sidebar's instance list. `route`: the page's (which instance is open, and which view a row opens). `items`: the
|
|
188
|
+
* rows (status frames' instances, or View's roster for an anonymous reader); `loaded` false shows a skeleton.
|
|
189
|
+
* `exec` / `awaiting`: live state by name (empty for the anonymous reader). `onPick`: a row was clicked (the drawer
|
|
190
|
+
* closes).
|
|
191
|
+
*/
|
|
192
|
+
export function InstanceNav({ route, items, loaded = true, exec = {}, awaiting = {}, viewOnly = false, onPick }) {
|
|
193
|
+
const nav = useStore(navStore);
|
|
194
|
+
const filterRef = useRef(null);
|
|
195
|
+
const list = useRef(null);
|
|
196
|
+
const drag = useRef(null);
|
|
197
|
+
const toView = viewOnly || (route && route.panel === "view");
|
|
198
|
+
const active = route && (route.panel === "chat" || route.panel === "view") ? route.instance : null;
|
|
199
|
+
useLayoutEffect(() => { keepActiveInView(list.current); }, [active, items.length, loaded]);
|
|
200
|
+
const own = (map, name) => Object.prototype.hasOwnProperty.call(map, name);
|
|
201
|
+
const byName = new Map(items.map((it) => [nameOf(it), it]));
|
|
202
|
+
const facetOf = (name) => statusFacet(byName.get(name), own(exec, name) ? exec[name] : null, own(awaiting, name) ? awaiting[name] : null);
|
|
203
|
+
const { groupNames, instByGroup } = computeOrder(items, nav.order);
|
|
204
|
+
const f = nav.filter;
|
|
205
|
+
const shown = filterSidebar(groupNames, instByGroup, byName, f.q, backendLabel, { status: f.status, cli: f.cli, statusOf: facetOf });
|
|
206
|
+
const backends = [...new Set(items.map((it) => it.backend).filter((b) => typeof b === "string" && b))].sort((a, b) => backendLabel(a).localeCompare(backendLabel(b)));
|
|
207
|
+
const toggle = (g) => { const c = new Set(navStore.get().collapsed); if (c.has(g)) c.delete(g); else c.add(g); setCollapsed(c); };
|
|
208
|
+
// From the store, not this render's copy: two clicks before the next render must both count.
|
|
209
|
+
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] }); };
|
|
210
|
+
// Drag to reorder: only the full list (a filtered subset would be ambiguous), groups among groups, instances within
|
|
211
|
+
// their group.
|
|
212
|
+
const dragProps = (item) => shown.active ? {} : {
|
|
213
|
+
draggable: "true",
|
|
214
|
+
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 */ } },
|
|
215
|
+
onDragEnd: (e) => { e.currentTarget.classList.remove("dragging"); drag.current = null; },
|
|
216
|
+
onDragOver: (e) => {
|
|
217
|
+
const d = drag.current;
|
|
218
|
+
if (!d || d.type !== item.type || d.name === item.name || (item.type === "instance" && d.group !== item.group)) return;
|
|
219
|
+
e.preventDefault();
|
|
220
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
221
|
+
const before = e.clientY < r.top + r.height / 2;
|
|
222
|
+
e.currentTarget.classList.toggle("drop-before", before);
|
|
223
|
+
e.currentTarget.classList.toggle("drop-after", !before);
|
|
224
|
+
},
|
|
225
|
+
onDragLeave: (e) => e.currentTarget.classList.remove("drop-before", "drop-after"),
|
|
226
|
+
onDrop: (e) => {
|
|
227
|
+
e.preventDefault();
|
|
228
|
+
const before = e.currentTarget.classList.contains("drop-before");
|
|
229
|
+
e.currentTarget.classList.remove("drop-before", "drop-after");
|
|
230
|
+
const d = drag.current;
|
|
231
|
+
if (!d || d.type !== item.type) return;
|
|
232
|
+
const names = [...groupNames], insts = new Map([...instByGroup].map(([g, n]) => [g, [...n]]));
|
|
233
|
+
if (item.type === "group") moveIn(names, d.name, item.name, before);
|
|
234
|
+
else if (d.group === item.group) moveIn(insts.get(item.group), d.name, item.name, before);
|
|
235
|
+
else return;
|
|
236
|
+
persistOrder(names, insts);
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const chips = (key, values, label, text) => html`<details class="nav-facet">
|
|
240
|
+
<summary>${label}${f[key].length ? html` <span class="nav-facet-n">${f[key].length}</span>` : null}</summary>
|
|
241
|
+
<div class="nav-facet-list" role="group" aria-label=${label}>${values.map((value) => html`<label key=${value} class="nav-chip">
|
|
242
|
+
<input type="checkbox" checked=${f[key].includes(value)} onChange=${() => flip(key, value)} /><span>${text(value)}</span></label>`)}</div>
|
|
243
|
+
</details>`;
|
|
244
|
+
// The list scrolls; the filter under it stays in view (#999), so it sits outside the scrolling section.
|
|
245
|
+
return html`<div class="view-roster inst-nav">
|
|
246
|
+
<div class="side-section" id="instanceList" ref=${list}>
|
|
247
|
+
<h2 class="side-label">${tn("instancesLabel")}</h2>
|
|
248
|
+
${!loaded ? html`<${Skeleton} lines=${4} />` : !items.length ? html`<p class="side-empty">${t("app.noInstances")}</p>` : null}
|
|
249
|
+
${shown.groups.map(({ group: g, names }) => {
|
|
250
|
+
const folded = nav.collapsed.has(g);
|
|
251
|
+
return html`<div key=${g} class="v-group">
|
|
252
|
+
<button type="button" class="v-group-head" aria-expanded=${folded ? "false" : "true"} onClick=${() => toggle(g)}
|
|
253
|
+
title=${tn(folded ? "expandGroup" : "collapseGroup", { group: g })} ...${dragProps({ type: "group", name: g })}>
|
|
254
|
+
<${Icon} name="chevron" size=${14} cls=${folded ? "caret folded" : "caret"} /><span class="grow">${g}</span><span class="count">${names.length}</span></button>
|
|
255
|
+
${folded ? null : html`<ul class="inst-list">${names.map((name) => {
|
|
256
|
+
const it = byName.get(name);
|
|
257
|
+
const alias = typeof it.display_name === "string" && it.display_name.trim() && it.display_name.trim() !== name ? it.display_name.trim() : "";
|
|
258
|
+
const facet = facetOf(name);
|
|
259
|
+
const waiting = own(awaiting, name) ? awaiting[name] : null;
|
|
260
|
+
const on = name === active;
|
|
261
|
+
return html`<li key=${name}><a class=${`inst v-inst${on ? " active" : ""}`} href=${toView ? viewPath(name) : chatPath(name)} title=${instanceTooltip(it)}
|
|
262
|
+
aria-current=${on ? "page" : undefined} onClick=${onPick} ...${dragProps({ type: "instance", name, group: g })}>
|
|
263
|
+
<span class=${`dot ${dotClass(facet, own(exec, name) ? exec[name] : null)}`} aria-hidden="true"></span>
|
|
264
|
+
<span class="inst-text"><span class="inst-name">${name}</span>
|
|
265
|
+
${alias || facet === "needs" ? html`<span class="inst-sub">${alias ? html`<span class="inst-alias">${alias}</span>` : null}
|
|
266
|
+
${facet === "needs" ? html`<span class="badge-await" title=${waiting || t("app.approxNote")}>${t("app.needsYou")}</span>` : null}</span>` : null}
|
|
267
|
+
<span class="sr-only">${statusText(facet)}</span></span>
|
|
268
|
+
<span class="v-meta">${it.context_pct != null ? `${Math.round(it.context_pct)}%` : ""}</span>
|
|
269
|
+
<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>
|
|
270
|
+
</a></li>`;
|
|
271
|
+
})}</ul>`}
|
|
272
|
+
</div>`;
|
|
273
|
+
})}
|
|
274
|
+
${loaded && shown.active && !shown.groups.length ? html`<p class="side-empty">${tn("filterNone")}</p>` : null}
|
|
275
|
+
</div>
|
|
276
|
+
<div class="v-filter">
|
|
277
|
+
<label class="v-filter-box"><${Icon} name="search" size=${14} /><span class="sr-only">${tn("filterPh")}</span>
|
|
278
|
+
<input ref=${filterRef} id="filterInput" type="text" autocomplete="off" spellcheck="false" placeholder=${tn("filterPh")} value=${f.q}
|
|
279
|
+
onInput=${(e) => setNavFilter({ q: e.target.value })}
|
|
280
|
+
onKeyDown=${(e) => { if (e.key === "Escape" && f.q) { e.preventDefault(); e.stopPropagation(); setNavFilter({ q: "" }); } }} />
|
|
281
|
+
${f.q ? html`<button type="button" class="icon-btn v-filter-clear" aria-label=${tn("filterClear")} title=${tn("filterClear")}
|
|
282
|
+
onClick=${() => { setNavFilter({ q: "" }); filterRef.current && filterRef.current.focus(); }}><${Icon} name="close" size=${14} /></button>` : null}
|
|
283
|
+
</label>
|
|
284
|
+
<div class="nav-facets">
|
|
285
|
+
${viewOnly ? null : chips("status", STATUS_FACETS, tn("filterStatus"), statusText)}
|
|
286
|
+
${backends.length > 1 || f.cli.length ? chips("cli", backends, tn("filterCli"), backendLabel) : null}
|
|
287
|
+
${f.status.length || f.cli.length ? html`<button type="button" class="chip-btn nav-facet-reset" onClick=${() => setNavFilter({ status: [], cli: [] })}>${tn("filterFacetsReset")}</button>` : null}
|
|
288
|
+
</div>
|
|
289
|
+
<p class="note" aria-live="polite">${shown.shown} / ${shown.total}${shown.active ? ` ${tn("filterShown")}` : ""}</p>
|
|
290
|
+
</div>
|
|
291
|
+
</div>`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Keep a sidebar list's current item in view by scrolling that list only (alpha.2): never scrollIntoView, which would
|
|
296
|
+
* also scroll every scrollable ancestor, the page included. A list the reader scrolled stays where it is otherwise.
|
|
297
|
+
*/
|
|
298
|
+
export function keepActiveInView(container) {
|
|
299
|
+
const a = container && container.querySelector('[aria-current="page"]');
|
|
300
|
+
if (!a) return;
|
|
301
|
+
const c = container.getBoundingClientRect(), r = a.getBoundingClientRect();
|
|
302
|
+
if (r.top < c.top) container.scrollTop -= c.top - r.top;
|
|
303
|
+
else if (r.bottom > c.bottom) container.scrollTop += r.bottom - c.bottom;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The list fed by View's roster read (/api/profiles): the anonymous View reader's, who has no status frames. */
|
|
307
|
+
export function RosterNav({ route, viewOnly = false, onPick }) {
|
|
308
|
+
const v = useStore(viewStore);
|
|
309
|
+
return html`<${InstanceNav} route=${route || { panel: "view", instance: v.current }} items=${v.roster} loaded=${v.loaded} viewOnly=${viewOnly} onPick=${onPick} />`;
|
|
310
|
+
}
|