@songsid/agend 2.3.3 → 2.3.4-beta.10
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 +3 -3
- package/README.zh-TW.md +3 -3
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +16 -3
- package/dist/auth-api.js +32 -8
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +15 -1
- package/dist/authority/web-authority.js +27 -10
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +100 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +136 -23
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +68 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +13 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +27 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +238 -18
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/chat-log-ids.d.ts +48 -0
- package/dist/chat-log-ids.js +182 -0
- package/dist/chat-log-ids.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +8 -1
- package/dist/classic-channel-manager.js +24 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +18 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +52 -0
- package/dist/daemon.js +538 -35
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- package/dist/env-file.d.ts +2 -0
- package/dist/env-file.js +13 -8
- package/dist/env-file.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +172 -12
- package/dist/fleet-manager.js +897 -139
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +14 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +22 -2
- package/dist/instance-lifecycle.js +103 -19
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +2 -2
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/interaction-observation.d.ts +1 -0
- package/dist/interaction-observation.js +1 -1
- package/dist/interaction-observation.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +130 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +30 -0
- package/dist/log-service.js +36 -3
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +31 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +22 -2
- package/dist/needs-you.js +17 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/next-steps.d.ts +12 -2
- package/dist/next-steps.js +19 -3
- package/dist/next-steps.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +57 -0
- package/dist/office-share-service.js +76 -11
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +8 -0
- package/dist/office-share.js +34 -0
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +59 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +10 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/page-share.d.ts +122 -0
- package/dist/page-share.js +199 -0
- package/dist/page-share.js.map +1 -0
- package/dist/public-preview.d.ts +4 -0
- package/dist/public-preview.js +5 -4
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +5 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/quickstart-api.d.ts +5 -0
- package/dist/quickstart-api.js +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +40 -0
- package/dist/quickstart.js +174 -28
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.js +2 -0
- package/dist/remote-card-answer.js.map +1 -1
- package/dist/remote-card-prompt.js +4 -2
- package/dist/remote-card-prompt.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/selection-card.d.ts +20 -0
- package/dist/selection-card.js +202 -0
- package/dist/selection-card.js.map +1 -0
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +16 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +5 -0
- 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/setup-host.js +2 -2
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/chat-store.js +28 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +52 -10
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/page-share-dialog.js +191 -0
- package/dist/ui/panel-chat.js +16 -0
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/panel-needs.js +17 -8
- package/dist/ui/panel-notices.js +98 -0
- package/dist/ui/panel-settings.js +9 -2
- package/dist/ui/preview-pages.js +41 -0
- package/dist/ui/preview-panel-actions.js +161 -0
- package/dist/ui/preview-panel.js +209 -28
- package/dist/ui/preview-versions.js +155 -0
- package/dist/ui/preview.js +88 -3
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +6 -2
- package/dist/ui/shared/app-i18n.js +6 -6
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-route.js +4 -0
- package/dist/ui/shared/app-shell.js +9 -5
- package/dist/ui/shared/app-store.js +71 -1
- package/dist/ui/shared/app-stream.js +27 -1
- package/dist/ui/shared/app.css +94 -4
- package/dist/ui/shared/app.js +22 -1
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +339 -17
- package/dist/ui/shared/signin.js +12 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +42 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +79 -0
- package/dist/unattended-install.js.map +1 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +31 -0
- package/dist/view-api.js +186 -41
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +32 -0
- package/dist/web-api.js +195 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-login.d.ts +6 -3
- package/dist/web-login.js +26 -15
- package/dist/web-login.js.map +1 -1
- package/dist/web-notices.d.ts +51 -0
- package/dist/web-notices.js +118 -0
- package/dist/web-notices.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-only-fleet.d.ts +22 -0
- package/dist/web-only-fleet.js +13 -0
- package/dist/web-only-fleet.js.map +1 -0
- package/dist/web-preview.d.ts +32 -3
- package/dist/web-preview.js +52 -19
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.d.ts +12 -0
- package/dist/web-session.js +22 -8
- 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 +2 -0
- package/dist/web-shell-routes.js +3 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// #2911 item 8: page share links on the dashboard. The preview panel's Share page action — a confirm dialog every time
|
|
2
|
+
// (what is shared, that it runs the page's own scripts, for how long), then the link shown once to copy — and, while that
|
|
3
|
+
// page is shared, "Shared until …" with Stop sharing; Settings → Access & security lists the open ones with Stop and
|
|
4
|
+
// Stop all. Design: #2911 issuecomment-6561078095 (approved).
|
|
5
|
+
// GET / POST /ui/page-share, DELETE /ui/page-share/<id|all> (web-api.ts): an admin session (directly or over the public
|
|
6
|
+
// link). The server reads the page itself from the chat history: the page only names the block.
|
|
7
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
8
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
9
|
+
import { Dialog } from "/assets/ui-dialog.js";
|
|
10
|
+
import { toast } from "/assets/ui-toast.js";
|
|
11
|
+
|
|
12
|
+
register("pshare", {
|
|
13
|
+
en: {
|
|
14
|
+
share: "Share page", shareTitle: "Make a link anyone can open to see this page, for a limited time",
|
|
15
|
+
shared: "Shared until {0}", sharedTitle: "This page is shared: see or stop it",
|
|
16
|
+
title: "Share this page", what: "Anyone with the link can open “{0}” from {1}'s reply ({2}).",
|
|
17
|
+
runs: "It runs the page's own scripts in their browser, apart from this dashboard. They see the page only — not the chat, the agent or the fleet.",
|
|
18
|
+
snapshot: "The link shows the page as it is now; a later version needs a new link. The chat is told when it starts and ends.",
|
|
19
|
+
cdn: "This page may load libraries from cdnjs / jsDelivr; viewers' browsers will request them.",
|
|
20
|
+
how: "For", create: "Share", creating: "Starting the link…", cancel: "Cancel", close: "Close",
|
|
21
|
+
made: "Anyone with this link can open the page until {0}. It is shown only now: copy it.", copy: "Copy link", copied: "Link copied",
|
|
22
|
+
stop: "Stop sharing", stopAll: "Stop all", stopping: "Stopping…", stopped: "The page is no longer shared.", untitled: "this page",
|
|
23
|
+
nowShared: "Shared until {0}. Stop sharing ends the link at once.",
|
|
24
|
+
m15: "15 minutes", h1: "1 hour", h8: "8 hours", h24: "24 hours",
|
|
25
|
+
e_disabled: "Page share links are off on this fleet (web.page_share.enabled: false).",
|
|
26
|
+
e_duration: "That is longer than this fleet allows (web.page_share.max_hours).", e_too_many: "At most 10 pages can be shared at once: stop one first.",
|
|
27
|
+
e_too_large: "This page is too large to share (over 8 MB): download it instead.", e_page_gone: "This page is no longer in the chat's history.",
|
|
28
|
+
e_message_gone: "This page is no longer in the chat's history.", e_not_an_agent_page: "Only a page an agent sent can be shared.",
|
|
29
|
+
e_rate: "Cloudflare is limiting new quick tunnels right now. Try again in a few minutes.", e_tunnel: "The share link's tunnel did not start ({0}). Try again later.",
|
|
30
|
+
failed: "The link could not be made.",
|
|
31
|
+
cardTitle: "Shared pages", cardHint: "Links to single agent pages, made from the HTML preview panel's Share page. Each ends at its time; stop one to end it now.",
|
|
32
|
+
none: "No pages are shared.", from: "from {0}", ends: "until {0}",
|
|
33
|
+
},
|
|
34
|
+
"zh-TW": {
|
|
35
|
+
share: "分享頁面", shareTitle: "建立一個連結,讓任何人在限定時間內開啟這個頁面",
|
|
36
|
+
shared: "分享到 {0}", sharedTitle: "這個頁面正在分享:查看或停止",
|
|
37
|
+
title: "分享這個頁面", what: "拿到連結的人都能開啟 {1} 回覆裡的「{0}」({2})。",
|
|
38
|
+
runs: "頁面本身的腳本會在對方的瀏覽器執行,與這個儀表板分開。對方只看得到這個頁面——看不到對話、agent 或 fleet。",
|
|
39
|
+
snapshot: "連結顯示的是頁面現在的樣子;之後的新版本需要新的連結。開始與結束分享時會告知這個對話。",
|
|
40
|
+
cdn: "這個頁面可能從 cdnjs / jsDelivr 載入函式庫;觀看者的瀏覽器會向它們發出請求。",
|
|
41
|
+
how: "時間", create: "分享", creating: "正在啟動連結…", cancel: "取消", close: "關閉",
|
|
42
|
+
made: "拿到這個連結的人可以開啟頁面到 {0}。連結只會顯示這一次:請複製。", copy: "複製連結", copied: "已複製連結",
|
|
43
|
+
stop: "停止分享", stopAll: "全部停止", stopping: "正在停止…", stopped: "這個頁面已停止分享。", untitled: "這個頁面",
|
|
44
|
+
nowShared: "分享到 {0}。按「停止分享」會立即結束連結。",
|
|
45
|
+
m15: "15 分鐘", h1: "1 小時", h8: "8 小時", h24: "24 小時",
|
|
46
|
+
e_disabled: "這個 fleet 已關閉頁面分享連結(web.page_share.enabled: false)。",
|
|
47
|
+
e_duration: "超過這個 fleet 允許的時間(web.page_share.max_hours)。", e_too_many: "同時最多分享 10 個頁面:先停止一個。",
|
|
48
|
+
e_too_large: "這個頁面太大(超過 8 MB),無法分享:請改用下載。", e_page_gone: "對話紀錄裡已經沒有這個頁面。",
|
|
49
|
+
e_message_gone: "對話紀錄裡已經沒有這個頁面。", e_not_an_agent_page: "只能分享 agent 傳來的頁面。",
|
|
50
|
+
e_rate: "Cloudflare 目前限制新的臨時通道,請過幾分鐘再試。", e_tunnel: "分享連結的通道沒有啟動({0}),請稍後再試。",
|
|
51
|
+
failed: "無法建立連結。",
|
|
52
|
+
cardTitle: "分享中的頁面", cardHint: "單一 agent 頁面的連結,可在 HTML 預覽面板的「分享頁面」建立。每個連結到時間就結束;停止可立即結束。",
|
|
53
|
+
none: "目前沒有分享中的頁面。", from: "來自 {0}", ends: "到 {0} 為止",
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const tp = (k, ...v) => t(`pshare.${k}`, ...v);
|
|
57
|
+
export const DURATIONS = [["m15", 15], ["h1", 60], ["h8", 480], ["h24", 1440]];
|
|
58
|
+
const JSON_HEADERS = { "Content-Type": "application/json" };
|
|
59
|
+
const when = (ms) => { try { return new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; } };
|
|
60
|
+
/** The end of a share for the bar: the time today, with the day when it is not today (a share may last 24 h). */
|
|
61
|
+
const until = (ms) => { const d = new Date(ms), now = new Date(); return d.toDateString() === now.toDateString() ? when(ms) : whenLong(ms); };
|
|
62
|
+
const whenLong = (ms) => { try { return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(ms)); } catch { return ""; } };
|
|
63
|
+
|
|
64
|
+
/** What a refusal says (the host's `error` and `reason`). */
|
|
65
|
+
export function refusalText(d) {
|
|
66
|
+
const e = d && d.error;
|
|
67
|
+
if (e === "tunnel") return d.reason === "rate-limited" ? tp("e_rate") : tp("e_tunnel", d.reason || "unknown");
|
|
68
|
+
return ["disabled", "duration", "too_many", "too_large", "page_gone", "message_gone", "not_an_agent_page"].includes(e) ? tp(`e_${e}`) : tp("failed");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── The open page shares, one read for the page (an admin's; anyone else gets null) ──
|
|
72
|
+
let state = { loaded: false, canShare: false, enabled: false, cdn: false, shares: [] };
|
|
73
|
+
const subs = new Set();
|
|
74
|
+
let reading = null, expiry = null;
|
|
75
|
+
function set(next) { state = next; for (const fn of [...subs]) { try { fn(state); } catch { /* one listener's error stops no other */ } } }
|
|
76
|
+
/** Read the open shares again (after a change; at the soonest end). `get`: fetch. */
|
|
77
|
+
export function refreshPageShares(get = globalThis.fetch) {
|
|
78
|
+
if (reading) return reading;
|
|
79
|
+
reading = (async () => {
|
|
80
|
+
const auth = globalThis.AgendAuth;
|
|
81
|
+
const admin = !!(auth && typeof auth.canWrite === "function" && (await Promise.resolve(auth.canWrite()).catch(() => false)) === true);
|
|
82
|
+
let d = null;
|
|
83
|
+
if (admin) { try { const r = await get("/ui/page-share", { method: "GET", headers: JSON_HEADERS }); d = r.ok ? await r.json() : null; } catch { d = null; } }
|
|
84
|
+
const shares = d && Array.isArray(d.shares) ? d.shares : [];
|
|
85
|
+
set({ loaded: true, canShare: admin && !!d && d.enabled !== false, enabled: !!d && d.enabled !== false, cdn: !!d && d.cdn === true, shares });
|
|
86
|
+
// Read again when the soonest one ends, so its "Shared until" goes without a reload.
|
|
87
|
+
if (expiry) clearTimeout(expiry);
|
|
88
|
+
const soonest = Math.min(...shares.map((s) => s.expiresAt));
|
|
89
|
+
if (Number.isFinite(soonest)) expiry = setTimeout(() => { expiry = null; refreshPageShares(get); }, Math.max(1000, soonest - Date.now() + 1000));
|
|
90
|
+
})().finally(() => { reading = null; });
|
|
91
|
+
return reading;
|
|
92
|
+
}
|
|
93
|
+
export const pageShares = () => state;
|
|
94
|
+
/** The open shares, read once per page and on every change. */
|
|
95
|
+
export function usePageShares(get) {
|
|
96
|
+
const [st, setSt] = useState(state);
|
|
97
|
+
useEffect(() => { subs.add(setSt); if (!state.loaded) refreshPageShares(get); return () => { subs.delete(setSt); }; }, []);
|
|
98
|
+
return st;
|
|
99
|
+
}
|
|
100
|
+
/** The open share of a block `from` ({ boot, id, fence | att }) in `instance`, or null. */
|
|
101
|
+
export function shareOf(shares, instance, from) {
|
|
102
|
+
if (!from) return null;
|
|
103
|
+
return shares.find((s) => s.instance === instance && s.from && s.from.boot === from.boot && s.from.id === from.id
|
|
104
|
+
&& (from.att ? s.from.att === from.att : s.from.fence === from.fence)) || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function stopShare(id, get = globalThis.fetch) {
|
|
108
|
+
try { await get(`/ui/page-share/${id}`, { method: "DELETE", headers: JSON_HEADERS }); } catch { /* the list is read again */ }
|
|
109
|
+
await refreshPageShares(get);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The confirm dialog: what is shared (its title, whose reply, when), that it runs the page's own scripts, for how long;
|
|
114
|
+
* Share makes the link and shows it once. With `share` (this page is already shared): when it ends, and Stop sharing.
|
|
115
|
+
* `block`: { instance, from: {boot, id, fence | att}, title, sender, ts }.
|
|
116
|
+
*/
|
|
117
|
+
export function PageShareDialog({ block, share = null, get = globalThis.fetch, onClose }) {
|
|
118
|
+
const [minutes, setMinutes] = useState(60);
|
|
119
|
+
const [busy, setBusy] = useState(false);
|
|
120
|
+
const [made, setMade] = useState(null);
|
|
121
|
+
const [error, setError] = useState("");
|
|
122
|
+
const create = async () => {
|
|
123
|
+
setBusy(true); setError("");
|
|
124
|
+
try {
|
|
125
|
+
const r = await get("/ui/page-share", { method: "POST", headers: JSON_HEADERS, body: JSON.stringify({ instance: block.instance, boot: block.from.boot, id: block.from.id,
|
|
126
|
+
...(block.from.att ? { att: block.from.att } : { fence: block.from.fence }), minutes }) });
|
|
127
|
+
const d = await r.json().catch(() => ({}));
|
|
128
|
+
if (r.ok && d && typeof d.url === "string") setMade(d); else setError(refusalText(d));
|
|
129
|
+
} catch { setError(tp("failed")); }
|
|
130
|
+
setBusy(false);
|
|
131
|
+
refreshPageShares(get);
|
|
132
|
+
};
|
|
133
|
+
const stop = async (id) => { setBusy(true); await stopShare(id, get); setBusy(false); toast(tp("stopped")); onClose(); };
|
|
134
|
+
const copy = async () => { try { await navigator.clipboard.writeText(made.url); toast(tp("copied")); } catch { /* the link is on screen to copy by hand */ } };
|
|
135
|
+
const open = made ? made.share : share;
|
|
136
|
+
const name = block.title || tp("untitled");
|
|
137
|
+
const actions = made || share
|
|
138
|
+
? html`<button type="button" class="btn danger ps-stop" disabled=${busy} onClick=${() => stop(open.id)}>${busy ? tp("stopping") : tp("stop")}</button>
|
|
139
|
+
<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${tp("close")}</button>`
|
|
140
|
+
: html`<button type="button" class="btn" onClick=${onClose} disabled=${busy}>${tp("cancel")}</button>
|
|
141
|
+
<button type="button" class="btn btn-primary ps-create" onClick=${create} disabled=${busy}>${busy ? tp("creating") : tp("create")}</button>`;
|
|
142
|
+
return html`<${Dialog} title=${tp("title")} onClose=${onClose} busy=${busy} actions=${actions}>
|
|
143
|
+
<div class="os ps">
|
|
144
|
+
<p class="ps-what">${tp("what", name, block.sender || block.instance, whenLong(block.ts ? Date.parse(block.ts) : Date.now()))}</p>
|
|
145
|
+
<p class="ps-runs"><strong>${tp("runs")}</strong></p>
|
|
146
|
+
${made ? html`<div class="os-made">
|
|
147
|
+
<input class="input mono os-url ps-url" readonly value=${made.url} aria-label=${tp("copy")} onFocus=${(e) => e.target.select && e.target.select()} />
|
|
148
|
+
<button type="button" class="btn btn-sm ps-copy" onClick=${copy}>${tp("copy")}</button>
|
|
149
|
+
<p class="note">${tp("made", whenLong(made.share.expiresAt))}</p>
|
|
150
|
+
</div>`
|
|
151
|
+
: share ? html`<p class="note ps-now">${tp("nowShared", whenLong(share.expiresAt))}</p>${share.cdn ? html`<p class="note ps-cdn">${tp("cdn")}</p>` : null}`
|
|
152
|
+
: html`<p class="muted">${tp("snapshot")}</p>
|
|
153
|
+
${state.cdn ? html`<p class="note ps-cdn">${tp("cdn")}</p>` : null}
|
|
154
|
+
<label class="field"><span>${tp("how")}</span>
|
|
155
|
+
<select class="tb-select ps-how" value=${String(minutes)} onChange=${(e) => setMinutes(Number(e.target.value))} disabled=${busy}>
|
|
156
|
+
${DURATIONS.map(([k, m]) => html`<option key=${k} value=${String(m)}>${tp(k)}</option>`)}
|
|
157
|
+
</select></label>`}
|
|
158
|
+
${error ? html`<p class="err os-err ps-err" role="alert">${error}</p>` : null}
|
|
159
|
+
</div>
|
|
160
|
+
<//>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Settings → Access & security: the shared pages, with Stop and Stop all. */
|
|
164
|
+
export function PageSharesCard({ get = globalThis.fetch }) {
|
|
165
|
+
const st = usePageShares(get);
|
|
166
|
+
const [busy, setBusy] = useState(false);
|
|
167
|
+
if (!st.loaded || !st.canShare && !st.shares.length) return null; // not kept on this fleet, or not this session's
|
|
168
|
+
const stop = async (id) => { setBusy(true); await stopShare(id, get); setBusy(false); };
|
|
169
|
+
return html`<section class="card os-card ps-card"><h3>${tp("cardTitle")}</h3>
|
|
170
|
+
<p class="note">${st.enabled ? tp("cardHint") : tp("e_disabled")}</p>
|
|
171
|
+
${!st.shares.length ? html`<p class="muted os-none">${tp("none")}</p>` : html`<div class="os-list">
|
|
172
|
+
<ul class="os-rows">${st.shares.map((s) => html`<li key=${s.id} class="os-row">
|
|
173
|
+
<span class="os-what"><strong>${s.title || tp("untitled")}</strong>
|
|
174
|
+
<span class="muted"> · ${tp("from", s.instance)} · ${tp("ends", whenLong(s.expiresAt))}</span></span>
|
|
175
|
+
<button type="button" class="btn btn-sm" disabled=${busy} onClick=${() => stop(s.id)}>${tp("stop")}</button>
|
|
176
|
+
</li>`)}</ul>
|
|
177
|
+
${st.shares.length > 1 ? html`<button type="button" class="btn btn-sm danger os-all" disabled=${busy} onClick=${() => stop("all")}>${tp("stopAll")}</button>` : null}
|
|
178
|
+
</div>`}
|
|
179
|
+
</section>`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The panel's action: Share page, or "Shared until …" when this block is shared. `block` as PageShareDialog's. */
|
|
183
|
+
export function PageShareButton({ block, Icon }) {
|
|
184
|
+
const st = usePageShares();
|
|
185
|
+
const [open, setOpen] = useState(false);
|
|
186
|
+
if (!block || !st.canShare) return null;
|
|
187
|
+
const share = shareOf(st.shares, block.instance, block.from);
|
|
188
|
+
return html`<button type="button" class=${`btn btn-sm ${share ? "pv-fb-shared on" : "pv-fb-share"}`} title=${tp(share ? "sharedTitle" : "shareTitle")} onClick=${() => setOpen(true)}>
|
|
189
|
+
<${Icon} name="globe" size=${14} /><span class="lbl">${share ? tp("shared", until(share.expiresAt)) : tp("share")}</span></button>
|
|
190
|
+
${open ? html`<${PageShareDialog} block=${block} share=${share} onClose=${() => setOpen(false)} />` : null}`;
|
|
191
|
+
}
|
package/dist/ui/panel-chat.js
CHANGED
|
@@ -76,6 +76,7 @@ import { installMessageActions } from "./chat-msg-actions.js";
|
|
|
76
76
|
import { createEmojiIndex } from "./chat-emojis.js";
|
|
77
77
|
import { pollPublicPreview } from "./preview-status.js";
|
|
78
78
|
import { createMentionIndex } from "./chat-mentions.js";
|
|
79
|
+
import { setPanelFeedback } from "./preview-panel-actions.js";
|
|
79
80
|
import { PreviewPanel, claimPanel, openPanel, panelClaims, panelStore, panelVersion, shownKey } from "./preview-panel.js";
|
|
80
81
|
import { needsArgument, paletteFor, parseCommandLine } from "./chat-commands.js";
|
|
81
82
|
import { installTour, refreshTourSpot } from "./chat-tour.js";
|
|
@@ -142,6 +143,19 @@ export function boot({ stream, boot: bootData, deps = {} }) {
|
|
|
142
143
|
if (auth && typeof auth.sessionHandle === "function") auth.sessionHandle().then((h) => P().setBannerScope(h), () => P().setBannerScope(null));
|
|
143
144
|
else P().setBannerScope(null);
|
|
144
145
|
}
|
|
146
|
+
// #2911: the preview panel's "Reply about this page" reaches the composer through these (an undoable edit, as typing).
|
|
147
|
+
setPanelFeedback({
|
|
148
|
+
findMsg: (instance, key) => (store.state.msgs[instance] || []).find((x) => msgKey(x) === key) || null,
|
|
149
|
+
startReply: (instance, x) => store.startReply(instance, x),
|
|
150
|
+
clearReply: (instance) => store.clearReply(instance),
|
|
151
|
+
addToDraft: (instance, line) => { recordEdit(composerField()); store.addToDraft(instance, line); },
|
|
152
|
+
focus: () => {
|
|
153
|
+
const el = composerField(); if (!el) return;
|
|
154
|
+
el.focus();
|
|
155
|
+
// After the render that shows the line: the caret at its end.
|
|
156
|
+
setTimeout(() => { try { const n = el.value.length; el.setSelectionRange(n, n); } catch { /* not a text field */ } }, 0);
|
|
157
|
+
},
|
|
158
|
+
});
|
|
145
159
|
// #1604: the preview opt-in and Tour live in Settings → This device now (and on each preview card's menu).
|
|
146
160
|
installTour();
|
|
147
161
|
return store;
|
|
@@ -783,6 +797,8 @@ function DeleteDialog({ name, onClose }) {
|
|
|
783
797
|
} catch (err) { r = { error: err && err.message ? err.message : t("chat.disconnected") }; }
|
|
784
798
|
if (r && r.error) { toast(r.error, false); if (!handedOver && lease.current()) setBusy(false); return; }
|
|
785
799
|
toast(t("chat.instanceDeleted", name));
|
|
800
|
+
// The agent's own topic went too, or stayed with the reason (the server's own words: no new string here).
|
|
801
|
+
if (r.topic_note) toast(r.topic_note, r.topic_deleted !== false);
|
|
786
802
|
if (handedOver || !lease.current()) return;
|
|
787
803
|
setBusy(false);
|
|
788
804
|
onClose();
|
package/dist/ui/panel-log.js
CHANGED
|
@@ -49,6 +49,7 @@ register("log", {
|
|
|
49
49
|
user: "Asked", assistant: "Replied", error: "Error", errorCode: "Error {0}", turnEnd: "Turn ended", turnEndIn: "Turn ended · {0}",
|
|
50
50
|
compacted: "Conversation compacted", turn: "Turn",
|
|
51
51
|
running: "running", done: "done", failed: "failed", interrupted: "interrupted", exit: "exit {0}",
|
|
52
|
+
showScreen: "Show what was on screen", hideScreen: "Hide what was on screen",
|
|
52
53
|
showOutput: "Show output", hideOutput: "Hide output", args: "Arguments", output: "Output", noOutput: "No output.",
|
|
53
54
|
showAll: "Show all", showLess: "Show less", cut: "… {0} more characters not shown.", tooLarge: "Output of {0}: only its start is shown.",
|
|
54
55
|
times: "×{0}", newer: "{0} new",
|
|
@@ -87,6 +88,7 @@ register("log", {
|
|
|
87
88
|
user: "收到要求", assistant: "回覆", error: "錯誤", errorCode: "錯誤 {0}", turnEnd: "回合結束", turnEndIn: "回合結束 · {0}",
|
|
88
89
|
compacted: "對話已壓縮", turn: "回合",
|
|
89
90
|
running: "執行中", done: "完成", failed: "失敗", interrupted: "中斷", exit: "結束碼 {0}",
|
|
91
|
+
showScreen: "顯示當時的畫面", hideScreen: "隱藏當時的畫面",
|
|
90
92
|
showOutput: "顯示輸出", hideOutput: "隱藏輸出", args: "參數", output: "輸出", noOutput: "沒有輸出。",
|
|
91
93
|
showAll: "顯示全部", showLess: "收合", cut: "… 還有 {0} 個字元沒有顯示。", tooLarge: "輸出共 {0},只顯示開頭。",
|
|
92
94
|
times: "×{0}", newer: "{0} 則新的",
|
|
@@ -402,7 +404,11 @@ function Row({ x, prev }) {
|
|
|
402
404
|
};
|
|
403
405
|
let row;
|
|
404
406
|
if (x.kind === "event") {
|
|
405
|
-
|
|
407
|
+
// M-2269: a dialog or prompt AgEnD saw (the CLI never writes it to its transcript): its text as it was on screen,
|
|
408
|
+
// folded until asked for. Text only (a text node), masked by the server.
|
|
409
|
+
row = html`${head(KIND_ICON.event, eventLabel(x.type))}${x.detail ? html`<div class="log-text muted">${x.detail}</div>` : null}
|
|
410
|
+
${x.screen ? html`<button type="button" class="link-btn log-toggle log-screen-toggle" aria-expanded=${open ? "true" : "false"} onClick=${() => setOpen(!open)}>${open ? tl("hideScreen") : tl("showScreen")}</button>` : null}
|
|
411
|
+
${x.screen && open ? html`<div class="log-detail"><div class="log-text mono log-screen">${x.screen}</div></div>` : null}`;
|
|
406
412
|
} else if (x.kind === "user" || x.kind === "assistant") {
|
|
407
413
|
row = html`${head(KIND_ICON[x.kind], tl(x.kind))}${text(x.body, x.bodyCut)}`;
|
|
408
414
|
} else if (x.kind === "error") {
|
package/dist/ui/panel-needs.js
CHANGED
|
@@ -25,7 +25,7 @@ import { openSignIn, signInOffered } from "./backend-signin.js";
|
|
|
25
25
|
register("needs", {
|
|
26
26
|
en: {
|
|
27
27
|
title: "Needs you", empty: "Nothing needs you right now", emptyHint: "When an agent waits for you, it shows here — and in your chat app's General.", emptyHintWeb: "When an agent waits for you, it shows here.",
|
|
28
|
-
open: "Open chat", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
|
|
28
|
+
open: "Open chat", kiroOpen: "Open agent", kiroMigration: "Migration instructions", openCard: "Open the card", noAssignee: "Task board (nobody assigned)", fleetSignIns: "Fleet sign-ins", signIn: "Sign in",
|
|
29
29
|
gap: "not wrapped up: {0}", gapUnchecked: "{0} checklist item(s) unchecked", gapNoResult: "no result", gapJoin: ", ", acknowledge: "Acknowledge", acknowledging: "Acknowledging…", answering: "Answering…",
|
|
30
30
|
ageNow: "just now", ageMin: "{0} min", ageHour: "{0} h", ageDay: "{0} d",
|
|
31
31
|
notifyTitle: "Notify me on this device", notifyOn: "Notifications are on for this browser while a tab is open.",
|
|
@@ -34,11 +34,11 @@ register("needs", {
|
|
|
34
34
|
notifyMobile: "On a phone, rely on your chat app's notification: a new item is a new post in General.", notifyMobileWeb: "On a phone, open this page to see what needs you.",
|
|
35
35
|
notifyInsecure: "Notifications need a secure address (localhost or the HTTPS public link), not a plain-HTTP LAN address.",
|
|
36
36
|
notifyUnsupported: "This browser cannot show notifications.", turnOn: "Turn on", turnOff: "Turn off",
|
|
37
|
-
typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery",
|
|
37
|
+
typePrompt: "Fleet prompt", typeAwaiting: "At its terminal", typeInstance: "Instance", typeDelivery: "Delivery", typeMissed: "Missed while offline",
|
|
38
38
|
},
|
|
39
39
|
"zh-TW": {
|
|
40
40
|
title: "等你處理", empty: "目前沒有需要你處理的事", emptyHint: "agent 在等你時,會出現在這裡——也會出現在聊天 app 的 General。", emptyHintWeb: "agent 在等你時,會出現在這裡。",
|
|
41
|
-
open: "開啟聊天", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
|
|
41
|
+
open: "開啟聊天", kiroOpen: "開啟 Agent", kiroMigration: "遷移說明", openCard: "打開卡片", noAssignee: "任務看板(沒有負責人)", fleetSignIns: "Fleet 登入", signIn: "登入",
|
|
42
42
|
gap: "還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapNoResult: "沒有產出總結", gapJoin: "、", acknowledge: "確認", acknowledging: "確認中…", answering: "回覆中…",
|
|
43
43
|
ageNow: "剛剛", ageMin: "{0} 分鐘", ageHour: "{0} 小時", ageDay: "{0} 天",
|
|
44
44
|
notifyTitle: "在這台裝置通知我", notifyOn: "這個瀏覽器的通知已開啟(需要開著分頁)。",
|
|
@@ -47,10 +47,18 @@ register("needs", {
|
|
|
47
47
|
notifyMobile: "在手機上請以聊天 app 的通知為主:每個新項目都會在 General 發一則新訊息。", notifyMobileWeb: "在手機上請打開這個頁面查看等你處理的事。",
|
|
48
48
|
notifyInsecure: "通知需要安全的網址(localhost 或 HTTPS 公開連結),區網的一般 HTTP 網址不行。",
|
|
49
49
|
notifyUnsupported: "這個瀏覽器無法顯示通知。", turnOn: "開啟", turnOff: "關閉",
|
|
50
|
-
typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送",
|
|
50
|
+
typePrompt: "Fleet 提示", typeAwaiting: "在終端機上", typeInstance: "Instance", typeDelivery: "傳送", typeMissed: "離線時錯過",
|
|
51
51
|
},
|
|
52
52
|
});
|
|
53
53
|
const tn = (k, ...v) => t(`needs.${k}`, ...v);
|
|
54
|
+
/** Stale shells keep the old app-i18n.js; a missing key would otherwise show as app.needs_reason_*. */
|
|
55
|
+
function itemReason(item) {
|
|
56
|
+
const text = reasonText(item);
|
|
57
|
+
if (typeof text === "string" && text.startsWith("app.needs_reason_") && (item.type === "missed" || item.reason === "missed_offline")) {
|
|
58
|
+
return tn("typeMissed");
|
|
59
|
+
}
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
56
64
|
* The page's claims on what Needs you writes, kept for the life of the page — not of a panel or a render (#1463
|
|
@@ -86,7 +94,7 @@ const setClaim = (kind, key, value) => claims.set((c) => {
|
|
|
86
94
|
return { ...c, [kind]: next };
|
|
87
95
|
});
|
|
88
96
|
|
|
89
|
-
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks" };
|
|
97
|
+
const ICON = { prompt: "chat", awaiting_input: "clock", instance: "alert", delivery: "send", task: "tasks", missed: "clock" };
|
|
90
98
|
const BAD = new Set(["crashed", "delivery_failed", "hang", "exited"]);
|
|
91
99
|
|
|
92
100
|
/**
|
|
@@ -195,16 +203,17 @@ function NeedsItem({ item, nowMs, owner }) {
|
|
|
195
203
|
return html`<article data-trust="permission" class=${`n-item${BAD.has(item.reason) ? " bad" : ""}`}>
|
|
196
204
|
<span class="n-icon"><${Icon} name=${ICON[item.type] || "alert"} size=${18} /></span>
|
|
197
205
|
<div class="n-body">
|
|
198
|
-
<div class="n-title"><strong>${
|
|
206
|
+
<div class="n-title"><strong>${itemReason(item)}</strong><span class="n-age">${age(item.since, nowMs)}</span></div>
|
|
199
207
|
${detailOf(item) ? html`<p class="n-detail">${detailOf(item)}</p>` : null}
|
|
200
208
|
<div class="n-actions">
|
|
201
209
|
${item.type === "prompt" && Array.isArray(item.actions) ? item.actions.filter((a) => !a.input).map((a) => html`<button key=${a.id} type="button" class="btn btn-sm"
|
|
202
210
|
disabled=${promptBusy} onClick=${() => answer(a.id)}>${(p ? p.busy : fallback === "busy") ? tn("answering") : a.label}</button>`) : null}
|
|
203
|
-
${item.type === "delivery" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
211
|
+
${item.type === "delivery" || item.type === "missed" ? html`<button type="button" class="btn btn-sm" disabled=${!!ack} onClick=${acknowledge}>
|
|
204
212
|
<${Icon} name="check" size=${14} />${ack === "busy" ? tn("acknowledging") : tn("acknowledge")}</button>` : null}
|
|
205
213
|
${(item.reason === "auth_paused" || item.reason === "backend_signin") && item.signIn && signInOffered() ? html`<button type="button" class="btn btn-sm btn-primary" onClick=${() => openSignIn(item.signIn)}>${tn("signIn")}</button>` : null}
|
|
206
214
|
${item.type === "task" && item.card ? html`<a class="btn btn-sm" href=${`/ui/fleet?card=${encodeURIComponent(item.card.code)}`}>${tn("openCard")}</a>` : null}
|
|
207
|
-
${item.
|
|
215
|
+
${item.kiroChoice === true ? html`<a class="btn btn-sm" href="https://github.com/songsid/AgEnD/blob/main/src/general-knowledge/skills/kiro-engine-migration/SKILL.md" target="_blank" rel="noopener noreferrer">${tn("kiroMigration")}</a>` : null}
|
|
216
|
+
${item.instance ? html`<a class="btn btn-sm btn-ghost" href=${chatPath(item.instance)}>${tn(item.kiroChoice === true ? "kiroOpen" : "open")}</a>` : null}
|
|
208
217
|
</div>
|
|
209
218
|
</div>
|
|
210
219
|
</article>`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// #2910 D2: a web-only fleet's Notices (/ui/notices) — what General would have said: fleet errors, an agent's crash,
|
|
2
|
+
// quota or sign-in notice, a schedule that could not run, an available update. Behind the session gate (/ui/js/),
|
|
3
|
+
// loaded the first time it is opened. The list is the app store's `notices` (read from GET /ui/notices/list, then each
|
|
4
|
+
// new one from SSE `notice`); opening the page marks what it shows as read (POST /ui/notices/seen).
|
|
5
|
+
import { html, useEffect, useState } from "/assets/app-html.js";
|
|
6
|
+
import { t, register } from "/assets/app-i18n.js";
|
|
7
|
+
import { appStore, applyNoticesSeen, useStore } from "/assets/app-store.js";
|
|
8
|
+
import { useLease } from "/assets/app-ctx.js";
|
|
9
|
+
import { PanelHeader, setTitle } from "/assets/app-shell.js";
|
|
10
|
+
import { chatPath } from "/assets/app-route.js";
|
|
11
|
+
import { Empty } from "/assets/ui-states.js";
|
|
12
|
+
import { Icon } from "/assets/ui-icons.js";
|
|
13
|
+
|
|
14
|
+
register("notices", {
|
|
15
|
+
en: {
|
|
16
|
+
title: "Notices", sub: "What General would have said", empty: "No notices yet",
|
|
17
|
+
emptyHint: "This fleet has no Discord or Telegram connection, so what its General would say — errors, an agent that crashed or ran out of quota, a sign-in, an available update — is kept here.",
|
|
18
|
+
unread: "New", open: "Open chat",
|
|
19
|
+
kind_fleet: "Fleet", kind_instance: "Agent", kind_update: "Update", kind_schedule: "Schedule", kind_summary: "Daily summary",
|
|
20
|
+
ageNow: "just now", ageMin: "{0} min ago", ageHour: "{0} h ago", ageDay: "{0} d ago",
|
|
21
|
+
},
|
|
22
|
+
"zh-TW": {
|
|
23
|
+
title: "通知", sub: "原本會發到 General 的訊息", empty: "還沒有通知",
|
|
24
|
+
emptyHint: "這個 fleet 沒有 Discord 或 Telegram 連線,所以原本會由 General 說的事——錯誤、agent 當掉或額度用完、登入、新版本——都會留在這裡。",
|
|
25
|
+
unread: "新", open: "開啟聊天",
|
|
26
|
+
kind_fleet: "Fleet", kind_instance: "Agent", kind_update: "更新", kind_schedule: "排程", kind_summary: "每日摘要",
|
|
27
|
+
ageNow: "剛剛", ageMin: "{0} 分鐘前", ageHour: "{0} 小時前", ageDay: "{0} 天前",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const tn = (k, ...v) => t(`notices.${k}`, ...v);
|
|
31
|
+
const ICON = { fleet: "alert", instance: "bot", update: "download", schedule: "clock", summary: "chart" };
|
|
32
|
+
|
|
33
|
+
/** "3 min ago": a notice's age, refreshed by a display timer. */
|
|
34
|
+
export function noticeAge(at, nowMs) {
|
|
35
|
+
const minutes = Math.max(0, Math.floor((nowMs - at) / 60_000));
|
|
36
|
+
if (minutes < 1) return tn("ageNow");
|
|
37
|
+
if (minutes < 60) return tn("ageMin", minutes);
|
|
38
|
+
const hours = Math.floor(minutes / 60);
|
|
39
|
+
return hours < 24 ? tn("ageHour", hours) : tn("ageDay", Math.floor(hours / 24));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The highest id a mark was sent for (a second ask for it, while the first is out or after it landed, sends nothing). */
|
|
43
|
+
let markAsked = 0;
|
|
44
|
+
/** Everything up to `id` is read: the server's mark, then the store's (a failure leaves both as they were). */
|
|
45
|
+
export async function markNoticesSeen(id) {
|
|
46
|
+
if (id <= markAsked) return;
|
|
47
|
+
markAsked = id;
|
|
48
|
+
try {
|
|
49
|
+
const r = await fetch("/ui/notices/seen", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }) });
|
|
50
|
+
const d = r.ok ? await r.json() : null;
|
|
51
|
+
// The mark only moves forward, and unread is counted from what the page has (a notice that arrived meanwhile stays unread).
|
|
52
|
+
if (d && typeof d.seen === "number") { const cur = appStore.get().notices, next = applyNoticesSeen(cur, d.seen); if (next !== cur) appStore.set({ notices: next }); }
|
|
53
|
+
else markAsked = Math.min(markAsked, id - 1);
|
|
54
|
+
} catch { markAsked = Math.min(markAsked, id - 1); /* stays unread; the next view marks it */ }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The newest notice the page has, marked read — while the page is visible and it is not read yet. */
|
|
58
|
+
function markShown() {
|
|
59
|
+
const cur = appStore.get().notices, top = cur && cur.items.length ? cur.items[0].id : 0;
|
|
60
|
+
if (!top || top <= (cur.seen | 0)) return;
|
|
61
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
62
|
+
markNoticesSeen(top);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function NoticesPanel({ navKey }) {
|
|
66
|
+
const lease = useLease(navKey);
|
|
67
|
+
const { notices, instances } = useStore(appStore);
|
|
68
|
+
const items = notices && Array.isArray(notices.items) ? notices.items : [];
|
|
69
|
+
// What was unread when the page opened stays marked "New" while it is open (it is marked read on the server at once).
|
|
70
|
+
const [seenAtOpen] = useState(() => (notices ? notices.seen | 0 : 0));
|
|
71
|
+
const [, tick] = useState(0);
|
|
72
|
+
useEffect(() => { setTitle(tn("title")); const read = appStore.get().readNotices; if (read) read(); }, [navKey]);
|
|
73
|
+
useEffect(() => { lease.interval(() => tick((n) => n + 1), 30_000); }, [lease]);
|
|
74
|
+
// Shown → read: the newest one on the page, whenever a newer one arrives while it is open and visible.
|
|
75
|
+
const newest = items.length ? items[0].id : 0;
|
|
76
|
+
// …and, if one arrived while the tab was hidden, when it is shown again (#2925 review): one listener per visit.
|
|
77
|
+
useEffect(() => { markShown(); }, [newest]);
|
|
78
|
+
useEffect(() => { if (typeof document !== "undefined") lease.on(document, "visibilitychange", markShown); }, [lease]);
|
|
79
|
+
const known = new Set((instances || []).map((i) => i && i.name));
|
|
80
|
+
const nowMs = Date.now();
|
|
81
|
+
return html`<div data-effect-panel="notices" class="panel p-notices">
|
|
82
|
+
<${PanelHeader} title=${tn("title")} />
|
|
83
|
+
<div data-trust="panel-content" class="panel-body"><div class="col">
|
|
84
|
+
${items.length ? html`<p class="note notice-sub">${tn("sub")}</p>` : null}
|
|
85
|
+
${!items.length ? html`<${Empty} icon="bell" title=${tn("empty")} hint=${tn("emptyHint")} />`
|
|
86
|
+
: items.map((n) => html`<article key=${n.id} class=${`n-item notice${n.id > seenAtOpen ? " unread" : ""}${n.kind === "fleet" ? " bad" : ""}`}>
|
|
87
|
+
<span class="n-icon"><${Icon} name=${ICON[n.kind] || "info"} size=${18} /></span>
|
|
88
|
+
<div class="n-body">
|
|
89
|
+
<div class="n-title"><strong>${tn(`kind_${n.kind}`)}${n.instance ? ` · ${n.instance}` : ""}</strong>
|
|
90
|
+
${n.id > seenAtOpen ? html`<span class="notice-new">${tn("unread")}</span>` : null}
|
|
91
|
+
<span class="n-age" title=${new Date(n.at).toLocaleString()}>${noticeAge(n.at, nowMs)}</span></div>
|
|
92
|
+
<p class="n-detail notice-text">${n.text}</p>
|
|
93
|
+
${n.instance && known.has(n.instance) ? html`<div class="n-actions"><a class="btn btn-sm btn-ghost" href=${chatPath(n.instance)}>${tn("open")}</a></div>` : null}
|
|
94
|
+
</div>
|
|
95
|
+
</article>`)}
|
|
96
|
+
</div></div>
|
|
97
|
+
</div>`;
|
|
98
|
+
}
|
|
@@ -48,6 +48,7 @@ import "./chat-strings.js";
|
|
|
48
48
|
import { AppearanceSection } from "./settings-appearance.js";
|
|
49
49
|
import { GuestsCard } from "./settings-guests.js";
|
|
50
50
|
import { OfficeSharesCard } from "./office-share-dialog.js";
|
|
51
|
+
import { PageSharesCard } from "./page-share-dialog.js";
|
|
51
52
|
import { syncStage, useDraft } from "./settings-draft.js";
|
|
52
53
|
import { WebAuthorityCard } from "./settings-web-authority.js";
|
|
53
54
|
import { NamedLinkTest, setTypedTunnelToken } from "./settings-named-test.js";
|
|
@@ -1200,13 +1201,14 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1200
1201
|
log: web.log_tab === true, // #1218: off by default
|
|
1201
1202
|
usagePub: web.usage_report_on_public_link === true, // #2122: off by default
|
|
1202
1203
|
levels: web.office_levels !== false, // #2661: on by default
|
|
1204
|
+
cdn: web.preview_cdn === true, // #2911 (7): off by default
|
|
1203
1205
|
pub: { allow_public: (web.public_link && web.public_link.allow_public) ?? true, ttl_minutes: (web.public_link && web.public_link.ttl_minutes) ?? 120, protocol: (web.public_link && web.public_link.protocol) ?? "http2", hostname: (web.public_link && web.public_link.hostname) ?? "", tunnel_token_env: (web.public_link && web.public_link.tunnel_token_env) ?? "", origin_port: (web.public_link && web.public_link.origin_port) ?? 19282, auto_open: (web.public_link && web.public_link.auto_open) ?? false },
|
|
1204
1206
|
session: { absolute_hours: local.absolute_hours ?? null, idle_hours: local.idle_hours ?? null },
|
|
1205
1207
|
terminal: { enabled: term.enabled ?? null, ttl_minutes: term.ttl_minutes ?? null },
|
|
1206
1208
|
}), []);
|
|
1207
1209
|
const text = (v) => (v === null || v === undefined ? "" : String(v));
|
|
1208
1210
|
/** #2347: a field's row class: `s-dirty` while it differs from what is saved (the draft may come from before). */
|
|
1209
|
-
const savedForm = useMemo(() => ({ notify: String(original.notify), echo: original.echo, log: original.log, usagePub: original.usagePub, levels: original.levels, pub: original.pub.allow_public,
|
|
1211
|
+
const savedForm = useMemo(() => ({ notify: String(original.notify), echo: original.echo, log: original.log, usagePub: original.usagePub, levels: original.levels, cdn: original.cdn, pub: original.pub.allow_public,
|
|
1210
1212
|
ttl: String(original.pub.ttl_minutes), protocol: original.pub.protocol, hostname: original.pub.hostname, originPort: String(original.pub.origin_port),
|
|
1211
1213
|
abs: text(original.session.absolute_hours), idle: text(original.session.idle_hours), termOn: original.terminal.enabled !== false,
|
|
1212
1214
|
termTtl: text(original.terminal.ttl_minutes), autoOpen: original.pub.auto_open }), []);
|
|
@@ -1220,7 +1222,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1220
1222
|
return {
|
|
1221
1223
|
notify: String(pick(pending, "notify_login", original.notify)),
|
|
1222
1224
|
echo: pick(pending, "echo_to_channel", original.echo), log: pick(pending, "log_tab", original.log), usagePub: pick(pending, "usage_report_on_public_link", original.usagePub),
|
|
1223
|
-
levels: pick(pending, "office_levels", original.levels),
|
|
1225
|
+
levels: pick(pending, "office_levels", original.levels), cdn: pick(pending, "preview_cdn", original.cdn),
|
|
1224
1226
|
pub: pick(pub, "allow_public", original.pub.allow_public), ttl: String(pick(pub, "ttl_minutes", original.pub.ttl_minutes)), protocol: pick(pub, "protocol", original.pub.protocol),
|
|
1225
1227
|
hostname: pick(pub, "hostname", original.pub.hostname), originPort: String(pick(pub, "origin_port", original.pub.origin_port)), autoOpen: pick(pub, "auto_open", original.pub.auto_open),
|
|
1226
1228
|
abs: text(pick(session, "absolute_hours", original.session.absolute_hours)), idle: text(pick(session, "idle_hours", original.session.idle_hours)),
|
|
@@ -1273,6 +1275,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1273
1275
|
...(f.log !== original.log ? { log_tab: f.log } : {}),
|
|
1274
1276
|
...(f.usagePub !== original.usagePub ? { usage_report_on_public_link: f.usagePub } : {}),
|
|
1275
1277
|
...(f.levels !== original.levels ? { office_levels: f.levels } : {}),
|
|
1278
|
+
...(f.cdn !== original.cdn ? { preview_cdn: f.cdn } : {}),
|
|
1276
1279
|
...(Object.keys(publicPatch).length ? { public_link: publicPatch } : {}),
|
|
1277
1280
|
...(Object.keys(sessionPatch).length ? { session: { local: sessionPatch } } : {}),
|
|
1278
1281
|
...(Object.keys(terminalPatch).length ? { web_terminal: terminalPatch } : {}),
|
|
@@ -1283,6 +1286,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1283
1286
|
...(body.log_tab !== undefined ? ["web.log_tab"] : []),
|
|
1284
1287
|
...(body.usage_report_on_public_link !== undefined ? ["web.usage_report_on_public_link"] : []),
|
|
1285
1288
|
...(body.office_levels !== undefined ? ["web.office_levels"] : []),
|
|
1289
|
+
...(body.preview_cdn !== undefined ? ["web.preview_cdn"] : []),
|
|
1286
1290
|
...Object.keys(publicPatch).map((k) => `web.public_link.${k}`),
|
|
1287
1291
|
...Object.keys(sessionPatch).map((k) => `web.session.local.${k}`),
|
|
1288
1292
|
...Object.keys(terminalPatch).map((k) => `web_terminal.${k}`),
|
|
@@ -1365,6 +1369,7 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1365
1369
|
</section>`) : null}
|
|
1366
1370
|
${has("access") && !overPublicLink ? html`<${PublicTunnelToken} />` : null}
|
|
1367
1371
|
${has("access") ? html`<${OfficeSharesCard} />` : null}
|
|
1372
|
+
${has("access") ? html`<${PageSharesCard} />` : null}
|
|
1368
1373
|
${has("access") && !overPublicLink ? html`<${GuestsCard} ctx=${ctx} staged=${staged} /><${WebAuthorityCard} ctx=${ctx} staged=${staged} />` : null}
|
|
1369
1374
|
${has("chat") ? lock(html`<section class="card"><h3>${tn("webChat")}</h3>
|
|
1370
1375
|
<label class=${fc("check", "echo")}><input id="webEchoToChannel" type="checkbox" checked=${f.echo} onChange=${(e) => set("echo")(e.target.checked)} /><span>${tn("webEcho")}</span><${Impact} schema=${schema} field="web.echo_to_channel" /></label>
|
|
@@ -1377,6 +1382,8 @@ function WebSection({ ctx, staged, part = "all" }) {
|
|
|
1377
1382
|
<p class="note">${tn("webUsagePublicHint")}</p>
|
|
1378
1383
|
<label class=${fc("check", "levels")}><input id="webOfficeLevels" type="checkbox" checked=${f.levels} onChange=${(e) => set("levels")(e.target.checked)} /><span>${tn("webOfficeLevels")}</span><${Impact} schema=${schema} field="web.office_levels" /></label>
|
|
1379
1384
|
<p class="note">${tn("webOfficeLevelsHint")}</p>
|
|
1385
|
+
<label class=${fc("check", "cdn")}><input id="webPreviewCdn" type="checkbox" checked=${f.cdn} onChange=${(e) => set("cdn")(e.target.checked)} /><span>${tn("webPreviewCdn")}</span><${Impact} schema=${schema} field="web.preview_cdn" /></label>
|
|
1386
|
+
<p class="note">${tn("webPreviewCdnHint")}</p>
|
|
1380
1387
|
</section>`) : null}
|
|
1381
1388
|
${has("readonly") ? html`<section class="card s-web-readonly"><h3>${tn("webReadOnlyTitle")}</h3>
|
|
1382
1389
|
<p class="note">${tn("webReadOnlyLead")}</p>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// #2911 (6): every page in the thread — the list the preview panel shows. A page is what preview-versions.js counts as
|
|
2
|
+
// versions of one thing: the agent's complete ```html blocks with one <title> (the untitled ones together), or its
|
|
3
|
+
// attached .html files of one name. Pure: the panel draws it; nothing here runs a preview.
|
|
4
|
+
import "./chat-render.js";
|
|
5
|
+
import { htmlTitle } from "./preview-versions.js";
|
|
6
|
+
|
|
7
|
+
const R = () => globalThis.AgendChatRender;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The pages in `msgs` (in thread order), newest first: [{ id, label, kind: "html" | "file", versions, latest }], where
|
|
11
|
+
* `latest` is the newest version as the panel opens it ({ key, instance, msgKey, n | att, code, sender, ts }; an
|
|
12
|
+
* attached file's code is read when it is opened). `label` is the title, the file name, or "" for untitled blocks.
|
|
13
|
+
*/
|
|
14
|
+
export function pagesOf(msgs, keyOf, instance) {
|
|
15
|
+
const pages = new Map();
|
|
16
|
+
const add = (id, label, kind, spec) => {
|
|
17
|
+
const p = pages.get(id);
|
|
18
|
+
if (p) { p.versions++; p.latest = spec; pages.delete(id); pages.set(id, p); } // re-inserted: newest last
|
|
19
|
+
else pages.set(id, { id, label, kind, versions: 1, latest: spec });
|
|
20
|
+
};
|
|
21
|
+
for (const x of Array.isArray(msgs) ? msgs : []) {
|
|
22
|
+
if (x.role !== "agent") continue;
|
|
23
|
+
const msgKey = keyOf(x);
|
|
24
|
+
R().htmlFences(x.text).forEach((f, n) => {
|
|
25
|
+
if (!f.terminated) return;
|
|
26
|
+
const title = htmlTitle(f.code);
|
|
27
|
+
add(`t:${title}`, title, "html", { key: `${msgKey}:f${n}`, instance, msgKey, n, code: f.code, sender: x.sender, ts: x.ts });
|
|
28
|
+
});
|
|
29
|
+
for (const a of Array.isArray(x.attachments) ? x.attachments : []) {
|
|
30
|
+
if (!R().isHtmlAttachment(a)) continue;
|
|
31
|
+
add(`a:${a.name.trim().toLowerCase()}`, a.name, "file", { key: `${msgKey}:a${a.id}`, instance, msgKey, att: { id: a.id, name: a.name, size: a.size }, code: null, sender: x.sender, ts: x.ts });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...pages.values()].reverse();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The page `open` belongs to: its id in pagesOf's terms. */
|
|
38
|
+
export function pageIdOf(open) {
|
|
39
|
+
if (!open) return null;
|
|
40
|
+
return open.att ? `a:${String(open.att.name || "").trim().toLowerCase()}` : `t:${htmlTitle(open.code)}`;
|
|
41
|
+
}
|