@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli-instructions.md +6 -2
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +3 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +16 -4
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +17 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +126 -18
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +27 -1
- package/dist/backend/kiro.js +63 -10
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +48 -2
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +26 -1
- package/dist/channel/adapters/discord.js +103 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +35 -3
- package/dist/channel/adapters/telegram.js +122 -23
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +6 -5
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +46 -4
- package/dist/cli.js +184 -34
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +129 -1
- package/dist/daemon.js +754 -94
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +64 -8
- package/dist/delivery-outbox.js +315 -24
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -1
- package/dist/fleet-manager.d.ts +392 -19
- package/dist/fleet-manager.js +2030 -275
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +12 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +76 -27
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +3 -3
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- package/dist/kiro-log-gate.d.ts +42 -0
- package/dist/kiro-log-gate.js +39 -0
- package/dist/kiro-log-gate.js.map +1 -0
- package/dist/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +268 -42
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +99 -0
- package/dist/log-service.js +243 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +169 -0
- package/dist/log-steps.js +858 -0
- package/dist/log-steps.js.map +1 -0
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +277 -93
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +30 -15
- package/dist/outbound-schemas.js +23 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +101 -0
- package/dist/public-preview.js +236 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.d.ts +2 -0
- package/dist/public-web-gateway.js +20 -6
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +24 -1
- package/dist/public-web-link.js +63 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +89 -5
- package/dist/reply-buttons-controller.js +273 -8
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +33 -2
- package/dist/restart-progress.js +30 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +55 -0
- package/dist/service-installer.js +90 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +14 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +4 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tips.js +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tool-permissions.js +3 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +71 -4
- package/dist/topic-commands.js +167 -34
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +85 -3
- package/dist/transcript-jsonl.js +156 -13
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +11 -0
- package/dist/transcript-monitor.js +61 -13
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +24 -0
- package/dist/transcript-sources.js +211 -56
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +22 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +80 -10
- package/dist/ui/chat-store.js +70 -6
- package/dist/ui/chat-strings.js +62 -32
- package/dist/ui/chat-thread.js +199 -24
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/chat-tour.js +17 -14
- package/dist/ui/panel-chat.js +45 -21
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +213 -58
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +14 -10
- package/dist/ui/panel-settings.js +82 -17
- package/dist/ui/preview-panel.js +55 -15
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +44 -3
- package/dist/ui/settings-dialogs.js +11 -6
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-strings.js +32 -12
- package/dist/ui/settings-token.js +8 -1
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/app-i18n.js +41 -15
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +6 -1
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +26 -5
- package/dist/ui/shared/app.css +170 -30
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +83 -13
- package/dist/ui/shared/instance-nav.js +178 -51
- package/dist/ui/shared/instance-switch.js +8 -5
- package/dist/ui/shared/panel-view.js +21 -4
- package/dist/ui/shared/signin.js +15 -9
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +40 -4
- package/dist/ui/signin.html +8 -3
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +13 -1
- package/dist/update-install.js +16 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +62 -13
- package/dist/usage/providers.js +256 -68
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.js +31 -9
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +82 -4
- package/dist/web-api.js +632 -88
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +5 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +26 -1
- package/dist/web-chat-history.js +47 -8
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +41 -5
- package/dist/web-commands.js +25 -7
- package/dist/web-commands.js.map +1 -1
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +81 -0
- package/dist/web-prefs.js +297 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +11 -1
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -0
- package/dist/web-shell-routes.js +9 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +135 -7
- package/dist/web-upload.js +315 -51
- package/dist/web-upload.js.map +1 -1
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/ui/chat-thread.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import "./chat-render.js";
|
|
10
10
|
import "./preview.js";
|
|
11
11
|
import { loadHtmlAttachment } from "./html-attachment.js";
|
|
12
|
+
import { chatTime, chatTimeFull, onDayChange } from "./chat-time.js";
|
|
12
13
|
|
|
13
14
|
const R = () => globalThis.AgendChatRender;
|
|
14
15
|
/** #1589: the public link shows attachments as downloads only (its page says so: <body data-public-link="1">). */
|
|
@@ -22,16 +23,25 @@ export const TEXT_PREVIEW_NODES = 4000;
|
|
|
22
23
|
/** A csv/tsv wider than this is shown as text, as Markdown tables are capped at 20 columns. */
|
|
23
24
|
export const TEXT_PREVIEW_COLUMNS = 50;
|
|
24
25
|
const P = () => globalThis.AgendPreview;
|
|
26
|
+
/** #1672: a status line longer than this (lines, or characters) is folded until it is opened. */
|
|
27
|
+
export const STATUS_FOLD_LINES = 6;
|
|
28
|
+
export const STATUS_FOLD_CHARS = 600;
|
|
25
29
|
const COPY = '<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
|
|
26
30
|
|
|
27
31
|
/** Escapes & " ' < >: safe for text and inside a quoted attribute. */
|
|
28
32
|
export function escAttr(s) { return String(s == null ? "" : s).replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">"); }
|
|
29
33
|
export const msgKey = (x) => `${x.boot}-${x.id}`;
|
|
34
|
+
/**
|
|
35
|
+
* Board 00ef2430: a thread opens with only its newest messages drawn — enough for a screen — so switching chats paints
|
|
36
|
+
* at once; the older ones are drawn after that, a chunk at a time between frames, the reader's place kept.
|
|
37
|
+
*/
|
|
38
|
+
export const THREAD_TAIL = 24;
|
|
39
|
+
export const THREAD_CHUNK = 40;
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* Bind a renderer to `list` (the thread element) inside `scroller`. opts: { t, tf, isUser(x), onJump({show, unseen}),
|
|
33
43
|
* setPreviewOptIn(on), copyText(text) → Promise<boolean>, download(code), panel? }. panel (#1481): { shown() → the card
|
|
34
|
-
* key the side panel shows, or null; open(spec, { run }) } — without it a card has no "Open in panel".
|
|
44
|
+
* key the side panel shows, or null; open(spec, { run }); version(); claim(); claims() } — without it a card has no "Open in panel".
|
|
35
45
|
*/
|
|
36
46
|
export function createThread(list, scroller, opts) {
|
|
37
47
|
const nodes = new Map(); // key → { html, node }
|
|
@@ -43,20 +53,68 @@ export function createThread(list, scroller, opts) {
|
|
|
43
53
|
// moves down or stays), and those must not count as the reader leaving the bottom. Reaching the bottom pins again.
|
|
44
54
|
let pinned = true;
|
|
45
55
|
let lastTop = 0;
|
|
56
|
+
// The oldest message drawn (by key): everything after it is drawn, so new messages never push the window. Null: none
|
|
57
|
+
// drawn yet — the next render draws the newest THREAD_TAIL and the rest follows (grow).
|
|
58
|
+
let firstKey = null;
|
|
59
|
+
let growTimer = null, growFrame = null, disposed = false;
|
|
46
60
|
const cardRefresh = new WeakMap(); // card node → re-read availability (weak: a card that left takes its source with it)
|
|
47
61
|
const tr = opts.t, trf = opts.tf;
|
|
48
62
|
const stopIn = (node) => (P() ? P().stopIn(node) : []);
|
|
63
|
+
/** #1770: write a message node's time label for now; unchanged text is not touched. */
|
|
64
|
+
function timeLabel(node) {
|
|
65
|
+
const el = node.querySelector(".meta .time[data-ts]");
|
|
66
|
+
if (!el) return;
|
|
67
|
+
const v = chatTime(el.dataset.ts, { yesterday: (hm) => trf("chat.yesterdayAt", hm) });
|
|
68
|
+
if (el.textContent !== v) el.textContent = v;
|
|
69
|
+
}
|
|
70
|
+
// #1770: "today" ends at the fleet's midnight; the labels are rewritten then (and on every render), while there are any.
|
|
71
|
+
let offDay = null;
|
|
72
|
+
function armDay() {
|
|
73
|
+
if (nodes.size && !offDay) offDay = onDayChange(() => { for (const v of nodes.values()) timeLabel(v.node); });
|
|
74
|
+
else if (!nodes.size && offDay) { offDay(); offDay = null; }
|
|
75
|
+
}
|
|
49
76
|
|
|
50
77
|
const TICKS = () => ({ queued: tr("chat.tick_queued"), processing: tr("chat.tick_processing"), delivered: tr("chat.tick_delivered"), failed: tr("chat.tick_failed"), cancelled: tr("chat.tick_cancelled") });
|
|
51
78
|
|
|
52
79
|
function msgHtml(x) {
|
|
80
|
+
// #1770: the label ("17:38", "Yesterday 17:38", "10/11 17:38") depends on today, so it is not part of the html a node
|
|
81
|
+
// is kept by: it is written into the node (timeLabels) — crossing midnight relabels in place and reloads no preview.
|
|
82
|
+
const time = x.ts ? `<span class="time" data-ts="${escAttr(x.ts)}" title="${escAttr(chatTimeFull(x.ts))}"></span>` : `<span class="time"></span>`;
|
|
83
|
+
if (x.role === "status") return statusHtml(x, time);
|
|
53
84
|
const u = opts.isUser(x);
|
|
54
|
-
const time = x.ts ? new Date(x.ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "";
|
|
55
85
|
// renderMarkdown escapes the whole text before it adds any tag of its own (chat-render.js).
|
|
56
86
|
const ticks = u && x.delivery ? R().deliveryHtml(x.delivery, TICKS()) : "";
|
|
57
87
|
const tools = u ? "" : `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
58
88
|
const buttons = x.role === "agent" ? replyButtonsHtml(x.buttons) : "";
|
|
59
|
-
|
|
89
|
+
// A person's click on a reply button shows as the button's label, never the line the agent was given (its value).
|
|
90
|
+
const clicked = x.role === "user" && x.click && typeof x.click.label === "string";
|
|
91
|
+
return `<div class="msg ${u ? "user" : "agent"}"><div class="meta"><span class="sender">${escAttr(x.sender)}</span>${time}${ticks}</div><div class="body">${clicked ? clickHtml(x.click) : `<div class="md">${R().renderMarkdown(x.text, { htmlCards: x.role === "agent", emojis: opts.emojis, mentions: opts.mentions })}</div>`}${R().attachmentsHtml(x.attachments, { htmlCards: x.role === "agent", goneTitle: tr("chat.attGone"), goneLabel: tr("chat.attUnavailable"), inlinePreviews: !PUBLIC_LINK() })}${buttons}</div>${tools}</div>`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A button click as a chip: 🔘 and the label (escaped: the label is the agent's text). With `of` (live, while its
|
|
95
|
+
* reply is in this thread) it is a button that shows that reply; restored from disk, plain.
|
|
96
|
+
*/
|
|
97
|
+
function clickHtml(click) {
|
|
98
|
+
const inner = `<span class="click-mark" aria-hidden="true">🔘</span><span class="sr-only">${escAttr(tr("chat.clicked"))} </span>${escAttr(click.label)}`;
|
|
99
|
+
const of = typeof click.of === "string" && /^[0-9a-f]{32}$/.test(click.of) ? click.of : null;
|
|
100
|
+
return `<div class="click-chip">${of
|
|
101
|
+
? `<button type="button" class="click-link" data-act="showButtons" data-arg="${escAttr(of)}" title="${escAttr(tr("chat.clickShow"))}">${inner}</button>`
|
|
102
|
+
: `<span class="click-text">${inner}</span>`}</div>`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* #1672: a status line (another instance's message, a daemon notice) is muted, read-only plain text — never Markdown,
|
|
106
|
+
* a preview card, a button or a person's bubble. Checked before isUser, which reads the sender: a fleet name need not
|
|
107
|
+
* start with "agend". A long one is folded until it is opened.
|
|
108
|
+
*/
|
|
109
|
+
function statusHtml(x, time) {
|
|
110
|
+
const text = String(x.text == null ? "" : x.text);
|
|
111
|
+
const lines = R().lineCount(text);
|
|
112
|
+
const manyLines = lines > STATUS_FOLD_LINES;
|
|
113
|
+
const more = manyLines || text.length > STATUS_FOLD_CHARS
|
|
114
|
+
? `<button type="button" class="st-more" data-act="toggleFold"${manyLines ? ` data-lines="${escAttr(lines)}"` : ""}>${escAttr(manyLines ? trf("chat.showAll", lines) : tr("chat.showMore"))}</button>`
|
|
115
|
+
: "";
|
|
116
|
+
const tools = `<div class="msg-tools"><button type="button" class="chip-btn icon-only" data-act="copyMsg" data-arg="${escAttr(msgKey(x))}" title="${escAttr(tr("chat.copyMessage"))}" aria-label="${escAttr(tr("chat.copyMessage"))}">${COPY}</button></div>`;
|
|
117
|
+
return `<div class="msg status-line${more ? " folded" : ""}"><div class="meta">${time}</div><div class="body"><div class="st-text">${escAttr(text)}</div>${more}</div>${tools}</div>`;
|
|
60
118
|
}
|
|
61
119
|
/**
|
|
62
120
|
* #1266: an agent reply's buttons. Labels are text (escaped, never Markdown); the click names the set and the index,
|
|
@@ -64,6 +122,7 @@ export function createThread(list, scroller, opts) {
|
|
|
64
122
|
*/
|
|
65
123
|
function replyButtonsHtml(b) {
|
|
66
124
|
if (!b || typeof b.id !== "string" || !Array.isArray(b.labels) || !b.labels.length) return "";
|
|
125
|
+
if (b.poll && typeof b.poll === "object") return pollHtml(b);
|
|
67
126
|
const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
|
|
68
127
|
const off = b.state !== "open" || busy;
|
|
69
128
|
const items = b.labels.map((label, i) => {
|
|
@@ -73,11 +132,51 @@ export function createThread(list, scroller, opts) {
|
|
|
73
132
|
const note = b.state === "chosen" ? trf("chat.rbChosen", b.by || "") : b.state === "expired" ? tr("chat.rbExpired") : "";
|
|
74
133
|
return `<div class="rb-row" role="group" aria-label="${escAttr(tr("chat.rbGroup"))}">${items}${note ? `<span class="rb-note">${escAttr(note)}</span>` : ""}</div>`;
|
|
75
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* #1823: a poll. Anyone here may click while it is open; the page marks its own vote (opts.pollMine), which no one else
|
|
137
|
+
* sees. Counts only when the server sends them (revealed: live, or closed); names only at close with show_voters. The
|
|
138
|
+
* time left is not in the html (a node is kept by its html): pollLeft writes it, every second.
|
|
139
|
+
*/
|
|
140
|
+
function pollHtml(b) {
|
|
141
|
+
const p = b.poll, closed = b.state === "closed";
|
|
142
|
+
const busy = !!(opts.replyButtonBusy && opts.replyButtonBusy(b.id));
|
|
143
|
+
const mine = (opts.pollMine && opts.pollMine(b.id)) || [];
|
|
144
|
+
const counts = Array.isArray(p.counts) && p.counts.length === b.labels.length ? p.counts : null;
|
|
145
|
+
const items = b.labels.map((label, i) => {
|
|
146
|
+
const me = !closed && mine.includes(i);
|
|
147
|
+
const tally = counts ? ` · ${counts[i]}${closed && p.answered ? ` (${Math.round((counts[i] / p.answered) * 100)}%)` : ""}` : "";
|
|
148
|
+
return `<button type="button" class="btn btn-sm rb-btn${me ? " chosen" : ""}" data-act="replyButton" data-arg="${escAttr(`${b.id}:${i}`)}" aria-pressed="${me ? "true" : "false"}"${closed || busy ? " disabled" : ""}>${escAttr(`${me ? "✓ " : ""}${label}${tally}`)}</button>`;
|
|
149
|
+
}).join("");
|
|
150
|
+
const status = closed
|
|
151
|
+
? escAttr(trf("chat.pollClosed", p.answered))
|
|
152
|
+
: `${escAttr(trf("chat.pollAnswered", p.answered))} · ⏳ <span class="poll-left" data-deadline="${escAttr(p.deadline)}" title="${escAttr(tr("chat.pollLeft"))}"></span>`;
|
|
153
|
+
const voters = closed && Array.isArray(p.voters)
|
|
154
|
+
? `<ul class="poll-voters" aria-label="${escAttr(tr("chat.pollVoters"))}">${b.labels.map((label, i) => `<li>${escAttr(`${label}: ${(p.voters[i] || []).join(", ") || "—"}`)}</li>`).join("")}</ul>`
|
|
155
|
+
: "";
|
|
156
|
+
return `<div class="rb-row poll" role="group" aria-label="${escAttr(tr("chat.pollGroup"))}">${items}<span class="rb-note">${status}</span></div>${voters}`;
|
|
157
|
+
}
|
|
158
|
+
/** #1823: the time left on this node's open polls, now. */
|
|
159
|
+
function pollLeft(node) {
|
|
160
|
+
for (const el of node.querySelectorAll(".poll-left[data-deadline]")) {
|
|
161
|
+
const v = R().pollRemaining(Number(el.dataset.deadline) - (opts.serverNow ? opts.serverNow() : Date.now()));
|
|
162
|
+
if (el.textContent !== v) el.textContent = v;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// #1823: while an open poll is on screen, its time left is rewritten every second.
|
|
166
|
+
let pollTick = null;
|
|
167
|
+
function armPolls() {
|
|
168
|
+
const open = nodes.size > 0 && !!list.querySelector(".poll-left[data-deadline]");
|
|
169
|
+
if (open && !pollTick) pollTick = setInterval(() => pollLeft(list), 1000);
|
|
170
|
+
else if (!open && pollTick) { clearInterval(pollTick); pollTick = null; }
|
|
171
|
+
}
|
|
172
|
+
|
|
76
173
|
function msgNode(html, x, stoppedKeys) {
|
|
77
174
|
const tpl = list.ownerDocument.createElement("template");
|
|
78
175
|
tpl.innerHTML = html;
|
|
79
176
|
const node = tpl.content.firstElementChild;
|
|
80
177
|
decorateCode(node);
|
|
178
|
+
timeLabel(node);
|
|
179
|
+
pollLeft(node);
|
|
81
180
|
if (x) decorateHtmlCards(node, x, stoppedKeys || []);
|
|
82
181
|
if (x) decorateTextCards(node, x);
|
|
83
182
|
return node;
|
|
@@ -299,14 +398,32 @@ export function createThread(list, scroller, opts) {
|
|
|
299
398
|
};
|
|
300
399
|
const ui = { state: (name, reason) => { state = name === "starting" || name === "running" ? name : "idle"; refresh(reason || null); } };
|
|
301
400
|
// Nothing is read for Preview on a device that does not allow previews: the button is not even shown there.
|
|
302
|
-
|
|
401
|
+
// A Preview click is newer than any Open in panel still reading (#1704): it claims the panel's turn too — and, its
|
|
402
|
+
// own HTML read, starts only if no newer click claimed meanwhile and no preview was started since (the panel's own
|
|
403
|
+
// Preview): a late read never undoes a newer choice. A close is neither, so it does not drop this click.
|
|
404
|
+
run.onclick = () => {
|
|
405
|
+
if (!P().availability().ok || reading) return; // this card already reading: that click stays the one
|
|
406
|
+
const mine = panel ? (panel.claim(), panel.claims()) : 0, started = P().startCount();
|
|
407
|
+
withCode((c) => {
|
|
408
|
+
if ((panel && panel.claims() !== mine) || P().startCount() !== started) { refresh(); return; }
|
|
409
|
+
P().start(key, holder, c, ui);
|
|
410
|
+
});
|
|
411
|
+
};
|
|
303
412
|
stopB.onclick = () => { if (reading) { reading = 0; refresh(); } else P().stop(key, "stopped", ""); };
|
|
304
|
-
//
|
|
305
|
-
//
|
|
413
|
+
// #1704: this click runs it in the panel, as Preview there would — a running preview moves (it stops here and
|
|
414
|
+
// starts there), an idle one starts there. Whether it can run is the panel's to say: a device that has not allowed
|
|
415
|
+
// previews is offered Allow first (and allowing runs it); where previews are off, nothing runs.
|
|
416
|
+
// An attachment's HTML is read first. The click claims the panel at once, so a newer click (even one still reading)
|
|
417
|
+
// wins: if the panel changed (another click, block, close) or any preview started meanwhile, this one is dropped —
|
|
418
|
+
// a late read never undoes a newer click.
|
|
306
419
|
if (toPanel) toPanel.onclick = () => {
|
|
307
|
-
|
|
308
|
-
if (
|
|
309
|
-
|
|
420
|
+
if (reading) return; // this card already reading: that click stays the one
|
|
421
|
+
if (P().running(key)) P().stop(key, "stopped", "");
|
|
422
|
+
const at = panel.claim(), started = P().startCount();
|
|
423
|
+
withCode((c) => {
|
|
424
|
+
if (panel.version() !== at || P().startCount() !== started) { refresh(); return; }
|
|
425
|
+
panel.open({ key, instance: x.instance, msgKey: msgKey(x), ...(att ? { att } : { n: source.n }), code: c, sender: x.sender, ts: x.ts }, { run: true });
|
|
426
|
+
});
|
|
310
427
|
};
|
|
311
428
|
if (dl) dl.onclick = () => opts.download(code);
|
|
312
429
|
ack.onclick = () => P().ackBanner(); // every card and the panel hear it (onChange)
|
|
@@ -334,6 +451,44 @@ export function createThread(list, scroller, opts) {
|
|
|
334
451
|
const ro = typeof ResizeObserver === "function" ? new ResizeObserver(resized) : null;
|
|
335
452
|
if (ro) { ro.observe(list); ro.observe(scroller); }
|
|
336
453
|
|
|
454
|
+
/** Where drawing starts in `msgs`: at the oldest drawn message, or (none yet) the newest THREAD_TAIL. */
|
|
455
|
+
function startIndex() {
|
|
456
|
+
const i = firstKey === null ? -1 : msgs.findIndex((x) => msgKey(x) === firstKey);
|
|
457
|
+
// Whatever is drawn stays drawn: the list may have reordered (merged across fleet restarts) or lost its oldest to
|
|
458
|
+
// the cap — so the window starts at the earlier of the oldest drawn key and the first message still drawn.
|
|
459
|
+
const still = nodes.size ? msgs.findIndex((x) => nodes.has(msgKey(x))) : -1;
|
|
460
|
+
const at = [i, still].filter((n) => n >= 0);
|
|
461
|
+
return at.length ? Math.min(...at) : Math.max(0, msgs.length - THREAD_TAIL);
|
|
462
|
+
}
|
|
463
|
+
/** Draw the next chunk of older messages, keeping the reader where they were; then the next, until all are drawn. */
|
|
464
|
+
function grow() {
|
|
465
|
+
growTimer = growFrame = null;
|
|
466
|
+
if (disposed) return; // the chat left: nothing more is drawn
|
|
467
|
+
const at = startIndex();
|
|
468
|
+
if (at <= 0) return;
|
|
469
|
+
firstKey = msgKey(msgs[Math.max(0, at - THREAD_CHUNK)]);
|
|
470
|
+
const fromBottom = scroller.scrollHeight - scroller.scrollTop;
|
|
471
|
+
draw({ older: true }); // drawn above the reader: never "new below" (render alone counts those)
|
|
472
|
+
if (pinned) toBottom();
|
|
473
|
+
else { scroller.scrollTop = scroller.scrollHeight - fromBottom; lastTop = scroller.scrollTop; }
|
|
474
|
+
scheduleGrow();
|
|
475
|
+
}
|
|
476
|
+
/** After the next frame has painted (the click's own frame stays light), draw a chunk more. */
|
|
477
|
+
function scheduleGrow() {
|
|
478
|
+
if (growTimer !== null || growFrame !== null || startIndex() <= 0) return;
|
|
479
|
+
const later = () => { growFrame = null; growTimer = setTimeout(grow, 0); };
|
|
480
|
+
if (typeof requestAnimationFrame === "function") growFrame = requestAnimationFrame(later); else later();
|
|
481
|
+
}
|
|
482
|
+
/** Everything drawn now (a remembered scroll position, a message to show that is not drawn yet). */
|
|
483
|
+
function growAll() {
|
|
484
|
+
if (startIndex() <= 0) return;
|
|
485
|
+
if (growFrame !== null && typeof cancelAnimationFrame === "function") cancelAnimationFrame(growFrame);
|
|
486
|
+
if (growTimer !== null) clearTimeout(growTimer);
|
|
487
|
+
growTimer = growFrame = null;
|
|
488
|
+
firstKey = msgs.length ? msgKey(msgs[0]) : null;
|
|
489
|
+
draw({ older: true });
|
|
490
|
+
}
|
|
491
|
+
|
|
337
492
|
/** Draw `next` (this instance's messages). `restore`: the scroll position to return to (null = the bottom). */
|
|
338
493
|
function render(next, renderOpts = {}) {
|
|
339
494
|
const stick = pinned || nearBottom();
|
|
@@ -341,35 +496,51 @@ export function createThread(list, scroller, opts) {
|
|
|
341
496
|
if (!msgs.length) {
|
|
342
497
|
for (const v of nodes.values()) stopIn(v.node);
|
|
343
498
|
nodes.clear();
|
|
499
|
+
firstKey = null;
|
|
500
|
+
armDay();
|
|
344
501
|
list.textContent = "";
|
|
345
502
|
opts.onEmpty(true);
|
|
346
503
|
return;
|
|
347
504
|
}
|
|
348
505
|
opts.onEmpty(false);
|
|
349
|
-
|
|
506
|
+
// A position to return to that is not the bottom needs the messages above it: all of them, at once (as before).
|
|
507
|
+
if ("restore" in renderOpts && renderOpts.restore != null) firstKey = msgKey(msgs[0]);
|
|
508
|
+
const added = draw({});
|
|
509
|
+
if (firstKey === null || startIndex() > 0) { firstKey = msgKey(msgs[startIndex()]); scheduleGrow(); }
|
|
510
|
+
if ("restore" in renderOpts) {
|
|
511
|
+
unseen = 0;
|
|
512
|
+
if (renderOpts.restore == null) toBottom();
|
|
513
|
+
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
514
|
+
} else if (stick) toBottom();
|
|
515
|
+
else unseen += added;
|
|
516
|
+
reportJump();
|
|
517
|
+
}
|
|
518
|
+
/** Put the messages from startIndex() on in the list (nodes reused when unchanged). Returns how many are new. */
|
|
519
|
+
function draw(drawOpts) {
|
|
520
|
+
const shown = msgs.slice(startIndex());
|
|
521
|
+
const keep = new Set(shown.map(msgKey));
|
|
350
522
|
for (const [k, v] of nodes) if (!keep.has(k)) { stopIn(v.node); v.node.remove(); nodes.delete(k); }
|
|
351
523
|
let prev = null, added = 0;
|
|
352
|
-
for (const x of
|
|
353
|
-
const key = msgKey(x)
|
|
524
|
+
for (const x of shown) {
|
|
525
|
+
const key = msgKey(x);
|
|
354
526
|
let have = nodes.get(key);
|
|
527
|
+
// Filling in older messages: a node drawn from this very message object is current — not drawn again.
|
|
528
|
+
if (drawOpts.older && have && have.x === x) { prev = have.node; continue; }
|
|
529
|
+
const html = msgHtml(x);
|
|
355
530
|
if (!have || have.html !== html) {
|
|
356
531
|
const stopped = have ? stopIn(have.node) : [];
|
|
357
532
|
const node = msgNode(html, x, stopped);
|
|
358
533
|
if (have) have.node.replaceWith(node); else added++;
|
|
359
|
-
have = { html, node }; nodes.set(key, have);
|
|
360
|
-
}
|
|
534
|
+
have = { html, node, x }; nodes.set(key, have);
|
|
535
|
+
} else { have.x = x; timeLabel(have.node); }
|
|
361
536
|
const want = prev ? prev.nextSibling : list.firstChild;
|
|
362
537
|
// A node already in the list is being moved: its preview stops first. A new node (still in its template) has none.
|
|
363
538
|
if (have.node !== want) { if (have.node.parentNode === list) stopIn(have.node); list.insertBefore(have.node, want); }
|
|
364
539
|
prev = have.node;
|
|
365
540
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
else { scroller.scrollTop = renderOpts.restore; pinned = nearBottom(); lastTop = scroller.scrollTop; }
|
|
370
|
-
} else if (stick) toBottom();
|
|
371
|
-
else unseen += added;
|
|
372
|
-
reportJump();
|
|
541
|
+
armDay();
|
|
542
|
+
armPolls();
|
|
543
|
+
return added;
|
|
373
544
|
}
|
|
374
545
|
|
|
375
546
|
function onScroll() {
|
|
@@ -384,6 +555,7 @@ export function createThread(list, scroller, opts) {
|
|
|
384
555
|
function jumpLatest() { toBottom(); unseen = 0; reportJump(); }
|
|
385
556
|
/** Show one message (the panel's "from …" link, #1481): scrolled into view and briefly marked. */
|
|
386
557
|
function reveal(key) {
|
|
558
|
+
if (!nodes.has(key) && msgs.some((x) => msgKey(x) === key)) growAll(); // an older message not drawn yet
|
|
387
559
|
const have = nodes.get(key); if (!have) return false;
|
|
388
560
|
if (typeof have.node.scrollIntoView === "function") have.node.scrollIntoView({ block: "center" });
|
|
389
561
|
have.node.classList.add("flash");
|
|
@@ -426,21 +598,24 @@ export function createThread(list, scroller, opts) {
|
|
|
426
598
|
if (act === "copyMsg") { const x = msgs.find(y => msgKey(y) === el.dataset.arg); if (x) copyFrom(el, x.text); }
|
|
427
599
|
else if (act === "copyCode") { const pre = el.closest(".codeblock")?.querySelector("pre"); if (pre) copyFrom(el, pre.textContent); }
|
|
428
600
|
else if (act === "toggleFold") {
|
|
429
|
-
const box = el.closest(".codeblock"); if (!box) return;
|
|
601
|
+
const box = el.closest(".codeblock, .msg.status-line"); if (!box) return;
|
|
430
602
|
const folded = box.classList.toggle("folded");
|
|
431
|
-
el.textContent = folded ? trf("chat.showAll", el.dataset.lines) : tr("chat.
|
|
603
|
+
el.textContent = !folded ? tr("chat.showLess") : el.dataset.lines ? trf("chat.showAll", el.dataset.lines) : tr("chat.showMore");
|
|
432
604
|
} else if (act === "toggleWrap") opts.toggleWrap();
|
|
433
605
|
else if (act === "replyButton") {
|
|
434
606
|
const m = /^([0-9a-f]{32}):(\d{1,2})$/.exec(el.dataset.arg || "");
|
|
435
607
|
if (m && opts.clickReplyButton) opts.clickReplyButton(m[1], Number(m[2]));
|
|
436
|
-
}
|
|
608
|
+
} else if (act === "showButtons") { const x = msgs.find(y => y.buttons && y.buttons.id === el.dataset.arg); if (x) reveal(msgKey(x)); }
|
|
437
609
|
}
|
|
438
610
|
|
|
439
611
|
/** The thread goes (the chat left, or another instance opened): every preview in it stops first. */
|
|
440
612
|
function dispose() {
|
|
613
|
+
disposed = true;
|
|
441
614
|
if (ro) ro.disconnect();
|
|
442
615
|
if (P()) P().stopAll("leave");
|
|
443
616
|
nodes.clear();
|
|
617
|
+
armDay();
|
|
618
|
+
armPolls();
|
|
444
619
|
}
|
|
445
620
|
|
|
446
621
|
return { render, onScroll, onClick, jumpLatest, reveal, refreshCards, dispose, nodes, get unseen() { return unseen; } };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// #1770: the time a chat line shows, read in the fleet's zone (the one chat-logs, fleet.log and the #1586 DMs use) —
|
|
2
|
+
// "17:38" today, "Yesterday 17:38", "10/11 17:38" earlier this year, "2025/10/11 17:38" before. The hour and minute keep
|
|
3
|
+
// the browser's own format (12- or 24-hour), as they always had. No imports: the thread, the preview panel and the
|
|
4
|
+
// tests read it as it is.
|
|
5
|
+
|
|
6
|
+
const cache = new Map();
|
|
7
|
+
/** One formatter per (kind, zone, locale): a thread may label 500 lines on every render. */
|
|
8
|
+
function fmt(kind, tz, locale) {
|
|
9
|
+
const key = `${kind}|${tz || ""}|${locale || ""}`;
|
|
10
|
+
let f = cache.get(key);
|
|
11
|
+
if (!f) {
|
|
12
|
+
const o = kind === "hm" ? { hour: "2-digit", minute: "2-digit" }
|
|
13
|
+
: kind === "ymd" ? { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", second: "numeric", hourCycle: "h23" }
|
|
14
|
+
: { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", timeZoneName: "short" };
|
|
15
|
+
f = new Intl.DateTimeFormat(kind === "ymd" ? "en-US" : (locale || []), { ...o, timeZone: tz || undefined });
|
|
16
|
+
cache.set(key, f);
|
|
17
|
+
}
|
|
18
|
+
return f;
|
|
19
|
+
}
|
|
20
|
+
/** `tz` when this browser knows it, else undefined (the browser's own zone): a label must still be drawn. */
|
|
21
|
+
function known(tz) {
|
|
22
|
+
if (!tz) return undefined;
|
|
23
|
+
try { fmt("ymd", tz); return tz; } catch { return undefined; }
|
|
24
|
+
}
|
|
25
|
+
/** The fleet's zone, as the page was served it (<body data-fleet-tz>); undefined when absent or unknown here. */
|
|
26
|
+
export function fleetTz() { return known(globalThis.document?.body?.dataset?.fleetTz); }
|
|
27
|
+
|
|
28
|
+
const toMs = (ts) => (typeof ts === "number" ? ts : Date.parse(String(ts || "")));
|
|
29
|
+
function wall(ms, tz) {
|
|
30
|
+
const p = {};
|
|
31
|
+
for (const x of fmt("ymd", tz).formatToParts(ms)) p[x.type] = Number(x.value);
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The label for `ts` (ISO string or epoch ms), "" when it cannot be read. opts: tz (default the fleet's), now (epoch ms),
|
|
37
|
+
* yesterday(hm) → the word for "yesterday" around hm (default: hm alone), locale (tests).
|
|
38
|
+
*/
|
|
39
|
+
export function chatTime(ts, opts = {}) {
|
|
40
|
+
const ms = toMs(ts);
|
|
41
|
+
if (!Number.isFinite(ms)) return "";
|
|
42
|
+
const tz = "tz" in opts ? known(opts.tz) : fleetTz();
|
|
43
|
+
const hm = fmt("hm", tz, opts.locale).format(ms);
|
|
44
|
+
const d = wall(ms, tz), today = wall(opts.now ?? Date.now(), tz);
|
|
45
|
+
if (d.year === today.year && d.month === today.month && d.day === today.day) return hm;
|
|
46
|
+
const y = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
|
|
47
|
+
if (d.year === y.getUTCFullYear() && d.month === y.getUTCMonth() + 1 && d.day === y.getUTCDate()) return opts.yesterday ? opts.yesterday(hm) : hm;
|
|
48
|
+
return d.year === today.year ? `${d.month}/${d.day} ${hm}` : `${d.year}/${d.month}/${d.day} ${hm}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The full date, time and zone of `ts`, for a tooltip; "" when it cannot be read. */
|
|
52
|
+
export function chatTimeFull(ts, opts = {}) {
|
|
53
|
+
const ms = toMs(ts);
|
|
54
|
+
if (!Number.isFinite(ms)) return "";
|
|
55
|
+
return fmt("full", "tz" in opts ? known(opts.tz) : fleetTz(), opts.locale).format(ms);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Milliseconds until the fleet's next midnight (when "today" labels change), at most an hour (a DST day self-corrects). */
|
|
59
|
+
export function msToNextDay(now = Date.now(), tz = fleetTz()) {
|
|
60
|
+
const w = wall(now, known(tz));
|
|
61
|
+
const into = ((w.hour * 60 + w.minute) * 60 + w.second) * 1000 + (now % 1000);
|
|
62
|
+
return Math.max(1000, Math.min(3_600_000, 86_400_000 - into + 1000));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Call `fn` when the fleet's day may have changed — at its next midnight, then again for each one after (an hour at
|
|
67
|
+
* most between looks, so a DST day or a late timer corrects itself). Each subscriber holds its own timer, and no
|
|
68
|
+
* module-wide state: the unsubscribe it returns leaves nothing armed.
|
|
69
|
+
*/
|
|
70
|
+
export function onDayChange(fn) {
|
|
71
|
+
let timer = null, live = true;
|
|
72
|
+
const arm = () => {
|
|
73
|
+
timer = setTimeout(() => {
|
|
74
|
+
timer = null;
|
|
75
|
+
try { fn(); } finally { if (live) arm(); }
|
|
76
|
+
}, msToNextDay());
|
|
77
|
+
};
|
|
78
|
+
arm();
|
|
79
|
+
return () => { live = false; if (timer !== null) { clearTimeout(timer); timer = null; } };
|
|
80
|
+
}
|
package/dist/ui/chat-tour.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// #1366: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// #1366: the chat tour, in the app shell (#1408). Since #1854 (rank 6) it never starts by itself: a first visit opens
|
|
2
|
+
// on a quiet page, and Settings → This device → Chat tour starts it on an agent's chat. It walks only the steps whose
|
|
3
|
+
// target is on the page. Got it, Skip or Esc ends it and records it (agend_tour_done, kept as before: no new key). A
|
|
4
|
+
// card that outlines one thing at a time: no overlay, nothing blocked, and no style attribute — the outline is a class
|
|
5
|
+
// (#1300).
|
|
5
6
|
//
|
|
6
7
|
// Esc order: the phone drawer covers the card, so it closes first (the shell); then the tour; only then may Esc reach
|
|
7
8
|
// the chat and stop a reply. The Esc that closed the tour is spent: its key repeats (a held key) must not go on to stop
|
|
@@ -24,6 +25,8 @@ const STEPS = [
|
|
|
24
25
|
: document.querySelector(".badge-await") || byId("instanceList")) },
|
|
25
26
|
];
|
|
26
27
|
let at = -1, spot = null, returnTo = null, escHeld = false, installed = false;
|
|
28
|
+
/** The steps of the running tour: those whose target was on the page when it started (#1854: target-aware). */
|
|
29
|
+
let steps = STEPS;
|
|
27
30
|
|
|
28
31
|
export function tourDone() { try { return localStorage.getItem(TOUR_KEY) === "1"; } catch { return true; } }
|
|
29
32
|
export function tourStep() { return at; }
|
|
@@ -32,13 +35,15 @@ export function startTour() {
|
|
|
32
35
|
// Replayed from the drawer: the drawer closes first (focus goes back to ☰), so the card is seen.
|
|
33
36
|
closeDrawer();
|
|
34
37
|
if (at < 0) returnTo = document.activeElement;
|
|
38
|
+
steps = STEPS.filter((s) => s.spot());
|
|
39
|
+
if (!steps.length) return;
|
|
35
40
|
show(0);
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/** The current step's outline follows a redraw (a new badge, Send swapped for Stop reply) — same card, same focus. */
|
|
39
44
|
export function refreshTourSpot() {
|
|
40
45
|
if (at < 0) return;
|
|
41
|
-
const next =
|
|
46
|
+
const next = steps[at].spot() || null;
|
|
42
47
|
if (next === spot && (!next || next.classList.contains("tour-spot"))) return;
|
|
43
48
|
if (spot) spot.classList.remove("tour-spot");
|
|
44
49
|
spot = next;
|
|
@@ -46,10 +51,10 @@ export function refreshTourSpot() {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
function show(i) {
|
|
49
|
-
if (i < 0 || i >=
|
|
54
|
+
if (i < 0 || i >= steps.length) return;
|
|
50
55
|
at = i;
|
|
51
56
|
if (spot) spot.classList.remove("tour-spot");
|
|
52
|
-
spot =
|
|
57
|
+
spot = steps[i].spot() || null;
|
|
53
58
|
if (spot) spot.classList.add("tour-spot");
|
|
54
59
|
let card = byId("tour");
|
|
55
60
|
if (!card) {
|
|
@@ -60,18 +65,18 @@ function show(i) {
|
|
|
60
65
|
}
|
|
61
66
|
card.textContent = "";
|
|
62
67
|
const title = document.createElement("h2"); title.id = "tourTitle"; title.textContent = t("chat.tourTitle");
|
|
63
|
-
const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(
|
|
68
|
+
const text = document.createElement("p"); text.className = "tour-text"; text.setAttribute("aria-live", "polite"); text.textContent = t(steps[i].text);
|
|
64
69
|
const foot = document.createElement("div"); foot.className = "tour-foot";
|
|
65
|
-
const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1,
|
|
70
|
+
const count = document.createElement("span"); count.className = "tour-count"; count.textContent = t("chat.tourStep", i + 1, steps.length);
|
|
66
71
|
const button = (label, act, arg, cls) => {
|
|
67
72
|
const b = document.createElement("button"); b.type = "button"; b.className = `btn btn-sm ${cls}`; b.dataset.tour = act;
|
|
68
73
|
if (arg != null) b.dataset.arg = String(arg);
|
|
69
74
|
b.textContent = t(label); return b;
|
|
70
75
|
};
|
|
71
76
|
foot.append(count);
|
|
72
|
-
if (i <
|
|
77
|
+
if (i < steps.length - 1) foot.append(button("chat.tourSkip", "end", null, "btn-ghost"));
|
|
73
78
|
if (i > 0) foot.append(button("chat.tourBack", "move", -1, "btn-ghost"));
|
|
74
|
-
const next = i <
|
|
79
|
+
const next = i < steps.length - 1 ? button("chat.tourNext", "move", 1, "btn-primary") : button("chat.tourDone", "end", null, "btn-primary");
|
|
75
80
|
foot.append(next);
|
|
76
81
|
card.append(title, text, foot);
|
|
77
82
|
next.focus();
|
|
@@ -105,12 +110,10 @@ export function onTourKey(e) {
|
|
|
105
110
|
if (at >= 0) { e.preventDefault(); e.stopPropagation(); endTour(); escHeld = true; }
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
/** Once per page: the key handler
|
|
113
|
+
/** Once per page: the key handler and the language follow-up. Nothing is shown: the tour is started by a person (#1854). */
|
|
109
114
|
export function installTour() {
|
|
110
115
|
if (installed) return;
|
|
111
116
|
installed = true;
|
|
112
117
|
document.addEventListener("keydown", onTourKey, true);
|
|
113
118
|
onLang(() => { if (at >= 0) show(at); });
|
|
114
|
-
// Shown first on the chat (it points at the chat's own controls), never over View or another panel.
|
|
115
|
-
if (!tourDone() && /^\/ui(\/chat\/|$)/.test(location.pathname)) setTimeout(startTour, 0);
|
|
116
119
|
}
|